AURAD / text2layout /plot_boxplot.py
diing's picture
Upload folder using huggingface_hub
41c8683 verified
Raw
History Blame Contribute Delete
7.77 kB
"""
逐样本指标 CSV + Gen vs GT 带统计显著性的箱线图
================================================
输入: 与 eval_mask_population.py 一致
--jsonl, --gen_root, --gt_root
输出:
per_sample_shape.csv 每个 mask 的形态学描述子 (gen + gt 各一行)
boxplot_shape.png 2x2 箱线图: area / aspect_ratio / circularity / solidity
每个子图按疾病分面, gen vs GT 两个箱, 带 Mann-Whitney U 显著性星号
用法:
python plot_shape_boxplot.py \\
--jsonl /path/test.json \\
--gen_root /home/jovyan/AURAD_infer/mask/det-test-cp9000 \\
--gt_root /home/jovyan/AURAD_dataset \\
--out_dir ./boxplot_out
"""
import argparse
import json
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from PIL import Image
from scipy.ndimage import binary_erosion
from scipy.spatial import ConvexHull
from scipy.stats import mannwhitneyu
# ======================================================================
def load_mask(path):
arr = np.array(Image.open(path).convert("L"))
return (arr > 127).astype(np.uint8)
def shape_descriptors(mask):
if mask.sum() == 0:
return {k: np.nan for k in
["area", "aspect_ratio", "circularity", "solidity"]}
ys, xs = np.where(mask > 0)
area = int(len(ys))
h = ys.max() - ys.min() + 1
w = xs.max() - xs.min() + 1
aspect = h / w if w > 0 else np.nan
eroded = binary_erosion(mask)
perimeter = int((mask & ~eroded).sum())
circ = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else np.nan
try:
pts = np.column_stack([xs, ys])
if len(pts) >= 3:
hull = ConvexHull(pts)
solidity = area / hull.volume
else:
solidity = 1.0
except Exception:
solidity = np.nan
return {"area": area, "aspect_ratio": aspect,
"circularity": circ, "solidity": solidity}
# ======================================================================
def collect_descriptors(jsonl, gen_root, gt_root):
gen_root, gt_root = Path(gen_root), Path(gt_root)
items = [json.loads(l) for l in open(jsonl) if l.strip()]
rows = []
n_skip = 0
for it in items:
gen_p = gen_root / it["attn_list"][0][1]
gt_p = gt_root / it["mask"]
if not gen_p.is_file() or not gt_p.is_file():
n_skip += 1
continue
img_id = Path(it["file_name"]).parent.name
disease = it["attn_list"][0][0]
for side, path in [("gen", gen_p), ("gt", gt_p)]:
d = shape_descriptors(load_mask(path))
d.update({"id": img_id, "disease": disease, "source": side})
rows.append(d)
print(f"Computed descriptors for {len(rows)//2} mask pairs (skipped {n_skip})")
return pd.DataFrame(rows)
# ======================================================================
def sig_label(p):
if np.isnan(p):
return ""
if p < 1e-4:
return "****"
if p < 1e-3:
return "***"
if p < 1e-2:
return "**"
if p < 5e-2:
return "*"
return "ns"
def plot_boxplot(df, out_path):
metrics = ["area", "aspect_ratio", "circularity", "solidity"]
titles = {
"area": "Area (log10)",
"aspect_ratio": "Aspect Ratio (h/w)",
"circularity": "Circularity",
"solidity": "Solidity",
}
diseases = sorted(df["disease"].unique())
# 4 行 1 列, 每行一个指标横向展开所有疾病
fig, axes = plt.subplots(4, 1, figsize=(max(len(diseases)*1.0 + 2, 12), 16))
for ax, metric in zip(axes, metrics):
gen_data, gt_data, labels, p_values = [], [], [], []
for dis in diseases:
g = df[(df.disease == dis) & (df.source == "gen")][metric].dropna().values
t = df[(df.disease == dis) & (df.source == "gt")][metric].dropna().values
if metric == "area":
g = np.log10(g + 1)
t = np.log10(t + 1)
gen_data.append(g)
gt_data.append(t)
labels.append(f"{dis}\n(n={len(t)})")
if len(g) >= 3 and len(t) >= 3:
_, p = mannwhitneyu(g, t, alternative="two-sided")
else:
p = np.nan
p_values.append(p)
x = np.arange(len(diseases))
width = 0.35
bp_gen = ax.boxplot(gen_data, positions=x - width/2, widths=width*0.9,
patch_artist=True, showfliers=False,
medianprops=dict(color="black", linewidth=1.5))
bp_gt = ax.boxplot(gt_data, positions=x + width/2, widths=width*0.9,
patch_artist=True, showfliers=False,
medianprops=dict(color="black", linewidth=1.5))
for b in bp_gen["boxes"]:
b.set_facecolor("#E89A6B"); b.set_edgecolor("#a66033"); b.set_alpha(0.85)
for b in bp_gt["boxes"]:
b.set_facecolor("#4DBBA1"); b.set_edgecolor("#2c7a66"); b.set_alpha(0.85)
# 显著性星号
for i, p in enumerate(p_values):
label = sig_label(p)
if not label:
continue
all_vals = np.concatenate([gen_data[i], gt_data[i]]) if \
(len(gen_data[i]) and len(gt_data[i])) else np.array([0])
ytop = np.percentile(all_vals, 95) if len(all_vals) > 5 else (
all_vals.max() if len(all_vals) else 0)
yrange = ax.get_ylim()
yoff = (yrange[1] - yrange[0]) * 0.02 if yrange[1] > yrange[0] else 0.02
color = "#888" if label == "ns" else "black"
fs = 9 if label == "ns" else 12
ax.text(i, ytop + yoff, label, ha="center", va="bottom",
fontsize=fs, color=color, weight="bold")
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=30, ha="right", fontsize=9)
ax.set_ylabel(titles[metric], fontsize=12, weight="bold")
ax.grid(axis="y", linestyle="--", alpha=0.4)
ax.set_axisbelow(True)
from matplotlib.patches import Patch
handles = [
Patch(facecolor="#E89A6B", edgecolor="#a66033", label="Generated"),
Patch(facecolor="#4DBBA1", edgecolor="#2c7a66", label="Real (GT)"),
]
fig.legend(handles=handles, loc="upper center", bbox_to_anchor=(0.5, 0.995),
ncol=2, fontsize=14, frameon=False)
fig.text(0.5, 0.005,
"Mann-Whitney U: **** p<1e-4 *** p<1e-3 ** p<1e-2 * p<5e-2 ns p>=5e-2",
ha="center", fontsize=10, color="#555")
fig.tight_layout(rect=[0, 0.015, 1, 0.97])
fig.savefig(out_path, dpi=180, bbox_inches="tight")
plt.close(fig)
print(f"Boxplot saved: {out_path}")
# ======================================================================
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--jsonl", required=True)
ap.add_argument("--gen_root", required=True)
ap.add_argument("--gt_root", required=True)
ap.add_argument("--out_dir", default="./boxplot_out")
args = ap.parse_args()
out_dir = Path(args.out_dir); out_dir.mkdir(parents=True, exist_ok=True)
df = collect_descriptors(args.jsonl, args.gen_root, args.gt_root)
csv_path = out_dir / "per_sample_shape.csv"
df.to_csv(csv_path, index=False)
print(f"Saved per-sample CSV: {csv_path} ({len(df)} rows)")
plot_boxplot(df, out_dir / "boxplot_shape.png")
if __name__ == "__main__":
main()
"""
python plot_boxplot.py \
--jsonl /home/jovyan/AURAD_infer/mask/det-test-cp9000/test_prompt_text2layout_single.json \
--gen_root /home/jovyan/AURAD_infer/mask/det-test-cp9000 \
--gt_root /home/jovyan/AURAD_dataset \
--out_dir ./mask_eval
"""