Плакатный стиль и монохромное преобразование в WinRT

130

В большинстве графических редакторов предусмотрен режим «плакатного» преобразования растрового изображения. Цветовое разрешение сокращается до ограниченной палитры, в результате чего изображение больше напоминает плакат, а не фотографию. Также изображения часто подвергаются монохромному преобразованию. Вероятно, это две самые простые операции в области обработки компьютерных изображений.

Программа Posterizer, как и ImageFileIO, которую мы создали в предыдущей статье, содержит кнопки "Открыть" и "Сохранить как", но на странице также присутствует «панель управления» — набор элементов управления RadioButton, позволяющих выбрать количество битов в цветовом разрешении (независимо для трех цветовых каналов), и элемент управления CheckBox для преобразования изображений в монохромный режим.

Допустим, пользователь загружает растровое изображение и щелкает на флажке CheckBox, чтобы преобразовать его в монохромный режим. Программа послушно объединяет красную, зеленую и синюю составляющие каждого пиксела в оттенок серого. А потом пользователь снимает флажок... Остается надеяться, что ваша программа сохранила исходное изображение! По этой причине в программе Posterizer хранятся два полных массива пикселов: с исходными пикселами (srcPixels) и измененными пикселами (dstPixels).

Файл XAML содержит панель управления, элемент Image и строку приложения:

<Page ...>

    <Page.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="TextAlignment" Value="Center" />
            <Setter Property="FontSize" Value="18" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Grid Name="controlPanelGrid"
              Margin="12 0"
              HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <TextBlock Text="Red" />
            <TextBlock Text="Green" Grid.Column="1" />
            <TextBlock Text="Blue" Grid.Column="2" />
            <TextBlock Text="Все" Grid.Column="3" />

            <CheckBox Name="monochromeCheckBox"
                      Content="Монохром"
                      Grid.Row="9"
                      Grid.ColumnSpan="4"
                      Margin="0 12"
                      HorizontalAlignment="Center"
                      Checked="monochromeCheckBox_Checked"
                      Unchecked="monochromeCheckBox_Checked" />
        </Grid>

        <Image Name="image" Grid.Column="1" />
    </Grid>

    <Page.BottomAppBar>
        <AppBar>
            <Grid>
                <StackPanel Orientation="Horizontal" 
                            HorizontalAlignment="Right">
                    <AppBarButton Label="Открыть файл" Icon="OpenFile" Name="open"
                            Click="openFile_Click" />

                    <AppBarButton Name="saveAsButton"
                            IsEnabled="False"
                            Label="Сохранить" Icon="SaveLocal"
                            Click="saveAsButton_Click" />
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.BottomAppBar>
</Page>

Однако в файле XAML нет самих элементов управления RadioButton. Я решил, что пользователь должен управлять тремя цветовыми каналами по отдельности, а четвертый столбец должен изменять все три цветовых канала одновременно. Кнопки создаются в обработчике Loaded с удобным свойством Tag, которое используется для их идентификации:

// ...

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        public MainPage()
        {
            this.InitializeComponent();
            this.Loaded += Page_Loaded;
        }

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            // Создание элементов управления RadioButton
            // ПРИМЕЧАНИЕ: 'a' здесь означает "Все" не "Alpha"!
            string[] prefix = { "r", "g", "b", "a" };

            for (int col = 0; col < 4; col++)
                for (int row = 1; row < 9; row++)
                {
                    RadioButton radio = new RadioButton
                    {
                        Content = row.ToString(),
                        Margin = new Thickness(12, 6, 12, 6),
                        GroupName = prefix[col],
                        Tag = prefix[col] + row,
                        IsChecked = row == 8
                    };
                    radio.Checked += radiobutton_Checked;

                    Grid.SetColumn(radio, col);
                    Grid.SetRow(radio, row);
                    controlPanelGrid.Children.Add(radio);
                }
        }
        
        // ...
    }
}

Файловый ввод/вывод выглядит практически так же, как в проекте ImageFileIO, не считая того, что при загрузке изображения создается второй массив пикселов, а метод UpdateBitmap (который будет описан ниже) отвечает за обновление объекта WriteableBitmap этим вторым массивом. При сохранении файла используется массив dstPixels:

// ...

public sealed partial class MainPage : Page
{
    byte[] srcPixels;
    byte[] dstPixels;
    WriteableBitmap bitmap;
    Stream pixelStream;

        
    // ...

    private async void openFile_Click(object sender, RoutedEventArgs e)
    {
        // Создание объекта FileOpenPicker
        FileOpenPicker picker = new FileOpenPicker();
        picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

        // Инициализируется расширениями файлов
        IReadOnlyList<BitmapCodecInformation> codecInfos =
                        BitmapDecoder.GetDecoderInformationEnumerator();

        foreach (BitmapCodecInformation codecInfo in codecInfos)
            foreach (string extension in codecInfo.FileExtensions)
                picker.FileTypeFilter.Add(extension);

        // Получение выбранного файла
        StorageFile storageFile = await picker.PickSingleFileAsync();

        if (storageFile == null)
            return;

        // Открытие потока и создание декодера
        BitmapDecoder decoder = null;

        using (IRandomAccessStreamWithContentType stream = await storageFile.OpenReadAsync())
        {
            string exception = null;

            try
            {
                decoder = await BitmapDecoder.CreateAsync(stream);
            }
            catch (Exception exc)
            {
                exception = exc.Message;
            }

            if (exception != null)
            {
                MessageDialog msgdlg =
                new MessageDialog("Данный файл изображения не может быть загружен. Ошибка: " + exception);
                await msgdlg.ShowAsync();
                return;
            }

            // Получение первого кадра
            BitmapFrame bitmapFrame = await decoder.GetFrameAsync(0);

            // Получение пикселов источника
            PixelDataProvider dataProvider =
                await bitmapFrame.GetPixelDataAsync(BitmapPixelFormat.Bgra8,
                                        BitmapAlphaMode.Premultiplied,
                                        new BitmapTransform(),
                                        ExifOrientationMode.RespectExifOrientation,
                                        ColorManagementMode.ColorManageToSRgb);

            srcPixels = dataProvider.DetachPixelData();
            dstPixels = new byte[srcPixels.Length];

            // Создание объекта WriteableBitmap и обновление его значения
            // источником данных Image
            bitmap = new WriteableBitmap((int)bitmapFrame.PixelWidth,
                                 (int)bitmapFrame.PixelHeight);
            pixelStream = bitmap.PixelBuffer.AsStream();
            image.Source = bitmap;

            // Обновить изображение
            UpdateBitmap();
        }

        // Снятие блокировки с кнопки Сохранить
        saveAsButton.IsEnabled = true;
    }

    private async void saveAsButton_Click(object sender, RoutedEventArgs e)
    {
        FileSavePicker picker = new FileSavePicker();
        picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

        // Получение информации о кодере
        Dictionary<string, Guid> imageTypes = new Dictionary<string, Guid>();
        IReadOnlyList<BitmapCodecInformation> codecInfos =
                        BitmapEncoder.GetEncoderInformationEnumerator();

        foreach (BitmapCodecInformation codecInfo in codecInfos)
        {
            List<string> extensions = new List<string>();

            foreach (string extension in codecInfo.FileExtensions)
                extensions.Add(extension);

            string filetype = codecInfo.FriendlyName.Split(' ')[0];
            picker.FileTypeChoices.Add(filetype, extensions);

            foreach (string mimeType in codecInfo.MimeTypes)
                imageTypes.Add(mimeType, codecInfo.CodecId);
        }

        // Получение выбранного объекта StorageFile
        StorageFile storageFile = await picker.PickSaveFileAsync();

        if (storageFile == null)
            return;

        // Открытие StorageFile
        using (IRandomAccessStream fileStream =
                        await storageFile.OpenAsync(FileAccessMode.ReadWrite))
        {
            // Создать кодер изображения
            Guid codecId = imageTypes[storageFile.ContentType];
            BitmapEncoder encoder = await BitmapEncoder.CreateAsync(codecId, fileStream);

            // Запись целевых пикселов в первый кадр
            encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied,
                         (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight,
                         96, 96, dstPixels);


            await encoder.FlushAsync();
        }
    }

    // ...
}

Обработчик RadioButton получается достаточно сложным из-за четвертого столбца. Я хотел, чтобы щелчок на кнопке RadioButton в четвертом столбце также приводил к установке трех других кнопок этой строки, но чтобы это однозначно не приводило к множественным вызовам UpdateBitmap. По этой причине массив из трех байтовых масок хранится в поле, а значения элементов задаются в обработчике события RadioButton.

Метод UpdateBitmap вызывается только при изменении состояния одной из трех масок:

public sealed partial class MainPage : Page
{
    // ...

    // Маски для RGB
    byte[] masks = { 0xFF, 0xFF, 0xFF };

    // ...

    private void radiobutton_Checked(object sender, RoutedEventArgs e)
    {
        // Декодирование свойств Tag кнопки RadioButton
        RadioButton radio = sender as RadioButton;
        string tag = radio.Tag as string;
        int maskIndex = -1;
        int bits = Int32.Parse(tag[1].ToString()); // от 1 до 8
        byte mask = (byte)(0xFF << 8 - bits);
        bool needsUpdate;

        // Определение индекса в массиве масок
        switch (tag[0])
        {
            case 'r': maskIndex = 2; break;
            case 'g': maskIndex = 1; break;
            case 'b': maskIndex = 0; break;
        }

        // Для "Всех", останавливает остальные переключатели этой строки
        if (tag[0] == 'a')
        {
            needsUpdate = masks[0] != mask && masks[1] != mask && masks[2] != mask;

            if (needsUpdate)
                masks[0] = masks[1] = masks[2] = mask;

            foreach (UIElement child in (radio.Parent as Panel).Children)
            {
                if (child != radio &&
                Grid.GetRow(child as FrameworkElement) == Grid.GetRow(radio))
                {
                (child as RadioButton).IsChecked = true;
                }
            }
        }
        else
        {
            needsUpdate = masks[maskIndex] != mask;

            if (needsUpdate)
                masks[maskIndex] = mask;
        }

        if (needsUpdate)
            UpdateBitmap();
    }

    private void monochromeCheckBox_Checked(object sender, RoutedEventArgs e)
    {
        UpdateBitmap();
    }


    // ...
}

Остается лишь сам метод UpdateBitmap. Три маски применяются к синей, зеленой и красной составляющим, которые затем объединяются для создания оттенка серого в том случае, если установлен флажок Monochrome. Весовые коэффициенты оттенков серого определяются стандартными коэффициентами преобразования для телевизионных стандартов NTSC и PAL:

public sealed partial class MainPage : Page
{
   // ...

   private void UpdateBitmap()
   {
            if (bitmap == null)
                return;

            for (int index = 0; index < srcPixels.Length; index += 4)
            {
                // Применение маски к пикселам источника
                byte B = (byte)(masks[0] & srcPixels[index + 0]);
                byte G = (byte)(masks[1] & srcPixels[index + 1]);
                byte R = (byte)(masks[2] & srcPixels[index + 2]);
                byte A = srcPixels[index + 3];

                // Возможное преобразование в оттенки серого
                if (monochromeCheckBox.IsChecked.Value)
                    B = G = R = (byte)(0.30 * R + 0.59 * G + 0.11 * B);

                // Сохранение итоговых пикселов
                dstPixels[index + 0] = B;
                dstPixels[index + 1] = G;
                dstPixels[index + 2] = R;
                dstPixels[index + 3] = A;
            }

            // Обновление изображения
            pixelStream.Seek(0, SeekOrigin.Begin);
            pixelStream.Write(dstPixels, 0, dstPixels.Length);
            bitmap.Invalidate();
    }
}

Если бы эта программа не была учебным примером, я бы непременно вынес обработку данных в отдельный поток. Впрочем, в этом потоке должны выполняться только циклы, изменяющие биты пикселов. Все модификации объекта WriteableBitmap должны выполняться в потоке пользовательского интерфейса.

На следующем рисунке цветовое разрешение изображения сокращается до двух битов, то есть в изображении используются всего 64 цвета:

Пример использования цветофильтров для изображения в программе Windows Runtime
Пройди тесты
Лучший чат для C# программистов