File size: 2,808 Bytes
3ecb175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""Run the morphometrics stage end-to-end across all trials.

Reads YOLO-seg .txt predictions and the matching source images, computes
per-tuber polygon morphometrics, converts to real-scale (inches and
square inches), assigns USDA size class, and concatenates the three
trials into a single combined CSV.

Usage:
    uv run python scripts/extract_morphometrics.py \\
        --out outputs/df_class_real_scale_ALL.csv
"""

from __future__ import annotations

import argparse
from pathlib import Path

import pandas as pd

from potatopheno.calibration import BELT_WIDTH_IN_DEFAULT, apply_real_scale
from potatopheno.classification import assign_usda_class
from potatopheno.io import build_morphometrics_dataframe, plot_id_from_image_id

TRIALS = ("NCPT", "ADC", "NCR")


def _parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument(
        "--preds-root",
        type=Path,
        required=True,
        help="Root of YOLO preds. Expects <trial>/preds/*.txt under it.",
    )
    p.add_argument(
        "--images-root",
        type=Path,
        required=True,
        help="Root of raw images. Expects 2025<TRIAL>/*.JPG under it.",
    )
    p.add_argument(
        "--out",
        type=Path,
        required=True,
        help="Path of the combined per-tuber CSV to write.",
    )
    p.add_argument(
        "--trials",
        type=lambda s: tuple(t.strip() for t in s.split(",") if t.strip()),
        default=TRIALS,
        help=f"Comma-separated trial names. Default: {','.join(TRIALS)}.",
    )
    p.add_argument(
        "--belt-width-in",
        type=float,
        default=BELT_WIDTH_IN_DEFAULT,
        help="Conveyor belt width in inches used for pixel-to-inch calibration.",
    )
    return p.parse_args()


def main() -> None:
    args = _parse_args()
    args.out.parent.mkdir(parents=True, exist_ok=True)

    per_trial: list[pd.DataFrame] = []
    for trial in args.trials:
        preds_dir = args.preds_root / trial / "preds"
        images_dir = args.images_root / f"2025{trial}"

        df = build_morphometrics_dataframe(trial, images_dir, preds_dir)
        df["Plot"] = plot_id_from_image_id(df["image_id"])
        df = apply_real_scale(df, belt_width_in=args.belt_width_in)
        df = assign_usda_class(df)

        print(
            f"[{trial}] images={df['image_id'].nunique()} "
            f"instances={len(df)} "
            f"class_counts={df['class'].value_counts().to_dict()}"
        )
        per_trial.append(df)

    combined = pd.concat(per_trial, ignore_index=True)
    combined.to_csv(args.out, index=False)

    print(
        f"\n[DONE] wrote {len(combined)} rows across "
        f"{combined['trial'].nunique()} trials → {args.out}"
    )


if __name__ == "__main__":
    main()