"""Brown-spot area (%) of banana photos, the Python version of the ImageJ threshold method. python experiments/browning_area.py photos/ --out data/E5/browning.csv --preview Take the photos on a plain white or blue background with a fixed light and camera height. For each image: banana pixels = saturated enough and not background-bright (HSV) brown pixels = banana pixels with hue 5-40 deg and low brightness brown_pct = brown pixels / banana pixels x 100 Check the thresholds on a few photos with --preview (writes *_mask.png next to the CSV) and adjust them before measuring every photo; then keep them fixed for the whole study. """ from __future__ import annotations import argparse import sys from pathlib import Path import numpy as np import pandas as pd from PIL import Image sys.path.insert(0, str(Path(__file__).resolve().parents[1])) SATURATION_MIN: float = 0.25 # below this it is background or glare BROWN_HUE: tuple[float, float] = (5.0, 40.0) # degrees BROWN_VALUE_MAX: float = 0.55 # brown is darker than yellow peel def masks(path: Path) -> tuple[np.ndarray, np.ndarray]: hsv = np.asarray(Image.open(path).convert("RGB").convert("HSV"), dtype=float) / 255.0 hue, saturation, value = hsv[..., 0] * 360, hsv[..., 1], hsv[..., 2] banana = (saturation > SATURATION_MIN) & (hue < 90) # yellow, green-yellow and brown peel brown = banana & (hue >= BROWN_HUE[0]) & (hue <= BROWN_HUE[1]) & (value < BROWN_VALUE_MAX) banana |= (value < 0.25) & (saturation > 0.1) # very dark rotten patches still count as banana brown |= (value < 0.25) & (saturation > 0.1) return banana, brown def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("folder", type=Path) parser.add_argument("--out", type=Path, default=Path("browning.csv")) parser.add_argument("--preview", action="store_true") arguments = parser.parse_args() arguments.out.parent.mkdir(parents=True, exist_ok=True) rows = [] for path in sorted(p for p in arguments.folder.iterdir() if p.suffix.lower() in (".jpg", ".jpeg", ".png")): banana, brown = masks(path) percent = 100 * brown.sum() / max(banana.sum(), 1) rows.append({"photo": path.name, "banana_pixels": int(banana.sum()), "brown_pct": round(percent, 1)}) if arguments.preview: preview = np.zeros(banana.shape + (3,), dtype=np.uint8) preview[banana] = (240, 210, 60) preview[brown] = (110, 60, 20) Image.fromarray(preview).save(arguments.out.parent / f"{path.stem}_mask.png") table = pd.DataFrame(rows) table.to_csv(arguments.out, index=False) print(table.to_string(index=False)) if __name__ == "__main__": main()