OCT_Segmentation_a1 / src /annotations.py
farrell236's picture
Deploy native Gradio OCT inference app
8fca640 verified
Raw History Blame Contribute Delete
5.87 kB
"""Canonical boundary conversion primitives; cohort/geometry audit must precede use.
Source depths are stored pixel coordinates. RWVM is retained for provenance and is
not blindly applied: Zeiss metadata can carry lateral rather than axial scale.
"""
from pathlib import Path
import numpy as np
import pydicom
def code_key(segment):
codes=segment.get('SegmentedPropertyTypeCodeSequence',[])
if len(codes)!=1:raise ValueError('Boundary needs one unambiguous anatomical code')
return str(codes[0].CodingSchemeDesignator)+'_'+str(codes[0].CodeValue)
FRAME_MAPPING_VERSION='aireadi-heightmap-anatomy-v2'
def heightmap_frame_numbers(ds,device=None):
"""Return effective anatomy IDs for stored frames, with a Topcon export erratum.
AI-READI Topcon payloads are in anatomical depth order. Their sequential frame
IDs use a different SegmentSequence order for OPL/ELM/EZ/RPE. The fixed mapping
below is supported by train/validation raw B-scan overlays, layer definitions,
and depth ordering; it does not sort or otherwise alter per-location values.
Heidelberg and Zeiss follow their explicit frame IDs. See discovery evidence.
"""
declared=[int(f.SegmentIdentificationSequence[0].ReferencedSegmentNumber) for f in ds.get('PerFrameFunctionalGroupsSequence',[])]
if device not in ['topcon_maestro2','topcon_triton']:return declared
expected_n=9 if device=='topcon_maestro2' else 10
expected_codes=['SCT_280677004','DCM_128289','DCM_128290','DCM_128291','DCM_128320','DCM_128297','DCM_128299','DCM_128293','SCT_76710003']+(['DCM_128301'] if expected_n==10 else [])
segments={int(s.SegmentNumber):s for s in ds.SegmentSequence}
if sorted(segments)!=list(range(1,expected_n+1)) or [code_key(segments[n]) for n in sorted(segments)]!=expected_codes:raise ValueError('Unsupported Topcon codebook')
effective=[1,2,3,4,8,9,5,6,7]+([10] if expected_n==10 else [])
if declared==effective:return declared
if declared!=list(range(1,expected_n+1)):raise ValueError('Unsupported Topcon frame identity pattern')
return effective
def read_heightmap(path,height,device=None):
ds=pydicom.dcmread(path)
if str(ds.get('SegmentationType',''))!='HEIGHTMAP':raise ValueError('Unsupported segmentation representation')
raw=ds.pixel_array.astype(np.float32)
if raw.ndim==2:raw=raw[None]
frames=ds.get('PerFrameFunctionalGroupsSequence',[])
if len(frames)!=len(raw):raise ValueError('Missing frame-to-segment identities')
frame_numbers=heightmap_frame_numbers(ds,device)
segments={int(s.SegmentNumber):s for s in ds.SegmentSequence}
if len(frame_numbers)!=len(set(frame_numbers)) or set(frame_numbers)!=set(segments):raise ValueError('Ambiguous or incomplete heightmap frame mapping')
keys=[code_key(segments[n]) for n in sorted(segments)]
if len(keys)!=len(set(keys)):raise ValueError('Duplicate anatomical boundary code')
values=raw[[frame_numbers.index(n) for n in sorted(segments)]]
valid=np.isfinite(values)
padding=ds.get('FloatPixelPaddingValue',None)
if padding is not None:
low=float(padding);high=float(ds.get('FloatPixelPaddingRangeLimit',padding))
valid &= ~((values>=min(low,high))&(values<=max(low,high)))
# Zero is a real location. Values outside the imaged axial support cannot be
# scored in original-volume coordinates and remain source-invalid locations.
valid &= (values>=0)&(values<=height-1)
values=np.where(valid,values,0).astype(np.float32)
return values,np.asarray(keys),valid,ds
def source_support_statistics(ds,height,device=None):
"""Audit every source location without changing reference measurement support."""
raw=np.asarray(ds.pixel_array,dtype=np.float32)
if raw.ndim==2:raw=raw[None]
numbers=heightmap_frame_numbers(ds,device)
segments={int(s.SegmentNumber):s for s in ds.SegmentSequence}
padding=ds.get('FloatPixelPaddingValue',None)
result={}
for number in sorted(segments):
values=raw[numbers.index(number)];finite=np.isfinite(values);pad=np.zeros(values.shape,bool)
if padding is not None:
low=float(padding);high=float(ds.get('FloatPixelPaddingRangeLimit',padding))
pad=(values>=min(low,high))&(values<=max(low,high))
outside=finite&~pad&((values<0)|(values>height-1));valid=finite&~pad&~outside
result[code_key(segments[number])]={'locations':int(values.size),'valid_points':int(valid.sum()),
'nonfinite_points':int((~finite).sum()),'padding_points':int((finite&pad).sum()),
'outside_imaged_axial_support':int(outside.sum()),'valid_zero_depth_points':int(((values==0)&valid).sum()),
'b_scans_with_support':int(valid.any(-1).sum())}
return result
def axial_spacing_um(headers,device):
spacings=[]
for path,ds in headers:
shared=ds.get('SharedFunctionalGroupsSequence',[])
base=shared[0] if shared else ds
for fr in ds.get('PerFrameFunctionalGroupsSequence',[ds]):
pm=fr.get('PixelMeasuresSequence',base.get('PixelMeasuresSequence',[]))
if not pm:return None
spacing=float(pm[0].PixelSpacing[0])
if not np.isfinite(spacing) or spacing<=0:return None
spacings.append(spacing)
if not spacings or not np.allclose(spacings,spacings[0],rtol=1e-5,atol=1e-9):return None
return spacings[0]*1000
def convert_case(row,root,headers,geometry,with_audit=False):
b,h,a=geometry
if len(row['segmentation_paths'])!=1:raise ValueError('Multiple segmentation assembly requires explicit frozen mapping')
values,keys,valid,ds=read_heightmap(Path(root)/row['segmentation_paths'][0],h,row['device'])
if values.shape[1:]!=(b,a):raise ValueError('Heightmap and structural grid differ')
# Frame order and axes are verified by frozen metadata audit plus source overlays.
spacing=axial_spacing_um(headers,row['device'])
data={'positions':values,'boundary_keys':keys,'valid':valid,'height':np.asarray(h,dtype=np.int32),**({'axial_spacing_um':np.asarray(spacing)} if spacing else {})}
if with_audit:return data,source_support_statistics(ds,h,row['device'])
return data