Fahimeh Orvati Nia
commited on
Commit
·
93d0941
1
Parent(s):
2ff67cd
update
Browse files- SSL_greenhouse_tip_detection.pt +3 -0
- app.py +13 -3
- sorghum_pipeline/features/morphology.py +125 -20
- sorghum_pipeline/output/manager.py +66 -0
- sorghum_pipeline/pipeline.py +16 -5
- wrapper.py +15 -0
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
|
| 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 |
-
"""
|
| 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 |
-
"""
|
| 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 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|