File size: 13,660 Bytes
fc5324e
 
c421799
3b61715
 
65137d3
8d8f9ef
 
 
3b61715
 
 
ade40fc
fc5324e
c421799
 
 
 
fc5324e
 
3b61715
fc5324e
 
8d8f9ef
fc5324e
503cb09
fc5324e
 
 
503cb09
 
fc5324e
 
 
 
c421799
 
3b61715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c421799
503cb09
 
65137d3
c421799
503cb09
 
3b61715
503cb09
3b61715
fc5324e
503cb09
3b61715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d8f9ef
3b61715
 
 
 
 
503cb09
3b61715
503cb09
fc5324e
503cb09
 
 
3b61715
 
 
ade40fc
 
 
 
 
 
 
 
 
3b61715
 
ade40fc
 
3b61715
 
 
ade40fc
 
3b61715
 
 
 
 
 
ade40fc
 
3b61715
 
 
 
 
 
 
 
ade40fc
 
3b61715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d8f9ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# yolo_predictor.py
import os
import logging
import tempfile
import numpy as np
import tifffile
from io import BytesIO
import cv2
from PIL import Image
from rasterio.transform import from_bounds
from ultralytics import YOLO
from ndvi_predictor import normalize_rgb, predict_ndvi
from resize_image import resize_image_optimized

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def load_yolo_model(model_path):
    """Load YOLO model from .pt file"""
    logger.info(f"Loading YOLO model from: {model_path}")
    return YOLO(model_path)

def predict_yolo(yolo_model, image_path, conf=0.25):
    """
    Predict using YOLO model on 4-channel TIFF image
    
    Args:
        yolo_model: Loaded YOLO model
        image_path: Path to 4-channel TIFF image
        conf: Confidence threshold
    
    Returns:
        results: YOLO results object
    """
    logger.info(f"Starting YOLO prediction on: {image_path} with confidence: {conf}")
    
    # Verify file exists and has correct format
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"Image file not found: {image_path}")
    
    try:
        # Quick validation of the TIFF file
        test_array = tifffile.imread(image_path)
        logger.info(f"TIFF file shape: {test_array.shape}, dtype: {test_array.dtype}")
        
        # Validate channels
        if len(test_array.shape) == 3:
            channels = test_array.shape[0] if test_array.shape[0] <= 4 else test_array.shape[2]
        else:
            channels = 1
        
        if channels != 4:
            raise ValueError(f"Expected 4-channel image, got {channels} channels")
            
    except Exception as e:
        logger.error(f"Error validating TIFF file: {e}")
        raise
    
    logger.info("Running YOLO model inference...")
    # Run YOLO prediction directly on the input file
    results = yolo_model([image_path], conf=conf)
    
    logger.info(f"YOLO prediction completed. Results type: {type(results[0])}")
    return results[0]  # Return first result

def create_4channel_tiff(rgb_array, ndvi_array, output_path):
    """
    Create a 4-channel TIFF file with RGB channels + NDVI channel
    
    Args:
        rgb_array: RGB image array (H, W, 3)
        ndvi_array: NDVI array (H, W) with values in [-1, 1]
        output_path: Path to save the 4-channel TIFF
    """
    logger.info(f"Creating 4-channel TIFF file at: {output_path}")
    logger.info(f"RGB shape: {rgb_array.shape}, NDVI shape: {ndvi_array.shape}")
    
    # Ensure RGB is in uint8 format
    if rgb_array.dtype != np.uint8:
        if rgb_array.max() <= 1.0:
            rgb_uint8 = (rgb_array * 255).astype(np.uint8)
        else:
            rgb_uint8 = rgb_array.astype(np.uint8)
    else:
        rgb_uint8 = rgb_array
    
    # Convert NDVI from [-1, 1] to [0, 255] uint8 format (same as reference code)
    ndvi_scaled = (((ndvi_array + 1) / 2) * 255).astype(np.uint8)
    
    logger.info(f"RGB range: [{rgb_uint8.min()}, {rgb_uint8.max()}]")
    logger.info(f"NDVI scaled range: [{ndvi_scaled.min()}, {ndvi_scaled.max()}]")
    
    # Stack RGB + NDVI to create 4-channel image
    # Format: (channels, height, width) - channel-first format
    four_channel = np.stack([
        rgb_uint8[:, :, 0],  # R channel
        rgb_uint8[:, :, 1],  # G channel  
        rgb_uint8[:, :, 2],  # B channel
        ndvi_scaled          # NDVI channel
    ], axis=0)
    
    logger.info(f"4-channel array shape: {four_channel.shape}, dtype: {four_channel.dtype}")
    logger.info(f"4-channel range: [{four_channel.min()}, {four_channel.max()}]")
    
    # Save as TIFF using tifffile
    tifffile.imwrite(output_path, four_channel)
    logger.info(f"Successfully saved 4-channel TIFF (RGB+NDVI format) to: {output_path}")

def predict_pipeline(ndvi_model, yolo_model, rgb_array, conf=0.25):
    """
    Full pipeline: RGB -> NDVI -> 32-bit 4-channel TIFF (RGB+NDVI) -> YOLO prediction
    
    Args:
        ndvi_model: Loaded NDVI prediction model
        yolo_model: Loaded YOLO model
        rgb_array: RGB image as numpy array (H, W, 3)
        conf: Confidence threshold for YOLO
    
    Returns:
        results: YOLO results object
    """
    logger.info("Starting full prediction pipeline")
    logger.info(f"Input RGB array shape: {rgb_array.shape}, dtype: {rgb_array.dtype}")
    
    # Step 1: Resize RGB image to target size
    logger.info("Step 1: Resizing RGB image to target size")
    target_size = (640, 640)  # (height, width)
    rgb_resized = resize_image_optimized(rgb_array, target_size)
    logger.info(f"Resized RGB shape: {rgb_resized.shape}")
    
    # Step 2: Normalize RGB image
    logger.info("Step 2: Normalizing RGB image")
    normalized_rgb = normalize_rgb(rgb_resized)
    logger.info(f"Normalized RGB shape: {normalized_rgb.shape}, range: [{normalized_rgb.min():.3f}, {normalized_rgb.max():.3f}]")
    
    # Step 3: Predict NDVI
    logger.info("Step 3: Predicting NDVI from RGB")
    ndvi_prediction = predict_ndvi(ndvi_model, normalized_rgb)
    logger.info(f"NDVI prediction shape: {ndvi_prediction.shape}, range: [{ndvi_prediction.min():.3f}, {ndvi_prediction.max():.3f}]")
    
    # Step 4: Create 4-channel TIFF file
    logger.info("Step 4: Creating 4-channel TIFF file (RGB+NDVI)")
    
    # Create temporary file for the 4-channel TIFF
    with tempfile.NamedTemporaryFile(delete=False, suffix='.tiff') as tmp_file:
        tiff_path = tmp_file.name
    
    try:
        # Create the 4-channel TIFF using resized RGB and predicted NDVI
        create_4channel_tiff(rgb_resized, ndvi_prediction, tiff_path)
        
        # Verify the created file
        if not os.path.exists(tiff_path):
            raise FileNotFoundError(f"Failed to create 4-channel TIFF at: {tiff_path}")
        
        file_size = os.path.getsize(tiff_path)
        logger.info(f"Created 4-channel TIFF file size: {file_size} bytes")
        
        # Step 5: Run YOLO prediction on the 4-channel TIFF
        logger.info("Step 5: Running YOLO prediction on 4-channel TIFF")
        results = predict_yolo(yolo_model, tiff_path, conf=conf)
        
        logger.info("Full pipeline completed successfully")
        return results
        
    except Exception as e:
        logger.error(f"Error in pipeline: {e}")
        raise
    finally:
        # Clean up temporary file
        if os.path.exists(tiff_path):
            try:
                os.unlink(tiff_path)
                logger.info(f"Cleaned up temporary file: {tiff_path}")
            except Exception as cleanup_error:
                logger.warning(f"Failed to clean up temporary file: {cleanup_error}")

def validate_4channel_tiff(tiff_path):
    """
    Validate that a TIFF file has exactly 4 channels
    
    Args:
        tiff_path: Path to TIFF file
    
    Returns:
        bool: True if valid 4-channel TIFF, False otherwise
    """
    try:
        array = tifffile.imread(tiff_path)
        
        if len(array.shape) == 3:
            channels = array.shape[0] if array.shape[0] <= 4 else array.shape[2]
        else:
            channels = 1
            
        logger.info(f"TIFF validation - Shape: {array.shape}, Channels: {channels}")
        return channels == 4
        
    except Exception as e:
        logger.error(f"Error validating TIFF file: {e}")
        return False

# Additional functions for yolo_predictor.py

def predict_yolo_with_image(yolo_model, image_path, conf=0.25, save_path=None):
    """
    Predict using YOLO model on 4-channel TIFF image and return annotated image
    
    Args:
        yolo_model: Loaded YOLO model
        image_path: Path to 4-channel TIFF image
        conf: Confidence threshold
        save_path: Optional path to save the annotated image
    
    Returns:
        annotated_image: PIL Image object with annotations
    """
    logger.info(f"Starting YOLO prediction with image output on: {image_path} with confidence: {conf}")
    
    # Verify file exists and has correct format
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"Image file not found: {image_path}")
    
    try:
        # Quick validation of the TIFF file
        test_array = tifffile.imread(image_path)
        logger.info(f"TIFF file shape: {test_array.shape}, dtype: {test_array.dtype}")
        
        # Validate channels
        if len(test_array.shape) == 3:
            channels = test_array.shape[0] if test_array.shape[0] <= 4 else test_array.shape[2]
        else:
            channels = 1
        
        if channels != 4:
            raise ValueError(f"Expected 4-channel image, got {channels} channels")
            
    except Exception as e:
        logger.error(f"Error validating TIFF file: {e}")
        raise
    
    logger.info("Running YOLO model inference with image output...")
    
    # Run YOLO prediction directly on the input file
    results = yolo_model([image_path], conf=conf)
    result = results[0]
    
    # Create temporary file for saving annotated image
    if save_path is None:
        with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp_file:
            save_path = tmp_file.name
    
    try:
        # Save the annotated image using ultralytics built-in method
        result.save(save_path)
        logger.info(f"Annotated image saved to: {save_path}")
        
        # Load the saved image and convert to PIL Image
        annotated_image = Image.open(save_path).convert('RGB')
        logger.info(f"YOLO prediction with image output completed successfully")
        
        return annotated_image
        
    except Exception as e:
        logger.error(f"Error saving annotated image: {e}")
        raise
    finally:
        # Clean up temporary file if we created it
        if save_path.endswith('.png') and os.path.exists(save_path):
            try:
                os.unlink(save_path)
                logger.info(f"Cleaned up temporary annotated image file: {save_path}")
            except Exception as cleanup_error:
                logger.warning(f"Failed to clean up temporary file: {cleanup_error}")

def predict_pipeline_with_image(ndvi_model, yolo_model, rgb_array, conf=0.25):
    """
    Full pipeline with image output: RGB -> NDVI -> 32-bit 4-channel TIFF (RGB+NDVI) -> YOLO prediction -> Annotated Image
    
    Args:
        ndvi_model: Loaded NDVI prediction model
        yolo_model: Loaded YOLO model
        rgb_array: RGB image as numpy array (H, W, 3)
        conf: Confidence threshold for YOLO
    
    Returns:
        annotated_image: PIL Image object with YOLO annotations
    """
    logger.info("Starting full prediction pipeline with image output")
    logger.info(f"Input RGB array shape: {rgb_array.shape}, dtype: {rgb_array.dtype}")
    
    # Step 1: Resize RGB image to target size
    logger.info("Step 1: Resizing RGB image to target size")
    target_size = (640, 640)  # (height, width)
    rgb_resized = resize_image_optimized(rgb_array, target_size)
    logger.info(f"Resized RGB shape: {rgb_resized.shape}")
    
    # Step 2: Normalize RGB image
    logger.info("Step 2: Normalizing RGB image")
    normalized_rgb = normalize_rgb(rgb_resized)
    logger.info(f"Normalized RGB shape: {normalized_rgb.shape}, range: [{normalized_rgb.min():.3f}, {normalized_rgb.max():.3f}]")
    
    # Step 3: Predict NDVI
    logger.info("Step 3: Predicting NDVI from RGB")
    ndvi_prediction = predict_ndvi(ndvi_model, normalized_rgb)
    logger.info(f"NDVI prediction shape: {ndvi_prediction.shape}, range: [{ndvi_prediction.min():.3f}, {ndvi_prediction.max():.3f}]")
    
    # Step 4: Create 4-channel TIFF file
    logger.info("Step 4: Creating 4-channel TIFF file (RGB+NDVI)")
    
    # Create temporary file for the 4-channel TIFF
    with tempfile.NamedTemporaryFile(delete=False, suffix='.tiff') as tmp_file:
        tiff_path = tmp_file.name
    
    try:
        # Create the 4-channel TIFF using resized RGB and predicted NDVI
        create_4channel_tiff(rgb_resized, ndvi_prediction, tiff_path)
        
        # Verify the created file
        if not os.path.exists(tiff_path):
            raise FileNotFoundError(f"Failed to create 4-channel TIFF at: {tiff_path}")
        
        file_size = os.path.getsize(tiff_path)
        logger.info(f"Created 4-channel TIFF file size: {file_size} bytes")
        
        # Step 5: Run YOLO prediction on the 4-channel TIFF and get annotated image
        logger.info("Step 5: Running YOLO prediction on 4-channel TIFF with image output")
        annotated_image = predict_yolo_with_image(yolo_model, tiff_path, conf=conf)
        
        logger.info("Full pipeline with image output completed successfully")
        return annotated_image
        
    except Exception as e:
        logger.error(f"Error in pipeline with image output: {e}")
        raise
    finally:
        # Clean up temporary file
        if os.path.exists(tiff_path):
            try:
                os.unlink(tiff_path)
                logger.info(f"Cleaned up temporary file: {tiff_path}")
            except Exception as cleanup_error:
                logger.warning(f"Failed to clean up temporary file: {cleanup_error}")

def pil_image_to_bytes(image, format='PNG'):
    """
    Convert PIL Image to bytes for API response
    
    Args:
        image: PIL Image object
        format: Image format ('PNG', 'JPEG', etc.)
    
    Returns:
        BytesIO: Image as bytes buffer
    """
    img_bytes = BytesIO()
    image.save(img_bytes, format=format)
    img_bytes.seek(0)
    return img_bytes