File size: 8,507 Bytes
24a683e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# utils.py - Helper functions for insect detection demo

import cv2
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Dict, Tuple, Optional
from ultralytics import YOLO

def perform_detection(
    yolo_model: YOLO,
    frame: np.ndarray,
    conf_threshold: float=0.5
) -> Optional[List[Dict]]:
    """
    Runs the YOLO model inference on a single frame.
    """
    if frame is None:
        print("Error: Input frame is None in perform_detection.")
        return None
    try:
        # Perform inference using the model
        results = yolo_model.predict(source=frame, conf=conf_threshold, verbose=False)
        return results
    except Exception as e:
        print(f"Error during model prediction: {e}")
        return None

def create_motion_mask(frame, threshold=25):
    """
    Creates a simple motion mask from an image.
    For the demo, we'll use a basic thresholding approach.
    """
    # Convert to grayscale
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # Apply Gaussian blur to reduce noise
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    
    # Apply adaptive thresholding
    thresh = cv2.adaptiveThreshold(
        blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
        cv2.THRESH_BINARY_INV, 11, threshold
    )
    
    # Apply morphological operations to clean up the mask
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
    
    return mask

def postprocess_results(
    results: Optional[List[Dict]],
    model_class_names: Dict[int, str],
    mask: Optional[np.ndarray] = None
) -> List[Dict]:
    """
    Extracts information from detection results.
    If a mask is provided, only keeps detections that overlap with the mask.
    """
    detections_list = []
    if results is None or not results:
        return detections_list

    try:
        boxes = results[0].boxes
    except (IndexError, AttributeError) as e:
        print(f"Warning: Could not access boxes in results: {e}")
        return detections_list

    for box in boxes:
        try:
            # Extract bounding box coordinates (xyxy format)
            xyxy = box.xyxy[0].cpu().numpy().astype(int)
            x1, y1, x2, y2 = xyxy

            # If we have a mask, check if this detection overlaps with it
            if mask is not None:
                # Check if the center of the bounding box is within the mask
                center_x = (x1 + x2) // 2
                center_y = (y1 + y2) // 2
                
                # Also check if a significant portion of the box overlaps with the mask
                # First make sure we stay within mask boundaries
                y1_safe = max(0, min(y1, mask.shape[0]-1))
                y2_safe = max(0, min(y2, mask.shape[0]-1))
                x1_safe = max(0, min(x1, mask.shape[1]-1))
                x2_safe = max(0, min(x2, mask.shape[1]-1))
                
                # Extract the region of the mask corresponding to the bounding box
                box_region = mask[y1_safe:y2_safe, x1_safe:x2_safe]
                
                # Calculate overlap
                if box_region.size > 0:
                    mask_coverage = np.sum(box_region > 0) / box_region.size
                else:
                    mask_coverage = 0
                
                # Skip this detection if it doesn't overlap with the mask
                if not (0 <= center_y < mask.shape[0] and 0 <= center_x < mask.shape[1] and 
                       (mask[center_y, center_x] > 0 or mask_coverage > 0.5)):
                    continue

            # Extract confidence score
            conf = float(box.conf[0].cpu().numpy())

            # Extract class ID and map to class name
            cls_id = int(box.cls[0].cpu().numpy())
            class_name = model_class_names.get(cls_id, f"Unknown Class {cls_id}")

            # Store detection info
            detections_list.append({
                'class_name': class_name,
                'confidence': conf,
                'bbox_xyxy': [x1, y1, x2, y2]
            })
        except Exception as e:
            print(f"Error processing a detection box: {e}")
            continue
    return detections_list

def draw_detections(
    frame: np.ndarray,
    detections: List[Dict],
    mask: Optional[np.ndarray] = None
) -> np.ndarray:
    """
    Draws bounding boxes and labels on the frame.
    If mask is provided, overlays it on the frame.
    """
    output_frame = frame.copy()
    
    # If we have a mask, overlay it with transparency
    if mask is not None and mask.shape[0] > 0 and mask.shape[1] > 0:
        # Create a colored mask for overlay
        mask_overlay = np.zeros_like(output_frame)
        mask_overlay[mask > 0] = [0, 100, 0]  # Green tint for mask regions
        
        # Blend mask with the frame
        output_frame = cv2.addWeighted(output_frame, 0.7, mask_overlay, 0.3, 0)
    
    # Draw bounding boxes
    color = (0, 255, 0)  # Green color for bounding box
    font_scale = 1.2
    font = cv2.FONT_HERSHEY_SIMPLEX
    for detection in detections:
        try:
            x1, y1, x2, y2 = detection['bbox_xyxy']
            class_name = detection['class_name']
            conf = detection['confidence']

            # Draw Bounding Box
            cv2.rectangle(output_frame, (x1, y1), (x2, y2), color, 5)

            # Prepare and Draw Label
            label = f"{class_name}: {conf:.2f}"

            # Calculate text size for background
            (label_width, label_height), baseline = cv2.getTextSize(label, font, font_scale, 3)
            label_ymin = max(y1, label_height + 10)

            # Draw background for text
            cv2.rectangle(output_frame,
                          (x1, label_ymin - label_height - 10),
                          (x1 + label_width, label_ymin - baseline),
                          color,
                          cv2.FILLED)

            # Add text
            cv2.putText(output_frame,
                        label,
                        (x1, label_ymin - 5),
                        font,
                        font_scale,
                        (255, 255, 255),  # White color
                        3)
        except Exception as e:
            continue
    return output_frame

def load_yolo_model(model_path):
    """
    Loads the YOLO model from the specified path.
    """
    print("Loading the YOLO model...")
    try:
        model = YOLO(model_path)
        class_names = model.names
        print(f"Model loaded with {len(class_names)} classes!")
        return model, class_names
    except Exception as e:
        print(f"Error loading model: {e}")
        return None, None

def load_image(image_path):
    """
    Loads an image from the specified path.
    """
    print(f"Opening image: {image_path}")
    image = cv2.imread(image_path)
    if image is None:
        print(f"Error: Could not read image file '{image_path}'.")
    return image

def load_or_create_mask(image, mask_path=None):
    """
    Either loads a mask from disk or creates a new one from the image.
    """
    if mask_path and os.path.exists(mask_path):
        print(f"Loading mask: {mask_path}")
        mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
    else:
        print("Creating mask from image...")
        mask = create_motion_mask(image)
    
    return mask

def display_results(output_frame, detections, mask=None):
    """
    Displays detection results and saves the output image.
    """
    # Display results in console
    print("\n--- Insects Detected ---")
    if detections:
        for i, obj in enumerate(detections, 1):
            print(f"{i}. {obj['class_name']} (confidence: {obj['confidence']:.2f})")
    else:
        print("No insects detected.")
    
    # Save mask if it exists
    if mask is not None:
        cv2.imwrite("motion_mask.png", mask)
        print("Motion mask saved to: motion_mask.png")
    
    # Convert from BGR to RGB for display
    output_rgb = cv2.cvtColor(output_frame, cv2.COLOR_BGR2RGB)
    
    # Display the image
    plt.figure(figsize=(10, 8))
    plt.imshow(output_rgb)
    plt.title("Insect Detection Results")
    plt.axis('off')
    plt.show()
    
    # Save result
    result_path = "detection_result.jpg"
    cv2.imwrite(result_path, output_frame)
    print(f"Result saved to: {result_path}")

import os  # Added for file path operations