位图卷积






4.07/5 (25投票s)
2004 年 3 月 27 日
3分钟阅读

227014

2411
讨论如何对图像进行卷积的文章,特别是位图的卷积。
引言
图像卷积在计算机图形应用程序中起着重要作用。图像卷积允许对图像进行修改,从而可以从旧图像创建新图像。 卷积还允许重要的功能,例如边缘检测,具有许多广泛的用途。 图像的卷积是一个简单的过程,通过该过程,图像的像素乘以内核(或掩码)以创建新的像素值。 卷积通常称为滤波。
详细说明
首先,对于给定的像素(x,y),我们给每个周围的像素赋予权重。 可以认为它是给出一个数字,告诉我们像素对我们有多重要。 该数字可以是任何整数或浮点数,尽管我通常坚持使用浮点数,因为浮点数也将接受整数。 包含滤波器的内核或掩码实际上可以是任何大小(3x3、5x5、7x7); 但是,3x3 非常常见。 由于每个过程都相同,因此我将只专注于 3x3 内核。
其次,卷积的实际过程包括获取给定像素(x,y)附近的每个像素,并将每个像素的通道乘以加权的内核值。 这意味着对于 3x3 内核,我们将像这样将像素相乘
(x-1,y-1) * kernel_value[row0][col0]
(x ,y-1) * kernel_value[row0][col1]
(x+1,y-1) * kernel_value[row0][col2]
(x-1,y ) * kernel_value[row1][col0]
(x ,y ) * kernel_value[row1][col1]
(x+1,y ) * kernel_value[row1][col2]
(x-1,y+1) * kernel_value[row2][col0]
(x ,y+1) * kernel_value[row2][col1]
(x+1,y+1) * kernel_value[row2][col2]
该过程对图像的每个通道重复进行。 这意味着红色、绿色和蓝色通道(如果在 RGB 色彩空间中工作)必须各自乘以内核值。 内核位置与它所乘的像素位置相关。 简而言之,内核分配在 kernel[rows][cols]
中,在这种情况下为 kernel[3][3]
。 然后将像素(x,y)周围的 3x3(如果使用更大的内核,则为 5x5 或 7x7)区域乘以内核以获得总和。 如果我们正在处理一个 100x100 的图像,分配为 image[100][100]
,并且我们想要像素 (10,10) 的值,则每个通道的过程将如下所示
float fTotalSum = Pixel(10-1,10-1) * kernel_value[row0][col0] + Pixel(10 ,10-1) * kernel_value[row0][col1] + Pixel(10+1,10-1) * kernel_value[row0][col2] + Pixel(10-1,10 ) * kernel_value[row1][col0] + Pixel(10 ,10 ) * kernel_value[row1][col1] + Pixel(10+1,10 ) * kernel_value[row1][col2] + Pixel(10-1,10+1) * kernel_value[row2][col0] + Pixel(10 ,10+1) * kernel_value[row2][col1] + Pixel(10+1,10+1) * kernel_value[row2][col2] +
最后,将每个值添加到总和中,然后将总和除以内核的总权重。 内核的权重是通过添加内核中包含的每个值来给出的。 如果该值小于或等于零,则赋予权重 1 以避免除以零。
卷积图像的实际代码是
for (int i=0; i <= 2; i++)//loop through rows { for (int j=0; j <= 2; j++)//loop through columns { //get pixel near source pixel /* if x,y is source pixel then we loop through and get pixels at coordinates: x-1,y-1 x-1,y x-1,y+1 x,y-1 x,y x,y+1 x+1,y-1 x+1,y x+1,y+1 */ COLORREF tmpPixel = pDC->GetPixel(sourcex+(i-(2>>1)), sourcey+(j-(2>>1))); //get kernel value float fKernel = kernel[i][j]; //multiply each channel by kernel value, and add to sum //notice how each channel is treated separately rSum += (GetRValue(tmpPixel)*fKernel); gSum += (GetGValue(tmpPixel)*fKernel); bSum += (GetBValue(tmpPixel)*fKernel); //add the kernel value to the kernel sum kSum += fKernel; } } //if kernel sum is less than 0, reset to 1 to avoid divide by zero if (kSum <= 0) kSum = 1; //divide each channel by kernel sum rSum/=kSum; gSum/=kSum; bSum/=kSum;
包含的源代码执行一些常见的图像卷积。 还包括一个“卷积图像”菜单选项,允许用户输入他们自己的内核。 常见的 3x3 内核包括
gaussianBlur[3][3] = {0.045, 0.122, 0.045, 0.122, 0.332, 0.122, 0.045, 0.122, 0.045}; gaussianBlur2[3][3] = {1, 2, 1, 2, 4, 2, 1, 2, 1}; gaussianBlur3[3][3] = {0, 1, 0, 1, 1, 1, 0, 1, 0}; unsharpen[3][3] = {-1, -1, -1, -1, 9, -1, -1, -1, -1}; sharpness[3][3] = {0,-1,0,-1,5,-1,0,-1,0}; sharpen[3][3] = {-1, -1, -1, -1, 16, -1, -1, -1, -1}; edgeDetect[3][3] = {-0.125, -0.125, -0.125, -0.125, 1, -0.125, -0.125, -0.125, -0.125}; edgeDetect2[3][3] = {-1, -1, -1, -1, 8, -1, -1, -1, -1}; edgeDetect3[3][3] = {-5, 0, 0, 0, 0, 0, 0, 0, 5}; edgeDetect4[3][3] = {-1, -1, -1, 0, 0, 0, 1, 1, 1}; edgeDetect5[3][3] = {-1, -1, -1, 2, 2, 2, -1, -1, -1}; edgeDetect6[3][3] = {-5, -5, -5, -5, 39, -5, -5, -5, -5}; sobelHorizontal[3][3] = {1, 2, 1, 0, 0, 0, -1, -2, -1 }; sobelVertical[3][3] = {1, 0, -1, 2, 0, -2, 1, 0, -1 }; previtHorizontal[3][3] = {1, 1, 1, 0, 0, 0, -1, -1, -1 }; previtVertical[3][3] = {1, 0, -1, 1, 0, -1, 1, 0, -1}; boxBlur[3][3] = {0.111f, 0.111f, 0.111f, 0.111f, 0.111f, 0.111f, 0.111f, 0.111f, 0.111f}; triangleBlur[3][3] = { 0.0625, 0.125, 0.0625, 0.125, 0.25, 0.125, 0.0625, 0.125, 0.0625};
最后但并非最不重要的是,能够将卷积图像显示为灰度结果。 为了将过滤后的图像显示为灰度,我们只需在 Convolve
函数的底部添加几行代码
//return new pixel value if (bGrayscale) { int grayscale=0.299*rSum + 0.587*gSum + 0.114*bSum; rSum=grayscale; gSum=grayscale; bSum=grayscale; } clrReturn = RGB(rSum,gSum,bSum);
这意味着整个 Convolve
函数现在看起来像
COLORREF CImageConvolutionView::Convolve(CDC* pDC, int sourcex, int sourcey, float kernel[3][3], int nBias,BOOL bGrayscale) { float rSum = 0, gSum = 0, bSum = 0, kSum = 0; COLORREF clrReturn = RGB(0,0,0); for (int i=0; i <= 2; i++)//loop through rows { for (int j=0; j <= 2; j++)//loop through columns { //get pixel near source pixel /* if x,y is source pixel then we loop through and get pixels at coordinates: x-1,y-1 x-1,y x-1,y+1 x,y-1 x,y x,y+1 x+1,y-1 x+1,y x+1,y+1 */ COLORREF tmpPixel = pDC->GetPixel(sourcex+(i-(2>>1)), sourcey+(j-(2>>1))); //get kernel value float fKernel = kernel[i][j]; //multiply each channel by kernel value, and add to sum //notice how each channel is treated separately rSum += (GetRValue(tmpPixel)*fKernel); gSum += (GetGValue(tmpPixel)*fKernel); bSum += (GetBValue(tmpPixel)*fKernel); //add the kernel value to the kernel sum kSum += fKernel; } } //if kernel sum is less than 0, reset to 1 to avoid divide by zero if (kSum <= 0) kSum = 1; //divide each channel by kernel sum rSum/=kSum; gSum/=kSum; bSum/=kSum; //add bias if desired rSum += nBias; gSum += nBias; bSum += nBias; //prevent channel overflow by clamping to 0..255 if (rSum > 255) rSum = 255; else if (rSum < 0) rSum = 0; if (gSum > 255) gSum = 255; else if (gSum < 0) gSum = 0; if (bSum > 255) bSum = 255; else if (bSum < 0) bSum = 0; //return new pixel value if (bGrayscale) { int grayscale=0.299*rSum + 0.587*gSum + 0.114*bSum; rSum=grayscale; gSum=grayscale; bSum=grayscale; } clrReturn = RGB(rSum,gSum,bSum); return clrReturn; }
最后但并非最不重要的是,我做了一些调整,以使程序从资源 (IDB_BITMAP1
) 加载默认图像。 然后,我添加了卷积此默认图像的功能。 该程序仍然可以从文件加载图像,唯一的区别是它现在会在启动时显示默认图像。
请注意,这篇文章绝不是快速处理像素的示例。 它仅仅是为了展示如何可以在图像上进行卷积。 如果您想要更高级的图像处理器,请随时给我发送主题为“WANT CODE:ImageEdit Please”的电子邮件。 那是我做的一个未发布的图像处理器,尽管由于时间不足,部分功能尚未实现,但它包含更多的功能,使用 CxImage 库作为其读取和保存图像的基础。