"""Original scene/measurement functions; fixed-palette OpenCV-equivalent segmentation.""" import math,json from pathlib import Path import numpy as np def scene(dim=False, tag=False, clipped=False): """Generate three geometric leaf-like ellipses and their visible union.""" y, x = np.mgrid[:240, :320] dx = -95 if clipped else 0 left = ((x-(115+dx))/60)**2 + ((y-120)/32)**2 <= 1 right = ((x-(205+dx))/60)**2 + ((y-120)/32)**2 <= 1 upper = ((x-(160+dx))/32)**2 + ((y-82)/58)**2 <= 1 reference = left | right | upper rgb = np.full((240, 320, 3), 235, dtype=np.uint8) rgb[reference] = (50, 160, 65) if dim: rgb[reference & (x < 160)] = (20, 60, 25) if tag: rgb[10:30, 10:50] = (50, 160, 65) return rgb, reference def palette_hsv(rgb): """Exact saved OpenCV conversion for the generator palette, not general HSV.""" data=json.loads(Path(__file__).with_name('palette.json').read_text()) out=np.zeros_like(rgb);known=np.zeros(rgb.shape[:2],dtype=bool) for color,hsv in zip(data['rgb'],data['hsv']): selected=np.all(rgb==color,axis=2);out[selected]=hsv;known|=selected if not known.all():raise ValueError('Only the original synthetic palette is supported') return out def segment(rgb, min_v=100, roi=None): if not isinstance(min_v, int) or not 0 <= min_v <= 255: raise ValueError('min_v must be an integer in [0,255]') if rgb.dtype != np.uint8 or rgb.ndim != 3 or rgb.shape[2] != 3: raise ValueError('uint8 RGB image required') hsv = palette_hsv(rgb) mask = np.all((hsv >= (35, 80, min_v)) & (hsv <= (85, 255, 255)), axis=2) if roi is not None: if len(roi) != 4 or any(not isinstance(v, int) for v in roi): raise ValueError('ROI must contain four integers') x, y, width, height = roi if min(x,y) < 0 or min(width,height) <= 0 or x+width > rgb.shape[1] or y+height > rgb.shape[0]: raise ValueError('ROI outside image') inside = np.zeros(mask.shape, dtype=bool) inside[y:y+height, x:x+width] = True mask &= inside return mask def measure(mask, reference, mm_per_pixel): if not math.isfinite(mm_per_pixel) or mm_per_pixel <= 0: raise ValueError('positive finite scale required') if mask.dtype != bool or reference.dtype != bool or mask.shape != reference.shape or mask.ndim != 2 or mask.size == 0: raise ValueError('matching nonempty 2D boolean masks required') tp = int(np.count_nonzero(mask & reference)) fp = int(np.count_nonzero(mask & ~reference)) fn = int(np.count_nonzero(~mask & reference)) pixels = tp+fp truth = tp+fn union = tp+fp+fn return {'pixels':pixels, 'reference_pixels':truth, 'projected_mm2':pixels*mm_per_pixel**2, 'mask_area_error_percent':100*(pixels-truth)/truth if truth else None, 'iou':tp/union if union else None, 'precision':tp/pixels if pixels else None, 'recall':tp/truth if truth else None, 'touches_frame':bool(mask[0].any() or mask[-1].any() or mask[:,0].any() or mask[:,-1].any())}