diff --git a/results_simple/gridding_simple.png b/results_simple/gridding_simple.png new file mode 100644 index 0000000..3493df0 Binary files /dev/null and b/results_simple/gridding_simple.png differ diff --git a/src/cDNA_gridding_simple.py b/src/cDNA_gridding_simple.py new file mode 100644 index 0000000..8cbd08d --- /dev/null +++ b/src/cDNA_gridding_simple.py @@ -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()