name: genek-split-figures
支持两种模式:
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
fig1_A.pngfig1_BC.png、fig1_DEFG.png(按原 panel 标签排列拼接)严格要求(禁止规避):
_whole、_full、_all 等占位组名(如 fig1_whole.png)。输出文件的 {panels} 必须为论文图中真实存在的 panel 标签(如 A、B、BC、DEFG),与图中标注一致。1. 确定分组方案(用户指定 或 LLM 自动判断)
2. LLM 看图 → 定位每个 panel 的位置 + 给出每组粗坐标
3. 像素分析 → 运行 analyze_image,解读间隙输出,与粗坐标对齐确定精确裁剪坐标
4. 裁剪并保存到 papers/{Author}{Year}/figures/panels/
5. 生成带彩色边框的验证图 → Read 工具逐张查看
6. 如有偏差 → 调整坐标,重新裁剪 + 验证
7. 全部确认后删除验证图
用户输入示例:从 fig1.png 中提取 A, DEFG, BCH
解析规则:
BCH = 面板 B + C + H)输出定位表:
单 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) |
自动模式的核心约束:每张 Figure 最终输出 2 或 3 个子图组,绝不超过 3 个。
LLM 看图后按以下步骤输出分组表:
若 panel 数 > 3,必须聚合到 ≤ 3 组(按下方合并策略)
| 组名 | 包含 panel | 分组理由 |
|-------|-----------|---------------------------|
| ABC | A, B, C | 上层行:系统发育树 + 质量条形图 + 表格 |
| DEFG | D, E, F, G | 中层行:4 张相关散点图 |
| H | H | 底层行:独立 3D 建模图 |
合并策略(按优先级):
禁止:
两种模式共同输出每组的粗坐标估计 (left, top, right, bottom)。
用 Python + PIL + numpy,对图像做白色间隙检测。
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
(start, end) → 上方 panel 底边 = start,下方 panel 顶边 = end(start, end)对每张 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)]
解读方法:
(951, 1007) 表示 y=951~1007 是白色间隙,将图像分成上 [0:951]、中 [1007:1494]、下 [1543:2000] 三个水平带(866, 891) 表示 x=866~891 是垂直白色间隙A = (0, 0, 866, 951),DEFG = (891, 0, 1928, 951)关键原则:
start 是上/左 panel 的底/右边界,end 是下/右 panel 的顶/左边界确定所有坐标后,批量裁剪:
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 为例):
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),
})
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)
draw_boundaries 生成 fig{N}_verify.pngrm papers/{Author}{Year}/figures/panels/*_verify.png| 问题 | 原因 | 解决方法 |
|---|---|---|
| Scaffold N50 柱状图被截断 | 内部边框线被误判为 panel 分界 | 在粗坐标附近多检查几个间隙候选 |
| fig4 被切成 11 块 | 数字表格的行间空白被当成 panel 间隙 | LLM 先确定"应该有几个分区" |
| OCR 漏检 panel 标签 | Tesseract 对粗体单字母识别率仅 60-70% | 不依赖 OCR,以 LLM 视觉为主 |
| 纯像素分析过度切分 | 无语义理解 | 必须 LLM 先定分组,再用像素精修 |