feat: 简化版网格划分算法,适合课堂讲解

算法流程(仅划线,不分割):
1. 彩色图转灰度图
2. 横/纵轴投影:每列/行灰度值求和
3. 阈值 X = (max-min) × 10%
4. 曲线减去 X,正=斑点,负=空隙
5. 过零点配对,中点即划线位置

与原版误差为0,代码带详细中文注释。
This commit is contained in:
2026-05-06 20:21:19 +08:00
parent b8a8ff2bc6
commit ad8e5041f2
2 changed files with 189 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

+189
View File
@@ -0,0 +1,189 @@
"""
cDNA微阵列图像处理 —— 简化版网格划分
======================================
算法步骤(适合课堂讲解):
1. 彩色图 → 灰度图
2. 横轴投影:对每一列的所有像素灰度值求和 → 得到一条曲线
纵轴投影:对每一行的所有像素灰度值求和 → 得到一条曲线
3. 在曲线上,求出 max 和 min,阈值 X = (max - min) × 10%
4. 曲线上每个值都减去 X
5. 减完之后:
- 大于 0 的地方 = 斑点区域
- 小于 0 的地方 = 斑点之间的空隙
- 等于 0 的地方 = 斑点与空隙的分界线(过零点)
6. 配对相邻的过零点(离开斑点 + 进入下一个斑点),
中点就是空隙的中心 = 划线位置
"""
import os
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from skimage import color
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
# 路径设置
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(SCRIPT_DIR)
DATA_DIR = os.path.join(BASE_DIR, 'cDNA图像处理实例', '数据', 'cDNA')
OUTPUT_DIR = os.path.join(BASE_DIR, 'results_simple')
def draw_grid_lines(gray: np.ndarray, pct: float = 0.10):
"""
核心算法:检测网格分割线。
原理:
灰度图的每一列/行,属于斑点的像素灰度值高,属于背景的灰度值低。
把每列/行的灰度值加起来,就能得到一条曲线:
——曲线凸起的地方 = 斑点所在位置
——曲线凹陷的地方 = 斑点之间的空隙
去掉一个阈值后,曲线在空隙处会变成负数,
过零点的位置就是斑点和空隙的分界线,
两个分界线中点就是划线位置。
参数:
gray: 灰度图 (高×宽)
pct: 阈值百分比,默认10%
返回:
(纵线x坐标列表, 横线y坐标列表)
"""
H, W = gray.shape
# ================================================================
# 步骤1:横轴投影 —— 统计每一列的灰度总和
# ================================================================
# gray 是一个 H×W 的二维数组,gray[行, 列] 是某个像素的灰度值
# np.sum(gray, axis=0) 沿行方向求和 → 得到长度为 W 的一维数组
# 含义:每一列上所有像素的灰度值加起来
# 斑点所在列 → 亮像素多 → 和较大(曲线凸起)
# 空隙所在列 → 暗像素多 → 和较小(曲线凹陷)
col_profile = np.sum(gray, axis=0).astype(float)
# ================================================================
# 步骤2:纵轴投影 —— 统计每一行的灰度总和
# ================================================================
# np.sum(gray, axis=1) 沿列方向求和 → 得到长度为 H 的一维数组
# 含义:每一行上所有像素的灰度值加起来
row_profile = np.sum(gray, axis=1).astype(float)
# ================================================================
# 步骤3:计算阈值 X = (max - min) × 10%
# ================================================================
# max-min 是曲线的"振幅",取10%作为阈值
# 大于这个阈值的才是真正的斑点信号,小于的是噪声
col_T = (np.max(col_profile) - np.min(col_profile)) * pct
row_T = (np.max(row_profile) - np.min(row_profile)) * pct
# ================================================================
# 步骤4:曲线上所有值减去阈值
# ================================================================
# 减去阈值后:
# 原本在空隙处的值(本来就小)→ 变成负数
# 原本在斑点处的值(本来就大)→ 仍然为正数
# 等于0的位置 = 斑点与空隙的分界线 = 过零点
col_shifted = col_profile - col_T
row_shifted = row_profile - row_T
# ================================================================
# 步骤5:找过零点,两两配对,中间点即划线位置
# ================================================================
def find_gap_lines(prof_shifted: np.ndarray) -> np.ndarray:
"""
在减去阈值后的投影曲线上,找到每两个斑点之间的空隙中线。
图解说明(设阈值为30):
原始曲线: ___/‾‾‾\___/‾‾‾‾\___/‾‾‾\___
数值: 10 50 60 55 8 45 70 48 5 55 50 12
减阈值后: -20 20 30 25 -22 15 40 18 -25 25 20 -18
正负: ─ + + + ─ + + + ─ + + ─
↑ ↑ ↑ ↑
过零点 过零点 过零点 过零点
过零点之间的区域:
第一个 + 区域 = 一个斑点 (过零点1 → 过零点2)
第一个 - 区域 = 空隙 (过零点2 → 过零点3) ← 我们要的!
第二个 + 区域 = 下一个斑点 (过零点3 → 过零点4)
划线位置 = 空隙(负数区域)的中点
= (离开斑点的过零点 + 进入下一个斑点的过零点) / 2
"""
# 判断每个位置是正(斑点)还是负(空隙)
is_positive = prof_shifted > 0
# 收集所有符号变化的位置(过零点)
crossings = []
for i in range(1, len(is_positive)):
# 如果当前和前一个正负不同 → 发生了跨越零点
if is_positive[i] != is_positive[i - 1]:
crossings.append(i)
if len(crossings) < 2:
return np.array([])
# 过零点交替:正→负,负→正,正→负,负→正……
# 我们要的是「空隙区域」的中点 → 配对「离开斑点 → 进入下一个斑点」
# 即:从第一个"正→负"开始配对
# 如果开头就是负值(图像左侧是空隙),第一个过零点是"负→正",
# 跳过它,从下一个"正→负"开始
start = 1 if not is_positive[0] else 0
lines = []
for k in range(start, len(crossings) - 1, 2):
if k + 1 < len(crossings):
# crossings[k]: 正→负(离开斑点)
# crossings[k+1]: 负→正(进入下一个斑点)
# 中点 = 空隙中央 = 划线位置
mid = int((crossings[k] + crossings[k + 1]) / 2)
lines.append(mid)
return np.array(lines)
x_lines = find_gap_lines(col_shifted)
y_lines = find_gap_lines(row_shifted)
return x_lines, y_lines
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
# ---- 读取图像并转为灰度图 ----
img = np.array(Image.open(os.path.join(DATA_DIR, 'cDNA.png')))
# 原图是 RGBA(红绿蓝+透明通道),取前三个通道转为灰度
gray = (color.rgb2gray(img[:, :, :3]) * 255).astype(np.uint8)
# ---- 执行网格检测 ----
x_lines, y_lines = draw_grid_lines(gray)
print(f"检测到 {len(x_lines)} 条纵线, {len(y_lines)} 条横线")
# ---- 在原图上划线并保存 ----
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(gray, cmap='gray')
# 画纵向分割线(竖线)
for x in x_lines:
ax.axvline(x=x, color='lime', linewidth=0.5)
# 画横向分割线(横线)
for y in y_lines:
ax.axhline(y=y, color='lime', linewidth=0.5)
ax.set_title(f'cDNA微阵列网格划分 ({len(x_lines)}×{len(y_lines)})', fontsize=14)
ax.axis('off')
out_path = os.path.join(OUTPUT_DIR, 'gridding_simple.png')
fig.savefig(out_path, dpi=150, bbox_inches='tight')
plt.close(fig)
print(f"保存: {out_path}")
if __name__ == '__main__':
main()