Metro 风格的轻量级图像处理
传感器和图像是程序员在平板电脑上最感兴趣的两个神奇的东西。
引言
传感器和图像是程序员在平板电脑和移动计算机上最感兴趣的两个神奇的东西。利用这两个特性,我们可以制作出许多令人惊叹的应用。
图像处理主要有两种方式,一种是使用DirectX HLSL,另一种是使用位图像素。对于应用程序开发人员来说,用HDLS编写一个普通的休闲游戏或APP可能“有点大材小用”。这就是我写这篇文章的原因。
最近,我尝试将过去用GDI+编写的图像处理程序移植到Metro风格的应用程序中。我的应用程序的主要部分是获取位图文件的像素,用数学算法重新计算每个像素的值,然后将其恢复到原始图像中。通过这种方式,程序可以实现许多有趣的效果,只要你能想出有趣的算法。在我的应用程序中,我已经实现了一些算法,如负片、色调、油画、浮雕、灰度、阳光等。
Metro风格的应用程序更像WPF。我以前写的程序是用GDI+编写的,所以我想把它修改成WPF应用程序。有趣的是,你会发现将它移植到不同的Windows平台需要一些努力。这是因为一些API和命名空间不同,所以修改程序的结构需要时间。为了获得一个全面的认识,我将介绍如何在GDI+、WPF、Windows Phone 7和Metro风格的应用程序中处理位图图像。
不同平台的位图访问
GDI+处理位图的方式很直接。它使用Bitmap
类来访问位图。最有用的方法是SetPixel
和GetPixel
。要完成这项工作,我们首先需要声明一个Bitmap
类,如下所示:
Bitmap original;
在此之后,我们声明一个名为‘Target
’的像素,并使用System.Drawing.Color
结构来存储R、G、B的值。
System.Drawing.Color Target;
在此之后,我们可以使用GetPixel来获取像素的值并将其存储到目标中。
Target= original.GetPixel(x, y);
int r = Target.R;
int g = Target.G;
int b = Target.B;
在此之后,我们可以处理每个R、G、B的内容。例如,在下面的代码中,我们只取RGB的平均值。
int avg = (r + g + b) / 3;
然后,我们可以使用SetPixel
来设置位图的新值。
original.SetPixel(x, y, System.Drawing.Color.FromArgb(avg, avg, avg));
在GDI+之后,让我们谈谈WPF。在WPF中,处理位图的方式略有不同。它使用BitmapImage
类来访问位图,并使用CopyPixels
将位图图像的像素复制到一个字节数组中。要使用CopyPixels
,我们需要使用Rect结构来设置位图的范围。
BitmapImage iSrc;
var array = new int[iSrc.PixelWidth * iSrc.PixelHeight];
var rect = new Int32Rect(0, 0, iSrc.PixelWidth,iSrc.PixelHeight);
iSrc.CopyPixels(rect, array, iSrc.PixelWidth * 4, 0);
要获取像素的R、G、B值,因为它是一个整数数组,我们需要进行位移运算来获取R、G、B的值。像素的蓝色值是:
byte Blue= (byte)((array [index]& 0x000000FF);
像素的绿色值是:
byte Green= (byte)((array [index]& 0x0000FF00)>>8);
像素的红色值是:
byte Red =(byte)(( array [index]& 0x00FF0000) >> 16);
像素的Alpha值是:
byte Alpha=(byte)(( array [index]& 0xFF000000) >> 24);
在此之后,我们也可以处理像素的R、G、B值。然后将值组合到整数数组中。
array [index]=(Blue)|(Green<<8)|(Red<<16)|(Alpha<<24);
然后,我们使用修改后的像素数组创建一个新的位图来显示结果。
BitmapImage.Create(modifiedImage.PixelWidth,
modifiedImage.PixelHeight, 96, 96, PixelFormats.Bgra32, null, array,
pixelsNewsize);
在WPF之后,让我们谈谈WP7。在WP7中,我们不能直接修改位图的像素,我们需要使用‘WriteableBitmap
’来访问位图。创建‘WriteableBitmap
’的代码如下:
BitmapImage bitmap= new BitmapImage();
bitmap.SetSource(value);WriteableBitmap;
modifiedImage = new WriteableBitmap(bitmap);
‘WriteableBitmap
’包含一个名为‘Pixel
’的字节数组,用于存储像素的全部R、G、B值。
byet Blue = (byte)(rawpixel.Pixels[offset]& 0x000000FF);
byet Green =(byte)((rawpixel.Pixels[offset] & 0x0000FF00) >> 8);
byet Red = (byte)((rawpixel.Pixels[offset]& 0x00FF0000) >> 16);
byet Alpha= (byte)((rawpixel.Pixels[offset] & 0xFF000000) >> 24);
计算新值后,我们使用以下方式将新值写回:
rawimagepixel.Pixels[offset] =
(pixels.Blue) | (pixels.Green << 8) | (pixels.Red << 16) | (pixels.Alpha<<24);
Metro风格的应用使用WinRT。Silverlight和WinRT之间的一个主要变化是命名空间名称。不再是System.Windows.-----
,而是使用Windows.UI.Xaml.----
。有一份有趣的文件可以参考:迁移Windows Phone 7应用到XAML。
在Metro应用中,我们使用以下方式访问位图。你会发现虽然也有一个WriteableBitmap
,但它没有pixels
属性。因此,我们需要找到一种方法来访问像素。我可以使用‘Stream
’来完成这项工作。代码如下:
WriteableBitmap bitmap;
byte[] pixels;
Stream pixelStream;
bitmap = new WriteableBitmap(width, height);
pixels = new byte[4 * bitmap.PixelWidth *
bitmap.PixelHeight];
pixelStream = bitmap.PixelBuffer.AsStream();
以下方法将一个像素设置为特定的颜色:
int index = 4*(y * bitmap.PixelWidth + x);
pixels[index + 0] = Pcolor.B;
pixels[index + 1] = Pcolor.G;
pixels[index + 2] = Pcolor.R;
pixels[index + 3] = Pcolor.A;
当需要从像素数组更新WriteableBitmap
时,需要使用seek来设置像素的位置,并使用write方法进行写入。
pixelStream.Seek(0,SeekOrigin.Begin);pixelStream.Write(pixels,0, pixels.Length);
下面是我为图像处理设计的示例Metro应用程序的概览:
图像处理算法
好了,现在我们知道如何在Metro风格的应用程序中处理像素了,接下来我想介绍如何使用酷炫的算法来修改像素。首先,让我们尝试处理一个负片效果。算法是用255减去每个像素的R、G、B值。结果应该在0~255之间,算法如下:
b= (byte)(255 - PC.Blue);
g= (byte)(255 - PC.Green);
r= (byte)(255 - PC.Red);
StreamBuffer[offset+2] = (byte)(r > 255 ? 255 : (r < 0 ? 0 : r));
StreamBuffer[offset+1] =(byte)(g > 255 ? 255 : (g< 0 ? 0 : g));
StreamBuffer[offset+0]=(byte)(b> 255 ? 255 : (b< 0 ? 0 : b));
处理负片算法后,结果如下所示:
我接下来要讲的第二个算法是色彩滤镜。在下面的代码中,R、G、B是控制像素R、G、B值的比例因子。我们可以使用R、G、B作为增益来控制新的R、G、B值。同样,值必须位于255和0之间。算法如下:
b= (byte)(PC.Blue * R);
g= (byte)(PC.Green * G);
r= (byte)(PC.Red * B);
StreamBuffer[offset+2]= (byte)(r > 255 ? 255 : (r < 0 ? 0 : r));
StreamBuffer[offset+1] =(byte)(g > 255 ? 255 : (g< 0 ? 0 : g));
StreamBuffer[offset+0]=(byte)(b> 255 ? 255 : (b< 0 ? 0 : b));
这里我们将G设为1,R设为0,B设为0。输出如下:
第三种图像算法称为浮雕,我们将通过获取两个相邻像素并计算这两个像素的R、G、B差值,然后添加一个偏移量作为新的R、G、B值来实现这种效果。算法如下:
PC1 = GetPixel(i, j, streambuffer, w, h);
PC2 = GetPixel(i+1, j+1, streambuffer,w, h);
r= Math.Abs(PC1.Red - PC2.Red + 128);
g= Math.Abs(PC1.Green - PC2.Green + 128);
b= Math.Abs(PC1.Blue - PC2.Blue + 128);
StreamBuffer[offset+2]= (byte)(r > 255 ? 255 : (r < 0 ? 0 : r));
StreamBuffer[offset+1] =(byte)(g > 255 ? 255 : (g< 0 ? 0 : g));
StreamBuffer[offset+0]=(byte)(b> 255 ? 255 : (b< 0 ? 0 : b));
照片的输出如下:
第四种是阳光效果,我们定义一个半径R,如果当前点的距离小于R,则使用200 * (1 - MyLength / R)作为新值。算法如下:
float MyLength = (float)Math.Sqrt(Math.Pow((i - MyCenter.X), 2) +
Math.Pow((j - MyCenter.Y),2));
if (MyLength < R)
{
PC = GetPixel(i, j, streambuffer, w, h);
float MyPixel = 200 * (1 - MyLength / R);
int r = PC.Red + (int)MyPixel;
PC.Red = (byte)Math.Max(0, Math.Min(r, 255));
Int g = PC.Green + (int)MyPixel;
PC.Green = (byte)Math.Max(0, Math.Min(g,255));
Int b = PC.Blue + (int)MyPixel;
PC.Blue = (byte)Math.Max(0, Math.Min(b, 255));
PutPixel(streambuffer, w, h, PC, i, j);
}
照片的输出如下:
第五种算法是设置为灰度风格,我们将新的r、g、b设置为相同的值。算法如下:
r = (byte)((0.311 * r1) + (0.486 * g1)+(0.213 * b1));
StreamBuffer[offset+2]= (byte)(r > 255 ? 255 : (r < 0 ? 0 : r));
StreamBuffer[offset+1] =r;
StreamBuffer[offset+0]=r;
照片的输出如下:
第六种是亮度,这很简单。我们将一个常数值加到新的像素上。算法如下:
r = r_original + bright;
g = g_original+ bright;
b = b_original+ bright;
照片的输出如下:
第七种是油画效果,其主要概念是获取位图的随机点,并将其新位置替换为软件获取的随机数据。算法如下:
Random rnd = new Random();
int iModel= 10;
int i =w - iModel;
while (i> 1)
{
int j= h - iModel;
while(j > 1)
{
int iPos = rnd.Next(100000) % iModel;
PC = GetPixel((int)(i + iPos), (int)(j + iPos), modifiedstreambuffer, w,
h);
PutPixel(bitmapstreambuffer, w, h, PC, i, j);
j= j - 1;
}
i = i - 1;
}
输出如下
所有的效果都很有趣,对吧?当然,你也可以自己创造更令人惊叹的图像效果算法。顺便说一句,我们也可以在图像处理程序中添加相机功能,这样在拍照后,我们可以立即为其添加特殊效果。部分代码如下:
var ui = new CameraCaptureUI();
ui.PhotoSettings.CroppedAspectRatio= new Size(4, 3);
var file = await
ui.CaptureFileAsync(CameraCaptureUIMode.Photo);
stream = await file.OpenAsync(FileAccessMode.Read);
var bitmap = new BitmapImage();
bitmap.SetSource(stream);
Image1.Source = bitmap;
在文章的最后,让我们谈谈位图处理的一些性能问题。我们以负片算法为例进行讨论。为了提高处理速度,我们可以:
- 将两个‘for’循环改为一个‘for’循环,即一次处理一个条带而不是一个像素。
- 将所有像素读取到内存缓冲区,而不是直接读取一个像素。
在示例程序中,一开始,我使用两个‘for
’循环处理像素,这需要很长时间来处理像素的值。性能不好。
for (i = 0;i < modifiedImage.PixelWidth; i++)
{
for(int j = 0; j < modifiedImage.PixelHeight; j++)
{
PC = GetPixel(i, j, streambuffer,w,h);
PC.Blue = (byte)(255 - PC.Blue);
PC.Green = (byte)(255 - PC.Green);
PC.Red = (byte)(255 - PC.Red);
PutPixel(streambuffer,w,h, PC, i, j);
}
}
在我将两个for循环合并为一个如下所示之后:
for (i = 0;i < streambuffer.Length-4; i=i+4)
{
streambuffer[i + 3] = (byte)( 0xff - streambuffer[i + 3]);
streambuffer[i + 2] = (byte)( 0xff - streambuffer[i + 2]);
streambuffer[i + 1] = (byte)( 0xff - streambuffer[i + 1]);
streambuffer[i + 0] = (byte)( 0xff - streambuffer[i + 0]);
}
它极大地提高了性能。
希望本文和示例程序能帮助您更多地了解Metro风格应用程序中的图像效果。