280060db82
- Introduced multiple regression experiment configurations targeting vf_md, including: - cd_solo_reg_set.json: CD tower only regression setup. - img_solo_reg_set.json: Image tower only regression setup. - reg_head_epoch_sweep.json: Baseline regression sweeps at different epochs (50, 75, 100). - reg_head_set.json: Various regression setups including baseline and OrthoBridge configurations. - single_eye_reg.json: Single-eye regression setup for worst-eye aggregation analysis. - Added ensemble configurations for OrthoBridge with different inner bridges: - ortho_alts_ensemble.json: Ensemble tests with ConcatBridge, PairwiseAdditiveBridge, and GatedAdditiveBridge. - ortho_alts_tritower.json: Tritower tests with the same inner bridges. - Created V2-M specific configurations: - baseline_reg_nt50.json: Regression baseline with V2-M backbone. - geom_vec_gt.json and geom_vec_unet.json: Geometry vector injection experiments with V2-M. - single_l1_bridges.json: Single-eye ensemble experiments with various bridge types. - tritower_geom_gt.json: Tritower setup with GT contour-rasterized masks. - Promoted existing experiments to higher repetitions for robustness.
190 lines
7.7 KiB
Python
190 lines
7.7 KiB
Python
"""F1 — System architecture diagram.
|
|
|
|
Bilateral multimodal fusion architecture. Modelled on v3's architecture_fused_head
|
|
but adapted for v4 + manuscript terminology:
|
|
|
|
* "OD HyperTower" / "OS HyperTower" -> "OD Fusion" / "OS Fusion"
|
|
* Bridge boxes show their math explicitly (image projection, clinical
|
|
projection, fusion operation), no longer abbreviated "Bridge"
|
|
* Title drops the HyperTower brand
|
|
|
|
Re-run anytime:
|
|
python -m v4.figures.F1_architecture
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.patches import FancyBboxPatch
|
|
|
|
OUT = Path(__file__).parent / "output" / "F1_architecture.png"
|
|
|
|
# ── Palette (matches v3 plot_architecture.py for visual consistency) ────────
|
|
C_IMG = "#4e8d3a" # green — image / CNN
|
|
C_MD = "#4c72b0" # blue — clinical network
|
|
C_BRIDGE = "#c44e52" # red — fusion bridge
|
|
C_HEAD = "#d4a017" # gold — patient-level head
|
|
C_OUT = "#8c6bb1" # purple — output classes
|
|
C_INPUT = "#a0a0a0" # grey — raw inputs
|
|
C_BG = "#e8e8e8"
|
|
C_ARROW = "#444444"
|
|
FONT = "DejaVu Sans"
|
|
|
|
|
|
# ── Primitives ───────────────────────────────────────────────────────────────
|
|
|
|
def _box(ax, cx, cy, w, h, color, text="", fontsize=9, text_color="white",
|
|
bold=False, alpha=0.92, radius=0.12, lw=1.5):
|
|
patch = FancyBboxPatch(
|
|
(cx - w / 2, cy - h / 2), w, h,
|
|
boxstyle=f"round,pad=0,rounding_size={radius}",
|
|
facecolor=color, edgecolor="white", linewidth=lw, alpha=alpha, zorder=3,
|
|
transform=ax.transData,
|
|
)
|
|
ax.add_patch(patch)
|
|
if text:
|
|
ax.text(cx, cy, text, ha="center", va="center",
|
|
fontsize=fontsize, color=text_color,
|
|
fontweight="bold" if bold else "normal",
|
|
fontfamily=FONT, zorder=4)
|
|
return patch
|
|
|
|
|
|
def _arrow(ax, x0, y0, x1, y1, lw=1.4, color=C_ARROW, style="-|>"):
|
|
ax.annotate("", xy=(x1, y1), xytext=(x0, y0),
|
|
arrowprops=dict(arrowstyle=style, color=color, lw=lw),
|
|
zorder=2)
|
|
|
|
|
|
def _text(ax, x, y, s, fontsize=9, color="#333", ha="center", va="center", bold=False):
|
|
ax.text(x, y, s, ha=ha, va=va, fontsize=fontsize, color=color,
|
|
fontfamily=FONT, fontweight="bold" if bold else "normal", zorder=5)
|
|
|
|
|
|
def _bracket(ax, x, y0, y1, text="", pad=0.20, fontsize=9, badge_color="#555"):
|
|
mid = (y0 + y1) / 2
|
|
ax.plot([x, x + pad, x + pad, x], [y1, y1, y0, y0],
|
|
color=badge_color, lw=1.4, solid_capstyle="round", zorder=2)
|
|
if text:
|
|
ax.text(x + pad * 1.4, mid, text, ha="left", va="center",
|
|
fontsize=fontsize, color="white", fontfamily=FONT, fontweight="bold",
|
|
zorder=6,
|
|
bbox=dict(facecolor=badge_color, edgecolor="none", pad=3.5,
|
|
boxstyle="round,pad=0.3"))
|
|
|
|
|
|
def _draw_output(ax, x, y, classes=("Glaucoma", "Normal")):
|
|
bw, bh, gap = 1.10, 0.38, 0.08
|
|
n = len(classes)
|
|
total = n * bh + (n - 1) * gap
|
|
y_top = y + total / 2 - bh / 2
|
|
for i, cls in enumerate(classes):
|
|
cy = y_top - i * (bh + gap)
|
|
_box(ax, x + bw / 2, cy, bw, bh, C_OUT, cls, fontsize=8.5, radius=0.08)
|
|
_arrow(ax, x, y, x, cy, lw=1.1, style="-|>")
|
|
_text(ax, x + bw / 2, y - total / 2 - 0.20, "Softmax",
|
|
fontsize=7.5, color=C_OUT)
|
|
|
|
|
|
def _draw_eye_fusion(ax, x_left, y_img, y_md, eye_label):
|
|
"""One eye's row: Image Network box + Clinical Network box -> Fusion bridge box.
|
|
|
|
Returns (x_right_of_bridge, y_bridge_center).
|
|
"""
|
|
bw_img, bh_img = 1.45, 0.72
|
|
bw_md, bh_md = 1.48, 0.66
|
|
bw_br, bh_br = 1.75, 1.30
|
|
|
|
# Image network box
|
|
_box(ax, x_left + bw_img / 2, y_img, bw_img, bh_img, C_IMG,
|
|
f"{eye_label}\nImage Network", fontsize=8.5, radius=0.08)
|
|
# Clinical network box
|
|
_box(ax, x_left + bw_md / 2, y_md, bw_md, bh_md, C_MD,
|
|
f"{eye_label}\nClinical Network", fontsize=8.5, radius=0.08)
|
|
|
|
# Fusion bridge — with math detail (replaces compact "Bridge" label)
|
|
br_x = x_left + max(bw_img, bw_md) + 1.40
|
|
cy_br = (y_img + y_md) / 2
|
|
_arrow(ax, x_left + bw_img, y_img, br_x - bw_br / 2, cy_br, lw=1.2, style="-|>")
|
|
_arrow(ax, x_left + bw_md, y_md, br_x - bw_br / 2, cy_br, lw=1.2, style="-|>")
|
|
_box(ax, br_x, cy_br, bw_br, bh_br, C_BRIDGE,
|
|
"Fusion Bridge\nFC(img → 256)\nFC(md → 256)\nHadamard product",
|
|
fontsize=8, radius=0.10)
|
|
return br_x + bw_br / 2, cy_br
|
|
|
|
|
|
# ── Main figure ──────────────────────────────────────────────────────────────
|
|
|
|
def main() -> None:
|
|
W, H = 13.0, 7.8
|
|
fig, ax = plt.subplots(figsize=(W, H))
|
|
ax.set_xlim(0, W); ax.set_ylim(0, H); ax.axis("off")
|
|
ax.set_facecolor(C_BG); fig.patch.set_facecolor(C_BG)
|
|
|
|
ax.set_title("Bilateral Multimodal Fusion Architecture",
|
|
fontsize=13, fontweight="bold", fontfamily=FONT, pad=10, color="#222")
|
|
|
|
x_left = 3.2
|
|
inp_cx = 1.85
|
|
inp_w = 0.95
|
|
inp_h = 0.55
|
|
|
|
# OD (top)
|
|
od_y_img, od_y_md = 6.10, 4.80
|
|
br_od_x, cy_od = _draw_eye_fusion(ax, x_left, od_y_img, od_y_md, "OD")
|
|
_text(ax, 0.50, (od_y_img + od_y_md) / 2, "OD\n(Right Eye)",
|
|
fontsize=9.5, color="#444", bold=True)
|
|
_box(ax, inp_cx, od_y_img, inp_w, inp_h, C_INPUT, "Fundus\nImage",
|
|
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
|
|
_box(ax, inp_cx, od_y_md, inp_w, inp_h, C_INPUT, "Clinical\nData",
|
|
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
|
|
_arrow(ax, inp_cx + inp_w / 2, od_y_img, x_left, od_y_img, lw=1.2, style="-|>")
|
|
_arrow(ax, inp_cx + inp_w / 2, od_y_md, x_left, od_y_md, lw=1.2, style="-|>")
|
|
|
|
# OS (bottom)
|
|
os_y_img, os_y_md = 2.95, 1.65
|
|
br_os_x, cy_os = _draw_eye_fusion(ax, x_left, os_y_img, os_y_md, "OS")
|
|
_text(ax, 0.50, (os_y_img + os_y_md) / 2, "OS\n(Left Eye)",
|
|
fontsize=9.5, color="#444", bold=True)
|
|
_box(ax, inp_cx, os_y_img, inp_w, inp_h, C_INPUT, "Fundus\nImage",
|
|
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
|
|
_box(ax, inp_cx, os_y_md, inp_w, inp_h, C_INPUT, "Clinical\nData",
|
|
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
|
|
_arrow(ax, inp_cx + inp_w / 2, os_y_img, x_left, os_y_img, lw=1.2, style="-|>")
|
|
_arrow(ax, inp_cx + inp_w / 2, os_y_md, x_left, os_y_md, lw=1.2, style="-|>")
|
|
|
|
# Side brackets — re-labelled "OD Fusion" / "OS Fusion"
|
|
_bracket(ax, x=br_od_x + 0.05,
|
|
y0=od_y_md - 0.50, y1=od_y_img + 0.50,
|
|
text="OD Fusion", pad=0.22, fontsize=9, badge_color="#555")
|
|
_bracket(ax, x=br_os_x + 0.05,
|
|
y0=os_y_md - 0.50, y1=os_y_img + 0.50,
|
|
text="OS Fusion", pad=0.22, fontsize=9, badge_color="#555")
|
|
|
|
# Patient-level head (Fused Head)
|
|
head_y = (cy_od + cy_os) / 2
|
|
head_x = max(br_od_x, br_os_x) + 2.55
|
|
head_w, head_h = 1.95, 1.30
|
|
_arrow(ax, br_od_x + 0.05, cy_od, head_x - head_w / 2, head_y, lw=1.4, style="-|>")
|
|
_arrow(ax, br_os_x + 0.05, cy_os, head_x - head_w / 2, head_y, lw=1.4, style="-|>")
|
|
_box(ax, head_x, head_y, head_w, head_h, C_HEAD,
|
|
"Patient Head\ncat(z_OD, z_OS)\n→ FC(256) → FC(2)",
|
|
fontsize=8.5, radius=0.10)
|
|
|
|
# Output nodes
|
|
out_x = head_x + head_w / 2 + 0.55
|
|
_arrow(ax, head_x + head_w / 2, head_y, out_x, head_y, lw=1.5, style="-|>")
|
|
_draw_output(ax, out_x, head_y)
|
|
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
fig.savefig(OUT, dpi=180, bbox_inches="tight")
|
|
plt.close(fig)
|
|
print(f"saved {OUT}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|