feat: 简化版网格划分算法,适合课堂讲解
算法流程(仅划线,不分割): 1. 彩色图转灰度图 2. 横/纵轴投影:每列/行灰度值求和 3. 阈值 X = (max-min) × 10% 4. 曲线减去 X,正=斑点,负=空隙 5. 过零点配对,中点即划线位置 与原版误差为0,代码带详细中文注释。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
@@ -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()
|
||||
Reference in New Issue
Block a user