Parcourir la source

Add genek-extract-figures and genek-split-figures skills

Made-with: Cursor
zhxd il y a 2 mois
Parent
commit
a81027f3ba

+ 176 - 0
genek-extract-figures/SKILL.md

@@ -0,0 +1,176 @@
+---
+name: genek-extract-figures
+description: 从学术论文 PDF 中提取高质量 Figure 图片。优先从出版商网站下载原始图,备选 pdftoppm 整页转图+裁剪。支持 Springer Nature、Elsevier、Wiley、PLOS 等主流出版商。当用户提及"提取论文图片"、"下载论文 figure"、"论文插图"、"extract figures"时使用此技能。
+---
+
+# 学术论文图片提取
+
+## 工作流程
+
+```
+1. 从论文 PDF 中识别 DOI 和 Figure 数量
+   │
+2. 判断出版商 → 选择提取方式
+   ├── 已知出版商 URL 模板 → 方法一:直接下载(推荐)
+   ├── 未知出版商 → 方法二:浏览器访问论文网页版,手动定位图片 URL
+   └── 无法访问网页版 → 方法三:pdftoppm 整页转图 + 裁剪
+   │
+3. 输出到 papers/{Author}{Year}/figures/ 目录,命名 fig1.png ~ figN.png
+4. 删除中间文件,验证文件大小和完整性
+```
+
+## 方法优先级
+
+| 优先级 | 方法 | 质量 | 适用场景 |
+|--------|------|------|----------|
+| **1** | 出版商网站下载 | 最佳,纯图表 | 已发表论文,出版商 URL 可推断 |
+| **2** | 浏览器访问网页版 | 最佳 | 未知出版商,但论文有网页版 |
+| **3** | `pdftoppm` + 裁剪 | 需后处理 | 无法访问网页版(预印本、内部文档等) |
+| **4** | `pdfimages` | 碎片化,不推荐 | 仅作最后手段 |
+
+## 方法一:出版商网站下载(推荐)
+
+### 第一步:识别出版商
+
+从论文 PDF 中提取 DOI(通常在首页页脚或页眉),根据 DOI 前缀判断出版商:
+
+| DOI 前缀 | 出版商 | URL 模板可用 |
+|----------|--------|-------------|
+| `10.1038/` | Springer Nature | ✅ |
+| `10.1186/` | BMC (Springer) | ✅ |
+| `10.1007/` | Springer | ✅ |
+| `10.1016/` | Elsevier | ⚠️ 需从网页解析 |
+| `10.1002/` | Wiley | ⚠️ 需从网页解析 |
+| `10.1371/` | PLOS | ✅ |
+| `10.3390/` | MDPI | ✅ |
+| `10.1093/` | Oxford Univ Press | ⚠️ 需从网页解析 |
+
+### 第二步:构造 URL 并下载
+
+**Springer Nature / BMC 系列**(Nature, BMC Biology, Genome Biology 等):
+
+```
+https://media.springernature.com/full/springer-static/image/art%3A{DOI_ENCODED}/MediaObjects/{JOURNAL_ID}_{YEAR}_{ARTICLE_ID}_Fig{N}_HTML.png
+```
+
+DOI 解析规则(`10.1186/s12915-022-01301-7` 为例):
+- `JOURNAL_ID` = `12915`(`/s` 后到首个 `-` 之间的数字)
+- `YEAR` = `2022`
+- `ARTICLE_ID` = `1301`(去掉末尾修订号 `-7`)
+
+下载脚本(**替换前5个变量为目标论文参数**):
+
+```bash
+#!/bin/bash
+AUTHOR_YEAR="Li2022"                # ← 替换:第一作者姓氏+年份
+DOI="10.1186/s12915-022-01301-7"    # ← 替换
+JOURNAL_ID="12915"                  # ← 替换
+YEAR="2022"                         # ← 替换
+ARTICLE_ID="1301"                   # ← 替换
+FIG_COUNT=6                         # ← 替换为实际 Figure 数量
+
+OUT="papers/${AUTHOR_YEAR}/figures"
+DOI_ENCODED=$(echo "$DOI" | sed 's|/|%2F|g; s|:|%3A|g')
+mkdir -p "$OUT"
+for i in $(seq 1 $FIG_COUNT); do
+  url="https://media.springernature.com/full/springer-static/image/art%3A${DOI_ENCODED}/MediaObjects/${JOURNAL_ID}_${YEAR}_${ARTICLE_ID}_Fig${i}_HTML.png"
+  curl -sL -o "${OUT}/fig${i}.png" "$url"
+  size=$(wc -c < "${OUT}/fig${i}.png" 2>/dev/null || echo 0)
+  if [ "$size" -gt 10000 ]; then
+    echo "fig${i}.png: OK (${size} bytes)"
+  else
+    echo "fig${i}.png: FAILED (${size} bytes) - URL 可能不正确"
+    rm -f "${OUT}/fig${i}.png"
+  fi
+done
+```
+
+其他出版商 URL 模板详见 → [references/publishers.md](references/publishers.md)
+
+## 方法二:浏览器访问网页版
+
+URL 模板未知或下载失败时:
+
+1. 用浏览器打开 `https://doi.org/{DOI}` 跳转到论文网页版
+2. 在文章中找到 Figure,点击 "Full size image" / "Download" / "High resolution"
+3. 用开发者工具(F12 → Network)获取图片真实 URL
+4. 用 `curl -sL -o figures/figN.png "URL"` 逐张下载
+5. **发现新的 URL 规律时**,补充到 [references/publishers.md](references/publishers.md)
+
+## 方法三:PDF 整页转图(备选)
+
+前置条件:`brew install poppler`(macOS)或 `apt install poppler-utils`(Linux)
+
+```bash
+pdftoppm -png -r 300 paper.pdf figures/page
+```
+
+转换后人工确认 Figure 对应的页码,然后重命名:
+
+```bash
+mv figures/page-03.png figures/fig1.png
+mv figures/page-05.png figures/fig2.png
+# ...按实际页码调整
+rm figures/page-*.png
+```
+
+### 裁剪(精度差,尽量避免)
+
+```python
+from PIL import Image
+img = Image.open("figures/fig1.png")
+# (left, upper, right, lower) — 需逐张手动确定坐标
+cropped = img.crop((35, 210, 2445, 2815))
+cropped.save("figures/fig1.png")
+```
+
+已知问题:页眉位置因期刊不同而变化;图注与正文分界不清晰;耗时且效果不稳定。
+
+## 方法四:pdfimages(不推荐)
+
+```bash
+pdfimages -png paper.pdf figures/img
+```
+
+学术 Figure 通常由多个子面板(A, B, C...)+ 向量图形混合存储,`pdfimages` 只能提取嵌入位图,结果碎片化。
+
+## 输出目录规范
+
+每篇论文以 `papers/{Author}{Year}/` 为根目录,PDF 和提取图片均放在该目录下:
+
+```
+papers/
+├── Li2022/
+│   ├── paper.pdf
+│   └── figures/
+│       ├── fig1.png
+│       └── fig2.png
+├── Zhang2023/
+│   ├── paper.pdf
+│   └── figures/
+│       ├── fig1.png
+│       └── fig2.png
+└── Wang2024a/              ← 同姓同年加 a/b 后缀
+    ├── paper.pdf
+    └── figures/
+        └── fig1.png
+```
+
+命名规则:
+- 目录名取第一作者 **姓氏**(首字母大写)+ 发表年份,如 `Li2022`
+- 同姓同年冲突时追加小写字母:`Wang2024a`、`Wang2024b`
+- 从论文 PDF 首页提取作者和年份信息
+
+### 文件命名
+
+- 主图:`fig1.png` ~ `fig{N}.png`
+- 补充图:`sfig1.png` ~ `sfig{N}.png`
+- 删除所有中间文件(`page-*.png`、`tmp-*.png`、`img-*.png`)
+
+## 验证清单
+
+- [ ] 每张图片 > 10KB(空文件说明 URL 错误)
+- [ ] 图片数量与论文 Figure 数一致
+- [ ] 图片内容是纯图表(无页眉页脚、无正文)
+- [ ] 命名连续且与论文编号对应
+- [ ] 多论文时各子目录命名正确(作者姓氏+年份)

+ 136 - 0
genek-extract-figures/references/publishers.md

@@ -0,0 +1,136 @@
+# 出版商图片 URL 模板
+
+> 遇到新出版商时,访问论文网页版,查找 "Full size image" 链接,
+> 用浏览器 DevTools 获取图片真实 URL,总结模板后补充到此文档。
+
+## Springer Nature / BMC
+
+适用:Nature, Nature Methods, BMC Biology, Genome Biology, Scientific Reports 等。
+
+**URL 格式**:
+
+```
+https://media.springernature.com/full/springer-static/image/art%3A{DOI_ENCODED}/MediaObjects/{JOURNAL_ID}_{YEAR}_{ARTICLE_ID}_Fig{N}_HTML.png
+```
+
+**DOI 解析规则**:
+
+```
+DOI:  10.1186/s12915-022-01301-7
+               ^^^^^  ^^^^  ^^^^
+               |      |     |
+               |      |     ARTICLE_ID = 1301(去掉 -7 修订号)
+               |      YEAR = 2022
+               JOURNAL_ID = 12915(/s 后到首个 - 之间)
+```
+
+| DOI 示例 | JOURNAL_ID | YEAR | ARTICLE_ID |
+|----------|-----------|------|------------|
+| `10.1186/s12915-022-01301-7` | 12915 | 2022 | 1301 |
+| `10.1038/s41586-023-06004-9` | 41586 | 2023 | 6004 |
+| `10.1186/s13059-021-02500-z` | 13059 | 2021 | 2500 |
+| `10.1038/s41467-024-45678-3` | 41467 | 2024 | 45678 |
+
+**注意**:部分旧文章或 review 的 URL 可能不符合此模板,需回退到浏览器方式。
+
+---
+
+## PLOS
+
+适用:PLOS ONE, PLOS Biology, PLOS Genetics 等。
+
+**URL 格式**:
+
+```
+https://journals.plos.org/plosone/article/figure/image?id=10.1371/journal.pone.{ARTICLE_ID}.g{NNN}
+```
+
+其中 `NNN` 为 3 位数 Figure 编号(`001`, `002`, ...)。
+
+示例(DOI `10.1371/journal.pone.0250000`):
+
+```bash
+for i in $(seq 1 5); do
+  n=$(printf "%03d" $i)
+  curl -sL -o "figures/fig${i}.png" \
+    "https://journals.plos.org/plosone/article/figure/image?id=10.1371/journal.pone.0250000.g${n}"
+done
+```
+
+**注意**:URL 中的期刊名需匹配(`plosone` / `plosbiology` / `plosgenetics` 等)。
+
+---
+
+## MDPI
+
+适用:所有 MDPI 期刊(Cells, IJMS, Genes, Cancers 等)。
+
+**URL 格式**:
+
+```
+https://www.mdpi.com/{JOURNAL_PATH}/{VOLUME}/{ISSUE}/{ARTICLE_ID}/{ARTICLE_SLUG}-g{NNN}.png
+```
+
+MDPI 文章网页版图片直接嵌入且可右键保存,URL 规律性强。建议直接访问网页版下载。
+
+---
+
+## Elsevier (Cell, Lancet, etc.)
+
+**无固定 URL 模板**,图片通过 CDN 分发(`ars.els-cdn.com`),路径含随机 hash。
+
+提取策略:
+1. 访问 `https://doi.org/{DOI}` → 跳转到 ScienceDirect 文章页
+2. 点击 Figure → "Download high-res image"
+3. 从 DevTools Network 面板复制图片 URL
+4. URL 通常形如:`https://ars.els-cdn.com/content/image/1-s2.0-S{ID}-gr{N}_lrg.jpg`
+
+---
+
+## Wiley
+
+**无固定 URL 模板**。
+
+提取策略:
+1. 访问 `https://doi.org/{DOI}` → 跳转到 Wiley Online Library
+2. 文章页内图片通常可直接右键保存
+3. 高分辨率图片需点击 "Open in figure viewer" → "Download PowerPoint"
+
+---
+
+## Oxford University Press (OUP)
+
+适用:Nucleic Acids Research, Bioinformatics 等。
+
+**URL 格式**(部分期刊):
+
+```
+https://oup.silverchair-cdn.com/oup/backfile/Content_public/Journal/{JOURNAL}/{VOLUME}/{ISSUE}/{DOI_SUFFIX}/{FIGURE_FILE}
+```
+
+路径不稳定,建议优先从文章网页版下载。
+
+---
+
+## 预印本(bioRxiv / medRxiv)
+
+**URL 格式**:
+
+```
+https://www.biorxiv.org/content/biorxiv/early/{YEAR}/{MONTH}/{DAY}/{ID}/F{N}.large.jpg
+```
+
+预印本图片质量通常较低(JPEG 压缩),建议:
+1. 若已正式发表,从正式出版商下载
+2. 若仅预印本可用,从网页版下载 `.large.jpg` 版本
+
+---
+
+## 通用回退策略
+
+当上述模板均不适用时:
+
+1. **浏览器访问** `https://doi.org/{DOI}`
+2. **搜索页面** 中的 "Full size image"、"Download figure"、"High resolution" 链接
+3. **DevTools (F12)** → Network → 过滤 `image` → 刷新页面 → 找到图片请求
+4. **记录 URL 规律** → 补充到本文档对应出版商章节

+ 284 - 0
genek-split-figures/SKILL.md

@@ -0,0 +1,284 @@
+---
+name: genek-split-figures
+description: 识别科研论文 Figure 中子图(panel)的边界并裁剪。采用 LLM 语义分组 + 像素精修的混合方案,输出每个 panel 组的精确矩形坐标并生成可视化验证图。当用户提及"子图边界"、"panel 识别"、"figure 拆分"、"提取子图"、"split figure"时使用此技能。
+---
+
+# 科研 Figure 子图边界识别
+
+支持两种模式:
+- **自动模式**:LLM 看图自行判断分组
+- **用户指定模式**:用户给出分组方案(如 `A, DEFG, BCH`),LLM 据此定位
+
+采用混合方案:语义理解(确定每个 panel 位置)+ 像素级白色间隙分析(精确定位边界)。
+
+## 目录约定
+
+与 `genek-extract-figures` 共用 `papers/{Author}{Year}/` 结构:
+
+- **输入**:`papers/{Author}{Year}/figures/fig{N}.png`(原图)
+- **输出**:`papers/{Author}{Year}/figures/panels/fig{N}_{panels}.png`(拆分后的 panel)
+
+```
+papers/Li2022/
+├── paper.pdf
+├── figures/
+│   ├── fig1.png                  ← 输入(extract-figures 产出)
+│   ├── fig2.png
+│   └── panels/                   ← 输出目录
+│       ├── fig1_A.png            ← 单 panel
+│       ├── fig1_DEFG.png         ← 多 panel 合并为一组
+│       ├── fig2_BC.png
+│       └── ...
+```
+
+Panel 文件命名:`fig{N}_{panels}.png`
+- 单 panel:`fig1_A.png`
+- 多 panel 合为一组:`fig1_BC.png`、`fig1_DEFG.png`(按原 panel 标签排列拼接)
+
+## 流程
+
+```
+1. 确定分组方案(用户指定 或 LLM 自动判断)
+2. LLM 看图 → 定位每个 panel 的位置 + 给出每组粗坐标
+3. 像素分析 → 运行 analyze_image,解读间隙输出,与粗坐标对齐确定精确裁剪坐标
+4. 裁剪并保存到 papers/{Author}{Year}/figures/panels/
+5. 生成带彩色边框的验证图 → Read 工具逐张查看
+6. 如有偏差 → 调整坐标,重新裁剪 + 验证
+7. 全部确认后删除验证图
+```
+
+## 第一步:确定分组 + LLM 定位
+
+### 模式 A:用户指定分组
+
+用户输入示例:`从 fig1.png 中提取 A, DEFG, BCH`
+
+解析规则:
+- 每个逗号分隔项为一组,组名即 panel 标签拼接(如 `BCH` = 面板 B + C + H)
+- LLM 看图后定位每个**单独 panel** 的矩形范围
+- 每组的边界 = 组内所有 panel 的最小外接矩形(min x1, min y1, max x2, max y2)
+
+输出定位表:
+
+```
+单 panel 定位:
+| Panel | 位置 (x1, y1, x2, y2) |
+|-------|----------------------|
+| A     | (0, 0, 545, 570)    |
+| B     | (0, 580, 540, 940)   |
+| C     | (540, 580, 1928, 940)|
+| D     | (545, 0, 1240, 310)  |
+| E     | (1240, 0, 1928, 310) |
+| F     | (545, 310, 1240, 570) |
+| G     | (1240, 310, 1928, 570)|
+| H     | (0, 950, 1928, 1310) |
+
+用户分组聚合:
+| 组名  | 包含 panel | 聚合坐标              |
+|-------|-----------|----------------------|
+| A     | A         | (0, 0, 545, 570)     |
+| DEFG  | D,E,F,G   | (545, 0, 1928, 570)  |
+| BCH   | B,C,H     | (0, 580, 1928, 1310) |
+```
+
+### 模式 B:LLM 自动分组
+
+LLM 看图后自行输出分组表:
+
+```
+| 组名 | 包含 panel | 分组理由 |
+|------|-----------|---------|
+| AB   | A, B      | 两个 Hi-C 视图由箭头连接 |
+| C    | C         | 独立表格 |
+| DE   | D, E      | 底部分析行,内容相似 |
+```
+
+**自动分组判断依据**(按优先级):
+- 内容相似性(如两个散点图、两个柱状图)
+- 共享元素(共用图例、共用坐标轴)
+- 连接关系(箭头、虚线框)
+
+两种模式共同输出每组的粗坐标估计 `(left, top, right, bottom)`。
+
+## 第二步:像素精修
+
+用 Python + PIL + numpy,对图像做白色间隙检测。
+
+### 核心算法
+
+```python
+from PIL import Image
+import numpy as np
+
+def find_gaps(arr_1d, min_len=10, thresh=252):
+    """在一维亮度数组中找连续白色区段。"""
+    gaps = []
+    i, n = 0, len(arr_1d)
+    while i < n:
+        if arr_1d[i] > thresh:
+            start = i
+            while i < n and arr_1d[i] > thresh:
+                i += 1
+            if i - start >= min_len:
+                gaps.append((start, i, i - start))
+        else:
+            i += 1
+    return gaps
+
+def analyze_image(img_path):
+    gray = np.array(Image.open(img_path).convert('L'))
+    h, w = gray.shape
+    row_mean = gray.mean(axis=1)
+    h_gaps = find_gaps(row_mean, min_len=12)
+    # 过滤边缘间隙
+    h_interior = [(s, e, l) for s, e, l in h_gaps if s > 10 and e < h - 10]
+
+    # 按间隙大小排序,取前 4-5 个主要间隙定义水平分带
+    h_interior.sort(key=lambda x: x[2], reverse=True)
+    major_h = sorted(h_interior[:5], key=lambda x: x[0])
+
+    # 对每个水平带做垂直间隙分析
+    edges = [0]
+    for s, e, _ in major_h:
+        edges.extend([s, e])
+    edges.append(h)
+
+    bands = []
+    for i in range(0, len(edges) - 1, 2):
+        y1, y2 = edges[i], edges[i + 1]
+        if y2 - y1 < 40:
+            continue
+        band_col = gray[y1:y2, :].mean(axis=0)
+        v_gaps = find_gaps(band_col, min_len=8)
+        v_interior = [(s, e, l) for s, e, l in v_gaps if s > 10 and e < w - 10]
+        v_interior.sort(key=lambda x: x[2], reverse=True)
+        bands.append({'y': (y1, y2), 'v_gaps': v_interior[:3]})
+
+    return w, h, major_h, bands
+```
+
+### 精修策略
+
+1. **水平分界**:取主要 H gap 的 `(start, end)` → 上方 panel 底边 = `start`,下方 panel 顶边 = `end`
+2. **垂直分界**:在每个水平带内独立计算 → 取最大 V gap 的 `(start, end)`
+3. **对齐粗坐标**:LLM 粗坐标用于选择"哪个间隙是真正的 panel 分界"(而非 panel 内部空白)
+
+### 关键注意事项
+
+- **panel 内部也有空白**(如 fig1 的 Contig N50 和 Scaffold N50 之间的 "//" 断裂),不能盲目选最大间隙
+- **数字表格会产生大量假水平间隙**(如 fig4 的 CTCF 数值表),需要 LLM 判断哪些间隙是真实分界
+- **不同水平带的垂直分界位置往往不同**(如 fig1 上半部分 A|DEFG 分界在 x=845,中间部分 B|C 分界完全不同)
+
+### 实操流程
+
+对每张 Figure 运行 `analyze_image`,输出格式如下:
+
+```
+=== fig1.png (1928x2000) ===
+Top H gaps (y): [(951, 1007, 56), (1494, 1543, 49)]
+Top V gaps (x): []
+  Band y=[0:951] V gaps: [(866, 891, 25), (1419, 1437, 18)]
+  Band y=[1007:1494] V gaps: [(511, 566, 55)]
+  Band y=[1543:2000] V gaps: [(1107, 1168, 61), (730, 769, 39)]
+```
+
+解读方法:
+
+1. **H gaps → 水平分带**:`(951, 1007)` 表示 y=951~1007 是白色间隙,将图像分成上 `[0:951]`、中 `[1007:1494]`、下 `[1543:2000]` 三个水平带
+2. **每个带内的 V gaps → 垂直分界**:上层带 V gap `(866, 891)` 表示 x=866~891 是垂直白色间隙
+3. **与 LLM 粗坐标对齐**:LLM 判断上层带左侧为 A、右侧为 DEFG → 精确边界为 A 右边 = x=866,DEFG 左边 = x=891
+4. **确定裁剪坐标**:`A = (0, 0, 866, 951)`,`DEFG = (891, 0, 1928, 951)`
+
+关键原则:
+- 间隙的 `start` 是上/左 panel 的底/右边界,`end` 是下/右 panel 的顶/左边界
+- 选择哪个间隙作为分界由 LLM 粗坐标决定(选最接近粗坐标的间隙)
+- 如果一个带内没有 V gap,说明该带只有一个 panel 跨全宽
+
+## 第三步:裁剪并保存
+
+确定所有坐标后,批量裁剪:
+
+```python
+from PIL import Image
+import os
+
+def crop_panels(fig_dir, panel_dir, fig_num, panels):
+    """
+    panels: dict of {group_name: (x1, y1, x2, y2)}
+    例如: {"A": (0, 0, 866, 951), "DEFG": (891, 0, 1928, 951)}
+    """
+    os.makedirs(panel_dir, exist_ok=True)
+    img = Image.open(f"{fig_dir}/fig{fig_num}.png")
+    for name, box in panels.items():
+        cropped = img.crop(box)
+        out = f"{panel_dir}/fig{fig_num}_{name}.png"
+        cropped.save(out)
+        size = os.path.getsize(out)
+        print(f"  fig{fig_num}_{name}.png: {cropped.size[0]}x{cropped.size[1]} ({size} bytes)")
+```
+
+调用示例(以 Li2022 Fig 1 为例):
+
+```python
+fig_dir = "papers/Li2022/figures"
+panel_dir = "papers/Li2022/figures/panels"
+
+crop_panels(fig_dir, panel_dir, 1, {
+    "A":    (0,   0,    866, 951),
+    "DEFG": (891, 0,    1928, 951),
+    "B":    (0,   1007, 511, 1494),
+    "C":    (566, 1007, 1928, 1494),
+    "H":    (0,   1543, 1928, 2000),
+})
+```
+
+## 第四步:可视化验证
+
+```python
+from PIL import Image, ImageDraw, ImageFont
+
+COLORS = ["#E74C3C", "#2980B9", "#27AE60", "#8E44AD", "#E67E22", "#1ABC9C"]
+LINE_W = 5
+
+def draw_boundaries(img_path, panels, out_path):
+    """panels: dict of {name: (x1, y1, x2, y2)}"""
+    img = Image.open(img_path).convert('RGB')
+    draw = ImageDraw.Draw(img)
+    try:
+        font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 28)
+    except Exception:
+        font = ImageFont.load_default()
+
+    for i, (name, box) in enumerate(panels.items()):
+        c = COLORS[i % len(COLORS)]
+        x1, y1, x2, y2 = box
+        for j in range(LINE_W):
+            draw.rectangle([x1 + j, y1 + j, x2 - j, y2 - j], outline=c)
+        bb = draw.textbbox((0, 0), name, font=font)
+        tw, th = bb[2] - bb[0], bb[3] - bb[1]
+        tx, ty = x1 + 8, y1 + 8
+        draw.rectangle([tx - 2, ty - 2, tx + tw + 4, ty + th + 4],
+                        fill='white', outline=c)
+        draw.text((tx, ty), name, fill=c, font=font)
+    img.save(out_path)
+```
+
+### 验证流程
+
+1. 对每张 Figure 调用 `draw_boundaries` 生成 `fig{N}_verify.png`
+2. 用 Read 工具逐张查看验证图,确认彩色边框准确框住对应 panel
+3. 如有问题:
+   - 框太大(侵入相邻 panel)→ 缩小对应边
+   - 框太小(截断内容)→ 检查是否选错了间隙,或 panel 内部有大空白被误判为分界
+   - 框位置完全错误 → 重新看图修正 LLM 分组
+4. 修正后重新裁剪(第三步)并重新验证
+5. 全部确认后删除验证图:`rm papers/{Author}{Year}/figures/panels/*_verify.png`
+
+## 经验教训(来自实测)
+
+| 问题 | 原因 | 解决方法 |
+|------|------|---------|
+| Scaffold N50 柱状图被截断 | 内部边框线被误判为 panel 分界 | 在粗坐标附近多检查几个间隙候选 |
+| fig4 被切成 11 块 | 数字表格的行间空白被当成 panel 间隙 | LLM 先确定"应该有几个分区" |
+| OCR 漏检 panel 标签 | Tesseract 对粗体单字母识别率仅 60-70% | 不依赖 OCR,以 LLM 视觉为主 |
+| 纯像素分析过度切分 | 无语义理解 | 必须 LLM 先定分组,再用像素精修 |