"""
cDNA微阵列图像处理 —— 简化版
======================================

D:\ProgramData\anaconda3\envs\my_env\python.exe src/cDNA_gridding_simple.py

一、算法流程总览

  灰度图 ──→ Otsu求像素最佳阈值 T ──→ 百分比 = T/255（自适应）
     │
     ├─→ 投影/减阈值/过零点配对 ──→ 网格线
     │
     ├─→ 逐格 Otsu 分割 ──→ keep_largest_object（每格留最大块）
     │
     └─→ remove_small_objects（中位数25%以下判为噪声）──→ 统计斑点数

二、各步骤详解

  1. 彩色图 → 灰度图

  2. Otsu 自动阈值
     遍历灰度 0~255，每个候选 T 将像素分为前景(>T)和背景(≤T)，
     计算类内方差 w_bg×σ²_bg + w_fg×σ²_fg，选使方差最小的 T。

  3. 投影
     横轴：np.sum(每列) → 曲线，高点=斑点列，低点=空隙列
     纵轴：np.sum(每行) → 曲线，高点=斑点行，低点=空隙行

  4. 阈值 X = (max-min) × (T/255)

  5. 曲线减 X → 大于 0 = 斑点区域，小于 0 = 空隙
     过零点 = 斑点和空隙的分界线

  6. 过零点配对
     过零点交替：正→负（离开斑点）、负→正（进入下一斑点）
     配对「离开斑点 + 进入下一斑点」，中点 = 空隙中央 = 划线位置

  7. 逐格分割 + 后处理
     对每个格子独立做 Otsu → keep_largest_object（留最大块）
     → 全局 remove_small_objects（自动去噪）→ 统计斑点数

  8. 输出三栏图：左=网格，中=分割，右=后处理结果
"""

import os
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from skimage import color
from scipy import ndimage

# matplotlib 中文字体设置
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')


# ================================================================
# 函数1：Otsu 像素级阈值
# ================================================================
def otsu_threshold_pixels(gray: np.ndarray) -> int:
    """
    对图像像素做 Otsu 自动阈值检测。

    遍历灰度值 0~255，对每个候选 T：
      - 将像素分为两组：前景(>T) 和 背景(≤T)
      - 计算类内方差 = w_bg × σ²_bg + w_fg × σ²_fg
      - 选使类内方差最小的 T

    返回 T（0~255 整数）。
    """
    best_T = 0                 # 当前最佳阈值
    best_cost = float('inf')   # 当前最小类内方差
    total = gray.size          # 总像素数（用于算权重）

    for T in range(1, 255):
        # 按 T 分组
        bg = gray[gray <= T]   # 背景像素
        fg = gray[gray > T]    # 前景像素（斑点）
        w_bg = len(bg) / total # 背景占比
        w_fg = len(fg) / total # 前景占比

        if w_bg == 0 or w_fg == 0:
            continue           # 某组为空（T 太极端），跳过

        # 类内方差 = 加权平均方差
        # 方差小 = 组内像素灰度接近 = 分组效果好
        cost = w_bg * np.var(bg) + w_fg * np.var(fg)

        if cost < best_cost:
            best_cost = cost
            best_T = T

    return best_T


# ================================================================
# 函数2：网格划线
# ================================================================
def draw_grid_lines(gray: np.ndarray):
    """
    检测网格分割线。

    流程：
      Otsu 求自适应百分比 → 列/行投影 → 减阈值 → 过零点配对 → 空隙中点

    返回 (纵线, 横线, T, pct, 列投影, 行投影, 减阈值后的列投影, 减阈值后的行投影)
    """
    T = otsu_threshold_pixels(gray)  # 像素级最佳阈值
    pct = T / 255.0                  # 归一化为百分比
    H, W = gray.shape

    # ---- 1. 横轴投影 ----
    col_profile = np.sum(gray, axis=0).astype(float)

    # ---- 2. 纵轴投影 ----
    row_profile = np.sum(gray, axis=1).astype(float)

    # ---- 3. 投影阈值 ----
    col_T_val = (np.max(col_profile) - np.min(col_profile)) * pct
    row_T_val = (np.max(row_profile) - np.min(row_profile)) * pct

    # ---- 4. 曲线减去阈值 ----
    col_shifted = col_profile - col_T_val
    row_shifted = row_profile - row_T_val

    # ---- 5. 过零点配对 → 空隙中线 ----
    def find_gap_lines(prof_shifted: np.ndarray) -> np.ndarray:
        """
        在减去阈值后的曲线上，配对过零点，取空隙中央。

        原理图解：
        信号: ----++++----++++----++++
              ↑   ↑    ↑   ↑
          过零点配对：离开斑点 + 进入下一个斑点
          → 中点 = 空隙中央 = 划线位置
        """
        # 每个位置是正(斑点)还是负(空隙)
        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([])

        # 过零点交替：正→负(离开斑点), 负→正(进入下一斑点)
        # 要配对的是"离开斑点 → 进入下一斑点"，即空隙的两端
        # 如果信号开头是负，跳过第一个 crossing
        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, T, pct, col_profile, row_profile, col_shifted, row_shifted, col_T_val, row_T_val


# ================================================================
# 函数3：后处理（完全自动，无需人工设定阈值）
# ================================================================
def keep_largest_object(binary: np.ndarray) -> np.ndarray:
    """
    每个格子里只保留面积最大的连通域。

    ndimage.label 给每个白色连通域编号 → 算面积 → 只留最大那块。
    不需要设定任何阈值。
    """
    labeled, num = ndimage.label(binary)
    if num == 0:
        return np.zeros_like(binary)    # 全黑，直接返回
    # 统计每个连通域的像素数
    areas = [int(np.sum(labeled == i)) for i in range(1, num + 1)]
    # 找面积最大的编号
    max_idx = int(np.argmax(areas)) + 1
    return (labeled == max_idx).astype(np.uint8)

# ================================================================
# 函数4：自动去除小连通域（噪声）
# ================================================================
def remove_small_objects(binary: np.ndarray) -> np.ndarray:
    """
    自动去除小连通域（噪声）。

    统计所有连通域面积的中位数，
    小于中位数 25% 的视为噪声，直接剔除。
    换图换分辨率都自动适应，不需要手动调参。
    """
    labeled, num = ndimage.label(binary)
    if num == 0:
        return binary                  # 全黑，直接返回

    # 收集所有连通域的面积
    areas = [int(np.sum(labeled == i)) for i in range(1, num + 1)]
    median = np.median(areas)          # 面积中位数
    min_size = max(1, int(median * 0.25))  # 中位数的25%，最少1像素

    # 面积不达标的连通域整块置0
    result = binary.copy()
    for i in range(1, num + 1):
        if areas[i - 1] < min_size:
            result[labeled == i] = 0
    return result


# ================================================================
# 主流程
# ================================================================
def main():
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    # ---- 读取图像，转为灰度 ----
    img = np.array(Image.open(os.path.join(DATA_DIR, 'cDNA.png')))
    # 原图 RGBA，取前三个通道转为 0~255 灰度图
    gray = (color.rgb2gray(img[:, :, :3]) * 255).astype(np.uint8)

    # ---- 1. 网格划线 ----
    (x_lines, y_lines, T_otsu, pct,
     col_prof, row_prof, col_shifted, row_shifted,
     col_T_val, row_T_val) = draw_grid_lines(gray)
    print(f"检测到 {len(x_lines)} 条纵线, {len(y_lines)} 条横线")
    print(f"Otsu 阈值: T={T_otsu}, 自适应百分比: {pct*100:.1f}%")

    # ---- 2. 逐格分割 + 后处理 ----
    bw_full = np.zeros_like(gray)
    for i in range(len(y_lines) - 1):
        for j in range(len(x_lines) - 1):
            r1, r2 = y_lines[i], y_lines[i + 1]
            c1, c2 = x_lines[j], x_lines[j + 1]
            blk = gray[r1:r2, c1:c2]
            if blk.size == 0:
                continue
            T = otsu_threshold_pixels(blk)
            bw_blk = (blk > T).astype(np.uint8)
            bw_blk = keep_largest_object(bw_blk)
            bw_full[r1:r2, c1:c2] = bw_blk

    bw_clean = remove_small_objects(bw_full)

    # ---- 3. 统计斑点 ----
    labeled, num = ndimage.label(bw_clean)
    spot_sizes = [int(np.sum(labeled == i)) for i in range(1, num + 1)]
    valid = [s for s in spot_sizes if s >= 10]
    print(f"检测到 {len(valid)} 个斑点")

    # ---- 4. 可视化输出（每张图独立保存）----

    # 图1：网格线叠加原图
    fig1, ax1 = plt.subplots(figsize=(8, 8))
    ax1.imshow(gray, cmap='gray')
    for x in x_lines:
        ax1.axvline(x=x, color='lime', linewidth=0.5)
    for y in y_lines:
        ax1.axhline(y=y, color='lime', linewidth=0.5)
    ax1.set_title(f'网格划分 ({len(x_lines)}x{len(y_lines)})', fontsize=13)
    ax1.axis('off')
    fig1.savefig(os.path.join(OUTPUT_DIR, '01_grid_overlay.png'), dpi=150, bbox_inches='tight')
    plt.close(fig1)

    # 图2：列投影曲线（带阈值线和过零点标记）
    fig2, ax2 = plt.subplots(figsize=(10, 4))
    xs = np.arange(len(col_prof))
    ax2.plot(xs, col_prof, 'b-', linewidth=0.6, label='col profile')
    ax2.axhline(y=col_T_val, color='orange', linestyle='--', linewidth=1,
                label=f'threshold X={col_T_val:.0f}')
    ax2.plot(xs, col_shifted, 'g-', linewidth=0.6, alpha=0.5, label='after -X')
    ax2.fill_between(xs, 0, col_shifted, where=(col_shifted > 0), color='green', alpha=0.1)
    ax2.fill_between(xs, 0, col_shifted, where=(col_shifted < 0), color='red', alpha=0.1)
    zero_idx = np.where(np.diff(col_shifted > 0) != 0)[0]
    for zi in zero_idx[:50]:
        ax2.axvline(x=zi, color='purple', linewidth=0.3, alpha=0.5)
    for xl in x_lines:
        ax2.axvline(x=xl, color='red', linewidth=0.8, alpha=0.7)
    ax2.set_title('col projection', fontsize=12)
    ax2.set_xlabel('col')
    ax2.legend(fontsize=8)
    fig2.savefig(os.path.join(OUTPUT_DIR, '02_col_projection.png'), dpi=120, bbox_inches='tight')
    plt.close(fig2)

    # 图3：行投影曲线
    fig3, ax3 = plt.subplots(figsize=(10, 4))
    ys = np.arange(len(row_prof))
    ax3.plot(row_prof, ys, 'b-', linewidth=0.6, label='row profile')
    ax3.axvline(x=row_T_val, color='orange', linestyle='--', linewidth=1,
                label=f'threshold X={row_T_val:.0f}')
    ax3.plot(row_shifted, ys, 'g-', linewidth=0.6, alpha=0.5, label='after -X')
    ax3.fill_betweenx(ys, 0, row_shifted, where=(row_shifted > 0), color='green', alpha=0.1)
    ax3.fill_betweenx(ys, 0, row_shifted, where=(row_shifted < 0), color='red', alpha=0.1)
    zero_idx_r = np.where(np.diff(row_shifted > 0) != 0)[0]
    for zi in zero_idx_r[:50]:
        ax3.axhline(y=zi, color='purple', linewidth=0.3, alpha=0.5)
    for yl in y_lines:
        ax3.axhline(y=yl, color='red', linewidth=0.8, alpha=0.7)
    ax3.set_title('row projection', fontsize=12)
    ax3.set_ylabel('row')
    ax3.legend(fontsize=8)
    fig3.savefig(os.path.join(OUTPUT_DIR, '03_row_projection.png'), dpi=120, bbox_inches='tight')
    plt.close(fig3)

    # 图4：灰度直方图 + Otsu 阈值
    fig4, ax4 = plt.subplots(figsize=(8, 5))
    ax4.hist(gray.ravel(), bins=50, color='gray', edgecolor='black', linewidth=0.3)
    ax4.axvline(x=T_otsu, color='red', linestyle='--', linewidth=2,
                label=f'Otsu T={T_otsu} (pct={pct*100:.1f}%)')
    ax4.set_title('histogram + Otsu threshold', fontsize=12)
    ax4.set_xlabel('gray value')
    ax4.set_ylabel('pixel count')
    ax4.legend()
    fig4.savefig(os.path.join(OUTPUT_DIR, '04_histogram.png'), dpi=120, bbox_inches='tight')
    plt.close(fig4)

    # 图5：逐格 Otsu 分割（后处理前）
    fig5, ax5 = plt.subplots(figsize=(8, 8))
    ax5.imshow(bw_full, cmap='gray')
    ax5.set_title('per-cell Otsu (before post-processing)', fontsize=13)
    ax5.axis('off')
    fig5.savefig(os.path.join(OUTPUT_DIR, '05_segmentation_raw.png'), dpi=150, bbox_inches='tight')
    plt.close(fig5)

    # 图6：后处理结果（最终二值图）
    fig6, ax6 = plt.subplots(figsize=(8, 8))
    ax6.imshow(bw_clean, cmap='gray')
    ax6.set_title(f'post-processed ({len(valid)} spots)', fontsize=13)
    ax6.axis('off')
    fig6.savefig(os.path.join(OUTPUT_DIR, '06_post_processed.png'), dpi=150, bbox_inches='tight')
    plt.close(fig6)

    print(f"共保存 6 张图到: {OUTPUT_DIR}")


if __name__ == '__main__':
    main()
