一、图像处理设计目标
1.1 课题与技术指标
课题: 利用图像处理技术设计细胞识别程序。
设计指标:
- 实验 VS2019 MFC 开发平台
- 待识别图像为 24bit 的真彩色细胞图像进行处理
- 要求识别出细胞,并且保证准确度情况下,统计出细胞的个数和大小
1.2 任务点
- 完成图像标注:添加
cdib.h、cdib.cpp、添加函数、添加消息响应函数、添加菜单、打开图像 - 完成细胞的判别:持续判断 Maybe 点邻域有无 Mark 点、Sobel 计算边缘、删除孤立边缘、生成黑白图像
- 细胞收缩:利用
stack、vector获取孔洞坐标、填充孔洞、Edge 处置 0、4 方向、8 方向交替生成边缘 - 细胞中心点信息获取:递归算法、判别局部是否全是边缘、储存全部中心位置
- 错误信息的删除:计算中心点均值、半径、去除半径过小的点、包含圆的剔除、相交过大圆的剔除、信息统计与显示
二、设计思路
图像处理之细胞识别的主要工作就是识别细胞数目以及选择出指定大小细胞。设计思路如下图所示。同时我们要去除不符合要求的中心点,来确定最终的细胞数目,去除太小和太大的细胞1,并结合实际来修改各种参数,设计合理。
图1 系统设计思路
(流程图:打开图像 → HSI 转换 → 细胞标记 → 二值化 → 填洞 → 收缩 → 中心点获取 → 错误删除 → 统计输出)
三、创建工程及算法分析
3.1 预备工作及工程创建
(1)VS2019 安装 MFC 与 C++ 开发环境
(2)创建 MFC 工程,配置 MFC 应用程序向导

图2 创建工程 / 图3 MFC 配置
3.2 打开 BMP 图片与显示 HSI 值
代码分析
(1)在 OnDraw 函数实现显示图片;在 messages 中添加 display() 实现打开一幅图像的功能。在 LoadBmp() 函数中利用 fopen 实现自动打开图片,利用 fread 读取图片信息。
(2)通过:查看 → 建立类向导 → 添加 OnMouseMove() 函数,添加代码实现获取所要信息;实现鼠标移动显示 HSI。
(3)通过函数 RgbtoHsi 实现 RGB 向 HSI 的转化。
RGB → HSI 转换原理:
RGB 向 HSI 模型的转换是由一个基于笛卡尔直角坐标系的单位立方体向基于圆柱极坐标的双锥体的转换。基本要求是将 RGB 中的亮度因素分离,将色度分解为色调和饱和度,并用角向量表示色调2。如果直接对 R、G、B 处理,其处理过程中很可能会引起三个量不同程度的变化,这样就会产生色差问题,甚至带来颜色上的失真。HSI 模型的出现,使得在保持色彩无失真的情况下实现图像处理成为可能。
图4 HSI 显示效果
主要代码
// HSI 主要代码示意
void RgbtoHsi(RGB* rgb, HSI* hsi) {
// 将 RGB 转换为 HSI 色彩空间
// H: 色调, S: 饱和度, I: 亮度
// 分离亮度因素,保持色彩无失真
}
图5 HSI 主要代码
3.3 Mark - 细胞标记
代码分析
确定 Mark 与 mbMark 是通过半径来的,通过计算图像上的某个像素点与细胞内部像素点的欧几里得距离与归一化门限值分别为 0.09 和 0.15 比较:
r < 0.09→ Mark(红色,细胞内部)0.09 < r < 0.15→ mbMark(蓝色,可能边界)
OnMouseMove 函数里调用 RgbtoHsi(&rgb, &Hsi) 函数,可以在屏幕上显示鼠标所指点的坐标以及 RGB、HSI 和灰度值,通过 HSI 的可以选取合适的阈值来找到细胞以及边界3。
边缘提取的方法:
- 先滤波,去除噪声的影响,防止细胞内的噪声的影响
- 之后用 Sobel 算子 求出边界
- 之后画一个
5×5的矩形,如果没有遇到边界,是噪声,去除 - 边界点是绿色。细胞边界分别用红色、暗红、蓝色和绿色标记出来
主要代码
// Mark 主要代码示意
// 根据 HSI 距离判断像素属性
// Sobel 边缘检测 + 噪声去除
// 多色标记不同区域


图6 Mark 主要代码 / 图7 标记后情况
3.4 OnTwoValue() - 二值化
代码分析
背景灰度值为 128,细胞灰度值为 240,边缘为 255。二值化便于轮廓提取。
均值滤波的基本原理是用均值代替原图像中的各个像素值,即对待处理的当前像素点 (x, y),选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再把该均值赋予当前像素点 (x, y),作为处理后图像在该点上的灰度 g(x, y),即 g(x, y) = 1/m ∑f(x, y);m 为该模板中包含当前像素在内的像素总个数。
状态标记说明
| 标记值 | 含义 |
|---|---|
0x7X | edge 边界(表示边界没 Mark,存在为四边) |
0x8X | Mark -- not edge(是细胞) |
0xfX | Mark -- edge |
0xX1 | visited(已访问) |
0x2 | CENTERED(中心点) |
m_vCenterPoints.at(j)—— 存储最后中心点的地方
主要代码
// 二值化主要代码示意
// 灰度阈值分割 + 均值滤波
// 状态标记管理

图8 二值化主要代码 / 图9 二值化现象
3.5 OnFillHoles() - 填洞
代码分析
在阈值处理时,如果像素在阈值范围内,则像素将被标志。孔洞填充将先统计所有连通的非标志区域面积,总会有一个或者几个面积特别大的区域,其它的都是面积相对较小的区域。较小或者很小的往往就是系统所要填充的孔洞了。
填洞方法:
- 用栈(
stack)、向量(vector)处理 - 从四个方向遍历,没访问过的黑点进栈,直到边和走完
- 之后读值到
xt, yt并去值,v放的是点的位置 - 没有碰到边界和小于 100,则填洞,即令洞的各点值为 Mark 值
- 将细胞中灰度值为 128 的部分的灰度值设置为 2404
填完细胞中的洞(即为 0 的值)后,处理非 Mark 的边界值(目的是打开边缘),设为 0。
主要代码
// 填洞主要代码示意
// stack + vector 获取孔洞坐标
// 连通区域面积统计
// 小区域填充为 Mark 值

图10 填洞主要代码 / 图11 填洞现象
3.6 shrink() - 收缩
代码分析
收缩的目的是为了方便计数。扫描图像,对图像进行预先的 3 次腐蚀,判断所生成边界点,然后根据原理判定是否标注该点,存放所标志的中心点,便于统计细胞个数及计算细胞半径。
算法:
- 由 Mark 生成边界,我们有四邻域生成边界和八邻域生成边界
- 判断该点是否为 Mark 点,如果是 Mark 点的话,判断
i, j是否是我们选取图片的边界,如果是的话,将该点变成边缘点 - 否则判断它的上下左右(周围八个点)是否有非 Mark 点,如果有,则将这边变成边缘点,反之不变
- 最后去掉边界则完成收缩
主要代码
// 收缩主要代码示意
// 3次腐蚀预处理
// 4/8邻域边界生成
// 边界点判定与标记
图12 收缩主要代码及现象
3.7 findcenter() - 中心点获取
代码分析
通过判断 MARK 点上下左右四个方向或者八个方向是否有非 Mark 点,如果有的话即认为是边界,将该点加上边界标志。
在收缩的过程(直到没有边界可去)中:
- 清除所有标志点
- 在不超过边下,没有被访问过的边缘,四个方向全是非 Mark 的话(收缩
k < 2时)则是孤立点 k >= 2,是中心点,保存x, y, 半径- 还有除了边界就是背景,这也要保存(就是消失前保存)
Mark(i, j): 八个方向,没有访问过的 Mark,且是边界点,则保存
SaveIT(i, j): 这点不是中心点(相当于没有访问过)则设为中心点(并保存);已经是中心点则去除标志,八方向寻找中心点
值得注意的是我们也要去除半径不大于 2 的孤立点,因为我们认为它的半径太小,是噪声。如果是半径大于 2 的孤立点,我们对他进行标记成中心点,对半径做一点补偿(pt.radius = k + pre_shrink_count + 4,4 为补偿),然后入队。合并中心点
本点是中心点,则八个方向看都不是中心点,则保存。有相连的话,多点求中心与半径(用到了 CalcCenterArea(i, j))。
CalcCenterArea(i, j): 八方向求中心点后去除标志,循环统计。半径取最大,而 x, y 则是取平均。
去除多余圆或者错误圆
- 相近圆: 圆心距离小于 10,则取半径小的中心点为中心点,取半径最大值 + 2
- 去除潜在的错误: 即半径小于 9 的去掉。但四个方向,边部补偿,防止边部的细胞被去除
多圆相交时,不相交部分小于则去除:
- 首先,相交部分先取出来保存
- 之后,单个圆内的交上了被标志(画一个方框然后统计之内的点数)
- 之后单个圆不相交的进行统计
- 不相交的占 50% 以下则清除本圆
主要代码
// 中心点获取主要代码示意
// 递归算法判别局部边缘
// 孤立点过滤与半径补偿
// 中心点合并与错误圆剔除

图13 中心点获取主要代码及现象
3.8 count() - 统计
代码分析
首先图像重新打开,然后获取细胞内部的 HSI 的最大值和最小值,并且计算细胞、平均半径、平均面积等值。
Count() 函数中首先调用了重载函数 reload(),将图片还原,随后将之前细胞识别所保留下来的信息(细胞个数),从 view.h 中提取信息,将细胞数、半径、面积显示在对话框中。
- 利用前面修正的结果,在 Count 模块进行总的计数,计算出平均半径和平均面积并输出
文件重新加载模块: 如果
noclick置为true,则进行重新加载图片


图14 重新加载图片 / 图15 最终标记现象
3.9 OnCellprgAll() - 按序执行全部
void OnCellprgAll() {
// 依次调用前面各个函数
// 实现细胞识别的所有操作步骤
}

图16 OnCellprgAll()
代码分析: 不难看出调用了前面各个函数可以依次实现细胞识别的所有操作步骤。
四、错误情况及分析
4.1 未声明全局变量信息头

图17 错误案例1
原因: 缺少必要的头文件包含或全局变量声明
修正: 在适当位置添加全局变量声明
4.2 错把指针当作 int 型

图18 错误案例2
修正: 应将 int m_pImage; 改为 BYTE* m_pImage;
4.3 软件菜单栏突然丢失

图19 错误案例3
原因: 误碰软件,把菜单移出去了
修正: 可以移动菜单栏使其恢复正常
图20 修正错误
4.4 矩形框在鼠标未按下就出现了
原因: OnMouseMove 中判断条件不对
修正: 检查鼠标按下状态标志位的逻辑判断
五、总结
本项目基于 VS2019 MFC 平台,利用图像处理技术实现了细胞识别程序。主要技术路线包括:
- HSI 色彩空间转换 —— 分离亮度与色度,保持色彩无失真处理
- Sobel 边缘检测 + 噪声过滤 —— 提取细胞边界
- 二值化与形态学处理 —— 填洞、腐蚀收缩,简化细胞轮廓
- 递归中心点检测 —— 通过迭代收缩获取细胞中心
- 后处理优化 —— 去除噪声点、合并相近圆、剔除错误检测
- 统计输出 —— 细胞个数、平均半径、平均面积
