"""Browser adapter for fixed synthetic projected-area scenes; NumPy only.""" import json,math,time from pathlib import Path import numpy as np from leaf_kernel import scene,segment,measure CONTROLS = {'scene': {'type': 'enum', 'values': ['clean', 'green_tag', 'dim', 'clipped'], 'default': 'clean'}, 'min_v': {'type': 'number', 'min': 0, 'max': 255, 'default': 100, 'step': 1, 'integer': True, 'unit': '0–255'}, 'roi': {'type': 'enum', 'values': ['full', 'tag_safe', 'small'], 'default': 'full'}, 'mm_per_pixel': {'type': 'number', 'min': 0.1, 'max': 1, 'default': 0.5, 'step': 0.1, 'unit': 'mm/pixel'}} ROIS={'full':None,'tag_safe':(50,20,230,190),'small':(140,60,20,30)} UNITS={'pixels':'pixel','reference_pixels':'pixel','projected_mm2':'mm²','mask_area_error_percent':'%','iou':'1','precision':'1','recall':'1','touches_frame':'0/1'} def spans(name,mask): # Lossless horizontal runs; each x is a row-major pixel index, y a length. xs=[];ys=[] for row in range(mask.shape[0]): edges=np.diff(np.pad(mask[row].astype(np.int8),(1,1))) starts=np.flatnonzero(edges==1);ends=np.flatnonzero(edges==-1) xs.extend((row*320+starts).tolist());ys.extend((ends-starts).tolist()) return dict(id=name,x=xs,y=ys,x_unit='row-major start pixel',y_unit='run length (pixels)') def run(name,p): if name!='leaf' or set(p)!=set(CONTROLS):raise ValueError('Unknown experiment or parameters') for k,c in CONTROLS.items(): v=p[k] if c['type']=='enum': if type(v) is not str or v not in c['values']:raise ValueError('Unknown choice: '+k) else: if type(v) not in (int,float) or not math.isfinite(v) or not c['min']<=v<=c['max']:raise ValueError('Parameter out of range: '+k) ticks=(v-c['min'])/c['step'] if abs(ticks-round(ticks))>1e-8:raise ValueError('Parameter off step: '+k) start=time.perf_counter() rgb,reference=scene(dim=p['scene']=='dim',tag=p['scene']=='green_tag',clipped=p['scene']=='clipped') roi=ROIS[p['roi']];mask=segment(rgb,int(p['min_v']),roi);values=measure(mask,reference,p['mm_per_pixel']) metrics=[] for k,v in values.items(): item=dict(id=k,value=int(v) if type(v) is bool else v,unit=UNITS[k]) if v is None:item['reason']='Undefined ratio: denominator is zero' metrics.append(item) roi_mask=np.zeros(reference.shape,dtype=bool) if roi is not None: x,y,w,h=roi;roi_mask[y,x:x+w]=True;roi_mask[y+h-1,x:x+w]=True;roi_mask[y:y+h,x]=True;roi_mask[y:y+h,x+w-1]=True series=[spans('input_green',np.all(rgb==(50,160,65),axis=2)),spans('input_dim',np.all(rgb==(20,60,25),axis=2)),spans('reference_mask',reference),spans('selected_mask',mask),spans('false_positive',mask & ~reference),spans('false_negative',~mask & reference),spans('roi_outline',roi_mask)] return dict(metrics=metrics,series=series,stdout='',provenance=dict(data_type='synthetic',compute_ms=(time.perf_counter()-start)*1000)) if __name__=='__main__': import argparse parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--parameters',default=json.dumps({k:c['default'] for k,c in CONTROLS.items()})) print(json.dumps(run('leaf',json.loads(parser.parse_args().parameters)),allow_nan=False))