STANAANDREY

seg

Nov 29th, 2025
197
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.22 KB | None | 0 0
  1. import cv2
  2. import numpy as np
  3. import os
  4. import glob
  5. import sys
  6. import pandas as pd
  7. from skimage.segmentation import watershed
  8. from skimage.feature import peak_local_max
  9. from scipy import ndimage
  10. from dotenv import load_dotenv
  11.  
  12. # ================= CONFIGURATION DEFAULTS =================
  13. DEFAULT_CONFIG = {
  14. 'MICRONS_PER_PIXEL': 0.25,
  15. 'R_MAX_UM': 6.0, # (Step 3) Limita expansiune citoplasma (6-8 um)
  16. 'T_RING_UM': 1.0, # (Step 4) Grosime inel membrana (1-2 um)
  17. 'DAB_THRESHOLD_P90': 50,
  18. 'MIN_CELL_AREA_PX': 30
  19. }
  20.  
  21. def load_config():
  22. # ... (Aceeasi logica de incarcare config ca inainte) ...
  23. if len(sys.argv) < 2:
  24. print("Usage: python non_nucl_seg.py /path/to/config.env")
  25. sys.exit(1)
  26. env_path = sys.argv[1]
  27. if not os.path.exists(env_path):
  28. print(f"ERROR: Config file not found: {env_path}")
  29. sys.exit(1)
  30. load_dotenv(env_path)
  31. cfg = {}
  32. cfg['INPUT_ROOT'] = os.getenv('INPUT_ROOT')
  33. cfg['OUTPUT_ROOT'] = os.getenv('OUTPUT_ROOT')
  34. for key, default_val in DEFAULT_CONFIG.items():
  35. val = os.getenv(key)
  36. cfg[key] = float(val) if val else default_val
  37. mpp = cfg['MICRONS_PER_PIXEL']
  38. cfg['R_MAX_PX'] = int(cfg['R_MAX_UM'] / mpp)
  39. cfg['T_RING_PX'] = int(max(1, cfg['T_RING_UM'] / mpp))
  40. return cfg
  41.  
  42. def get_all_images_recursive(root_dir):
  43. extensions = ['*.jpg', '*.jpeg', '*.png', '*.tif', '*.tiff']
  44. image_files = []
  45. for dirpath, _, filenames in os.walk(root_dir):
  46. for filename in filenames:
  47. if any(filename.lower().endswith(ext.replace('*', '')) for ext in extensions):
  48. image_files.append(os.path.join(dirpath, filename))
  49. return image_files
  50.  
  51. def get_dab_channel(img_rgb):
  52. # Ruifrok-Johnston Deconvolution for DAB
  53. img_float = img_rgb.astype(float) + 1
  54. img_float = img_float / 255.0
  55. od = -np.log(img_float)
  56. dab_vector = np.array([0.268, 0.570, 0.776])
  57. dab_channel = np.dot(od, dab_vector)
  58. dab_channel = np.clip(dab_channel, 0, None)
  59. if dab_channel.max() > 0:
  60. dab_channel = (dab_channel / dab_channel.max()) * 255
  61. return dab_channel.astype(np.uint8)
  62.  
  63. def separate_nuclei_watershed(binary_mask):
  64. """
  65. Simuleaza 'Instance Segmentation': Sparge nucleii lipiti folosind distanta.
  66. (Step 1 - Replacement for HoVer-Net if not available)
  67. """
  68. # 1. Distanta de la centru la margine
  69. distance = ndimage.distance_transform_edt(binary_mask)
  70.  
  71. # 2. Gasim varfurile locale (centrii nucleilor)
  72. # min_distance=5 asigura ca nu avem nuclei mai aproape de 5px unul de altul
  73. coords = peak_local_max(distance, footprint=np.ones((3, 3)), labels=binary_mask, min_distance=5)
  74. mask = np.zeros(distance.shape, dtype=bool)
  75. mask[tuple(coords.T)] = True
  76. markers, _ = ndimage.label(mask)
  77.  
  78. # 3. Watershed pe inversul distantei pentru a separa nucleii
  79. labels = watershed(-distance, markers, mask=binary_mask)
  80. return labels
  81.  
  82. def process_pipeline():
  83. cfg = load_config()
  84. print(f"Running pipeline with Instance Separation logic...")
  85.  
  86. image_files = get_all_images_recursive(cfg['INPUT_ROOT'])
  87. all_cells_data = []
  88.  
  89. for filepath in image_files:
  90. try:
  91. filename = os.path.basename(filepath)
  92. rel_path = os.path.relpath(os.path.dirname(filepath), cfg['INPUT_ROOT'])
  93. current_output_dir = os.path.join(cfg['OUTPUT_ROOT'], rel_path)
  94. if not os.path.exists(current_output_dir): os.makedirs(current_output_dir)
  95.  
  96. img = cv2.imread(filepath)
  97. if img is None: continue
  98. h, w = img.shape[:2]
  99. img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  100.  
  101. # --- PASUL 1: NUCLEUS DETECTION & INSTANCE SPLITTING ---
  102. # Daca aveai HoVer-Net, aici incarcai direct masca de instante.
  103. # Acum simulam asta detectand rosu si spargand obiectele.
  104. hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
  105. # Thresholds ajustate pentru nuclei rosii/maro
  106. mask1 = cv2.inRange(hsv, np.array([0, 40, 40]), np.array([15, 255, 255]))
  107. mask2 = cv2.inRange(hsv, np.array([160, 40, 40]), np.array([180, 255, 255]))
  108. nuclei_binary = mask1 + mask2
  109. nuclei_binary = ndimage.binary_fill_holes(nuclei_binary) # Umplem gaurile
  110.  
  111. # CRITIC: Transformam masca binara in LABEL MAP (Instante separate)
  112. # Fiecare nucleu primeste un ID unic (1, 2, 3...)
  113. nuclei_labels = separate_nuclei_watershed(nuclei_binary)
  114. num_nuclei = nuclei_labels.max()
  115.  
  116. if num_nuclei < 1: continue
  117.  
  118. # --- PASUL 3: GEODESIC EXPANSION / VORONOI (Cytoplasm) ---
  119. # Vrem sa impartim tot spatiul dintre nuclei, dar cu limita R_MAX.
  120.  
  121. # Calculam distanta de la ORICE pixel la cel mai apropiat nucleu
  122. # distance_transform_edt pe 'nuclei_labels > 0' inversat
  123. dist_from_nuclei, indices = ndimage.distance_transform_edt(nuclei_labels == 0, return_indices=True)
  124.  
  125. # 'indices' ne spune pentru fiecare pixel de fundal, coordonatele celui mai apropiat nucleu.
  126. # Putem reconstrui 'Voronoi Map' complet folosind aceste indici:
  127. voronoi_map = nuclei_labels[indices[0], indices[1]]
  128.  
  129. # Acum aplicam limita R_MAX (Step 3: limitare expansiune)
  130. # Pseudo-celula = Voronoi Region AND (Distance < R_MAX)
  131. cytoplasm_mask = np.where(dist_from_nuclei <= cfg['R_MAX_PX'], voronoi_map, 0)
  132.  
  133. # --- PASII 4, 5, 6: MEMBRANE RING & CLIPPING ---
  134.  
  135. final_vis_overlay = img.copy()
  136. final_mask_membranes = np.zeros((h, w), dtype=np.uint8)
  137.  
  138. dab_image = get_dab_channel(img_rgb)
  139.  
  140. # Iteram prin fiecare celula (INSTANTA) pentru a calcula inelul specific ei
  141. # Aceasta este cheia evitarii suprapunerilor
  142.  
  143. # Optimizare: Putem face operatiile global, dar logic e mai clar per instanta
  144. # Pentru viteza, facem dilatarea pe fiecare 'island'
  145.  
  146. count_valid = 0
  147.  
  148. # Folosim 'regionprops' din skimage ar fi mai rapid, dar ramanem pe numpy/cv2 logic
  149. for cell_id in range(1, num_nuclei + 1):
  150. # 1. Extragem masca pseudo-celulei curente (Nucleu + Citoplasma)
  151. # Aceasta masca este DEJA taiata de vecini datorita Voronoi-ului de la Pasul 3
  152. current_cell_mask = (cytoplasm_mask == cell_id).astype(np.uint8)
  153.  
  154. if np.sum(current_cell_mask) < cfg['MIN_CELL_AREA_PX']:
  155. continue
  156.  
  157. # 2. (Pasul 4) Dilatam celula curenta
  158. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*cfg['T_RING_PX']+1, 2*cfg['T_RING_PX']+1))
  159. outer = cv2.dilate(current_cell_mask, kernel, iterations=1)
  160.  
  161. # 3. (Pasul 5) Ring = Outer \ Inner
  162. ring = cv2.subtract(outer, current_cell_mask)
  163.  
  164. # 4. (Pasul 6) CLIPPING STRICT
  165. # "Ring final = ring intersectat cu regiunea Voronoi a celulei"
  166. # Asta asigura ca inelul nu sare gardul la vecin.
  167. # Avem 'voronoi_map' calculat global la pasul 3.
  168.  
  169. # Masca regiunii Voronoi pure a acestei celule (fara limita de distanta)
  170. voronoi_region = (voronoi_map == cell_id).astype(np.uint8)
  171.  
  172. # Intersectie
  173. final_ring = cv2.bitwise_and(ring, voronoi_region)
  174.  
  175. # Adaugam la masca globala pentru vizualizare
  176. final_mask_membranes = cv2.bitwise_or(final_mask_membranes, final_ring)
  177.  
  178. # --- PASUL 7: DAB SCORE ---
  179. ring_pixels = dab_image[final_ring > 0]
  180.  
  181. status = 'Negative'
  182. dab_p90 = 0
  183. if len(ring_pixels) > 0:
  184. dab_p90 = np.percentile(ring_pixels, 90)
  185. if dab_p90 > cfg['DAB_THRESHOLD_P90']:
  186. status = 'Positive'
  187.  
  188. all_cells_data.append({
  189. 'Image': filename,
  190. 'Cell_ID': cell_id,
  191. 'DAB_P90': round(dab_p90, 2),
  192. 'Status': status
  193. })
  194. count_valid += 1
  195.  
  196. # Desenam conturul pe imagine (Galben=Pozitiv, Cyan=Negativ)
  197. color = (0, 255, 255) if status == 'Positive' else (255, 255, 0)
  198. contours, _ = cv2.findContours(final_ring, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  199. cv2.drawContours(final_vis_overlay, contours, -1, color, 1)
  200.  
  201. # --- SALVARE ---
  202. # Salvam imaginea cu contururi subtiri (care arata separarea)
  203. save_path = os.path.join(current_output_dir, f"analyzed_{filename}")
  204. cv2.imwrite(save_path, final_vis_overlay)
  205. print(f"Processed {filename}: {count_valid} cells.")
  206.  
  207. except Exception as e:
  208. print(f"ERROR processing {filepath}: {e}")
  209.  
  210. # Save Excel
  211. if all_cells_data:
  212. df = pd.DataFrame(all_cells_data)
  213. excel_path = os.path.join(cfg['OUTPUT_ROOT'], 'results.csv')
  214. df.to_csv(excel_path, index=False)
  215. print("Done.")
  216.  
  217. if __name__ == "__main__":
  218. process_pipeline()
Advertisement
Add Comment
Please, Sign In to add comment