Плакатный стиль и монохромное преобразование в WinRT
130Разработка под Windows 8/10 --- Плакатный стиль и монохромное преобразование
В большинстве графических редакторов предусмотрен режим «плакатного» преобразования растрового изображения. Цветовое разрешение сокращается до ограниченной палитры, в результате чего изображение больше напоминает плакат, а не фотографию. Также изображения часто подвергаются монохромному преобразованию. Вероятно, это две самые простые операции в области обработки компьютерных изображений.
Программа 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 цвета: