"""SAM 3 as the segmenter: lawn = union(lawn prompts) ∪ canopy; canopy = union(canopy prompts). Returns the same `(lawn_mask, canopy_mask)` contract as `segmentation.run_lawn_and_canopy`, so it drops straight into the decide-lawn block. Canopy is folded into the lawn mask because LiDAR sees the ground under trees (same rationale as the incumbent's class-1 ∪ class-4 union).""" from __future__ import annotations import numpy as np from PIL import Image from tuning.tools import sam3_common def lawn_and_canopy(image: Image.Image, recipe) -> tuple[np.ndarray, np.ndarray]: canopy = sam3_common.concept_mask( image, recipe.sam3_canopy_prompts, recipe.sam3_score_threshold, recipe.sam3_mask_threshold) lawn = sam3_common.concept_mask( image, recipe.sam3_lawn_prompts, recipe.sam3_score_threshold, recipe.sam3_mask_threshold) return lawn | canopy, canopy