Fahimeh Orvati Nia
update the morphology size to plant morphology
1bf989c
"""
Minimal output manager for demo (saves only 7 required images).
"""
import os
import numpy as np
import cv2
import matplotlib
if os.environ.get('MPLBACKEND') is None:
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from pathlib import Path
from typing import Dict, Any
import logging
logger = logging.getLogger(__name__)
class OutputManager:
"""Minimal output manager for demo."""
def __init__(self, output_folder: str, settings: Any):
"""Initialize output manager."""
self.output_folder = Path(output_folder)
self.settings = settings
try:
self.minimal_demo: bool = bool(int(os.environ.get('MINIMAL_DEMO', '0')))
except Exception:
self.minimal_demo = False
self.output_folder.mkdir(parents=True, exist_ok=True)
def create_output_directories(self) -> None:
"""Create output directories."""
self.output_folder.mkdir(parents=True, exist_ok=True)
def save_plant_results(self, plant_key: str, plant_data: Dict[str, Any]) -> None:
"""Save minimal demo outputs only."""
if not self.minimal_demo:
logger.warning("OutputManager configured for minimal demo only")
return
self._save_minimal_demo_outputs(plant_data)
def _save_input_image_only(self, plant_key: str, plant_data: Dict[str, Any]) -> None:
"""Quick save of just the input image for immediate display."""
results_dir = self.output_folder / 'results'
results_dir.mkdir(parents=True, exist_ok=True)
try:
norm_input = plant_data.get('normalized_input')
if isinstance(norm_input, np.ndarray):
vis = norm_input
if vis.ndim == 2:
vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_GRAY2RGB)
elif vis.ndim == 3 and vis.shape[2] == 3:
if vis.dtype != np.uint8:
vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
else:
vis_u8 = vis
# Assume BGR
vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_BGR2RGB)
else:
vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_GRAY2RGB)
titled = self._add_title_banner(vis_rgb, 'Input Image')
cv2.imwrite(str(results_dir / 'input_image.png'), titled)
except Exception as e:
logger.error(f"Failed to save input image: {e}")
def _save_minimal_demo_outputs(self, plant_data: Dict[str, Any]) -> None:
"""Save only the 7 required images."""
results_dir = self.output_folder / 'results'
veg_dir = self.output_folder / 'Vegetation_indices_images'
tex_dir = self.output_folder / 'texture_output'
results_dir.mkdir(parents=True, exist_ok=True)
veg_dir.mkdir(parents=True, exist_ok=True)
tex_dir.mkdir(parents=True, exist_ok=True)
# 1. Mask
try:
mask = plant_data.get('mask')
if isinstance(mask, np.ndarray):
titled = self._add_title_banner(mask, 'Mask')
cv2.imwrite(str(results_dir / 'mask.png'), titled)
except Exception as e:
logger.error(f"Failed to save mask: {e}")
# 2. Overlay
try:
base_image = plant_data.get('composite')
mask = plant_data.get('mask')
if isinstance(base_image, np.ndarray) and isinstance(mask, np.ndarray):
overlay = self._create_overlay(base_image, mask)
# Convert BGR→RGB for correct viewing in standard image viewers
overlay_rgb = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
titled = self._add_title_banner(overlay_rgb, 'Segmentation Overlay')
cv2.imwrite(str(results_dir / 'overlay.png'), titled)
except Exception as e:
logger.error(f"Failed to save overlay: {e}")
# 2b. Composite (input to segmentation)
try:
base_image = plant_data.get('composite')
if isinstance(base_image, np.ndarray):
# Ensure uint8
if base_image.dtype != np.uint8:
base_image = self._normalize_to_uint8(base_image.astype(np.float64))
# Convert BGR→RGB for human viewing
comp_rgb = cv2.cvtColor(base_image, cv2.COLOR_BGR2RGB)
titled = self._add_title_banner(comp_rgb, 'Composite (Segmentation Input)')
cv2.imwrite(str(results_dir / 'composite.png'), titled)
except Exception as e:
logger.error(f"Failed to save composite: {e}")
# 2c. Normalized input image visualization
try:
norm_input = plant_data.get('normalized_input')
if isinstance(norm_input, np.ndarray):
vis = norm_input
if vis.ndim == 2:
vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_GRAY2RGB)
elif vis.ndim == 3 and vis.shape[2] == 3:
if vis.dtype != np.uint8:
vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
else:
vis_u8 = vis
# Assume BGR
vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_BGR2RGB)
else:
vis_u8 = self._normalize_to_uint8(vis.astype(np.float64))
vis_rgb = cv2.cvtColor(vis_u8, cv2.COLOR_GRAY2RGB)
titled = self._add_title_banner(vis_rgb, 'Input Image')
cv2.imwrite(str(results_dir / 'input_image.png'), titled)
except Exception as e:
logger.error(f"Failed to save normalized input image: {e}")
# 3-5. Vegetation indices (NDVI, GNDVI, SAVI)
try:
veg = plant_data.get('vegetation_indices', {})
for name in ['NDVI', 'GNDVI', 'SAVI']:
data = veg.get(name, {})
values = data.get('values') if isinstance(data, dict) else None
if isinstance(values, np.ndarray) and values.size > 0:
try:
# Colormap with colorbar similar to src: use matplotlib savefig
cmap = cm.RdYlGn
# Value ranges
if name in ['NDVI', 'GNDVI']:
vmin, vmax = (-1, 1)
else:
vmin, vmax = (0, 1)
masked = np.ma.masked_invalid(values.astype(np.float64))
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_axis_off()
ax.set_facecolor('white')
im = ax.imshow(masked, cmap=cmap, vmin=vmin, vmax=vmax)
ax.set_title(f"{name}", fontsize=12, fontweight='bold', pad=8)
# add colorbar
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.ax.tick_params(labelsize=8)
plt.tight_layout()
plt.savefig(veg_dir / f"{name.lower()}.png", dpi=120, bbox_inches='tight')
plt.close(fig)
except Exception as e:
logger.error(f"Failed to save {name}: {e}")
except Exception as e:
logger.error(f"Failed to save vegetation indices: {e}")
# 6. Texture features: ONLY LBP on green band
try:
tex = plant_data.get('texture_features', {})
green_band = tex.get('green', {})
feats = green_band.get('features', {})
lbp = feats.get('lbp')
if isinstance(lbp, np.ndarray) and lbp.size > 0:
try:
img = lbp.astype(np.float64)
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_axis_off()
ax.set_facecolor('white')
im = ax.imshow(img, cmap='gray', vmin=0, vmax=255)
ax.set_title('Texture: LBP (Green Band)', fontsize=12, fontweight='bold', pad=8)
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.ax.tick_params(labelsize=8)
plt.tight_layout()
plt.savefig(tex_dir / 'lbp_green.png', dpi=120, bbox_inches='tight')
plt.close(fig)
except Exception as e:
logger.error(f"Failed to save LBP with colorbar: {e}")
# HOG visualization
hog = feats.get('hog')
if isinstance(hog, np.ndarray) and hog.size > 0:
try:
img = hog.astype(np.float64)
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_axis_off()
ax.set_facecolor('white')
im = ax.imshow(img, cmap='inferno', vmin=0, vmax=255)
ax.set_title('Texture: HOG (Green Band)', fontsize=12, fontweight='bold', pad=8)
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.ax.tick_params(labelsize=8)
plt.tight_layout()
plt.savefig(tex_dir / 'hog_green.png', dpi=120, bbox_inches='tight')
plt.close(fig)
except Exception as e:
logger.error(f"Failed to save HOG with colorbar: {e}")
# Lacunarity L1 visualization
lac1 = feats.get('lac1')
if isinstance(lac1, np.ndarray) and lac1.size > 0:
try:
img = lac1.astype(np.float64)
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_axis_off()
ax.set_facecolor('white')
im = ax.imshow(img, cmap='plasma', vmin=0, vmax=255)
ax.set_title('Texture: Lacunarity L1 (Green Band)', fontsize=12, fontweight='bold', pad=8)
cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
cbar.ax.tick_params(labelsize=8)
plt.tight_layout()
plt.savefig(tex_dir / 'lac1_green.png', dpi=120, bbox_inches='tight')
plt.close(fig)
except Exception as e:
logger.error(f"Failed to save Lacunarity L1 with colorbar: {e}")
except Exception as e:
logger.error(f"Failed to save texture: {e}")
# 9. Plant Morphology analysis
try:
morph = plant_data.get('morphology_features', {})
images = morph.get('images', {})
size_img = images.get('size_analysis')
if isinstance(size_img, np.ndarray) and size_img.size > 0:
titled = self._add_title_banner(size_img, 'Plant Morphology')
cv2.imwrite(str(results_dir / 'size.size_analysis.png'), titled)
else:
# Fallback: synthesize a simple size analysis from the mask if available
mask_for_size = plant_data.get('mask')
base_img_for_size = plant_data.get('composite')
if isinstance(mask_for_size, np.ndarray) and mask_for_size.size > 0:
synthesized = self._create_size_analysis_from_mask(mask_for_size, base_img_for_size)
titled = self._add_title_banner(synthesized, 'Plant Morphology')
cv2.imwrite(str(results_dir / 'size.size_analysis.png'), titled)
# YOLO disabled for speed - skip saving yolo_tips.png
except Exception as e:
logger.error(f"Failed to save size analysis: {e}")
def _create_overlay(self, image: np.ndarray, mask: np.ndarray) -> np.ndarray:
"""Create green overlay on brightened composite, following src pipeline style."""
if mask is None:
return image
if mask.shape[:2] != image.shape[:2]:
mask = cv2.resize(mask.astype(np.uint8), (image.shape[1], image.shape[0]),
interpolation=cv2.INTER_NEAREST)
binary = (mask.astype(np.int32) > 0).astype(np.uint8) * 255
base = image
if base.dtype != np.uint8:
base = self._normalize_to_uint8(base.astype(np.float64))
bright = cv2.convertScaleAbs(base, alpha=1.2, beta=15)
green_overlay = bright.copy()
green_overlay[binary == 255] = (0, 255, 0)
blended = cv2.addWeighted(bright, 1.0, green_overlay, 0.5, 0)
return blended
def _normalize_to_uint8(self, arr: np.ndarray) -> np.ndarray:
"""Normalize to uint8."""
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
ptp = np.ptp(arr)
if ptp > 0:
normalized = (arr - arr.min()) / (ptp + 1e-6) * 255
else:
normalized = np.zeros_like(arr)
return np.clip(normalized, 0, 255).astype(np.uint8)
def _add_title_banner(self, image: np.ndarray, title: str) -> np.ndarray:
"""Add a top banner with centered title text to an image using OpenCV.
Supports grayscale or color images; returns a BGR image.
"""
if image is None or image.size == 0:
return image
# Ensure 3-channel BGR for drawing
if image.ndim == 2:
base_bgr = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
elif image.ndim == 3 and image.shape[2] == 3:
base_bgr = image.copy()
elif image.ndim == 3 and image.shape[2] == 4:
base_bgr = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)
else:
# Fallback: normalize to uint8 then convert to BGR
norm = self._normalize_to_uint8(image.astype(np.float64))
base_bgr = cv2.cvtColor(norm, cv2.COLOR_GRAY2BGR)
h, w = base_bgr.shape[:2]
banner_height = max(30, int(0.08 * h))
banner = np.full((banner_height, w, 3), (245, 245, 245), dtype=np.uint8)
# Compose banner + image
composed = np.vstack([banner, base_bgr])
# Put centered title text
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = max(0.5, min(1.0, w / 800.0))
thickness = 1
text = str(title)
(tw, th), baseline = cv2.getTextSize(text, font, font_scale, thickness)
x = max(5, (w - tw) // 2)
y = (banner_height + th) // 2
# Slight shadow for readability
cv2.putText(composed, text, (x+1, y+1), font, font_scale, (0, 0, 0), thickness+1, cv2.LINE_AA)
cv2.putText(composed, text, (x, y), font, font_scale, (0, 80, 0), thickness+1, cv2.LINE_AA)
return composed
def _create_size_analysis_from_mask(self, mask: np.ndarray, base_image: Any = None) -> np.ndarray:
"""Create a simple size analysis visualization from a binary mask.
Draws contours and prints pixel area. If base_image is provided, overlays on it; otherwise uses a white canvas.
"""
if mask is None or mask.size == 0:
return np.zeros((1, 1, 3), dtype=np.uint8)
# Prepare base image
if isinstance(base_image, np.ndarray) and base_image.size > 0:
img = base_image
if img.dtype != np.uint8:
img = self._normalize_to_uint8(img.astype(np.float64))
if img.ndim == 2:
base_bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
elif img.ndim == 3 and img.shape[2] == 3:
base_bgr = img.copy()
elif img.ndim == 3 and img.shape[2] == 4:
base_bgr = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
else:
norm = self._normalize_to_uint8(img.astype(np.float64))
base_bgr = cv2.cvtColor(norm, cv2.COLOR_GRAY2BGR)
else:
h, w = mask.shape[:2]
base_bgr = np.full((h, w, 3), 255, dtype=np.uint8)
# Ensure binary mask
if mask.ndim == 3 and mask.shape[2] == 3:
gray = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
else:
gray = mask.astype(np.uint8)
_, bin_mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
# Contours and area
contours, _ = cv2.findContours(bin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(base_bgr, contours, -1, (0, 0, 255), 1)
area_px = int(cv2.countNonZero(bin_mask))
# Bounding box for the largest contour
if contours:
largest = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(largest)
cv2.rectangle(base_bgr, (x, y), (x + w, y + h), (255, 0, 0), 1)
# Put area text
cv2.putText(base_bgr, f"Area: {area_px} px", (10, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2, cv2.LINE_AA)
return base_bgr
def _create_fallback_yolo_panel(self, mask: Any, base_image: Any = None) -> np.ndarray:
"""Create a fallback YOLO tips panel when detections are unavailable.
Uses the composite image if available; otherwise, creates a white canvas sized to mask.
"""
try:
if isinstance(base_image, np.ndarray) and base_image.size > 0:
img = base_image
if img.dtype != np.uint8:
img = self._normalize_to_uint8(img.astype(np.float64))
if img.ndim == 2:
panel = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
elif img.ndim == 3 and img.shape[2] == 3:
panel = img.copy()
elif img.ndim == 3 and img.shape[2] == 4:
panel = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
else:
norm = self._normalize_to_uint8(img.astype(np.float64))
panel = cv2.cvtColor(norm, cv2.COLOR_GRAY2BGR)
else:
if isinstance(mask, np.ndarray) and mask.size > 0:
h, w = mask.shape[:2]
else:
h, w = 256, 256
panel = np.full((h, w, 3), 255, dtype=np.uint8)
# Optionally show mask centroid as hint
try:
if isinstance(mask, np.ndarray) and mask.size > 0:
m = mask
if m.ndim == 3:
m = cv2.cvtColor(m, cv2.COLOR_BGR2GRAY)
_, bin_m = cv2.threshold(m.astype(np.uint8), 0, 255, cv2.THRESH_BINARY)
moments = cv2.moments(bin_m)
if moments['m00'] != 0:
cx = int(moments['m10'] / moments['m00'])
cy = int(moments['m01'] / moments['m00'])
cv2.drawMarker(panel, (cx, cy), (0, 0, 255), markerType=cv2.MARKER_TILTED_CROSS, markerSize=12, thickness=2)
except Exception:
pass
return panel
except Exception:
return np.full((256, 256, 3), 255, dtype=np.uint8)