NMundhra's picture
Upload folder using huggingface_hub
be7e39c verified
Raw
History Blame Contribute Delete
5.62 kB
import cv2
import numpy as np
from typing import List, Dict
from backend.core_ml.segmentation.inference.data_models import Point, BoundingBox, RetinalLayer, LesionInstance, ClinicalMetrics, OCTScanAnalysis
class SegmentationAnalyzer:
"""
Parses a dense (H, W) granular segmentation mask into an object-oriented
OCTScanAnalysis containing discrete vector instances and metrics.
"""
# Example mapping (should match your dataset's exact class labels)
CLASS_MAP = {
0: "Background",
1: "ILM",
2: "NFL-IPL",
3: "INL",
4: "OPL",
5: "ONL-ISM",
6: "ISE",
7: "OS-RPE",
8: "RPE",
9: "Fluid",
10: "Hard Drusen",
11: "Soft Drusen",
12: "PED",
13: "Geographic Atrophy",
14: "Hyper-reflective Foci"
}
def __init__(self, layer_classes=list(range(1, 9)), lesion_classes=list(range(9, 15))):
self.layer_classes = layer_classes
self.lesion_classes = lesion_classes
def analyze(self, mask: np.ndarray) -> OCTScanAnalysis:
height, width = mask.shape
layers = self._extract_layers(mask)
lesions = self._extract_lesions(mask)
metrics = self._calculate_metrics(layers, lesions, width)
return OCTScanAnalysis(
image_width=width,
image_height=height,
layers=layers,
lesions=lesions,
clinical_metrics=metrics,
model_version="unet_hierarchical_v1.0"
)
def _extract_layers(self, mask: np.ndarray) -> List[RetinalLayer]:
layers = []
for class_id in self.layer_classes:
binary_mask = (mask == class_id).astype(np.uint8) * 255
# For continuous layers, we could extract the top boundary.
# A simple approach is taking the argmax along the y-axis for each x.
# But since layers might have gaps, contours are safer.
contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# To keep it as a simple "layer" concept for the frontend, we just take the largest contour
# and format it as a boundary. (Alternatively, return all contours as polygons).
if not contours:
continue
largest_contour = max(contours, key=cv2.contourArea)
# Simplify contour to reduce payload size (Ramer-Douglas-Peucker)
epsilon = 0.001 * cv2.arcLength(largest_contour, True)
approx = cv2.approxPolyDP(largest_contour, epsilon, True)
points = [Point(x=int(pt[0][0]), y=int(pt[0][1])) for pt in approx]
# Compute average depth (mean y)
if points:
avg_depth = float(np.mean([pt.y for pt in points]))
else:
avg_depth = 0.0
layers.append(RetinalLayer(
class_id=class_id,
class_name=self.CLASS_MAP.get(class_id, f"Layer_{class_id}"),
boundary_points=points,
avg_depth=avg_depth
))
return layers
def _extract_lesions(self, mask: np.ndarray) -> List[LesionInstance]:
lesions = []
for class_id in self.lesion_classes:
binary_mask = (mask == class_id).astype(np.uint8) * 255
contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
area = cv2.contourArea(cnt)
# Filter out microscopic noise
if area < 5.0:
continue
x, y, w, h = cv2.boundingRect(cnt)
bbox = BoundingBox(xmin=int(x), ymin=int(y), xmax=int(x+w), ymax=int(y+h))
# Simplify polygon
epsilon = 0.005 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
polygon = [Point(x=int(pt[0][0]), y=int(pt[0][1])) for pt in approx]
lesions.append(LesionInstance(
class_id=class_id,
class_name=self.CLASS_MAP.get(class_id, f"Lesion_{class_id}"),
polygon=polygon,
bounding_box=bbox,
area_pixels=float(area),
max_width=float(w),
max_height=float(h)
))
return lesions
def _calculate_metrics(self, layers: List[RetinalLayer], lesions: List[LesionInstance], width: int) -> ClinicalMetrics:
# Example Metric: Total fluid area
fluid_area = sum([L.area_pixels for L in lesions if L.class_name == "Fluid"])
# Example Metric: Max fluid height
fluid_heights = [L.max_height for L in lesions if L.class_name == "Fluid"]
max_fluid_h = max(fluid_heights) if fluid_heights else 0.0
# Example Metric: Average Retinal Thickness
# Estimated as distance between topmost layer (ILM) and bottommost layer (RPE)
layer_depths = [layer.avg_depth for layer in layers]
if len(layer_depths) >= 2:
thickness = float(max(layer_depths) - min(layer_depths))
else:
thickness = 0.0
return ClinicalMetrics(
average_retinal_thickness=thickness,
total_fluid_area=float(fluid_area),
max_fluid_height=float(max_fluid_h)
)