AI-RESEARCHER-2024 commited on
Commit
94fee7e
·
verified ·
1 Parent(s): 70fca4b

Upload 317 files

Browse files
README.md CHANGED
@@ -16,15 +16,21 @@ A variant of the neuron tracer that applies the **MedCLIPSeg** approach
16
  (Koleilat et al., *Probabilistic Vision–Language Adaptation for Data-Efficient
17
  and Generalizable Medical Image Segmentation*, CVPR 2026): a **text prompt**
18
  describes the target ("nerve fibers …") and a frozen **CLIP** backbone produces a
19
- **fiber-probability map**, which is then traced into a skeleton.
 
20
 
21
  ## What the app does
22
  - **Neurofilament (MIP):** the maximum-intensity projection of the input channel.
23
- - **Tracked skeleton (white on black):** the MedCLIP fiber-probability map is
24
- thresholded, skeletonised and pruned into a fiber skeleton.
25
- - **Total trace length (µm):** the spacing-aware total length of that MedCLIP
26
- skeleton, reported above the images. The text prompt and probability threshold
27
- are editable.
 
 
 
 
 
28
 
29
  ## Few-shot training (included)
30
  A runnable, few-shot version of the MedCLIPSeg approach — **frozen CLIP encoders +
 
16
  (Koleilat et al., *Probabilistic Vision–Language Adaptation for Data-Efficient
17
  and Generalizable Medical Image Segmentation*, CVPR 2026): a **text prompt**
18
  describes the target ("nerve fibers …") and a frozen **CLIP** backbone produces a
19
+ **fiber-probability map** that acts as a *semantic region prior* — telling the
20
+ tracer **where** the nerve fibers are.
21
 
22
  ## What the app does
23
  - **Neurofilament (MIP):** the maximum-intensity projection of the input channel.
24
+ - **Tracked skeleton (white on black):** MedCLIP-gated tubular tracing. The
25
+ MedCLIP probability map (WHERE) seeds a **Sato tubeness** ridge filter run on
26
+ the raw neurofilament signal (the thin fiber GEOMETRY); seeds grow along the
27
+ raw ridge across the whole field, so the long radial fibers below the band are
28
+ recovered, and a width gate removes any residual medial-axis mesh. This traces
29
+ clean thin fibers instead of skeletonising the coarse probability blob (which
30
+ produced a "cracked-mud" mesh).
31
+ - **Total trace length (µm):** the spacing-aware total length of that skeleton,
32
+ reported above the images. The text prompt and the MedCLIP **semantic gate**
33
+ (region-prior threshold) are editable.
34
 
35
  ## Few-shot training (included)
36
  A runnable, few-shot version of the MedCLIPSeg approach — **frozen CLIP encoders +
__pycache__/app.cpython-313.pyc CHANGED
Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ
 
__pycache__/processing.cpython-313.pyc CHANGED
Binary files a/__pycache__/processing.cpython-313.pyc and b/__pycache__/processing.cpython-313.pyc differ
 
app.py CHANGED
@@ -26,10 +26,11 @@ def analyze(file_obj, fg_prompt, threshold):
26
  if fg_prompt and fg_prompt.strip():
27
  MC.FG_PROMPTS = [p.strip() for p in fg_prompt.split("|") if p.strip()]
28
 
29
- # --- MedCLIP text-prompted fiber-probability map -> tracked skeleton ---
30
  prob, _ = MC.segment_best(nf_mip)
31
- trace = P.trace_from_probability(prob, img.voxel,
32
- threshold=float(threshold), prune_um=3.0)
 
33
  m = P.compute_metrics(trace, "MedCLIP", min_fiber_um=5.0)
34
  skel_img = P.skeleton_image(trace.skeleton, dilate=1) # white on black
35
 
@@ -37,8 +38,10 @@ def analyze(file_obj, fg_prompt, threshold):
37
  else "zero-shot CLIP prior")
38
  status = (f"**MedCLIP total trace length: "
39
  f"{m.total_length_um:,.1f} µm.**\n\n"
40
- f"Tracked from the MedCLIP fiber-probability map "
41
- f"({model}, text-prompted) at threshold {float(threshold):.2f}. "
 
 
42
  f"Prompt(s): *{', '.join(MC.FG_PROMPTS)}*.")
43
  return nf_mip, skel_img, status
44
  except Exception as e: # noqa: BLE001
@@ -101,8 +104,9 @@ with gr.Blocks(title="Neuron Quantification using AI — MedCLIPSeg",
101
  label="Text prompt(s) for the target (separate with | )",
102
  value="a fluorescence microscopy image of nerve fibers | "
103
  "neurofilament nerve fibers and axons")
104
- thresh = gr.Slider(0.1, 0.9, value=0.5, step=0.05,
105
- label="Fiber-probability threshold")
 
106
  btn = gr.Button("Analyze", variant="primary")
107
  with gr.Column(scale=2):
108
  status = gr.Markdown()
 
26
  if fg_prompt and fg_prompt.strip():
27
  MC.FG_PROMPTS = [p.strip() for p in fg_prompt.split("|") if p.strip()]
28
 
29
+ # --- MedCLIP text-prompted region prior -> raw-signal fiber geometry ---
30
  prob, _ = MC.segment_best(nf_mip)
31
+ trace = P.trace_fibers_medclip_gated(nf_mip, prob, img.voxel,
32
+ gate_threshold=float(threshold),
33
+ prune_um=3.0)
34
  m = P.compute_metrics(trace, "MedCLIP", min_fiber_um=5.0)
35
  skel_img = P.skeleton_image(trace.skeleton, dilate=1) # white on black
36
 
 
38
  else "zero-shot CLIP prior")
39
  status = (f"**MedCLIP total trace length: "
40
  f"{m.total_length_um:,.1f} µm.**\n\n"
41
+ f"Fiber geometry traced from the raw neurofilament signal "
42
+ f"(Sato tubeness), gated by the MedCLIP region prior "
43
+ f"({model}, text-prompted) at semantic gate "
44
+ f"{float(threshold):.2f}. "
45
  f"Prompt(s): *{', '.join(MC.FG_PROMPTS)}*.")
46
  return nf_mip, skel_img, status
47
  except Exception as e: # noqa: BLE001
 
104
  label="Text prompt(s) for the target (separate with | )",
105
  value="a fluorescence microscopy image of nerve fibers | "
106
  "neurofilament nerve fibers and axons")
107
+ thresh = gr.Slider(0.2, 0.8, value=0.4, step=0.05,
108
+ label="MedCLIP semantic gate (region prior) — "
109
+ "lower = include more of the band")
110
  btn = gr.Button("Analyze", variant="primary")
111
  with gr.Column(scale=2):
112
  status = gr.Markdown()
processing.py CHANGED
@@ -21,8 +21,9 @@ from typing import Optional
21
 
22
  import numpy as np
23
  from scipy import ndimage as ndi
24
- from skimage.filters import gaussian, threshold_otsu
25
- from skimage.morphology import remove_small_objects, skeletonize
 
26
  from skan import Skeleton, summarize
27
 
28
 
@@ -598,6 +599,130 @@ def trace_from_probability(prob: np.ndarray, voxel: tuple,
598
  return TraceResult(mask[None], skel, dist[None], voxel)
599
 
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  # --------------------------------------------------------------------------- #
602
  # Region definition (IHC vs OHC) from Myo7a
603
  # --------------------------------------------------------------------------- #
 
21
 
22
  import numpy as np
23
  from scipy import ndimage as ndi
24
+ from skimage.filters import gaussian, threshold_otsu, sato
25
+ from skimage.morphology import (remove_small_objects, remove_small_holes,
26
+ skeletonize)
27
  from skan import Skeleton, summarize
28
 
29
 
 
599
  return TraceResult(mask[None], skel, dist[None], voxel)
600
 
601
 
602
+ def _safe_otsu(values: np.ndarray, fallback: float) -> float:
603
+ """threshold_otsu that never raises on constant / tiny inputs."""
604
+ v = np.asarray(values).ravel()
605
+ v = v[np.isfinite(v)]
606
+ if v.size < 8 or np.unique(v).size < 2:
607
+ return float(fallback)
608
+ try:
609
+ return float(threshold_otsu(v))
610
+ except Exception:
611
+ return float(fallback)
612
+
613
+
614
+ def trace_fibers_medclip_gated(nf_mip: np.ndarray, prob: np.ndarray, voxel: tuple,
615
+ gate_threshold: float = 0.4,
616
+ fiber_radii_um: tuple = (0.2, 0.35, 0.6),
617
+ gate_dilate_um: float = 4.0,
618
+ max_fiber_half_width_um: float = 1.5,
619
+ bright_floor: float = 0.5,
620
+ min_object_px: int = 32,
621
+ prune_um: float = 3.0) -> TraceResult:
622
+ """MedCLIP-gated tubular fiber tracer.
623
+
624
+ MedCLIP ``prob`` is used ONLY as a semantic region prior (WHERE fibers are).
625
+ The thin fiber GEOMETRY comes from a multiscale Sato tubeness response on the
626
+ RAW neurofilament MIP, so ``skeletonize`` acts on thin ridges (clean lines),
627
+ never on the coarse probability slab (which would yield a medial-axis
628
+ honeycomb). Seeds are placed where strong tubeness meets the MedCLIP region;
629
+ those seeds are then grown along the raw ridge over the WHOLE image, so the
630
+ long radial (vertical) spiral-ganglion fibers descending below the band -
631
+ where ``prob`` is ~0 - are recovered. A final EDT half-width gate deletes any
632
+ residual wide medial-axis ridge. Returns a Z=1 ``TraceResult`` so
633
+ ``compute_metrics`` / ``skeleton_image`` / the length path are unchanged.
634
+
635
+ Parameters
636
+ ----------
637
+ nf_mip : 2D uint8/float RAW neurofilament MIP (``channel_preview`` output).
638
+ prob : 2D MedCLIP fiber-probability in [0, 1] at MIP resolution.
639
+ voxel : (dz, dy, dx) in microns.
640
+ gate_threshold : MedCLIP prob level defining the semantic region (UI slider).
641
+ """
642
+ dz, dy, dx = voxel
643
+ px = float(np.mean([dy, dx])) # microns / pixel
644
+ raw = np.asarray(nf_mip, np.float32)
645
+ prob = np.asarray(prob, np.float32)
646
+ if prob.shape != raw.shape: # robust to any mismatch
647
+ from skimage.transform import resize
648
+ prob = resize(prob, raw.shape, order=1, preserve_range=True).astype(np.float32)
649
+
650
+ def _empty():
651
+ z = np.zeros((1,) + raw.shape, bool)
652
+ return TraceResult(z, z.copy(), np.zeros(z.shape, np.float32), voxel)
653
+
654
+ # 1. Raw intensity, robustly normalised (reuse existing helper).
655
+ I = _norm(raw)
656
+
657
+ # 2. Multiscale tubeness on the RAW signal -> thin, orientation-invariant
658
+ # ridge response (present for vertical radials as much as band fibers).
659
+ sigmas = sorted({max(1.0, round(float(r) / max(dx, 1e-6), 2))
660
+ for r in fiber_radii_um})
661
+ T = sato(I, sigmas=sigmas, black_ridges=False).astype(np.float32)
662
+ T = np.nan_to_num(T, nan=0.0, posinf=0.0, neginf=0.0)
663
+ thi = np.percentile(T, 99.5)
664
+ T = np.clip(T / (thi + 1e-6), 0.0, 1.0)
665
+
666
+ # 3. MedCLIP semantic region (WHERE) -> generous, dilated gate.
667
+ region = prob >= float(gate_threshold)
668
+ if not region.any():
669
+ region = prob >= np.percentile(prob, 90.0) # never empty
670
+ if region.any():
671
+ region = remove_small_objects(region, int(min_object_px))
672
+ region = ndi.binary_closing(region, structure=np.ones((3, 3), bool))
673
+ if not region.any():
674
+ return _empty()
675
+ iters = max(1, int(round(float(gate_dilate_um) / px)))
676
+ gate = ndi.binary_dilation(region, iterations=iters)
677
+
678
+ # 4. Weak brightness floor (rejects pure-noise ridges; keeps dim radials).
679
+ ipos = I[I > 0]
680
+ bright = I >= float(bright_floor) * _safe_otsu(ipos, 0.15)
681
+
682
+ # 5. Seed = strong tubeness INSIDE the semantic gate.
683
+ thr_hi = _safe_otsu(T[gate & (T > 0)], 0.3)
684
+ thr_hi = float(np.clip(thr_hi, 0.02, 0.99))
685
+ thr_lo = 0.5 * thr_hi
686
+ core = (T >= thr_hi) & gate & bright
687
+
688
+ # 6. Growth domain = thin raw ridges over the WHOLE image (radial recovery).
689
+ permissive = (T >= thr_lo) & bright
690
+
691
+ if core.any():
692
+ # 8-connected reconstruction: follow each in-band seed down its fiber,
693
+ # out of the band, recovering the long vertical radials.
694
+ fiber = ndi.binary_propagation(core, mask=permissive,
695
+ structure=np.ones((3, 3), bool))
696
+ else:
697
+ # Empty-seed fallback: thin tubeness inside the gate. NEVER skeletonise
698
+ # the prob blob (that is the original honeycomb bug).
699
+ fiber = (T >= thr_hi) & gate & bright
700
+
701
+ fiber = remove_small_objects(fiber, int(min_object_px))
702
+ if not fiber.any():
703
+ return _empty()
704
+ # Bridge sub-pixel gaps without fusing adjacent parallel fibers.
705
+ fiber = remove_small_holes(fiber, area_threshold=4)
706
+
707
+ # 7. Skeletonise the THIN mask -> clean single-pixel centrelines.
708
+ dist = ndi.distance_transform_edt(fiber, sampling=(dy, dx)).astype(np.float32)
709
+ skel = skeletonize(fiber)
710
+
711
+ # 8. Artifact (filled-slab) rejection: drop skeleton pixels whose local
712
+ # half-width exceeds a real fiber -> removes any residual medial-axis mesh
713
+ # in the dense band, keyed on WIDTH (not orientation), so thin verticals
714
+ # are never touched.
715
+ max_hw = max(px, float(max_fiber_half_width_um))
716
+ skel = skel & (dist <= max_hw)
717
+ if not skel.any():
718
+ return _empty()
719
+
720
+ skel = skel[None]
721
+ if prune_um and prune_um > 0:
722
+ skel = prune_skeleton(skel, voxel, spur_um=float(prune_um))
723
+ return TraceResult(fiber[None], skel, dist[None], voxel)
724
+
725
+
726
  # --------------------------------------------------------------------------- #
727
  # Region definition (IHC vs OHC) from Myo7a
728
  # --------------------------------------------------------------------------- #