尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

BMP读取到频域滤波:C++数字图像处理实验全流程解析

BMP读取到频域滤波:C++数字图像处理实验全流程解析 简介数字图像处理实验报告全C面向电子信息科学与技术专业学生及数字图像处理初学者完整收录VC6.0环境下BMP位图读取与显示以及实验二至实验八的图像基本运算、代数逻辑运算、几何运算、直方图均衡化、平滑与锐化、伪彩色处理、傅里叶变换与低通/高通滤波等全套实验内容。文档为1个doc文件压缩包3.12MB源自昆明理工大学理学院电子信息科学与技术专业实验安排系统每项实验均包含实验目的、原理讲解、C代码实现和结果分析并配有界面截图与常见问题说明可直接用于算法对比和实验报告模板。目前已有698人学习下载适合正在修读数字图像处理课程、需要完整代码参考或报告格式范例的读者。读者可由此系统掌握BMP文件头/位图信息结构、各类图像处理算法的编程思路与参数调整方法为后续科研或工程实践打下扎实基础。1. 真正的第一道坎BMP 文件结构没啃干净后续算法再对也是白搭很多数字图像处理实验课的第一道坎不是傅里叶变换公式也不是直方图均衡推导而是 BMP 位图怎么读进内存。第一次打开 VC 6.0 建好 MFC 工程照着代码敲到 ReadFile发现bmfh.bfType ! 0x4d42这个判断总是过不去或者图像显示出来全是花屏十有八九是对 DIB 结构理解不到位文件头、信息头、调色板、像素数据四段的边界没分清。这份实验报告把 BMP 读取到点运算、几何变换、直方图、空域滤波、频域滤波整套链路都走了一遍虽然代码带明显的课程设计痕迹但骨架是干净的。适合正在写数字图像处理实验报告、或者想找一套能在 VC6/MFC 下直接跑的 BMP 处理框架的 C 学习者。这里不重复教材里的公式推导只讲怎么把公式落成指针运算和字节拷贝。2. Bitmap 读取链路从 bfType 校验到 StretchDIBits 的 C 实现BMP 文件在 Windows 下属于设备无关位图DIB自带颜色信息可以在不同设备间保持一致的显示效果。DIB 在文件里按文件头、位图信息头、调色板、像素数据四段排列。实验里定义的那两个结构体 BITMAPFILEHEADER 和 BITMAPINFOHEADER 是整个链路的地基读错一个字段后面的算法全部白算。2.1 BITMAPFILEHEADER 与 BITMAPINFOHEADER 的字段边界文件头固定 14 字节信息头固定 40 字节。表里这几个字段是写读取代码时真正要用的字段说明读取代码中的用途bfType固定0x4D42即字符 BM文件合法性校验不是这个值直接拒绝bfSize整个文件的字节数可用来做文件长度预检bfOffBits像素数据相对文件头的偏移决定信息头调色板要读多长biWidth/biHeight像素宽高高度可为负数倒置位图遍历像素的行列范围biBitCount每像素位数1/4/8/16/24/32决定调色板项数和每行字节数biCompression压缩类型常见为 0BI_RGB非 0 时不能按裸数据直接读biSizeImage像素数据字节数可为 0为 0 时需要手动按行对齐计算biClrUsed调色板实际使用的颜色数为 0 时按位深推导注意bfOffBits才是像素数据的起点不是固定值。信息头之后可能跟调色板也可能没有直接拿sizeof(BITMAPFILEHEADER) sizeof(BITMAPINFOHEADER)去定位像素数据在 8 位图或者带自定义调色板的文件上会读到调色板区域显示出来就是一片乱色。2.2 每行字节数必须按 4 字节对齐BMP 规定每行像素数据从 4 字节边界开始行尾不足 4 字节的部分用 0 补齐。24 位图宽度不是 4 的倍数时尤其明显比如宽 30 像素每行实际占用 90 字节补齐后是 92 字节。如果按width * 3去定位下一行行尾的两个填充字节会被当成像素读出来图像整体斜切。行字节数计算公式LONG rowBytes ((m_DibHead-biWidth * m_DibHead-biBitCount 31) / 32) * 4;当biSizeImage 0时像素数据总字节数就是rowBytes * biHeight然后按这个值去 new 缓冲区、去 Read。实验报告里的SetPaletteSize做的就是这个事它把biWidth * biBitCount除以 32 再乘 4等价于这个公式。2.3 ReadFile 与 SetPaletteSize 的配合读取流程的核心是先读文件头做校验再按bfOffBits确定信息区长度最后单独读像素。代码在 MFC 的视图类里写成这样BOOL CImageView::ReadFile(CFile *pFile) { BITMAPFILEHEADER bmfh; // 先读固定 14 字节校验文件类型 int nCount pFile-Read(bmfh, sizeof(BITMAPFILEHEADER)); if (nCount ! sizeof(BITMAPFILEHEADER) || bmfh.bfType ! 0x4D42) return FALSE; // 信息头 调色板的总长度 int nInfoSize bmfh.bfOffBits - sizeof(BITMAPFILEHEADER); m_DibHead (LPBITMAPINFOHEADER) new char[nInfoSize]; pFile-Read(m_DibHead, nInfoSize); // 根据位深计算调色板项数再按行对齐规则算出像素缓冲区大小 SetPaletteSize(m_DibHead-biBitCount); GetPalette(); // m_ImageSize 在 SetPaletteSize 中已被正确赋值 m_Image (LPBYTE) new char[m_ImageSize]; pFile-Read(m_Image, m_ImageSize); return TRUE; }这段代码有两点值得注意。第一nInfoSize不是固定值它取决于bfOffBits所以调色板长度变化时不会读错位置。第二SetPaletteSize里同时完成了大小计算和调色板项数推导把这两件事放在一起是因为 8 位图的biSizeImage经常是 0必须通过位深和宽高反推。调色板项数规则1 位对应 2 项4 位对应 16 项8 位对应 256 项16/24/32 位没有调色板。这个推导写进switch里即可不用信文件里的biClrUsed它为 0 的情况太多了。2.4 DibToDC把内存里的 DIB 画到视图上像素数据读进m_Image后要显示出来有两类 API 可用。SetDIBitsToDevice做 1:1 像素拷贝不做缩放StretchDIBits按目标矩形缩放显示。实验里要在 OnDraw 里用m_x放大缩小图像所以用StretchDIBitsBOOL CImageView::DibToDC(CDC *pDC, CSize size) { if (m_DibHead NULL) return FALSE; // 把逻辑调色板选入 DC否则 8 位图颜色会不对 if (m_hPalette ! NULL) { ::SelectPalette(pDC-GetSafeHdc(), m_hPalette, TRUE); } pDC-SetStretchBltMode(COLORONCOLOR); ::StretchDIBits( pDC-GetSafeHdc(), // 目标 DC 0, 0, size.cx, size.cy, // 目标矩形可放大缩小 0, 0, m_DibHead-biWidth, m_DibHead-biHeight, // 源矩形 m_Image, // 像素数据 (LPBITMAPINFO)m_DibHead, // 信息头 调色板 DIB_RGB_COLORS, SRCCOPY); // 光栅操作码 return TRUE; }参数size.cx和size.cy是目标显示尺寸源矩形用biWidth和biHeight时缩放逻辑由 GDI 内部完成。COLORONCOLOR是缩放模式它直接从源像素中取点映射到目标速度快但高频细节会丢放大看边缘锯齿。有个常见的错误做法是先把 DIB 转成 CBitmapDDB再 BitBlt显示倒是正常但 DDB 跟设备绑定位深和调色板信息都会丢后面做逐像素运算时还得重新读文件。直接保留 DIB 内存后续算法都在m_Image字节数组上操作这是这份报告里最实用的设计也符合实验二到实验八对像素数据的依赖。3. 点运算与直方图均衡灰度映射在单字节缓冲区上的实现点运算的本质是灰度到灰度的映射g(x, y) T[f(x, y)]。它只改变像素值不改变像素位置所以实现上就是一个逐像素循环套一个映射函数。实验二里做的线性、分段线性、反色、对数、幂次、二值化全部属于这一类。3.1 线性变换与 clamp 逻辑线性变换s a * r b是灰度拉伸的核心。参数 a 控制斜率b 控制截距。用浮点计算后必须做边界截断否则强光区域会溢出成噪点void ApplyLinear(BYTE *pixels, LONG count, float a, float b) { for (LONG i 0; i count; i) { float s a * pixels[i] b; if (s 0.0f) s 0.0f; if (s 255.0f) s 255.0f; pixels[i] (BYTE)s; } }参数 a 和 b 对图像的影响可以归纳成一张表实验报告里可以直接抄这个结论ab效果0 a 10输出灰度范围压缩整体变暗a 10原图不变a 10灰度范围扩展对比度增强a -1255反色越亮的像素输出越暗任意 0整体亮度抬升注意截断实验代码里a 80其实是把 a 当作偏移量的写法这属于命名不规范但效果上等价于一个斜率接近 1 的平移变换。真要做对比度拉伸斜率应该取大于 1 的值比如s 1.5 * r 10。3.2 非线性映射的取值细节反色实现最简单一个表达式搞定pixels[i] 255 - pixels[i];对数变换要注意输入加 1 避免 log(0)公式是s c * log10(r 1)。c 的取值决定输出范围想让 0~255 的输入映射到 0~255 的输出c 应取255 / log10(256)约等于 106。实验里取 100 基本接近但最好按这个方式算出来再写死。幂次变换s c * pow(r, gamma)里r 是 0~255 的整数直接做浮点幂运算时小灰度值的变换曲线会很难看而且 c 和 gamma 必须配合调整否则输出很容易整体过曝。常见做法是先把 r 归一化到 0~1做完幂次再乘 255这样 c 就不需要了pixels[i] (BYTE)(255.0 * pow(pixels[i] / 255.0, gamma));3.3 直方图均衡的 LUT 生成直方图均衡不是逐像素直接算而是先用一次全图扫描统计灰度分布再生成映射表 LUT最后用 LUT 查表替换。两次遍历比把累积分布计算写进像素循环里干净也更便于观察中间结果void EqualizeHistogram(BYTE *pixels, LONG count) { int hist[256] {0}; // 第一遍统计每个灰度级出现次数 for (LONG i 0; i count; i) hist[pixels[i]]; // 计算累积分布函数 CDF生成灰度映射表 BYTE lut[256]; double cdf 0.0; LONG total 0; for (int i 0; i 256; i) total hist[i]; for (int g 0; g 256; g) { cdf (double)hist[g]; lut[g] (BYTE)(255.0 * cdf / total); } // 第二遍查表替换 for (LONG i 0; i count; i) pixels[i] lut[pixels[i]]; }CDF 的最后一个值一定是255.0 * total / total也就是 255所以映射表天然把最亮灰度拉到 255。低频灰度级会被拉开间距高频灰度级被压缩这就是均衡后直方图变得平铺的原因。3.4 灰度图实验最容易踩的行对齐坑点运算代码里用的是单字节索引pixels[i]它能成立的隐藏条件是图像必须是 8 位灰度图且每行没有填充字节。如果直接把 24 位图丢进来count实际是width * height * 3BGR 三通道被当成三个独立灰度处理颜色会完全错乱。同时 8 位图在宽度不是 4 的倍数时有行填充直接i*wj定位会错位。正确做法是统一用行字节数参与定位LONG rowBytes ((biWidth * biBitCount 31) / 32) * 4; #define PIXEL(y, x) m_Image[(y) * rowBytes (x)]实验二到实验五的所有点运算都可以基于这个宏改写算法循环不变只换像素访问方式。这也是为什么实验一的数据结构设计重要m_Image保持原始字节布局算法层再来适配行对齐。4. 空域邻域处理与傅里叶变换平滑、锐化、低通与高通滤波的落地实验六和实验八是两类完全不同的处理路径平滑锐化在空域做卷积傅里叶变换把图像搬到频域做掩码乘法。两者的共同点是都依赖邻域概念只是邻域的定义方式不同。4.1 卷积模板与边界策略空域滤波是模板与图像局部区域的加权求和。常用的几个 3×3 模板如下名称模板作用均值平滑[1 1 1; 1 1 1; 1 1 1] / 9去掉随机噪声代价是变模糊Sobel 水平梯度[-1 0 1; -2 0 2; -1 0 1]提取竖直边缘配合原图可锐化Laplacian[0 -1 0; -1 4 -1; 0 -1 0]提取二阶边缘直接叠加到原图可增强细节边界像素没有完整邻域常见的处理是补零或者复制边缘。补零会在图像四周生成黑边复制边缘则不会引入突兀的灰度跳变实验代码里推荐复制边缘。4.2 均值滤波与锐化的 C 写法均值滤波的朴素实现void BoxSmooth(BYTE *src, BYTE *dst, LONG w, LONG h) { for (LONG y 0; y h; y) { for (LONG x 0; x w; x) { int sum 0; for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { int nx x dx; int ny y dy; // 边界用边缘复制避免补零造成的黑边 if (nx 0) nx 0; if (nx w) nx w - 1; if (ny 0) ny 0; if (ny h) ny h - 1; sum src[ny * w nx]; } } dst[y * w x] (BYTE)(sum / 9); } } }注意卷积操作需要源缓冲区和目标缓冲区分离。如果直接在src上原地累加后面的像素会用到已经被覆盖的值滤波结果会向左下角漂移。中值滤波跟均值不同它不是线性卷积而是取窗口内 9 个像素排序后的中间值对椒盐噪声的去除效果远好于均值滤波代价是不能用模板系数表示得写一个排序逻辑。锐化的写法是锐化结果 原图 k * Laplacian(原图)k 通常取 1// laplacian 是模板卷积后的结果可正可负 dst[y * w x] src[y * w x] k * lap;k 大于 1 时边缘增强更明显但噪声也会被放大。实验时可以先从 0.5 起步逐次加 0.25 看效果。4.3 二维 DFT 与中心化实验八要做频谱分析但二维 DFT 的复杂度是 O(N^4)直接按定义写完整循环256×256 的图像在现代机器上也要跑十几秒。实验报告里只要求写出变换逻辑和滤波效果所以代码按定义实现即可但要提示性能边界// 按 DFT 定义实现N 取 64 或 128256 会非常慢 for (int u 0; u N; u) { for (int v 0; v N; v) { double re 0.0, im 0.0; for (int x 0; x N; x) { for (int y 0; y N; y) { double ang -2.0 * PI * (u * x v * y) / N; re f[x * N y] * cos(ang); im f[x * N y] * sin(ang); } } F[u * N v] re * re im * im; // 频谱能量 } }中心化是把坐标平移 N/2让零频落在频谱中心。不中心化时低通滤波器掩码要画在角落处理起来很别扭中心化后掩码以中心为圆心半径的物理意义直接对应截止频率。4.4 频域低通与高通滤波的掩码参数频域滤波流程是DFT 变换、中心化、频谱乘掩码、反变换、取实部。掩码生成时的参数决定滤波效果滤波器掩码公式特点理想低通D D0为 1否则 0边缘振铃最严重巴特沃斯低通1 / (1 (D/D0)^(2n))阶数 n 越大越接近理想低通高斯低通exp(-D^2 / (2*D0^2))无振铃平滑过渡高通滤波可以直接用1 - 低通掩码也可以用M(u,v) * (1 - M_lp(u,v))的组合得到带通效果。实验报告里低通滤波后图像变模糊是正常的因为高频细节被抑制高通滤波保留边缘均匀区域变成灰色。空域滤波和频域滤波是等价的卷积定理保证了这一点但实现的坑完全不同空域要注意边界和模板系数频域要注意复数运算和中心化。两类都做一遍才算把图像滤波的基本路径走通。5. 把实验代码改造成能复用的图像处理小工具前八个实验的功能都是独立的菜单消息函数每个函数里都有一段几乎一样的双层循环。复制粘贴多了以后改一个算法的边界条件要同步改七八处。这一步把公共逻辑抽出来实验代码就会从交作业变成工具。5.1 用函数指针抽离像素级操作实验二的线性变换、反色、对数、幂次、二值化区别只在灰度映射函数循环结构完全一样。可以定义一个统一的函数指针类型typedef BYTE (*PointOp)(BYTE); BYTE Invert(BYTE v) { return 255 - v; } BYTE GammaCorrect(BYTE v) { return (BYTE)(255.0 * pow(v / 255.0, 0.4)); } void ApplyPointOp(BYTE *pixels, LONG count, PointOp op) { for (LONG i 0; i count; i) pixels[i] op(pixels[i]); }新增一种点运算时只写映射函数本身不再复制整个循环。ApplyPointOp内部对每个像素调用op函数指针的调用开销在图像处理场景下可以忽略。同样的思路可以推广到 3×3 邻域操作把卷积模板作为参数传进通用卷积函数模板不同就切换滤波行为。5.2 内存分配标记必须和释放方式对应实验一的Clear()函数里用m_nBmpallocate和m_nImageallocate两个枚举标记内存来源分别对应new char[]、GlobalAlloc和内存映射文件三种情况。这个标记设计容易被忽略但实际很关键分配来源标记值释放方式new char[]crtallocatedelete []GlobalAllocheapallocateGlobalUnlockGlobalFree内存映射文件无标记UnmapViewOfFileCloseHandle不区分这三种情况统一走delete []在内存映射文件上会直接崩溃。Clear()里每次打开新图前先调一次能避免重复 Open 时的内存泄漏。5.3 建议先把 24 位图统一转成 8 位灰度点运算和直方图均衡的所有代码都假设m_Image是单字节灰度数据。24 位图直接喂进去会按 BGR 三通道独立处理结果完全不可用。在做实验二之前先把图像统一转成 8 位灰度转换公式用 BT.601 系数BYTE gray (BYTE)(0.299 * r 0.587 * g 0.114 * b);转换后biBitCount改成 8重建调色板更新rowBytes。这样实验二到实验六的代码全部可以沿用现有的单字节循环直方图均衡、均值滤波、Sobel 锐化都不需要再做多通道适配。把模板写成const int kernel[3][3]放在文件顶部后续换不同尺寸的 Sobel 变体时只需要改数组内容卷积函数本身一行不用动。本文还有配套的精品资源点击获取
返回列表