Fahimeh Orvati Nia commited on
Commit
93d0941
·
1 Parent(s): 2ff67cd
SSL_greenhouse_tip_detection.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ada9393d2ee2ad99651f0b26f1cef8ac43aa2ce2b470eacd80af9d8f585d2207
3
+ size 42273811
app.py CHANGED
@@ -39,11 +39,17 @@ def process(file_path):
39
  composite = load_pil(outputs.get('Composite'))
40
  overlay = load_pil(outputs.get('Overlay'))
41
  mask = load_pil(outputs.get('Mask'))
 
42
  size_img = load_pil(str(Path(tmpdir) / 'results/size.size_analysis.png'))
 
43
 
44
- # Texture LBP green path
45
  lbp_path = Path(tmpdir) / 'texture_output/lbp_green.png'
 
 
46
  texture_img = load_pil(str(lbp_path)) if lbp_path.exists() else None
 
 
47
 
48
  # Vegetation indices
49
  order = ['NDVI', 'GNDVI', 'SAVI']
@@ -51,7 +57,7 @@ def process(file_path):
51
 
52
  stats_text = outputs.get('StatsText', '')
53
 
54
- return size_img, composite, mask, overlay, texture_img, gallery_items, stats_text
55
 
56
 
57
  with gr.Blocks() as demo:
@@ -73,9 +79,13 @@ with gr.Blocks() as demo:
73
  composite_img = gr.Image(type="pil", label="Composite (Segmentation Input)", interactive=False)
74
  mask_img = gr.Image(type="pil", label="Mask", interactive=False)
75
  overlay_img = gr.Image(type="pil", label="Segmentation Overlay", interactive=False)
 
76
 
77
  with gr.Row():
 
78
  texture_img = gr.Image(type="pil", label="Texture LBP (Green Band)", interactive=False)
 
 
79
 
80
  gallery = gr.Gallery(label="Vegetation Indices", columns=3, height="auto")
81
  stats = gr.Textbox(label="Statistics", lines=4)
@@ -83,7 +93,7 @@ with gr.Blocks() as demo:
83
  run.click(
84
  process,
85
  inputs=inp,
86
- outputs=[size_img, composite_img, mask_img, overlay_img, texture_img, gallery, stats]
87
  )
88
 
89
  if __name__ == "__main__":
 
39
  composite = load_pil(outputs.get('Composite'))
40
  overlay = load_pil(outputs.get('Overlay'))
41
  mask = load_pil(outputs.get('Mask'))
42
+ input_img = load_pil(outputs.get('InputImage'))
43
  size_img = load_pil(str(Path(tmpdir) / 'results/size.size_analysis.png'))
44
+ yolo_img = load_pil(str(Path(tmpdir) / 'results/yolo_tips.png'))
45
 
46
+ # Texture images (green band)
47
  lbp_path = Path(tmpdir) / 'texture_output/lbp_green.png'
48
+ hog_path = Path(tmpdir) / 'texture_output/hog_green.png'
49
+ lac1_path = Path(tmpdir) / 'texture_output/lac1_green.png'
50
  texture_img = load_pil(str(lbp_path)) if lbp_path.exists() else None
51
+ hog_img = load_pil(str(hog_path)) if hog_path.exists() else None
52
+ lac1_img = load_pil(str(lac1_path)) if lac1_path.exists() else None
53
 
54
  # Vegetation indices
55
  order = ['NDVI', 'GNDVI', 'SAVI']
 
57
 
58
  stats_text = outputs.get('StatsText', '')
59
 
60
+ return size_img, composite, mask, overlay, input_img, yolo_img, texture_img, hog_img, lac1_img, gallery_items, stats_text
61
 
62
 
63
  with gr.Blocks() as demo:
 
79
  composite_img = gr.Image(type="pil", label="Composite (Segmentation Input)", interactive=False)
80
  mask_img = gr.Image(type="pil", label="Mask", interactive=False)
81
  overlay_img = gr.Image(type="pil", label="Segmentation Overlay", interactive=False)
82
+ input_img = gr.Image(type="pil", label="Input Image", interactive=False)
83
 
84
  with gr.Row():
85
+ yolo_img = gr.Image(type="pil", label="YOLO Tips", interactive=False)
86
  texture_img = gr.Image(type="pil", label="Texture LBP (Green Band)", interactive=False)
87
+ hog_img = gr.Image(type="pil", label="Texture HOG (Green Band)", interactive=False)
88
+ lac1_img = gr.Image(type="pil", label="Texture Lac1 (Green Band)", interactive=False)
89
 
90
  gallery = gr.Gallery(label="Vegetation Indices", columns=3, height="auto")
91
  stats = gr.Textbox(label="Statistics", lines=4)
 
93
  run.click(
94
  process,
95
  inputs=inp,
96
+ outputs=[size_img, composite_img, mask_img, overlay_img, input_img, yolo_img, texture_img, hog_img, lac1_img, gallery, stats]
97
  )
98
 
99
  if __name__ == "__main__":
sorghum_pipeline/features/morphology.py CHANGED
@@ -19,12 +19,14 @@ logger = logging.getLogger(__name__)
19
 
20
 
21
  class MorphologyExtractor:
22
- """Minimal morphology extraction (PlantCV size analysis)."""
23
 
24
- def __init__(self, pixel_to_cm: float = 0.1099609375, prune_sizes: List[int] = None):
 
25
  """Initialize."""
26
  self.pixel_to_cm = pixel_to_cm
27
  self.prune_sizes = prune_sizes or [200, 100, 50, 30, 10]
 
28
 
29
  if PLANT_CV_AVAILABLE:
30
  pcv.params.debug = None
@@ -34,29 +36,49 @@ class MorphologyExtractor:
34
  pcv.params.dpi = 100
35
 
36
  def extract_morphology_features(self, image: np.ndarray, mask: np.ndarray) -> Dict[str, Any]:
37
- """Extract only PlantCV size analysis image."""
38
- features = {'traits': {}, 'images': {}, 'success': False}
39
-
40
- if not PLANT_CV_AVAILABLE:
41
- logger.warning("PlantCV not available")
42
- return features
43
 
44
  try:
45
  clean_mask = self._preprocess_mask(mask)
46
  if clean_mask is None:
47
  return features
48
-
49
- # Size analysis only
50
- with contextlib.redirect_stdout(self._FilteredStream(sys.stdout)), \
51
- contextlib.redirect_stderr(self._FilteredStream(sys.stderr)):
52
- try:
53
- labeled_mask, n_labels = pcv.create_labels(clean_mask)
54
- size_analysis = pcv.analyze.size(image, labeled_mask, n_labels, label="default")
55
- features['images']['size_analysis'] = size_analysis
56
- features['success'] = True
57
- except Exception as e:
58
- logger.warning(f"Size analysis failed: {e}")
59
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  except Exception as e:
61
  logger.error(f"Morphology extraction failed: {e}")
62
 
@@ -79,6 +101,89 @@ class MorphologyExtractor:
79
 
80
  return clean_mask
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  class _FilteredStream:
83
  """Filter PlantCV output."""
84
  def __init__(self, stream):
 
19
 
20
 
21
  class MorphologyExtractor:
22
+ """Morphology extraction: size analysis image + simple traits + YOLO tips overlay."""
23
 
24
+ def __init__(self, pixel_to_cm: float = 0.1099609375, prune_sizes: List[int] = None,
25
+ yolo_weights_path: str = "/home/grads/f/fahimehorvatinia/plant-analysis-demo/SSL_greenhouse_tip_detection.pt"):
26
  """Initialize."""
27
  self.pixel_to_cm = pixel_to_cm
28
  self.prune_sizes = prune_sizes or [200, 100, 50, 30, 10]
29
+ self.yolo_weights_path = yolo_weights_path
30
 
31
  if PLANT_CV_AVAILABLE:
32
  pcv.params.debug = None
 
36
  pcv.params.dpi = 100
37
 
38
  def extract_morphology_features(self, image: np.ndarray, mask: np.ndarray) -> Dict[str, Any]:
39
+ """Return images {'size_analysis', 'yolo_tips'} and traits including 'plant_height_cm' and 'num_yolo_tips'."""
40
+ features: Dict[str, Any] = {'traits': {}, 'images': {}, 'success': False}
 
 
 
 
41
 
42
  try:
43
  clean_mask = self._preprocess_mask(mask)
44
  if clean_mask is None:
45
  return features
46
+
47
+ rgb = self._sanitize_image_for_pcv(image)
48
+ if rgb is None:
49
+ return features
50
+
51
+ # Size analysis image via PlantCV if available
52
+ if PLANT_CV_AVAILABLE:
53
+ with contextlib.redirect_stdout(self._FilteredStream(sys.stdout)), \
54
+ contextlib.redirect_stderr(self._FilteredStream(sys.stderr)):
55
+ try:
56
+ labeled_mask, n_labels = pcv.create_labels(clean_mask)
57
+ size_analysis = pcv.analyze.size(rgb, labeled_mask, n_labels, label="default")
58
+ features['images']['size_analysis'] = size_analysis
59
+ features['success'] = True
60
+ except Exception as e:
61
+ logger.warning(f"Size analysis failed: {e}")
62
+ else:
63
+ # Fallback: make a simple contour visualization
64
+ vis = self._simple_size_visual(rgb, clean_mask)
65
+ features['images']['size_analysis'] = vis
66
+ features['success'] = True
67
+
68
+ # Compute simple plant height from mask (px → cm)
69
+ rows = np.where(clean_mask > 0)[0]
70
+ if rows.size:
71
+ height_px = int(rows.max() - rows.min() + 1)
72
+ features['traits']['plant_height_cm'] = float(height_px * self.pixel_to_cm)
73
+ else:
74
+ features['traits']['plant_height_cm'] = 0.0
75
+
76
+ # YOLO tips overlay (optional)
77
+ yolo_img, tips = self._detect_yolo_tips(rgb, clean_mask)
78
+ if yolo_img is not None:
79
+ features['images']['yolo_tips'] = yolo_img
80
+ features['traits']['num_yolo_tips'] = int(len(tips) if tips is not None else 0)
81
+
82
  except Exception as e:
83
  logger.error(f"Morphology extraction failed: {e}")
84
 
 
101
 
102
  return clean_mask
103
 
104
+ def _sanitize_image_for_pcv(self, img: np.ndarray) -> np.ndarray:
105
+ """Ensure 3-channel uint8 RGB for PlantCV and drawing (accepts BGR)."""
106
+ if img is None:
107
+ return None
108
+ arr = img
109
+ if arr.ndim == 2:
110
+ arr = cv2.cvtColor(arr, cv2.COLOR_GRAY2RGB)
111
+ elif arr.ndim == 3 and arr.shape[2] == 4:
112
+ arr = cv2.cvtColor(arr, cv2.COLOR_BGRA2RGB)
113
+ elif arr.ndim == 3 and arr.shape[2] == 3:
114
+ # Assume BGR from OpenCV pipeline → convert to RGB
115
+ arr = cv2.cvtColor(arr, cv2.COLOR_BGR2RGB)
116
+ else:
117
+ return None
118
+ if arr.dtype != np.uint8:
119
+ a = arr.astype(np.float32)
120
+ mn, mx = np.nanmin(a), np.nanmax(a)
121
+ if mx > mn:
122
+ a = (a - mn) / (mx - mn)
123
+ a = np.clip(a * 255.0, 0, 255).astype(np.uint8)
124
+ arr = a
125
+ return arr
126
+
127
+ def _simple_size_visual(self, rgb: np.ndarray, mask: np.ndarray) -> np.ndarray:
128
+ """Draw contours, bbox, and area on RGB image."""
129
+ vis = rgb.copy()
130
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
131
+ cv2.drawContours(vis, contours, -1, (255, 0, 0), 2)
132
+ if contours:
133
+ largest = max(contours, key=cv2.contourArea)
134
+ x, y, w, h = cv2.boundingRect(largest)
135
+ cv2.rectangle(vis, (x, y), (x + w, y + h), (0, 255, 0), 2)
136
+ area_px = int(cv2.countNonZero(mask))
137
+ cv2.putText(vis, f"Area: {area_px} px", (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2, cv2.LINE_AA)
138
+ return vis
139
+
140
+ def _create_white_background_overlay(self, rgb: np.ndarray, mask: np.ndarray) -> np.ndarray:
141
+ """Return white background with plant pixels in original colors."""
142
+ img_no_bg = rgb.copy()
143
+ img_no_bg[mask == 0] = 0
144
+ overlay = np.full_like(rgb, 255, dtype=np.uint8)
145
+ overlay[mask > 0] = img_no_bg[mask > 0]
146
+ return overlay
147
+
148
+ def _detect_yolo_tips(self, rgb: np.ndarray, mask: np.ndarray):
149
+ """Detect tips using a YOLO model if available. Returns (overlay_img, tips_list)."""
150
+ try:
151
+ from ultralytics import YOLO # type: ignore
152
+ except Exception:
153
+ return None, []
154
+
155
+ try:
156
+ model = YOLO(self.yolo_weights_path)
157
+ except Exception:
158
+ return None, []
159
+
160
+ try:
161
+ overlay_img = self._create_white_background_overlay(rgb, mask)
162
+ # Run inference; allow low conf to let thresholding below handle
163
+ results = model(overlay_img, conf=0.01, imgsz=640)
164
+ tips = []
165
+ for r in results:
166
+ if getattr(r, 'keypoints', None) is not None and getattr(r.keypoints, 'xy', None) is not None:
167
+ kps_xy = r.keypoints.xy.cpu().numpy()
168
+ kps_conf = None
169
+ if getattr(r.keypoints, 'conf', None) is not None:
170
+ kps_conf = r.keypoints.conf.cpu().numpy()
171
+ for i, det_xy in enumerate(kps_xy):
172
+ for j, pt in enumerate(det_xy):
173
+ x, y = float(pt[0]), float(pt[1])
174
+ if not np.isnan(x) and not np.isnan(y):
175
+ conf = float(kps_conf[i][j]) if kps_conf is not None else 1.0
176
+ if conf >= 0.5:
177
+ tips.append((int(x), int(y), conf))
178
+
179
+ # Draw tips
180
+ vis = overlay_img.copy()
181
+ for (x, y, _c) in tips:
182
+ cv2.circle(vis, (int(x), int(y)), 8, (255, 0, 0), -1)
183
+ return vis, tips
184
+ except Exception:
185
+ return None, []
186
+
187
  class _FilteredStream:
188
  """Filter PlantCV output."""
189
  def __init__(self, stream):
sorghum_pipeline/output/manager.py CHANGED
@@ -87,6 +87,30 @@ class OutputManager:
87
  except Exception as e:
88
  logger.error(f"Failed to save composite: {e}")
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  # 3-5. Vegetation indices (NDVI, GNDVI, SAVI)
91
  try:
92
  veg = plant_data.get('vegetation_indices', {})
@@ -142,6 +166,42 @@ class OutputManager:
142
  plt.close(fig)
143
  except Exception as e:
144
  logger.error(f"Failed to save LBP with colorbar: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  except Exception as e:
146
  logger.error(f"Failed to save texture: {e}")
147
 
@@ -161,6 +221,12 @@ class OutputManager:
161
  synthesized = self._create_size_analysis_from_mask(mask_for_size, base_img_for_size)
162
  titled = self._add_title_banner(synthesized, 'Morphology Size')
163
  cv2.imwrite(str(results_dir / 'size.size_analysis.png'), titled)
 
 
 
 
 
 
164
  except Exception as e:
165
  logger.error(f"Failed to save size analysis: {e}")
166
 
 
87
  except Exception as e:
88
  logger.error(f"Failed to save composite: {e}")
89
 
90
+ # 2c. Normalized input image visualization
91
+ try:
92
+ norm_input = plant_data.get('normalized_input')
93
+ if isinstance(norm_input, np.ndarray):
94
+ vis = norm_input
95
+ if vis.ndim == 2:
96
+ vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
97
+ vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_GRAY2RGB)
98
+ elif vis.ndim == 3 and vis.shape[2] == 3:
99
+ if vis.dtype != np.uint8:
100
+ vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
101
+ else:
102
+ vis_u8 = vis
103
+ # Assume BGR
104
+ vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_BGR2RGB)
105
+ else:
106
+ vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
107
+ vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_GRAY2RGB)
108
+
109
+ titled = self._add_title_banner(vis_rgb, 'Input Image')
110
+ cv2.imwrite(str(results_dir / 'input_image.png'), titled)
111
+ except Exception as e:
112
+ logger.error(f"Failed to save normalized input image: {e}")
113
+
114
  # 3-5. Vegetation indices (NDVI, GNDVI, SAVI)
115
  try:
116
  veg = plant_data.get('vegetation_indices', {})
 
166
  plt.close(fig)
167
  except Exception as e:
168
  logger.error(f"Failed to save LBP with colorbar: {e}")
169
+
170
+ # HOG visualization
171
+ hog = feats.get('hog')
172
+ if isinstance(hog, np.ndarray) and hog.size > 0:
173
+ try:
174
+ img = hog.astype(np.float64)
175
+ fig, ax = plt.subplots(figsize=(5, 5))
176
+ ax.set_axis_off()
177
+ ax.set_facecolor('white')
178
+ im = ax.imshow(img, cmap='inferno', vmin=0, vmax=255)
179
+ ax.set_title('Texture: HOG (Green Band)', fontsize=12, fontweight='bold', pad=8)
180
+ cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
181
+ cbar.ax.tick_params(labelsize=8)
182
+ plt.tight_layout()
183
+ plt.savefig(tex_dir / 'hog_green.png', dpi=120, bbox_inches='tight')
184
+ plt.close(fig)
185
+ except Exception as e:
186
+ logger.error(f"Failed to save HOG with colorbar: {e}")
187
+
188
+ # Lacunarity L1 visualization
189
+ lac1 = feats.get('lac1')
190
+ if isinstance(lac1, np.ndarray) and lac1.size > 0:
191
+ try:
192
+ img = lac1.astype(np.float64)
193
+ fig, ax = plt.subplots(figsize=(5, 5))
194
+ ax.set_axis_off()
195
+ ax.set_facecolor('white')
196
+ im = ax.imshow(img, cmap='plasma', vmin=0, vmax=255)
197
+ ax.set_title('Texture: Lacunarity L1 (Green Band)', fontsize=12, fontweight='bold', pad=8)
198
+ cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
199
+ cbar.ax.tick_params(labelsize=8)
200
+ plt.tight_layout()
201
+ plt.savefig(tex_dir / 'lac1_green.png', dpi=120, bbox_inches='tight')
202
+ plt.close(fig)
203
+ except Exception as e:
204
+ logger.error(f"Failed to save Lacunarity L1 with colorbar: {e}")
205
  except Exception as e:
206
  logger.error(f"Failed to save texture: {e}")
207
 
 
221
  synthesized = self._create_size_analysis_from_mask(mask_for_size, base_img_for_size)
222
  titled = self._add_title_banner(synthesized, 'Morphology Size')
223
  cv2.imwrite(str(results_dir / 'size.size_analysis.png'), titled)
224
+
225
+ # Also save YOLO tips visualization if present
226
+ yolo_img = images.get('yolo_tips')
227
+ if isinstance(yolo_img, np.ndarray) and yolo_img.size > 0:
228
+ titled = self._add_title_banner(yolo_img, 'YOLO Tips')
229
+ cv2.imwrite(str(results_dir / 'yolo_tips.png'), titled)
230
  except Exception as e:
231
  logger.error(f"Failed to save size analysis: {e}")
232
 
sorghum_pipeline/pipeline.py CHANGED
@@ -86,6 +86,8 @@ class SorghumPipeline:
86
  "demo": {
87
  "raw_image": (img, Path(single_image_path).name),
88
  "plant_name": "demo",
 
 
89
  }
90
  }
91
 
@@ -116,12 +118,12 @@ class SorghumPipeline:
116
  return plants
117
 
118
  def _extract_features(self, plants: Dict[str, Any]) -> Dict[str, Any]:
119
- """Extract features: texture + vegetation indices."""
120
  for key, pdata in plants.items():
121
  composite = pdata['composite']
122
  mask = pdata.get('mask')
123
 
124
- # --- Texture: LBP on green band ---
125
  pdata['texture_features'] = {}
126
  spectral = pdata.get('spectral_stack', {})
127
  if 'green' in spectral:
@@ -140,7 +142,9 @@ class SorghumPipeline:
140
  gray8 = ((v - m) / denom * 255.0).astype(np.uint8)
141
 
142
  lbp_map = self.texture_extractor.extract_lbp(gray8)
143
- pdata['texture_features'] = {'green': {'features': {'lbp': lbp_map}}}
 
 
144
 
145
  # --- Vegetation indices ---
146
  if spectral and mask is not None:
@@ -148,8 +152,15 @@ class SorghumPipeline:
148
  else:
149
  pdata['vegetation_indices'] = {}
150
 
151
- # --- Morphology (currently empty) ---
152
- pdata['morphology_features'] = {}
 
 
 
 
 
 
 
153
 
154
  return plants
155
 
 
86
  "demo": {
87
  "raw_image": (img, Path(single_image_path).name),
88
  "plant_name": "demo",
89
+ # Keep original normalized array for visualization
90
+ "normalized_input": arr,
91
  }
92
  }
93
 
 
118
  return plants
119
 
120
  def _extract_features(self, plants: Dict[str, Any]) -> Dict[str, Any]:
121
+ """Extract features: texture + vegetation indices + morphology."""
122
  for key, pdata in plants.items():
123
  composite = pdata['composite']
124
  mask = pdata.get('mask')
125
 
126
+ # --- Texture: LBP/HOG/Lacunarity on green band ---
127
  pdata['texture_features'] = {}
128
  spectral = pdata.get('spectral_stack', {})
129
  if 'green' in spectral:
 
142
  gray8 = ((v - m) / denom * 255.0).astype(np.uint8)
143
 
144
  lbp_map = self.texture_extractor.extract_lbp(gray8)
145
+ hog_map = self.texture_extractor.extract_hog(gray8)
146
+ lac1_map = self.texture_extractor.compute_local_lacunarity(gray8)
147
+ pdata['texture_features'] = {'green': {'features': {'lbp': lbp_map, 'hog': hog_map, 'lac1': lac1_map}}}
148
 
149
  # --- Vegetation indices ---
150
  if spectral and mask is not None:
 
152
  else:
153
  pdata['vegetation_indices'] = {}
154
 
155
+ # --- Morphology ---
156
+ try:
157
+ if mask is not None and isinstance(composite, np.ndarray):
158
+ morph = self.morphology_extractor.extract_morphology_features(composite, mask)
159
+ pdata['morphology_features'] = morph
160
+ else:
161
+ pdata['morphology_features'] = {}
162
+ except Exception:
163
+ pdata['morphology_features'] = {}
164
 
165
  return plants
166
 
wrapper.py CHANGED
@@ -66,12 +66,18 @@ def run_pipeline_on_image(input_image_path: str, work_dir: str, save_artifacts:
66
  overlay_path = work / 'results/overlay.png'
67
  mask_path = work / 'results/mask.png'
68
  composite_path = work / 'results/composite.png'
 
 
69
  if overlay_path.exists():
70
  outputs['Overlay'] = str(overlay_path)
71
  if mask_path.exists():
72
  outputs['Mask'] = str(mask_path)
73
  if composite_path.exists():
74
  outputs['Composite'] = str(composite_path)
 
 
 
 
75
 
76
  # Extract simple stats for display if present in pipeline results
77
  try:
@@ -85,6 +91,15 @@ def run_pipeline_on_image(input_image_path: str, work_dir: str, save_artifacts:
85
  st = entry.get('statistics', {}) if isinstance(entry, dict) else {}
86
  if st:
87
  stats_lines.append(f"{name}: mean={st.get('mean', 0):.3f}, std={st.get('std', 0):.3f}")
 
 
 
 
 
 
 
 
 
88
  if stats_lines:
89
  outputs['StatsText'] = "\n".join(stats_lines)
90
  except Exception:
 
66
  overlay_path = work / 'results/overlay.png'
67
  mask_path = work / 'results/mask.png'
68
  composite_path = work / 'results/composite.png'
69
+ yolo_tips_path = work / 'results/yolo_tips.png'
70
+ input_img_path = work / 'results/input_image.png'
71
  if overlay_path.exists():
72
  outputs['Overlay'] = str(overlay_path)
73
  if mask_path.exists():
74
  outputs['Mask'] = str(mask_path)
75
  if composite_path.exists():
76
  outputs['Composite'] = str(composite_path)
77
+ if yolo_tips_path.exists():
78
+ outputs['YOLOTips'] = str(yolo_tips_path)
79
+ if input_img_path.exists():
80
+ outputs['InputImage'] = str(input_img_path)
81
 
82
  # Extract simple stats for display if present in pipeline results
83
  try:
 
91
  st = entry.get('statistics', {}) if isinstance(entry, dict) else {}
92
  if st:
93
  stats_lines.append(f"{name}: mean={st.get('mean', 0):.3f}, std={st.get('std', 0):.3f}")
94
+ # Morphology stats (height, yolo tips)
95
+ morph = pdata.get('morphology_features', {}) if isinstance(pdata, dict) else {}
96
+ traits = morph.get('traits', {}) if isinstance(morph, dict) else {}
97
+ height_cm = traits.get('plant_height_cm')
98
+ if isinstance(height_cm, (int, float)):
99
+ stats_lines.append(f"Plant height: {height_cm:.2f} cm")
100
+ num_tips = traits.get('num_yolo_tips')
101
+ if isinstance(num_tips, (int, float)):
102
+ stats_lines.append(f"YOLO tips: {int(num_tips)}")
103
  if stats_lines:
104
  outputs['StatsText'] = "\n".join(stats_lines)
105
  except Exception: