今天一朋友问我怎么实现PS中对指定区域进行填充的效果(这家伙在用C#做一个需要众多图形处理的项目)
据我所知,Graphics有个属性Clip :获取或设置 Region,该对象限定此 Graphics 的绘图区域。
好吧,这里我们不用这个方式,而是用另外一个位图(mask)来做有效区域标记。
这种做法在很多图形库中都是存在的(比如openCV)。
效果
纹理贴图 和 mask:
效果图:
左图是纹理贴图,右图是纯填充。
实现
首先需要准备一个mask
究竟mask是个什么东西呢?这里的mask 是一个深度为8的位图且只有两个颜色索引的(0x00 , 0xff)黑白图像,用黑色表示非作用域。这里我用PS随便画了一个
色阶如下:
代码如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Drawing.Imaging; namespace BitmapTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Size size = Properties.Resources.mask.Size; Bitmap bmp = new Bitmap(size.Width, size.Height); Fill(bmp, Color.Red, Properties.Resources.mask); pictureBox1.Image = bmp; Bitmap bmp2 = new Bitmap(size.Width, size.Height); DrawBitmap(bmp2, Properties.Resources.tex, Properties.Resources.mask); pictureBox2.Image = bmp2; } private void Fill(Bitmap bmp, Color clr, Bitmap mask) { if (!bmp.Size.Equals(mask.Size)) { throw new Exception("mask大小与bmp大小不一致"); } int w = bmp.Width; int h = bmp.Height; Rectangle rect = new Rectangle(0, 0, w, h); BitmapData srcBmData = bmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); BitmapData maskData = mask.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed); unsafe { byte* pSrc = (byte*)srcBmData.Scan0; byte* pMsk = (byte*)maskData.Scan0; int nClr = clr.ToArgb(); int i = 0, j = 0; for (; i < h; i++)//一行 { for (j = 0; j < w; j++)//一点 { if (pMsk[i * maskData.Stride + j] == 00) { continue; } else { *((int*)(pSrc + (i * w + j) * 4)) = nClr; } } } } bmp.UnlockBits(srcBmData); mask.UnlockBits(maskData); }
private void DrawBitmap(Bitmap bmp, Bitmap tex, Bitmap mask) { if (!bmp.Size.Equals(mask.Size)) { throw new Exception("mask大小与bmp大小不一致"); } int w = bmp.Width; int h = bmp.Height; Rectangle rect = new Rectangle(0, 0, w, h); BitmapData srcBmData = bmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); BitmapData texBmData = tex.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); BitmapData maskData = mask.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed); unsafe { byte* pSrc = (byte*)srcBmData.Scan0; byte* pTex = (byte*)texBmData.Scan0; byte* pMsk = (byte*)maskData.Scan0; int i = 0, j = 0; for (; i < h; i++)//一行 { for (j = 0; j < w; j++)//一点 { if (pMsk[i * maskData.Stride + j] == 00) { continue; } else { *((int*)(pSrc + (i * w + j) * 4)) = *((int*)(pTex + (i * w + j) * 4)); } } } } bmp.UnlockBits(srcBmData); mask.UnlockBits(maskData); } } }
这里用了C++的处理方式来实现(没办法,比较习惯这种方式 - -)。
其实实现起来非常简单,就是逐个扫描像素点,判断mask的值并确定是否改变这个像素点里的值……
值得注意的一点是,BitmapData的Stride属性。
因为系统考虑到效率问题,将每个扫描行的数据量与4字节对齐,Stride就是对齐后一行的字节数。
举个例子:
有一个99*99 深度为8的图片,那么在理论上一个扫描行的字节数只需要99byte就够了。你可能以为第二行的第一个像素点的数据是第100个字节的值。
但是事实上并不是这样,在对齐到4以后,每一行的字节数被对齐至100,这样一来第二行的第一个像素点则是第101个字节…
另外,这个问题在深度为32的图片中是不需要考虑的,因为32位的图片字节数肯定会对齐到4…