File size: 19,275 Bytes
b4123b8
dd1d7f5
b4123b8
 
 
 
 
dd1d7f5
 
 
 
 
b4123b8
dd1d7f5
b4123b8
 
 
 
 
 
dd1d7f5
b4123b8
 
dd1d7f5
b4123b8
 
 
dd1d7f5
b4123b8
dd1d7f5
b4123b8
 
 
dd1d7f5
b4123b8
 
 
dd1d7f5
 
 
b4123b8
 
dd1d7f5
981de0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b4123b8
dd1d7f5
 
 
 
 
 
 
 
b4123b8
dd1d7f5
 
 
 
5b8812f
 
b4123b8
dd1d7f5
b4123b8
dd1d7f5
b4123b8
dd1d7f5
 
 
 
c170961
 
5b8812f
 
b4123b8
dd1d7f5
b4123b8
5f6c42c
 
 
 
 
 
 
c170961
 
5b8812f
 
5f6c42c
 
 
93d0941
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c170961
b4123b8
dd1d7f5
c170961
dd1d7f5
 
 
 
c170961
 
 
 
 
 
 
 
dd1d7f5
 
 
 
c170961
5b8812f
c170961
 
 
dd1d7f5
c170961
dd1d7f5
 
 
b4123b8
 
dd1d7f5
c170961
b4123b8
dd1d7f5
c170961
 
 
 
 
 
 
 
 
 
 
5b8812f
c170961
 
 
 
 
 
 
93d0941
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b4123b8
dd1d7f5
 
1bf989c
b4123b8
dd1d7f5
 
 
 
1bf989c
5b8812f
2ff67cd
 
 
 
 
 
1bf989c
2ff67cd
93d0941
668a993
b4123b8
dd1d7f5
b4123b8
dd1d7f5
c170961
b4123b8
 
 
c170961
 
b4123b8
c170961
 
 
 
 
 
 
 
b4123b8
 
dd1d7f5
b4123b8
4c1c4a7
 
 
b4123b8
 
5b8812f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2ff67cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69cba14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
"""
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)