Compare commits
27 Commits
ad14755405
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e21595e72c | |||
| dd145d7651 | |||
| 8d81c20032 | |||
| 187068f6cc | |||
| a5f4275e19 | |||
| c7ba14a81a | |||
| 4dab42e932 | |||
| 06159eba19 | |||
| 8657937214 | |||
| d180ec5e56 | |||
| 1041a66270 | |||
| b07e7a1182 | |||
| 862d02dce6 | |||
| 34889647f8 | |||
| 99c9d0263a | |||
| 70790c1d3e | |||
| f95e3de5bd | |||
| dae90a8de0 | |||
| 085c27c050 | |||
| efc6704b14 | |||
| 00836cd302 | |||
| e726e62c44 | |||
| 52a6e1b244 | |||
| 8e30fd585b | |||
| bad3635f0a | |||
| 09d8b9d8fe | |||
| d273d45a5b |
@@ -15,3 +15,11 @@ Thumbs.db
|
||||
|
||||
# Obsidian
|
||||
.obsidian/
|
||||
|
||||
# Flask
|
||||
.playwright-mcp/
|
||||
|
||||
# PyInstaller 打包产物
|
||||
dist/
|
||||
build/
|
||||
*.spec
|
||||
@@ -57,4 +57,6 @@ Python:`D:\ProgramData\anaconda3\envs\my_env`
|
||||
|
||||
## 仓库
|
||||
|
||||
Gitea: `ssh://git@192.168.5.8:2222/Serendipity/cDNA-image-processing.git`
|
||||
- **首选**(局域网 SSH): `ssh://git@192.168.5.8:2222/Serendipity/cDNA-image-processing.git`
|
||||
- **备选**(外网 HTTPS): `https://lhy-git.liuhangyv.top/Serendipity/cDNA-image-processing.git`
|
||||
- SSH 连不上时自动切换到 HTTPS
|
||||
|
||||
@@ -1,25 +1,75 @@
|
||||
# cDNA 微阵列图像处理作业
|
||||
|
||||
> 刘航宇 | 河南理工大学计算机学院 | 2026
|
||||
测试
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
河南理工大学计算机学院图像处理课程作业。Python 实现 cDNA 微阵列图像的**网格划分**与**阈值分割**,配套 Web 可视化界面和 Windows 可执行文件(exe)。
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── cDNA_segmentation.py # 原版:网格划分 + 三种阈值分割 + TV去噪
|
||||
└── cDNA_gridding_simple.py # 简化版:仅网格划分,用于课堂讲解
|
||||
|
||||
results/ # 原版输出(6张PNG)
|
||||
results_simple/ # 简化版输出(网格叠加图 + 流程图)
|
||||
docs/
|
||||
└── gridding_simple_tutorial.md # 简化版逐行代码讲解教程
|
||||
cDNA微阵列图像处理作业/
|
||||
├── src/
|
||||
│ ├── cDNA_segmentation.py # 原版:网格划分 + 三种阈值分割 + TV去噪
|
||||
│ └── cDNA_gridding_simple.py # 简化版:仅网格划分,用于课堂讲解
|
||||
│
|
||||
├── web/ # Flask Web 应用
|
||||
│ ├── app.py # Flask 主程序
|
||||
│ ├── launcher.py # PyInstaller 打包入口
|
||||
│ ├── templates/index.html # 前端页面
|
||||
│ └── static/style.css # 样式文件
|
||||
│
|
||||
├── build_exe.py # PyInstaller 打包脚本
|
||||
├── cDNA_Analyzer.spec # PyInstaller spec 配置
|
||||
├── flowchart.drawio # 算法流程图(用 Draw.io 打开)
|
||||
│
|
||||
├── results/ # 原版输出图像(6张)
|
||||
├── results_simple/ # 简化版输出图像(6张)
|
||||
├── docs/ # 技术文档
|
||||
│ ├── cDNA 微阵列网格划分.md # 算法详解
|
||||
│ ├── 两版差异说明.md # 简化版 vs 原版对比
|
||||
│ └── 依赖库清单.md # Python 依赖
|
||||
│
|
||||
├── cDNA图像处理实例/ # 课程示例数据
|
||||
└── 参考资料/ # MATLAB 参考代码 + 论文 PDF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 两个Python实现
|
||||
## 快速开始
|
||||
|
||||
### 方式一:Windows 可执行文件(推荐)
|
||||
|
||||
下载 `dist/cDNA_Analyzer.exe` 并双击运行,浏览器自动打开 `http://localhost:5000`,上传图像即可处理。
|
||||
|
||||
### 方式二:Web 开发模式
|
||||
|
||||
```bash
|
||||
cd web
|
||||
python app.py
|
||||
# 浏览器自动打开 http://localhost:5000
|
||||
```
|
||||
|
||||
### 方式三:命令行脚本
|
||||
|
||||
```bash
|
||||
# 简化版(课堂主讲,约150行)
|
||||
python src/cDNA_gridding_simple.py
|
||||
# 输出:results_simple/ 下 6 张图
|
||||
|
||||
# 原版(完整实现,430行)
|
||||
python src/cDNA_segmentation.py
|
||||
# 输出:results/ 下 6 张图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 两种 Python 实现
|
||||
|
||||
### 简化版 `cDNA_gridding_simple.py`(课堂主讲)
|
||||
|
||||
@@ -27,16 +77,10 @@ docs/
|
||||
|
||||
```
|
||||
1. 灰度图 → 每列/行灰度值求和 → 投影曲线
|
||||
2. 阈值 X = (max - min) × 10%,曲线减去 X
|
||||
3. 正 = 斑点,负 = 空隙 → 找过零点 → 配对取中点 → 划线
|
||||
```
|
||||
|
||||
**运行**:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
python cDNA_gridding_simple.py
|
||||
# 输出:results_simple/gridding_simple.png
|
||||
2. Otsu 求自适应百分比 = T/255
|
||||
3. 阈值 X = (max - min) × 百分比,曲线减去 X
|
||||
4. 正 = 斑点,负 = 空隙 → 找过零点 → 配对取中点 → 划线
|
||||
5. 逐格 Otsu 分割 + keep_largest_object + remove_small_objects
|
||||
```
|
||||
|
||||
**特点**:
|
||||
@@ -44,67 +88,69 @@ python cDNA_gridding_simple.py
|
||||
- 约 150 行代码,带详细中文注释
|
||||
- 核心逻辑仅 30 行
|
||||
- 与原版网格线位置误差为 **0 像素**
|
||||
- `find_gap_lines` 函数:减阈值 → 判断正负 → 找过零点 → 配对空隙中点
|
||||
- 完全自动化,无需人工设定参数
|
||||
|
||||
### 原版 `cDNA_segmentation.py`(完整实现)
|
||||
|
||||
**包含模块**:
|
||||
|
||||
| 模块 | 算法 | 依赖 |
|
||||
| 模块 | 算法 | 参照 |
|
||||
|------|------|------|
|
||||
| 网格划分 | 投影 → 自相关 → 白顶帽 → Otsu → 质心 | numpy, scipy, skimage |
|
||||
| 阈值分割 | 人工阈值、Otsu 自动阈值、迭代阈值 | 同上 |
|
||||
| 去噪 | TV 全变分去噪(Chambolle 投影) | 同上 |
|
||||
| 后处理 | 去小连通域、保留最大连通域 | 同上 |
|
||||
|
||||
**运行**:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
python cDNA_segmentation.py
|
||||
# 输出:results/ 下 6 张图
|
||||
```
|
||||
|
||||
**输出文件**:
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `result_gridding.png` | 网格划分:原图+投影曲线+直方图 |
|
||||
| `result_gridding_overlay.png` | 网格线叠加到原图 |
|
||||
| `result_threshold_compare.png` | 三种阈值方法(人工/Otsu/迭代)对比 |
|
||||
| `result_iterative_convergence.png` | 迭代阈值收敛曲线 |
|
||||
| `result_full_segmentation.png` | 全图逐块 Otsu 分割结果 |
|
||||
| `result_I_bw.png` | 最终二值图 |
|
||||
| 网格划分 | 投影 → 自相关 → 白顶帽 → Otsu → 质心 | MATLAB `GriddingAndCV.m` |
|
||||
| 阈值分割 | 人工阈值、Otsu 自动阈值、迭代阈值 | — |
|
||||
| 去噪 | TV 全变分去噪(Chambolle 投影) | MATLAB `tvdenoise.m` |
|
||||
| 后处理 | 去小连通域、保留最大连通域 | MATLAB `choice.m` |
|
||||
|
||||
---
|
||||
|
||||
## 两版对比
|
||||
## Web 应用功能
|
||||
|
||||
| | 简化版 | 原版 |
|
||||
|---|---|---|
|
||||
| 功能 | 仅画网格线 | 网格 + 分割 + 去噪 |
|
||||
| 核心算法 | 加减乘除 | 自相关 + 形态学 + Otsu |
|
||||
| 代码行数 | 150 | 430 |
|
||||
| 检测网格线数 | 22×22 | 22×22 |
|
||||
| 线条位置 | 42, 77, 112, … | 42, 77, 112, … |
|
||||
| **误差** | **0 像素** | — |
|
||||
- **图像上传**:支持 PNG、JPG、TIFF 等格式,最大 50MB
|
||||
- **自动处理**:网格划分 + 逐格分割 + 后处理
|
||||
- **6 张可视化图表**:
|
||||
1. 网格线叠加原图
|
||||
2. 列投影曲线
|
||||
3. 行投影曲线
|
||||
4. 灰度直方图 + Otsu 阈值
|
||||
5. 分割结果(后处理前)
|
||||
6. 后处理结果
|
||||
- **统计信息**:检测到的斑点数、Otsu 阈值、网格尺寸等
|
||||
|
||||
---
|
||||
|
||||
## 技术文档
|
||||
|
||||
- `docs/gridding_simple_tutorial.md` — 简化版 190 行逐行讲解,含 ASCII 图解
|
||||
- `results_simple/flowchart.drawio` — 算法流程图,用 Draw.io 打开
|
||||
|
||||
## 输入数据
|
||||
|
||||
`cDNA.png`(820×820 RGB,来自 GEO 数据库 GSM16390,Cy3/Cy5 双色荧光)
|
||||
- `cDNA.png`(820×820 RGB,来自 GEO 数据库 GSM16390,Cy3/Cy5 双色荧光)
|
||||
- 23×23 斑点阵列,斑点间距约 35px,每斑点约 18px 宽
|
||||
|
||||
---
|
||||
|
||||
## 打包说明
|
||||
|
||||
使用 PyInstaller 将 Web 应用打包为单个 exe 文件:
|
||||
|
||||
```bash
|
||||
python build_exe.py
|
||||
# 输出:dist/cDNA_Analyzer.exe(约 68MB)
|
||||
```
|
||||
|
||||
打包配置已优化,排除了 torch、pandas 等无关重量级包。
|
||||
|
||||
---
|
||||
|
||||
## 运行环境
|
||||
|
||||
Python 3.10 + numpy, scipy, scikit-image, matplotlib, Pillow
|
||||
- **Python**: `D:\ProgramData\anaconda3\envs\my_env`
|
||||
- **依赖**: numpy, scipy, scikit-image, matplotlib, Pillow, Flask
|
||||
|
||||
---
|
||||
|
||||
## 参考文献
|
||||
|
||||
- 芦碧波等. 低对比度 cDNA 图像分割的局部水平集方法. 中国图象图形学报, 2014.
|
||||
- 芦碧波. 高污染基因芯片图像的网格划分. 河南理工大学学报, 2019.
|
||||
|
||||
---
|
||||
|
||||
## 仓库
|
||||
|
||||
- **首选**(局域网 SSH): `ssh://git@192.168.5.8:2222/Serendipity/cDNA-image-processing.git`
|
||||
- **备选**(外网 HTTPS): `https://lhy-git.liuhangyv.top/Serendipity/cDNA-image-processing.git`
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
PyInstaller打包脚本
|
||||
运行: python build_exe.py
|
||||
输出: dist/cDNA_Analyzer.exe
|
||||
"""
|
||||
|
||||
import PyInstaller.__main__
|
||||
import os
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
'web/launcher.py',
|
||||
'--name=cDNA_Analyzer',
|
||||
'--onefile',
|
||||
'--noconsole',
|
||||
'--add-data', f'web/templates;templates',
|
||||
'--add-data', f'web/static;static',
|
||||
# 排除不相关的重量级包(环境里有torch/pandas等)
|
||||
'--exclude-module', 'torch',
|
||||
'--exclude-module', 'torchvision',
|
||||
'--exclude-module', 'torchaudio',
|
||||
'--exclude-module', 'pandas',
|
||||
'--exclude-module', 'sklearn',
|
||||
'--exclude-module', 'sqlalchemy',
|
||||
'--exclude-module', 'pygame',
|
||||
'--exclude-module', 'zmq',
|
||||
'--exclude-module', 'IPython',
|
||||
'--exclude-module', 'jedi',
|
||||
'--exclude-module', 'numba',
|
||||
'--exclude-module', 'llvmlite',
|
||||
'--exclude-module', 'onnxruntime',
|
||||
'--exclude-module', 'lxml',
|
||||
'--exclude-module', 'cryptography',
|
||||
'--exclude-module', 'bcrypt',
|
||||
'--exclude-module', 'pygments',
|
||||
'--clean',
|
||||
'--noconfirm',
|
||||
])
|
||||
print('\n完成!exe 在 dist/cDNA_Analyzer.exe')
|
||||
@@ -0,0 +1,101 @@
|
||||
# 简化版 vs 原版 差异说明
|
||||
|
||||
> 写给以后的自己:简化版只是课堂演示用的,生产环境用原版。
|
||||
|
||||
---
|
||||
|
||||
## 一句话总结
|
||||
|
||||
| | 功能 | 用途 |
|
||||
|---|---|---|
|
||||
| 简化版 | 只画网格线 | 课堂讲解 |
|
||||
| 原版 | 网格 + 去噪 + 分割 + 后处理 + 可视化 | 完整作业 |
|
||||
|
||||
---
|
||||
|
||||
## 简化版缺少的模块
|
||||
|
||||
### 1. TV全变分去噪
|
||||
|
||||
**位置**:`cDNA_segmentation.py` 第 78-102 行,`tv_denoise()`
|
||||
|
||||
**干什么**:对每个子块做 Chambolle 投影去噪,去除荧光噪点。简化版完全没有这一步。
|
||||
|
||||
**实现**:Rudin-Osher-Fatemi 模型,`min TV(u) + λ/2||f-u||²`,迭代对偶变量 p1、p2 直到收敛。
|
||||
|
||||
### 2. 三种阈值分割
|
||||
|
||||
**位置**:`cDNA_segmentation.py` 第 35-71 行
|
||||
|
||||
| 函数 | 原理 |
|
||||
|------|------|
|
||||
| `manual_threshold()` | 灰度 > 固定值 T |
|
||||
| `otsu_threshold()` | 最大类间方差自动求 T |
|
||||
| `iterative_threshold()` | 初始 T=均值,迭代 T=(μ_fg+μ_bg)/2 直到收敛 |
|
||||
|
||||
简化版完全没有分割这一步。
|
||||
|
||||
### 3. 全图逐块分割
|
||||
|
||||
**位置**:`cDNA_segmentation.py` 第 374-411 行
|
||||
|
||||
**做什么**:对网格划分出的每个子块:
|
||||
1. 如果太暗(均值<5或<30),先增强
|
||||
2. TV 去噪
|
||||
3. Otsu 阈值二值化
|
||||
4. 保留最大连通域
|
||||
5. 拼接回全局二值图
|
||||
|
||||
简化版只画线,不处理子块。
|
||||
|
||||
### 4. 后处理
|
||||
|
||||
**位置**:`cDNA_segmentation.py` 第 206-226 行
|
||||
|
||||
| 函数 | 作用 |
|
||||
|------|------|
|
||||
| `remove_small_objects()` | 去掉面积 < 20 的连通域(噪声) |
|
||||
| `keep_largest_object()` | 每个子块只保留最大的那块(斑点) |
|
||||
|
||||
简化版不需要后处理(因为没生成二值图)。
|
||||
|
||||
### 5. 可视化差异
|
||||
|
||||
**简化版输出**(1张):
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `gridding_simple.png` | 灰度图 + 绿色网格线 |
|
||||
|
||||
**原版输出**(6张):
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `result_gridding.png` | 4合1:网格叠加 + 列投影 + 行投影 + 灰度直方图 |
|
||||
| `result_gridding_overlay.png` | 网格线叠在原图上 |
|
||||
| `result_threshold_compare.png` | 2×2:原图 + 人工/Otsu/迭代三种分割对比 |
|
||||
| `result_iterative_convergence.png` | 迭代阈值收敛曲线 |
|
||||
| `result_full_segmentation.png` | 全图逐块分割二值结果 |
|
||||
| `result_I_bw.png` | 单独的二值图 |
|
||||
|
||||
### 6. 网格划分算法不同
|
||||
|
||||
| 步骤 | 简化版 | 原版 |
|
||||
|------|--------|------|
|
||||
| 投影 | `np.sum` 求和 | `np.mean` 求均值 |
|
||||
| 估间距 | 无 | 自相关 `np.correlate` |
|
||||
| 去背景 | 无 | `ndimage.white_tophat` |
|
||||
| 找斑点 | 减10%阈值 → 过零点 | Otsu + 连通域质心 |
|
||||
|
||||
结果一致(误差 0 像素),但算法完全不同。
|
||||
|
||||
---
|
||||
|
||||
## 什么时候用哪个
|
||||
|
||||
| 场景 | 用哪个 |
|
||||
|------|--------|
|
||||
| 课堂讲算法思路 | 简化版 |
|
||||
| 实际做图像处理 | 原版 |
|
||||
| 对比阈值方法对同一张图的效果 | 原版 |
|
||||
| 只画网格线 | 都可以 |
|
||||
@@ -0,0 +1,36 @@
|
||||
# 依赖库清单
|
||||
|
||||
## 核心运行时依赖
|
||||
|
||||
| 库名 | 版本 | 用途 | 哪些文件用到 |
|
||||
|------|------|------|-------------|
|
||||
| **numpy** | 1.26.4 | 数组运算、投影求和、统计 | 全部 .py |
|
||||
| **scipy** | 1.15.3 | 连通域标记 `ndimage.label` | 全部 .py |
|
||||
| **scikit-image** | 0.25.2 | `rgb2gray` 灰度转换、Otsu 阈值 | 全部 .py |
|
||||
| **matplotlib** | 3.10.8 | 可视化绘图(投影曲线、直方图、结果图) | 全部 .py |
|
||||
| **Pillow** | 12.1.1 | 读取图像文件(png/tif/jpg) | 全部 .py |
|
||||
| **Flask** | 3.1.2 | Web 后端 | web/app.py |
|
||||
|
||||
## 仅打包时需要的额外依赖
|
||||
|
||||
| 库名 | 用途 |
|
||||
|------|------|
|
||||
| **PyInstaller** | 将 Python 项目打包成独立 exe |
|
||||
|
||||
## 安装命令
|
||||
|
||||
```bash
|
||||
pip install numpy scipy scikit-image matplotlib Pillow flask
|
||||
```
|
||||
|
||||
如需打包:
|
||||
|
||||
```bash
|
||||
pip install pyinstaller
|
||||
python build_exe.py
|
||||
```
|
||||
|
||||
## 版本兼容性
|
||||
|
||||
以上版本为当前开发环境(Python 3.10,Anaconda `my_env`)实测版本。
|
||||
其他相近版本通常兼容,无特殊版本锁定需求。
|
||||
@@ -0,0 +1,109 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram name="简化版完整算法流程" id="flowchart">
|
||||
<mxGraphModel dx="1475" dy="1478" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="2000" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="title" value="cDNA微阵列网格划分 —— 简化版完整算法流程" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1;fontColor=#1565C0" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="20" width="700" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s1" value="1. 读取图像" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="80" width="240" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s2" value="2. 转为灰度图
color.rgb2gray(img)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=13;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="155" width="240" height="45" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s_otsu" value="3. Otsu 自动阈值
遍历0~255,选最小类内方差 T
pct = T / 255(自适应)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=3;fontSize=12;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="230" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s3L" value="3a. 横轴投影
np.sum(gray, axis=0)
每列灰度求和" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#3949AB;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="327" width="210" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s3R" value="3b. 纵轴投影
np.sum(gray, axis=1)
每行灰度求和" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#3949AB;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="720" y="327" width="210" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s4" value="4. 计算投影阈值 X
X = (max-min) × pct" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=3;fontSize=13;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="437" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s5" value="5. 曲线减去 X
正 = 斑点,负 = 空隙,零 = 分界线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=3;fontSize=12;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="527" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s6" value="6. 找过零点,配对取空隙中点
离开斑点 + 进入下一斑点 → 划线位置" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;strokeWidth=3;fontSize=12;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="617" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s7" value="7. 逐格 Otsu 分割
对每个格子独立算 Otsu 阈值,二值化" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="717" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s8" value="8. keep_largest_object
每个格子只保留面积最大的连通域" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="807" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s9" value="9. remove_small_objects
全局去噪:面积 &lt; 中位数25% 自动剔除" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="897" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s10" value="10. 统计斑点数
ndimage.label 标记连通域,过滤面积&lt;10" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="987" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s11" value="11. 输出 6 张可视化图片
网格 / 投影曲线 / 直方图 / 分割 / 后处理" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=12;align=center;verticalAlign=middle;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="1077" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s1" target="s2" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s2" target="s_otsu" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e3L" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s_otsu" target="s3L" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e3R" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s_otsu" target="s3R" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e4L" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s3L" target="s4" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e4R" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s3R" target="s4" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s4" target="s5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s5" target="s6" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;fontStyle=1" parent="1" source="s6" target="s7" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;fontStyle=1" parent="1" source="s7" target="s8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;fontStyle=1" parent="1" source="s8" target="s9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;fontStyle=1" parent="1" source="s9" target="s10" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;fontStyle=1" parent="1" source="s10" target="s11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="legend_title" value="图例" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="929" width="60" height="25" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg1" value="输入/输出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="964" width="100" height="28" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg2" value="网格划线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="999" width="100" height="28" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg3" value="过零点/配对" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="1034" width="100" height="28" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg4" value="投影计算" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#3949AB;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="1069" width="100" height="28" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg5" value="分割/后处理" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="1104" width="100" height="28" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -1,130 +0,0 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram name="简化版网格划分算法流程" id="flowchart">
|
||||
<mxGraphModel dx="888" dy="899" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="1600" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="title" value="cDNA微阵列网格划分 —— 简化版算法流程图" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;fontColor=#1565C0" parent="1" vertex="1">
|
||||
<mxGeometry x="300" y="30" width="600" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s1" value="1. 读取图像" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="100" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s2" value="2. 转为灰度图
color.rgb2gray(img)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="190" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s3L" value="3. 纵轴投影
np.sum(gray, axis=0)
每一列灰度值求和" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#3949AB;strokeWidth=2;fontSize=13;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="300" y="290" width="240" height="65" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s3R" value="3. 横轴投影
np.sum(gray, axis=1)
每一行灰度值求和" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#3949AB;strokeWidth=2;fontSize=13;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="660" y="290" width="240" height="65" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s4" value="4. 计算阈值 X
X = (max - min) × 10%" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=3;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="400" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s5" value="5. 曲线减去 X
col_shifted = col_profile - X
row_shifted = row_profile - X" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=3;fontSize=13;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="500" width="240" height="65" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s6" value="6. 减阈值后结果" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFE0B2;strokeColor=#E65100;strokeWidth=2;fontSize=13;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="610" width="240" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="d1" value="> 0 ?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=2;fontSize=13;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="525" y="690" width="150" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s7pos" value="正 → 斑点区域" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=13;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="705" width="160" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s7neg" value="负 → 空隙" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFCDD2;strokeColor=#C62828;strokeWidth=2;fontSize=13;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="760" y="705" width="160" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s8" value="7. 找过零点" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;strokeWidth=3;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="820" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="note1" value="遍历 is_positive 数组
当相邻位置正负不同时
记录该位置 = 过零点" style="shape=note;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;strokeWidth=1;fontSize=11;align=center;verticalAlign=middle;backgroundOutline=1;size=14" parent="1" vertex="1">
|
||||
<mxGeometry x="800" y="820" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s9" value="8. 配对过零点" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;strokeWidth=3;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="920" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="note2" value="配对「离开斑点+进入下一斑点」
即:正→负 与 负→正
中点 = 空隙中央 = 划线位置" style="shape=note;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;strokeWidth=1;fontSize=11;align=center;verticalAlign=middle;backgroundOutline=1;size=14" parent="1" vertex="1">
|
||||
<mxGeometry x="800" y="920" width="180" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s10" value="9. 画出网格线
ax.axvline(x) / ax.axhline(y)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=13;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="1020" width="240" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="s11" value="10. 输出网格叠加图" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=14;fontStyle=1;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="1120" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s1" target="s2" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e2L" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s2" target="s3L" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e2R" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s2" target="s3R" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e3L" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s3L" target="s4" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e3R" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s3R" target="s4" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s4" target="s5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s5" target="s6" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s6" target="d1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e7y" value="是" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#2E7D32;strokeWidth=2;endArrow=classic;endFill=1;fontColor=#2E7D32;fontStyle=1;fontSize=12" parent="1" source="d1" target="s7pos" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e7n" value="否" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#C62828;strokeWidth=2;endArrow=classic;endFill=1;fontColor=#C62828;fontStyle=1;fontSize=12" parent="1" source="d1" target="s7neg" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e8p" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;dashed=1" parent="1" source="s7pos" target="s8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="350" y="845"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e8n" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1;dashed=1" parent="1" source="s7neg" target="s8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="840" y="790"/>
|
||||
<mxPoint x="600" y="790"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s8" target="s9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s9" target="s10" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;strokeWidth=2;endArrow=classic;endFill=1" parent="1" source="s10" target="s11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="legend_title" value="图例" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="952.5" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg1" value="输入/输出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C8E6C9;strokeColor=#2E7D32;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="992.5" width="100" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg2" value="数据处理" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9C4;strokeColor=#F9A825;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="1032.5" width="100" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg3" value="核心逻辑" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E3F2FD;strokeColor=#1565C0;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="1072.5" width="100" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="leg4" value="投影计算" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#3949AB;strokeWidth=2;fontSize=11;align=center;verticalAlign=middle" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="1112.5" width="100" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
Before Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -1,22 +1,45 @@
|
||||
"""
|
||||
cDNA微阵列图像处理 —— 简化版网格划分
|
||||
cDNA微阵列图像处理 —— 简化版
|
||||
======================================
|
||||
|
||||
D:\ProgramData\anaconda3\envs\my_env\python.exe src/cDNA_gridding_simple.py
|
||||
|
||||
算法步骤:
|
||||
一、算法流程总览
|
||||
|
||||
1. 彩色图 → 灰度图
|
||||
2. 横轴投影:对每一列的所有像素灰度值求和 → 得到一条曲线
|
||||
纵轴投影:对每一行的所有像素灰度值求和 → 得到一条曲线
|
||||
3. 在曲线上,求出 max 和 min,阈值 X = (max - min) × 10%
|
||||
4. 曲线上每个值都减去 X
|
||||
5. 减完之后:
|
||||
- 大于 0 的地方 = 斑点区域
|
||||
- 小于 0 的地方 = 斑点之间的空隙
|
||||
- 等于 0 的地方 = 斑点与空隙的分界线(过零点)
|
||||
6. 配对相邻的过零点(离开斑点 + 进入下一个斑点),
|
||||
中点就是空隙的中心 = 划线位置
|
||||
灰度图 ──→ 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
|
||||
@@ -24,124 +47,121 @@ 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')
|
||||
|
||||
|
||||
def draw_grid_lines(gray: np.ndarray, pct: float = 0.10):
|
||||
# ================================================================
|
||||
# 函数1:Otsu 像素级阈值
|
||||
# ================================================================
|
||||
def otsu_threshold_pixels(gray: np.ndarray) -> int:
|
||||
"""
|
||||
核心算法:检测网格分割线。
|
||||
对图像像素做 Otsu 自动阈值检测。
|
||||
|
||||
原理:
|
||||
灰度图的每一列/行,属于斑点的像素灰度值高,属于背景的灰度值低。
|
||||
把每列/行的灰度值加起来,就能得到一条曲线:
|
||||
——曲线凸起的地方 = 斑点所在位置
|
||||
——曲线凹陷的地方 = 斑点之间的空隙
|
||||
遍历灰度值 0~255,对每个候选 T:
|
||||
- 将像素分为两组:前景(>T) 和 背景(≤T)
|
||||
- 计算类内方差 = w_bg × σ²_bg + w_fg × σ²_fg
|
||||
- 选使类内方差最小的 T
|
||||
|
||||
去掉一个阈值后,曲线在空隙处会变成负数,
|
||||
过零点的位置就是斑点和空隙的分界线,
|
||||
两个分界线中点就是划线位置。
|
||||
|
||||
参数:
|
||||
gray: 灰度图 (高×宽)
|
||||
pct: 阈值百分比,默认10%
|
||||
|
||||
返回:
|
||||
(纵线x坐标列表, 横线y坐标列表)
|
||||
返回 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:横轴投影 —— 统计每一列的灰度总和
|
||||
# ================================================================
|
||||
# gray 是一个 H×W 的二维数组,gray[行, 列] 是某个像素的灰度值
|
||||
# np.sum(gray, axis=0) 沿行方向求和 → 得到长度为 W 的一维数组
|
||||
# 含义:每一列上所有像素的灰度值加起来
|
||||
# 斑点所在列 → 亮像素多 → 和较大(曲线凸起)
|
||||
# 空隙所在列 → 暗像素多 → 和较小(曲线凹陷)
|
||||
# ---- 1. 横轴投影 ----
|
||||
col_profile = np.sum(gray, axis=0).astype(float)
|
||||
|
||||
# ================================================================
|
||||
# 步骤2:纵轴投影 —— 统计每一行的灰度总和
|
||||
# ================================================================
|
||||
# np.sum(gray, axis=1) 沿列方向求和 → 得到长度为 H 的一维数组
|
||||
# 含义:每一行上所有像素的灰度值加起来
|
||||
# ---- 2. 纵轴投影 ----
|
||||
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
|
||||
# ---- 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:曲线上所有值减去阈值
|
||||
# ================================================================
|
||||
# 减去阈值后:
|
||||
# 原本在空隙处的值(本来就小)→ 变成负数
|
||||
# 原本在斑点处的值(本来就大)→ 仍然为正数
|
||||
# 等于0的位置 = 斑点与空隙的分界线 = 过零点
|
||||
col_shifted = col_profile - col_T
|
||||
row_shifted = row_profile - row_T
|
||||
# ---- 4. 曲线减去阈值 ----
|
||||
col_shifted = col_profile - col_T_val
|
||||
row_shifted = row_profile - row_T_val
|
||||
|
||||
# ================================================================
|
||||
# 步骤5:找过零点,两两配对,中间点即划线位置
|
||||
# ================================================================
|
||||
# ---- 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]:
|
||||
if is_positive[i] != is_positive[i - 1]: # 正负翻转
|
||||
crossings.append(i)
|
||||
|
||||
if len(crossings) < 2:
|
||||
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]: 负→正(进入下一个斑点)
|
||||
# crossings[k+1]: 负→正(进入下一斑点)
|
||||
# 中点 = 空隙中央 = 划线位置
|
||||
mid = int((crossings[k] + crossings[k + 1]) / 2)
|
||||
lines.append(mid)
|
||||
@@ -151,40 +171,197 @@ def draw_grid_lines(gray: np.ndarray, pct: float = 0.10):
|
||||
x_lines = find_gap_lines(col_shifted)
|
||||
y_lines = find_gap_lines(row_shifted)
|
||||
|
||||
return x_lines, y_lines
|
||||
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:
|
||||
"""
|
||||
自动去除小连通域(噪声)。
|
||||
|
||||
对连通域面积分布做 Otsu 阈值检测:
|
||||
面积分布天然双峰——噪声区(几个像素) 和 真斑点区(几百像素)。
|
||||
Otsu 自动找到两峰之间的最佳分界,小于该值的视为噪声。
|
||||
换图换分辨率都自动适应,不需要手动调参。
|
||||
"""
|
||||
labeled, num = ndimage.label(binary)
|
||||
if num == 0:
|
||||
return binary
|
||||
|
||||
# 收集所有连通域的面积
|
||||
areas = np.array([int(np.sum(labeled == i)) for i in range(1, num + 1)])
|
||||
if len(areas) < 2:
|
||||
return binary
|
||||
|
||||
# 对面积数组做 Otsu(与像素 Otsu 完全相同的原理)
|
||||
# 将面积值当作"灰度",找到最小类内方差的分界点
|
||||
best_T, best_cost, n_total = 0, float('inf'), len(areas)
|
||||
for T in np.unique(areas):
|
||||
small = areas[areas <= T] # 候选噪声组
|
||||
large = areas[areas > T] # 候选真斑点组
|
||||
w_s = len(small) / n_total
|
||||
w_l = len(large) / n_total
|
||||
if w_s == 0 or w_l == 0:
|
||||
continue
|
||||
cost = w_s * np.var(small) + w_l * np.var(large)
|
||||
if cost < best_cost:
|
||||
best_cost = cost
|
||||
best_T = T
|
||||
|
||||
min_size = best_T # Otsu 自动找到的面积分界线
|
||||
|
||||
# 面积不达标的连通域整块置0
|
||||
result = binary.copy()
|
||||
for i in range(1, num + 1):
|
||||
if int(np.sum(labeled == i)) < 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(红绿蓝+透明通道),取前三个通道转为灰度
|
||||
# 原图 RGBA,取前三个通道转为 0~255 灰度图
|
||||
gray = (color.rgb2gray(img[:, :, :3]) * 255).astype(np.uint8)
|
||||
|
||||
# ---- 执行网格检测 ----
|
||||
x_lines, y_lines = draw_grid_lines(gray)
|
||||
# ---- 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}%")
|
||||
|
||||
# ---- 在原图上划线并保存 ----
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
ax.imshow(gray, cmap='gray')
|
||||
# ---- 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:
|
||||
ax.axvline(x=x, color='lime', linewidth=0.5)
|
||||
|
||||
# 画横向分割线(横线)
|
||||
ax1.axvline(x=x, color='lime', linewidth=0.5)
|
||||
for y in y_lines:
|
||||
ax.axhline(y=y, color='lime', linewidth=0.5)
|
||||
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)
|
||||
|
||||
ax.set_title(f'cDNA微阵列网格划分 ({len(x_lines)}×{len(y_lines)})', fontsize=14)
|
||||
ax.axis('off')
|
||||
# 图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)
|
||||
|
||||
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}")
|
||||
# 图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__':
|
||||
|
||||
@@ -203,25 +203,48 @@ def gridding(gray: np.ndarray) -> tuple:
|
||||
# 第四部分:后处理(参考choice.m, choosemaxobj.m)
|
||||
# ============================================================
|
||||
|
||||
def remove_small_objects(binary: np.ndarray, min_size: int = 20) -> np.ndarray:
|
||||
"""去除面积小于min_size的连通域"""
|
||||
def remove_small_objects(binary: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
自动去除小连通域。
|
||||
|
||||
对连通域面积分布做 Otsu 阈值检测——面积天然双峰,
|
||||
Otsu 自动找到噪声峰和真斑点峰之间的最佳分界,零人工参数。
|
||||
"""
|
||||
labeled, num = ndimage.label(binary)
|
||||
if num == 0:
|
||||
return binary
|
||||
|
||||
areas = np.array([int(np.sum(labeled == i)) for i in range(1, num + 1)])
|
||||
if len(areas) < 2:
|
||||
return binary
|
||||
|
||||
best_T, best_cost, n_total = 0, float('inf'), len(areas)
|
||||
for T_val in np.unique(areas):
|
||||
small = areas[areas <= T_val]
|
||||
large = areas[areas > T_val]
|
||||
w_s = len(small) / n_total
|
||||
w_l = len(large) / n_total
|
||||
if w_s == 0 or w_l == 0:
|
||||
continue
|
||||
cost = w_s * np.var(small) + w_l * np.var(large)
|
||||
if cost < best_cost:
|
||||
best_cost = cost
|
||||
best_T = T_val
|
||||
|
||||
result = binary.copy()
|
||||
for i in range(1, num + 1):
|
||||
if np.sum(labeled == i) < min_size:
|
||||
if int(np.sum(labeled == i)) < best_T:
|
||||
result[labeled == i] = 0
|
||||
return result
|
||||
|
||||
|
||||
def keep_largest_object(binary: np.ndarray, min_size: int = 20) -> np.ndarray:
|
||||
"""只保留最大连通域"""
|
||||
def keep_largest_object(binary: np.ndarray) -> np.ndarray:
|
||||
"""每个格子里只保留面积最大的连通域(无需设定阈值)"""
|
||||
labeled, num = ndimage.label(binary)
|
||||
if num == 0:
|
||||
return binary
|
||||
areas = [int(np.sum(labeled == i)) for i in range(1, num + 1)]
|
||||
max_area = max(areas)
|
||||
if max_area < min_size:
|
||||
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)
|
||||
|
||||
@@ -396,11 +419,12 @@ def main() -> None:
|
||||
bw_blk = (blk_denoised > T).astype(np.uint8)
|
||||
except ValueError:
|
||||
bw_blk = np.zeros(blk.shape, dtype=np.uint8)
|
||||
# 后处理:保留最大连通域
|
||||
bw_blk = keep_largest_object(bw_blk, min_size=8)
|
||||
# 后处理:保留最大连通域(无需阈值)
|
||||
bw_blk = keep_largest_object(bw_blk)
|
||||
bw_full[r1:r2, c1:c2] = bw_blk
|
||||
|
||||
bw_full = remove_small_objects(bw_full, min_size=20)
|
||||
# 全局去除小连通域(中位数的25%以下自动判为噪声)
|
||||
bw_full = remove_small_objects(bw_full)
|
||||
|
||||
fig_full = plot_full_segmentation(gray, bw_full, "全图逐块Otsu分割结果")
|
||||
fig_full.savefig(os.path.join(OUTPUT_DIR, 'result_full_segmentation.png'), dpi=150, bbox_inches='tight')
|
||||
@@ -410,7 +434,23 @@ def main() -> None:
|
||||
bw_img.save(os.path.join(OUTPUT_DIR, 'result_I_bw.png'))
|
||||
print(" 保存: result_I_bw.png")
|
||||
|
||||
# ---- 步骤4: 网格叠加图 ----
|
||||
# ---- 步骤4: 统计点数 ----
|
||||
print("\n[步骤4] 统计斑点数量...")
|
||||
labeled_spots, num_spots = ndimage.label(bw_full)
|
||||
# 统计每个斑点的面积
|
||||
spot_sizes = []
|
||||
for i in range(1, num_spots + 1):
|
||||
size = int(np.sum(labeled_spots == i))
|
||||
spot_sizes.append(size)
|
||||
spot_sizes = np.array(spot_sizes)
|
||||
# 过滤极小噪声
|
||||
valid_spots = spot_sizes[spot_sizes >= 10]
|
||||
print(f" 检测到 {len(valid_spots)} 个斑点 (面积≥10像素)")
|
||||
print(f" 斑点面积: 最小 {valid_spots.min()}, 最大 {valid_spots.max()}, "
|
||||
f"均值 {valid_spots.mean():.1f}, 中位数 {np.median(valid_spots):.0f}")
|
||||
print(f" 网格子块数 (理论值): {(len(x_grid)-1) * (len(y_grid)-1)}")
|
||||
|
||||
# ---- 步骤5: 网格叠加图 ----
|
||||
if img.ndim == 3:
|
||||
overlay = img[:, :, :3].copy()
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
cDNA微阵列图像处理 - Web UI (Flask)
|
||||
=====================================
|
||||
启动:python web/app.py
|
||||
打包:python build_exe.py
|
||||
打开:http://localhost:5000
|
||||
"""
|
||||
|
||||
import os, sys, io, base64
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from skimage import color
|
||||
from scipy import ndimage
|
||||
|
||||
# 项目根目录加到 sys.path
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(BASE_DIR, 'src'))
|
||||
|
||||
# PyInstaller 打包后资源路径
|
||||
if getattr(sys, 'frozen', False):
|
||||
bundle_dir = sys._MEIPASS
|
||||
template_dir = os.path.join(bundle_dir, 'templates')
|
||||
static_dir = os.path.join(bundle_dir, 'static')
|
||||
else:
|
||||
template_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
|
||||
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
||||
|
||||
app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
|
||||
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
|
||||
UPLOAD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
# ================================================================
|
||||
# 图像处理函数(同简化版逻辑)
|
||||
# ================================================================
|
||||
def otsu_threshold_pixels(gray):
|
||||
best_T, best_cost, total = 0, float('inf'), gray.size
|
||||
for T in range(1, 255):
|
||||
bg, fg = gray[gray <= T], gray[gray > T]
|
||||
if len(bg) == 0 or len(fg) == 0:
|
||||
continue
|
||||
cost = len(bg)/total*np.var(bg) + len(fg)/total*np.var(fg)
|
||||
if cost < best_cost:
|
||||
best_cost, best_T = cost, T
|
||||
return best_T
|
||||
|
||||
def draw_grid_lines(gray):
|
||||
T = otsu_threshold_pixels(gray)
|
||||
pct = T / 255.0
|
||||
H, W = gray.shape
|
||||
col_prof = np.sum(gray, axis=0).astype(float)
|
||||
row_prof = np.sum(gray, axis=1).astype(float)
|
||||
col_T = (np.max(col_prof)-np.min(col_prof))*pct
|
||||
row_T = (np.max(row_prof)-np.min(row_prof))*pct
|
||||
col_s, row_s = col_prof-col_T, row_prof-row_T
|
||||
|
||||
def find_gap_lines(prof):
|
||||
is_pos = prof > 0
|
||||
crossings = [i for i in range(1, len(is_pos)) if is_pos[i] != is_pos[i-1]]
|
||||
if len(crossings) < 2:
|
||||
return np.array([])
|
||||
start = 1 if not is_pos[0] else 0
|
||||
return np.array([int((crossings[k]+crossings[k+1])/2) for k in range(start, len(crossings)-1, 2)])
|
||||
|
||||
xl = find_gap_lines(col_s)
|
||||
yl = find_gap_lines(row_s)
|
||||
return xl, yl, T, pct, col_prof, row_prof, col_s, row_s, col_T, row_T
|
||||
|
||||
def keep_largest_object(binary):
|
||||
L, n = ndimage.label(binary)
|
||||
if n == 0: return np.zeros_like(binary)
|
||||
return (L == (np.argmax([int(np.sum(L==i)) for i in range(1,n+1)])+1)).astype(np.uint8)
|
||||
|
||||
def remove_small_objects(binary):
|
||||
L, n = ndimage.label(binary)
|
||||
if n == 0: return binary
|
||||
areas = np.array([int(np.sum(L==i)) for i in range(1,n+1)])
|
||||
if len(areas) < 2: return binary
|
||||
best_T, best_cost, n_total = 0, float('inf'), len(areas)
|
||||
for T in np.unique(areas):
|
||||
s, l = areas[areas<=T], areas[areas>T]
|
||||
w_s, w_l = len(s)/n_total, len(l)/n_total
|
||||
if w_s==0 or w_l==0: continue
|
||||
cost = w_s*np.var(s) + w_l*np.var(l)
|
||||
if cost < best_cost: best_cost, best_T = cost, T
|
||||
r = binary.copy()
|
||||
for i in range(1, n+1):
|
||||
if int(np.sum(L==i)) < best_T: r[L==i] = 0
|
||||
return r
|
||||
|
||||
|
||||
def fig_to_base64(fig):
|
||||
"""matplotlib figure → base64 PNG"""
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format='png', dpi=120, bbox_inches='tight')
|
||||
buf.seek(0)
|
||||
b64 = base64.b64encode(buf.read()).decode()
|
||||
plt.close(fig)
|
||||
return f'data:image/png;base64,{b64}'
|
||||
|
||||
|
||||
def process_image(img_array):
|
||||
"""对上传的图像运行完整处理流程,返回 dict"""
|
||||
# 转灰度
|
||||
if img_array.ndim == 3 and img_array.shape[2] >= 3:
|
||||
gray = (color.rgb2gray(img_array[:,:,:3])*255).astype(np.uint8)
|
||||
else:
|
||||
gray = img_array.astype(np.uint8)
|
||||
|
||||
# 网格划线
|
||||
xl, yl, T, pct, cp, rp, cs, rs, cT, rT = draw_grid_lines(gray)
|
||||
|
||||
# 逐格分割
|
||||
bw = np.zeros_like(gray)
|
||||
for i in range(len(yl)-1):
|
||||
for j in range(len(xl)-1):
|
||||
r1, r2 = yl[i], yl[i+1]
|
||||
c1, c2 = xl[j], xl[j+1]
|
||||
blk = gray[r1:r2, c1:c2]
|
||||
if blk.size == 0: continue
|
||||
bt = otsu_threshold_pixels(blk)
|
||||
bb = keep_largest_object((blk > bt).astype(np.uint8))
|
||||
bw[r1:r2, c1:c2] = bb
|
||||
bw_clean = remove_small_objects(bw)
|
||||
|
||||
# 统计
|
||||
L, n = ndimage.label(bw_clean)
|
||||
spots = [int(np.sum(L==i)) for i in range(1,n+1)]
|
||||
valid = [s for s in spots if s >= 10]
|
||||
|
||||
# ---- 生成6张图 ----
|
||||
images = {}
|
||||
|
||||
# 1: grid overlay
|
||||
fig, ax = plt.subplots(figsize=(6,6))
|
||||
ax.imshow(gray, cmap='gray')
|
||||
for x in xl: ax.axvline(x=x, color='lime', linewidth=0.5)
|
||||
for y in yl: ax.axhline(y=y, color='lime', linewidth=0.5)
|
||||
ax.set_title(f'Grid ({len(xl)}x{len(yl)})', fontsize=12); ax.axis('off')
|
||||
images['grid_overlay'] = fig_to_base64(fig)
|
||||
|
||||
# 2: col projection
|
||||
fig, ax = plt.subplots(figsize=(10,4))
|
||||
xs = np.arange(len(cp)); ax.plot(xs,cp,'b-',lw=0.6); ax.axhline(y=cT,color='orange',ls='--',lw=1)
|
||||
ax.plot(xs,cs,'g-',lw=0.6,alpha=0.5)
|
||||
ax.fill_between(xs,0,cs,where=(cs>0),color='green',alpha=0.1)
|
||||
ax.fill_between(xs,0,cs,where=(cs<0),color='red',alpha=0.1)
|
||||
for x in xl: ax.axvline(x=x,color='red',lw=0.5,alpha=0.5)
|
||||
ax.set_title('Column Projection', fontsize=12); ax.set_xlabel('column')
|
||||
images['col_projection'] = fig_to_base64(fig)
|
||||
|
||||
# 3: row projection
|
||||
fig, ax = plt.subplots(figsize=(10,4))
|
||||
ys = np.arange(len(rp)); ax.plot(rp,ys,'b-',lw=0.6); ax.axvline(x=rT,color='orange',ls='--',lw=1)
|
||||
ax.plot(rs,ys,'g-',lw=0.6,alpha=0.5)
|
||||
for y in yl: ax.axhline(y=y,color='red',lw=0.5,alpha=0.5)
|
||||
ax.set_title('Row Projection', fontsize=12); ax.set_ylabel('row')
|
||||
images['row_projection'] = fig_to_base64(fig)
|
||||
|
||||
# 4: histogram
|
||||
fig, ax = plt.subplots(figsize=(7,4))
|
||||
ax.hist(gray.ravel(),bins=50,color='#2d8a4e',edgecolor='white',linewidth=0.3)
|
||||
ax.axvline(x=T,color='#ff4444',ls='--',lw=2,label=f'Otsu T={T} ({pct*100:.1f}%)')
|
||||
ax.set_title('Histogram + Otsu', fontsize=12); ax.legend()
|
||||
images['histogram'] = fig_to_base64(fig)
|
||||
|
||||
# 5: segmentation raw
|
||||
fig, ax = plt.subplots(figsize=(6,6))
|
||||
ax.imshow(bw, cmap='gray'); ax.set_title('Segmentation (raw)', fontsize=12); ax.axis('off')
|
||||
images['segmentation_raw'] = fig_to_base64(fig)
|
||||
|
||||
# 6: post processed
|
||||
fig, ax = plt.subplots(figsize=(6,6))
|
||||
ax.imshow(bw_clean, cmap='gray')
|
||||
ax.set_title(f'Post-processed ({len(valid)} spots)', fontsize=12); ax.axis('off')
|
||||
images['post_processed'] = fig_to_base64(fig)
|
||||
|
||||
stats = {
|
||||
'spots': len(valid),
|
||||
'T_otsu': int(T),
|
||||
'pct': round(pct*100, 1),
|
||||
'lines_x': int(len(xl)),
|
||||
'lines_y': int(len(yl)),
|
||||
'width': int(gray.shape[1]),
|
||||
'height': int(gray.shape[0])
|
||||
}
|
||||
|
||||
return {'images': images, 'stats': stats}
|
||||
|
||||
|
||||
# ================================================================
|
||||
# Flask 路由
|
||||
# ================================================================
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/process', methods=['POST'])
|
||||
def process():
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': '未找到文件'}), 400
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'error': '文件名为空'}), 400
|
||||
|
||||
# 读取图像
|
||||
img_bytes = file.read()
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
img_array = np.array(img)
|
||||
|
||||
# 处理
|
||||
try:
|
||||
result = process_image(img_array)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'处理失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import threading
|
||||
def open_browser():
|
||||
os.startfile('http://localhost:5000')
|
||||
threading.Timer(1.5, open_browser).start()
|
||||
app.run(debug=True, port=5000)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""cDNA Analyzer 启动器 — 供 PyInstaller 打包"""
|
||||
import os, sys, threading
|
||||
|
||||
# PyInstaller 打包后资源路径
|
||||
if getattr(sys, 'frozen', False):
|
||||
base = sys._MEIPASS
|
||||
else:
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# 设置模板和静态文件目录
|
||||
os.environ['TEMPLATE_DIR'] = os.path.join(base, 'templates')
|
||||
os.environ['STATIC_DIR'] = os.path.join(base, 'static')
|
||||
|
||||
# 导入 Flask app
|
||||
from app import app
|
||||
|
||||
def open_browser():
|
||||
os.startfile('http://localhost:5000')
|
||||
|
||||
if __name__ == '__main__':
|
||||
threading.Timer(1.5, open_browser).start()
|
||||
app.run(debug=False, port=5000)
|
||||
@@ -0,0 +1,202 @@
|
||||
/* ============================================================
|
||||
cDNA Lab Console — Clean Academic Theme
|
||||
Deep navy + white panels + subtle blue accents
|
||||
============================================================ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #0c1524;
|
||||
--bg-panel: #14233a;
|
||||
--bg-card: #1a2d4a;
|
||||
--border: #1e3354;
|
||||
--border-light: #2a4570;
|
||||
--accent: #4d8ef7;
|
||||
--accent-dim: #2b5db8;
|
||||
--accent-soft: rgba(77, 142, 247, 0.12);
|
||||
--text: #8899b4;
|
||||
--text-dim: #4e6280;
|
||||
--text-bright: #d0ddf0;
|
||||
--white: #f0f4fc;
|
||||
--red: #f55050;
|
||||
--red-soft: rgba(245, 80, 80, 0.12);
|
||||
--green: #52c97d;
|
||||
--font-mono: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
--font-ui: 'Inter', 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-ui);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ---- Header ---- */
|
||||
.header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 20px 36px; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.header-left { display: flex; align-items: center; gap: 10px; }
|
||||
.logo-bracket { color: var(--text-dim); font-size: 1.3rem; font-family: var(--font-mono); }
|
||||
.logo-text {
|
||||
font-family: var(--font-mono); font-size: 1.25rem; font-weight: 500;
|
||||
color: var(--white); letter-spacing: 0.12em;
|
||||
}
|
||||
.logo-accent { color: var(--accent); font-weight: 600; }
|
||||
.header-right { display: flex; align-items: center; gap: 10px; }
|
||||
.status-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; background: var(--green);
|
||||
box-shadow: 0 0 0 3px rgba(82, 201, 125, 0.2);
|
||||
}
|
||||
.status-label { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-dim); letter-spacing: 0.15em; }
|
||||
|
||||
/* ---- Container ---- */
|
||||
.container { max-width: 1120px; margin: 0 auto; padding: 36px 28px; }
|
||||
|
||||
/* ---- Upload Zone ---- */
|
||||
.upload-section { margin-bottom: 32px; }
|
||||
.upload-zone-wrapper { display: flex; flex-direction: column; gap: 16px; }
|
||||
.upload-zone {
|
||||
border: 2px dashed var(--border); border-radius: 10px;
|
||||
padding: 52px 24px; text-align: center; cursor: pointer;
|
||||
transition: all 0.25s ease; background: var(--bg-panel);
|
||||
}
|
||||
.upload-zone:hover {
|
||||
border-color: var(--accent); background: var(--accent-soft);
|
||||
}
|
||||
.upload-zone.dragover { border-color: var(--accent); border-style: solid; background: var(--accent-soft); }
|
||||
.upload-svg { color: var(--text-dim); margin-bottom: 14px; transition: color 0.25s; }
|
||||
.upload-zone:hover .upload-svg { color: var(--accent); }
|
||||
.upload-text {
|
||||
font-family: var(--font-mono); font-size: 1.05rem; color: var(--white);
|
||||
letter-spacing: 0.08em; margin-bottom: 6px;
|
||||
}
|
||||
.upload-cursor { display: inline-block; color: var(--accent); animation: blink 1s step-end infinite; }
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.upload-sub { font-size: 0.85rem; color: var(--text-dim); }
|
||||
|
||||
.upload-preview { display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
||||
.upload-preview img {
|
||||
max-width: 100%; max-height: 320px; border-radius: 8px;
|
||||
border: 1px solid var(--border); object-fit: contain; background: #000;
|
||||
}
|
||||
.btn-process {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 12px 36px; border: none; border-radius: 6px;
|
||||
background: var(--accent); color: #fff;
|
||||
font-family: var(--font-mono); font-size: 0.9rem; font-weight: 500; letter-spacing: 0.1em;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.btn-process:hover { background: var(--accent-dim); transform: translateY(-1px); }
|
||||
|
||||
/* ---- Status Bar ---- */
|
||||
.status-bar {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 10px 18px; background: var(--bg-panel); border: 1px solid var(--border);
|
||||
border-radius: 6px; margin-bottom: 28px;
|
||||
}
|
||||
.status-track { flex: 1; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
|
||||
.status-fill { height: 100%; width: 0%; background: var(--accent); transition: width 0.1s; }
|
||||
.status-text { font-family: var(--font-mono); font-size: 0.75rem; color: var(--accent); letter-spacing: 0.12em; }
|
||||
|
||||
/* ---- Error ---- */
|
||||
.error-msg {
|
||||
background: var(--red-soft); border: 1px solid var(--red); color: var(--red);
|
||||
padding: 14px 20px; border-radius: 6px; margin-bottom: 24px;
|
||||
font-family: var(--font-mono); font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ---- Stats Panel ---- */
|
||||
.stats-panel { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 32px; }
|
||||
.stat-card {
|
||||
flex: 1; min-width: 150px; padding: 20px 24px;
|
||||
background: var(--bg-panel); border: 1px solid var(--border); border-radius: 8px;
|
||||
text-align: center; transition: border-color 0.25s;
|
||||
}
|
||||
.stat-card:hover { border-color: var(--border-light); }
|
||||
.stat-card.spot { border-color: var(--accent-dim); background: var(--accent-soft); }
|
||||
.stat-label {
|
||||
display: block; font-family: var(--font-mono); font-size: 0.68rem;
|
||||
color: var(--text-dim); letter-spacing: 0.18em; margin-bottom: 8px; font-weight: 500;
|
||||
}
|
||||
.stat-value {
|
||||
display: block; font-family: var(--font-mono); font-size: 1.5rem;
|
||||
color: var(--white); font-weight: 500;
|
||||
}
|
||||
.stat-card.spot .stat-value { color: var(--accent); font-size: 2rem; font-weight: 600; }
|
||||
|
||||
/* ---- Gallery ---- */
|
||||
.gallery { margin-bottom: 48px; }
|
||||
.gallery-header {
|
||||
display: flex; justify-content: space-between; align-items: center; margin-bottom: 22px;
|
||||
}
|
||||
.gallery-title {
|
||||
font-family: var(--font-mono); font-size: 1rem; font-weight: 500;
|
||||
color: var(--white); letter-spacing: 0.16em;
|
||||
}
|
||||
.btn-dl-all {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 9px 22px; border: 1px solid var(--border-light); border-radius: 6px;
|
||||
background: transparent; color: var(--text);
|
||||
font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.1em;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.btn-dl-all:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
||||
.btn-dl-all span { font-size: 1rem; }
|
||||
|
||||
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||||
@media (max-width: 800px) { .gallery-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 500px) { .gallery-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.gallery-card {
|
||||
border-radius: 8px; overflow: hidden; background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
transition: border-color 0.25s, transform 0.2s;
|
||||
}
|
||||
.gallery-card:hover { border-color: var(--border-light); transform: translateY(-2px); }
|
||||
.gallery-card-inner {
|
||||
aspect-ratio: 1; overflow: hidden; display: flex; align-items: center; justify-content: center;
|
||||
background: #060c18; cursor: pointer;
|
||||
}
|
||||
.gallery-card-inner img {
|
||||
width: 100%; height: 100%; object-fit: contain; transition: transform 0.3s;
|
||||
}
|
||||
.gallery-card-inner:hover img { transform: scale(1.04); }
|
||||
.gallery-card-label {
|
||||
padding: 10px 14px; font-family: var(--font-mono); font-size: 0.65rem;
|
||||
color: var(--text-dim); letter-spacing: 0.1em; text-align: center;
|
||||
border-top: 1px solid var(--border); font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- Lightbox ---- */
|
||||
.lightbox {
|
||||
display: none; position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(4, 10, 20, 0.95); flex-direction: column; align-items: center; justify-content: center;
|
||||
}
|
||||
.lightbox img {
|
||||
max-width: 92vw; max-height: 88vh; object-fit: contain;
|
||||
border-radius: 4px; border: 1px solid var(--border-light); background: #000;
|
||||
}
|
||||
.lightbox-close {
|
||||
position: absolute; top: 24px; right: 36px;
|
||||
font-size: 2rem; color: var(--text-dim); cursor: pointer; transition: color 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
.lightbox-close:hover { color: var(--white); }
|
||||
.lightbox-dl {
|
||||
margin-top: 18px; padding: 10px 28px; border: 1px solid var(--accent); border-radius: 6px;
|
||||
color: var(--accent); text-decoration: none; font-family: var(--font-mono); font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.lightbox-dl:hover { background: var(--accent); color: #fff; }
|
||||
|
||||
/* ---- Scrollbar ---- */
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
|
||||
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>cDNA Microarray Processing - Lab Console</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="noise-overlay"></div>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="logo-bracket">[</span>
|
||||
<h1 class="logo-text">cDNA<span class="logo-accent">//</span>PROCESS</h1>
|
||||
<span class="logo-bracket">]</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-label">SYSTEM READY</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="upload-section" id="uploadSection">
|
||||
<div class="upload-zone-wrapper">
|
||||
<div class="upload-zone" id="dropZone">
|
||||
<svg class="upload-svg" width="52" height="52" viewBox="0 0 52 52">
|
||||
<circle cx="26" cy="20" r="8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||
<path d="M13 42c-5 0-8-1.5-8-6s3.5-9 8-9c0-6 6-10 13-10s13 4 13 10c4.5 0 8 4.5 8 9s-3.5 6-8 6H13z" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||
<circle cx="22" cy="16" r="2.2" fill="currentColor"/>
|
||||
<circle cx="26" cy="11" r="2.8" fill="currentColor"/>
|
||||
</svg>
|
||||
<p class="upload-text">Drop image here<span class="upload-cursor">_</span></p>
|
||||
<p class="upload-sub">click to browse · TIFF / PNG / JPEG</p>
|
||||
<input type="file" id="fileInput" accept="image/*" hidden>
|
||||
</div>
|
||||
<div class="upload-preview" id="uploadPreview" style="display:none">
|
||||
<img id="previewImg" alt="Preview">
|
||||
<button class="btn-process" id="btnProcess">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><polygon points="3,1 16,8 3,15" fill="currentColor"/></svg>
|
||||
START ANALYSIS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="status-bar" id="statusBar" style="display:none">
|
||||
<div class="status-track">
|
||||
<div class="status-fill" id="statusFill"></div>
|
||||
</div>
|
||||
<span class="status-text" id="statusText">PROCESSING...</span>
|
||||
</div>
|
||||
|
||||
<div class="error-msg" id="errorMsg" style="display:none"></div>
|
||||
|
||||
<section class="stats-panel" id="statsPanel" style="display:none">
|
||||
<div class="stat-card"><span class="stat-label">THRESHOLD</span><span class="stat-value" id="statT">--</span></div>
|
||||
<div class="stat-card"><span class="stat-label">ADAPTIVE %</span><span class="stat-value" id="statPct">--</span></div>
|
||||
<div class="stat-card"><span class="stat-label">GRID</span><span class="stat-value" id="statGrid">--</span></div>
|
||||
<div class="stat-card spot"><span class="stat-label">SPOTS FOUND</span><span class="stat-value" id="statSpots">--</span></div>
|
||||
<div class="stat-card"><span class="stat-label">DIMENSIONS</span><span class="stat-value" id="statSize">--</span></div>
|
||||
</section>
|
||||
|
||||
<section class="gallery" id="gallery" style="display:none">
|
||||
<div class="gallery-header">
|
||||
<h2 class="gallery-title">RESULTS</h2>
|
||||
<button class="btn-dl-all" id="btnDownloadAll"><span>↓</span> DOWNLOAD ALL</button>
|
||||
</div>
|
||||
<div class="gallery-grid" id="galleryGrid"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="lightbox" id="lightbox">
|
||||
<span class="lightbox-close" id="lightboxClose">×</span>
|
||||
<img id="lightboxImg" src="" alt="Full size">
|
||||
<a id="lightboxDownload" class="lightbox-dl" download>↓</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var dz=document.getElementById('dropZone');
|
||||
var fi=document.getElementById('fileInput');
|
||||
var up=document.getElementById('uploadPreview');
|
||||
var pi=document.getElementById('previewImg');
|
||||
var bp=document.getElementById('btnProcess');
|
||||
var sb=document.getElementById('statusBar');
|
||||
var sf=document.getElementById('statusFill');
|
||||
var st=document.getElementById('statusText');
|
||||
var er=document.getElementById('errorMsg');
|
||||
var sp=document.getElementById('statsPanel');
|
||||
var ga=document.getElementById('gallery');
|
||||
var gg=document.getElementById('galleryGrid');
|
||||
var lb=document.getElementById('lightbox');
|
||||
var li=document.getElementById('lightboxImg');
|
||||
var lc=document.getElementById('lightboxClose');
|
||||
var ld=document.getElementById('lightboxDownload');
|
||||
var file=null;
|
||||
|
||||
dz.addEventListener('dragover',function(e){e.preventDefault();dz.classList.add('dragover')});
|
||||
dz.addEventListener('dragleave',function(){dz.classList.remove('dragover')});
|
||||
dz.addEventListener('drop',function(e){e.preventDefault();dz.classList.remove('dragover');if(e.dataTransfer.files.length)h(e.dataTransfer.files[0])});
|
||||
dz.addEventListener('click',function(){fi.click()});
|
||||
fi.addEventListener('change',function(e){if(e.target.files.length)h(e.target.files[0])});
|
||||
|
||||
function h(f){
|
||||
file=f;
|
||||
var r=new FileReader();
|
||||
r.onload=function(e){
|
||||
pi.src=e.target.result;
|
||||
up.style.display='flex';
|
||||
er.style.display='none';
|
||||
sp.style.display='none';
|
||||
ga.style.display='none';
|
||||
};
|
||||
r.readAsDataURL(f);
|
||||
}
|
||||
|
||||
bp.addEventListener('click',function(){
|
||||
if(!file) return;
|
||||
var fd=new FormData(); fd.append('file',file);
|
||||
sb.style.display='block'; sf.style.width='0%'; st.textContent='UPLOADING...'; er.style.display='none';
|
||||
var w=0; var iv=setInterval(function(){w=Math.min(w+2,85);sf.style.width=w+'%'},50);
|
||||
fetch('/process',{method:'POST',body:fd}).then(function(r){
|
||||
clearInterval(iv); sf.style.width='100%'; st.textContent='COMPLETE';
|
||||
if(!r.ok) throw new Error('Server error');
|
||||
return r.json();
|
||||
}).then(function(d){
|
||||
if(d.error) throw new Error(d.error);
|
||||
render(d);
|
||||
setTimeout(function(){sb.style.display='none'},1500);
|
||||
}).catch(function(e){
|
||||
clearInterval(iv); sb.style.display='none';
|
||||
er.textContent='ERROR: '+e.message; er.style.display='block';
|
||||
});
|
||||
});
|
||||
|
||||
function render(d){
|
||||
var s=d.stats;
|
||||
document.getElementById('statT').textContent=s.T_otsu;
|
||||
document.getElementById('statPct').textContent=s.pct+'%';
|
||||
document.getElementById('statGrid').textContent=s.lines_x+' x '+s.lines_y;
|
||||
document.getElementById('statSpots').textContent=s.spots;
|
||||
document.getElementById('statSize').textContent=s.width+' x '+s.height;
|
||||
sp.style.display='flex';
|
||||
var names=['grid_overlay','col_projection','row_projection','histogram','segmentation_raw','post_processed'];
|
||||
var labels=['GRID OVERLAY','COLUMN PROJECTION','ROW PROJECTION','HISTOGRAM + OTSU','SEGMENTATION','POST-PROCESSED'];
|
||||
gg.innerHTML='';
|
||||
names.forEach(function(n,i){
|
||||
var c=document.createElement('div'); c.className='gallery-card';
|
||||
c.innerHTML='<div class="gallery-card-inner"><img src="'+d.images[n]+'" onclick="zoom(\''+n+'.png\',\''+d.images[n]+'\')"></div><div class="gallery-card-label">'+labels[i]+'</div>';
|
||||
gg.appendChild(c);
|
||||
});
|
||||
ga.style.display='block';
|
||||
document.getElementById('btnDownloadAll').onclick=function(){
|
||||
names.forEach(function(n,i){
|
||||
setTimeout(function(){
|
||||
var a=document.createElement('a'); a.href=d.images[n]; a.download=n+'.png';
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
},i*200);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
window.zoom=function(name,src){
|
||||
lb.style.display='flex'; li.src=src; ld.href=src; ld.download=name;
|
||||
};
|
||||
|
||||
lc.addEventListener('click',function(){lb.style.display='none'});
|
||||
lb.addEventListener('click',function(e){if(e.target===lb) lb.style.display='none'});
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape') lb.style.display='none'});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||