fullseye

図注(figure annotation)— 学術図に「どこに何があるか」を描く 使い方ガイド

この族は何をする道具箱か

論文・報告書の図は「どこに何があるか」を矢印や線で示すのが作法です。Fullseye の annotate 族(台帳 opsannotate.py、46 op / 7 カテゴリ)は、その作法を op として持ちます。numpy + scipy(文字だけ Pillow)で動き、matplotlib は使いません。

ファミリ共通の契約(fail-closed)

代表的なパイプライン(op の繋がり)

flowchart LR
    M[マスク / 点 / 場] --> L[annotate_*_layout<br/>幾何を table で]
    L --> D[annotate_leader / dimension / angle /<br/>scale_bar / outline / inset / text_path]
    F[スカラ場] --> C[annotate_colorbar]
    D --> P[annotate_panel_label]
    C --> P
    P --> G[annotate_figure_grid<br/>一枚の図]
    V[mesh + pose + K] --> R[render3d.render_mesh<br/>depth]
    R --> A3[annotate3d_label / arrow / scale_bar /<br/>axes / bbox / measure]
    A3 --> P

使い方

2-D: 引き出し線・寸法・角度・スケールバー・輪郭

import numpy as np
import annotate as A

img = np.full((240, 320, 3), 0.15)                       # (H,W,3) float [0,1]
mask = np.zeros((240, 320), bool); mask[70:150, 70:150] = True

# 幾何を先に決める(table)→ 描く。同じ layout を別の絵にも渡せる
pts = [(110, 110), (240, 60)]
lay = A.annotate_leader_layout(img.shape[:2], pts, ["disk", "bar"], gap=24)
out = A.annotate_leader(img, pts, ["disk", "bar"], gap=24, layout=lay)
assert lay["items"][0]["elbow"] == (110 + lay["items"][0]["side"][0] * 24,
                                    110 + lay["items"][0]["side"][1] * 24)

out = A.annotate_dimension(out, (60, 200), (160, 200), 0.4, "mm", offset=-24)   # 40.0 mm
out = A.annotate_angle(out, (260, 150), (210, 150), (210, 100), radius=30)      # 90.0°
sb = A.annotate_scale_bar_layout(img.shape[:2], 0.4, "mm", corner="rb")          # 切りのよい長さ
out = A.annotate_scale_bar(out, 0.4, "mm", corner="rb", layout=sb)
out = A.annotate_outline(out, mask, label="ROI")                                 # 輪郭 + 重心ラベル
out = A.annotate_markers(out, [(80, 40), (280, 200)], start=1)
out = A.annotate_legend(out, ["defect", "reference"], (310, 230), anchor="rb", start=1)
out = A.annotate_panel_label(out, "a")
print(sb["length"], sb["px"], A.annotate_outline_layout(mask)["area"])

2-D: 図の組版

import numpy as np
import annotate as A

panels = [np.full((120, 160, 3), v) for v in (0.3, 0.5, 0.7)]
lay = A.annotate_figure_grid_layout([p.shape[:2] for p in panels], ncols=2, pad=12, caption_h=30)
fig = A.annotate_figure_grid(panels, ["before", "after", "difference"], ncols=2, pad=12, caption_h=30)
assert fig.shape[:2] == lay["size"]           # 大きさは閉形式
assert lay["letters"] == ["(a)", "(b)", "(c)"]

3-D: レンダリングの上に射影して描く

import numpy as np
import annotate3d as T
import render3d

K = render3d.intrinsics_from_fov(45.0, 320, 240)
pose = render3d.look_at((0.0, 0.0, 10.0), (0.0, 0.0, 0.0), up=(0.0, 1.0, 0.0))
img = np.full((240, 320, 3), 0.2)
out = T.annotate3d_axes(img, pose, K, length=1.0)
out = T.annotate3d_bbox(out, ((-1, -1, -1), (1, 1, 1)), pose, K)
out = T.annotate3d_label(out, "apex", (0.0, 1.0, 0.0), pose, K, offset=(30, -26))
out = T.annotate3d_scale_bar(out, (-1.0, -2.0, 0.0), (1, 0, 0), 2.0, pose, K, unit="mm")
out = T.annotate3d_measure(out, (-1.0, 0.0, 0.0), (1.0, 0.0, 0.0), pose, K, unit="mm")

tab = T.annotate3d_project([(1.0, 2.0, 0.0)], pose, K, shape=(240, 320))
f, cx, cy = K[0, 0], K[0, 2], K[1, 2]
assert np.allclose(tab["uv"][0], (f * 1.0 / 10 + cx, cy - f * 2.0 / 10), atol=1e-9)
# render_mesh(...)["depth"] を depth= に渡すと tab["hidden"] が立ち、ラベルは破線になる

台帳での呼び方

import opsannotate, ops3d
opsannotate.list_ops("paper")                  # 21 op
opsannotate.call("annotate_scale_bar_layout", (240, 320), 0.4, "mm")
ops3d.list_ops("annotate3d")                   # 7 op

正直な限界