Muhammed Sezer
commited on
Commit
Β·
40c6797
1
Parent(s):
34d2ee5
parameters added
Browse files
app.py
CHANGED
|
@@ -7,7 +7,10 @@ visual annotations and structured data output.
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import logging
|
|
|
|
| 10 |
import sys
|
|
|
|
|
|
|
| 11 |
from typing import Tuple, Optional
|
| 12 |
|
| 13 |
import cv2
|
|
@@ -70,50 +73,55 @@ def preprocess_image(image: Image.Image) -> np.ndarray:
|
|
| 70 |
raise
|
| 71 |
|
| 72 |
|
| 73 |
-
def detect_objects(image: Image.Image) -> Tuple[Image.Image, pd.DataFrame]:
|
| 74 |
"""
|
| 75 |
Main object detection function that processes images through YOLOv8.
|
| 76 |
|
| 77 |
Args:
|
| 78 |
image (Image.Image): PIL Image object to process
|
|
|
|
| 79 |
|
| 80 |
Returns:
|
| 81 |
-
Tuple[Image.Image, pd.DataFrame]: Annotated image
|
| 82 |
"""
|
| 83 |
if model is None:
|
| 84 |
logger.error("YOLOv8 model not loaded")
|
| 85 |
# Return original image and empty dataframe
|
| 86 |
empty_df = pd.DataFrame(columns=['class', 'confidence', 'x1', 'y1', 'x2', 'y2'])
|
| 87 |
-
return image, empty_df
|
| 88 |
|
| 89 |
try:
|
| 90 |
# Preprocess the image
|
| 91 |
-
logger.info("Starting object detection
|
| 92 |
processed_image = preprocess_image(image)
|
| 93 |
|
| 94 |
-
# Run YOLOv8 inference
|
| 95 |
-
results = model(processed_image)
|
| 96 |
|
| 97 |
# Parse detection results
|
| 98 |
-
annotated_image, detection_data = parse_detection_results(results[0], image)
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
except Exception as e:
|
| 104 |
logger.error("Error during object detection: %s", str(e))
|
| 105 |
# Return original image and empty dataframe on error
|
| 106 |
empty_df = pd.DataFrame(columns=['class', 'confidence', 'x1', 'y1', 'x2', 'y2'])
|
| 107 |
-
return image, empty_df
|
| 108 |
|
| 109 |
|
| 110 |
-
def parse_detection_results(results, original_image: Image.Image) -> Tuple[Image.Image, pd.DataFrame]:
|
| 111 |
"""
|
| 112 |
Parse YOLOv8 detection results to extract classes, confidence scores, and bounding boxes.
|
| 113 |
|
| 114 |
Args:
|
| 115 |
results: YOLOv8 detection results object
|
| 116 |
original_image (Image.Image): Original PIL image for annotation
|
|
|
|
| 117 |
|
| 118 |
Returns:
|
| 119 |
Tuple[Image.Image, pd.DataFrame]: Annotated image and structured detection data
|
|
@@ -132,7 +140,7 @@ def parse_detection_results(results, original_image: Image.Image) -> Tuple[Image
|
|
| 132 |
classes = results.boxes.cls.cpu().numpy()
|
| 133 |
confidences = results.boxes.conf.cpu().numpy()
|
| 134 |
|
| 135 |
-
# Process each detection
|
| 136 |
for i in range(len(boxes)):
|
| 137 |
class_id = int(classes[i])
|
| 138 |
class_name = class_names[class_id]
|
|
@@ -151,9 +159,9 @@ def parse_detection_results(results, original_image: Image.Image) -> Tuple[Image
|
|
| 151 |
|
| 152 |
detection_list.append(detection_record)
|
| 153 |
|
| 154 |
-
logger.info("Parsed %d detections", len(detection_list))
|
| 155 |
else:
|
| 156 |
-
logger.info("No objects detected
|
| 157 |
|
| 158 |
# Create DataFrame from detection list with proper columns
|
| 159 |
if detection_list:
|
|
@@ -183,7 +191,7 @@ def generate_annotated_image(results, original_image: Image.Image) -> Image.Imag
|
|
| 183 |
original_image (Image.Image): Original PIL image
|
| 184 |
|
| 185 |
Returns:
|
| 186 |
-
Image.Image: Annotated PIL image with bounding boxes and labels
|
| 187 |
"""
|
| 188 |
try:
|
| 189 |
# Use YOLOv8's built-in plot method to generate annotations
|
|
@@ -196,11 +204,16 @@ def generate_annotated_image(results, original_image: Image.Image) -> Image.Imag
|
|
| 196 |
font_size=12 # Font size for labels
|
| 197 |
)
|
| 198 |
|
| 199 |
-
#
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
# Convert NumPy array back to PIL Image
|
| 203 |
-
annotated_image = Image.fromarray(annotated_rgb)
|
| 204 |
|
| 205 |
# Ensure the annotated image maintains aspect ratio
|
| 206 |
# The YOLOv8 plot method should preserve the original dimensions
|
|
@@ -214,9 +227,55 @@ def generate_annotated_image(results, original_image: Image.Image) -> Image.Imag
|
|
| 214 |
return original_image
|
| 215 |
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
def create_gradio_interface():
|
| 218 |
"""
|
| 219 |
-
Create Gradio Interface with image upload
|
| 220 |
|
| 221 |
Returns:
|
| 222 |
gr.Interface: Configured Gradio interface
|
|
@@ -224,16 +283,26 @@ def create_gradio_interface():
|
|
| 224 |
# Create the Gradio interface
|
| 225 |
demo = gr.Interface(
|
| 226 |
fn=detect_objects,
|
| 227 |
-
inputs=
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
outputs=[
|
| 234 |
gr.Image(
|
| 235 |
type="pil",
|
| 236 |
-
label="Annotated Image with Detections",
|
| 237 |
height=400,
|
| 238 |
show_label=True
|
| 239 |
),
|
|
@@ -244,9 +313,13 @@ def create_gradio_interface():
|
|
| 244 |
col_count=(6, "fixed"),
|
| 245 |
row_count=(10, "dynamic"),
|
| 246 |
interactive=False
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
)
|
| 248 |
],
|
| 249 |
-
title="π― YOLOv8 Object Detection",
|
| 250 |
description="""
|
| 251 |
**Upload an image to detect objects using YOLOv8!**
|
| 252 |
|
|
@@ -254,14 +327,29 @@ def create_gradio_interface():
|
|
| 254 |
You'll get both a visual representation with bounding boxes and a detailed data table.
|
| 255 |
|
| 256 |
**Supported formats:** JPG, PNG, JPEG
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
""",
|
| 258 |
article="""
|
| 259 |
### How it works:
|
| 260 |
1. **Upload** an image using the interface above
|
| 261 |
-
2. **
|
| 262 |
-
3. **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
The confidence scores are rounded to 3 decimal places, and coordinates to 1 decimal place for clarity.
|
|
|
|
| 265 |
""",
|
| 266 |
theme=gr.themes.Soft(),
|
| 267 |
css="""
|
|
@@ -271,12 +359,15 @@ def create_gradio_interface():
|
|
| 271 |
.output-image {
|
| 272 |
height: 400px !important;
|
| 273 |
}
|
|
|
|
|
|
|
|
|
|
| 274 |
""",
|
| 275 |
examples=[
|
| 276 |
# Add example images if available
|
| 277 |
],
|
| 278 |
cache_examples=False,
|
| 279 |
-
|
| 280 |
)
|
| 281 |
|
| 282 |
return demo
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import logging
|
| 10 |
+
import os
|
| 11 |
import sys
|
| 12 |
+
import tempfile
|
| 13 |
+
from datetime import datetime
|
| 14 |
from typing import Tuple, Optional
|
| 15 |
|
| 16 |
import cv2
|
|
|
|
| 73 |
raise
|
| 74 |
|
| 75 |
|
| 76 |
+
def detect_objects(image: Image.Image, conf_threshold: float = 0.25) -> Tuple[Image.Image, pd.DataFrame, str]:
|
| 77 |
"""
|
| 78 |
Main object detection function that processes images through YOLOv8.
|
| 79 |
|
| 80 |
Args:
|
| 81 |
image (Image.Image): PIL Image object to process
|
| 82 |
+
conf_threshold (float): Confidence threshold for filtering detections (0.0 to 1.0)
|
| 83 |
|
| 84 |
Returns:
|
| 85 |
+
Tuple[Image.Image, pd.DataFrame, str]: Annotated image, detection results table, and CSV file path
|
| 86 |
"""
|
| 87 |
if model is None:
|
| 88 |
logger.error("YOLOv8 model not loaded")
|
| 89 |
# Return original image and empty dataframe
|
| 90 |
empty_df = pd.DataFrame(columns=['class', 'confidence', 'x1', 'y1', 'x2', 'y2'])
|
| 91 |
+
return image, empty_df, None
|
| 92 |
|
| 93 |
try:
|
| 94 |
# Preprocess the image
|
| 95 |
+
logger.info("Starting object detection with confidence threshold: %.2f", conf_threshold)
|
| 96 |
processed_image = preprocess_image(image)
|
| 97 |
|
| 98 |
+
# Run YOLOv8 inference with confidence threshold
|
| 99 |
+
results = model(processed_image, conf=conf_threshold)
|
| 100 |
|
| 101 |
# Parse detection results
|
| 102 |
+
annotated_image, detection_data = parse_detection_results(results[0], image, conf_threshold)
|
| 103 |
|
| 104 |
+
# Create CSV file for download
|
| 105 |
+
csv_file_path = create_csv_download(detection_data, conf_threshold)
|
| 106 |
+
|
| 107 |
+
logger.info("Object detection completed successfully with %d detections", len(detection_data))
|
| 108 |
+
return annotated_image, detection_data, csv_file_path
|
| 109 |
|
| 110 |
except Exception as e:
|
| 111 |
logger.error("Error during object detection: %s", str(e))
|
| 112 |
# Return original image and empty dataframe on error
|
| 113 |
empty_df = pd.DataFrame(columns=['class', 'confidence', 'x1', 'y1', 'x2', 'y2'])
|
| 114 |
+
return image, empty_df, None
|
| 115 |
|
| 116 |
|
| 117 |
+
def parse_detection_results(results, original_image: Image.Image, conf_threshold: float = 0.25) -> Tuple[Image.Image, pd.DataFrame]:
|
| 118 |
"""
|
| 119 |
Parse YOLOv8 detection results to extract classes, confidence scores, and bounding boxes.
|
| 120 |
|
| 121 |
Args:
|
| 122 |
results: YOLOv8 detection results object
|
| 123 |
original_image (Image.Image): Original PIL image for annotation
|
| 124 |
+
conf_threshold (float): Confidence threshold used for filtering
|
| 125 |
|
| 126 |
Returns:
|
| 127 |
Tuple[Image.Image, pd.DataFrame]: Annotated image and structured detection data
|
|
|
|
| 140 |
classes = results.boxes.cls.cpu().numpy()
|
| 141 |
confidences = results.boxes.conf.cpu().numpy()
|
| 142 |
|
| 143 |
+
# Process each detection (already filtered by YOLOv8 based on conf_threshold)
|
| 144 |
for i in range(len(boxes)):
|
| 145 |
class_id = int(classes[i])
|
| 146 |
class_name = class_names[class_id]
|
|
|
|
| 159 |
|
| 160 |
detection_list.append(detection_record)
|
| 161 |
|
| 162 |
+
logger.info("Parsed %d detections above confidence threshold %.2f", len(detection_list), conf_threshold)
|
| 163 |
else:
|
| 164 |
+
logger.info("No objects detected above confidence threshold %.2f", conf_threshold)
|
| 165 |
|
| 166 |
# Create DataFrame from detection list with proper columns
|
| 167 |
if detection_list:
|
|
|
|
| 191 |
original_image (Image.Image): Original PIL image
|
| 192 |
|
| 193 |
Returns:
|
| 194 |
+
Image.Image: Annotated PIL image with bounding boxes and labels in RGB format
|
| 195 |
"""
|
| 196 |
try:
|
| 197 |
# Use YOLOv8's built-in plot method to generate annotations
|
|
|
|
| 204 |
font_size=12 # Font size for labels
|
| 205 |
)
|
| 206 |
|
| 207 |
+
# IMPORTANT: YOLOv8's plot() method returns BGR format, but Gradio expects RGB
|
| 208 |
+
# Convert BGR to RGB to fix color display issue
|
| 209 |
+
if len(annotated_array.shape) == 3 and annotated_array.shape[2] == 3:
|
| 210 |
+
annotated_rgb = cv2.cvtColor(annotated_array, cv2.COLOR_BGR2RGB)
|
| 211 |
+
else:
|
| 212 |
+
# Fallback if image format is unexpected
|
| 213 |
+
annotated_rgb = annotated_array
|
| 214 |
|
| 215 |
# Convert NumPy array back to PIL Image
|
| 216 |
+
annotated_image = Image.fromarray(annotated_rgb.astype(np.uint8))
|
| 217 |
|
| 218 |
# Ensure the annotated image maintains aspect ratio
|
| 219 |
# The YOLOv8 plot method should preserve the original dimensions
|
|
|
|
| 227 |
return original_image
|
| 228 |
|
| 229 |
|
| 230 |
+
def create_csv_download(detection_data: pd.DataFrame, conf_threshold: float = 0.25) -> str:
|
| 231 |
+
"""
|
| 232 |
+
Create a CSV file from detection results for download.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
detection_data (pd.DataFrame): Detection results dataframe
|
| 236 |
+
conf_threshold (float): Confidence threshold used for filtering
|
| 237 |
+
|
| 238 |
+
Returns:
|
| 239 |
+
str: Path to the created CSV file
|
| 240 |
+
"""
|
| 241 |
+
try:
|
| 242 |
+
# Create a temporary CSV file
|
| 243 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 244 |
+
csv_filename = f"detection_results_{timestamp}.csv"
|
| 245 |
+
|
| 246 |
+
# Use a temporary directory
|
| 247 |
+
temp_dir = tempfile.gettempdir()
|
| 248 |
+
csv_path = os.path.join(temp_dir, csv_filename)
|
| 249 |
+
|
| 250 |
+
# Save DataFrame to CSV with additional metadata
|
| 251 |
+
if not detection_data.empty:
|
| 252 |
+
# Add metadata to the CSV
|
| 253 |
+
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
|
| 254 |
+
f.write(f"# YOLOv8 Object Detection Results\n")
|
| 255 |
+
f.write(f"# Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 256 |
+
f.write(f"# Confidence threshold: {conf_threshold:.2f} ({conf_threshold*100:.0f}%)\n")
|
| 257 |
+
f.write(f"# Total detections: {len(detection_data)}\n")
|
| 258 |
+
f.write("# Columns: class, confidence, x1, y1, x2, y2\n")
|
| 259 |
+
f.write("# Coordinates are in pixels (x1,y1 = top-left, x2,y2 = bottom-right)\n")
|
| 260 |
+
f.write("#\n")
|
| 261 |
+
|
| 262 |
+
# Append the actual data
|
| 263 |
+
detection_data.to_csv(csv_path, mode='a', index=False)
|
| 264 |
+
else:
|
| 265 |
+
# Create empty CSV with headers
|
| 266 |
+
detection_data.to_csv(csv_path, index=False)
|
| 267 |
+
|
| 268 |
+
logger.info("CSV file created successfully: %s", csv_path)
|
| 269 |
+
return csv_path
|
| 270 |
+
|
| 271 |
+
except Exception as e:
|
| 272 |
+
logger.error("Error creating CSV file: %s", str(e))
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
+
|
| 276 |
def create_gradio_interface():
|
| 277 |
"""
|
| 278 |
+
Create Gradio Interface with image upload, confidence slider, and triple output.
|
| 279 |
|
| 280 |
Returns:
|
| 281 |
gr.Interface: Configured Gradio interface
|
|
|
|
| 283 |
# Create the Gradio interface
|
| 284 |
demo = gr.Interface(
|
| 285 |
fn=detect_objects,
|
| 286 |
+
inputs=[
|
| 287 |
+
gr.Image(
|
| 288 |
+
type="pil",
|
| 289 |
+
label="Upload Image for Object Detection",
|
| 290 |
+
sources=["upload", "clipboard"],
|
| 291 |
+
height=400
|
| 292 |
+
),
|
| 293 |
+
gr.Slider(
|
| 294 |
+
minimum=0.01,
|
| 295 |
+
maximum=1.0,
|
| 296 |
+
value=0.25,
|
| 297 |
+
step=0.01,
|
| 298 |
+
label="Confidence Threshold",
|
| 299 |
+
info="Only show detections above this confidence level (0.01 = 1%, 1.0 = 100%)"
|
| 300 |
+
)
|
| 301 |
+
],
|
| 302 |
outputs=[
|
| 303 |
gr.Image(
|
| 304 |
type="pil",
|
| 305 |
+
label="Annotated Image with Detections (RGB Fixed)",
|
| 306 |
height=400,
|
| 307 |
show_label=True
|
| 308 |
),
|
|
|
|
| 313 |
col_count=(6, "fixed"),
|
| 314 |
row_count=(10, "dynamic"),
|
| 315 |
interactive=False
|
| 316 |
+
),
|
| 317 |
+
gr.File(
|
| 318 |
+
label="Download Results as CSV",
|
| 319 |
+
visible=True
|
| 320 |
)
|
| 321 |
],
|
| 322 |
+
title="π― YOLOv8 Object Detection with Confidence Control",
|
| 323 |
description="""
|
| 324 |
**Upload an image to detect objects using YOLOv8!**
|
| 325 |
|
|
|
|
| 327 |
You'll get both a visual representation with bounding boxes and a detailed data table.
|
| 328 |
|
| 329 |
**Supported formats:** JPG, PNG, JPEG
|
| 330 |
+
|
| 331 |
+
**Features:**
|
| 332 |
+
- β
Fixed RGB color display issue
|
| 333 |
+
- ποΈ Adjustable confidence threshold slider
|
| 334 |
+
- π₯ Download detection results as CSV
|
| 335 |
+
- π Real-time filtering based on confidence scores
|
| 336 |
""",
|
| 337 |
article="""
|
| 338 |
### How it works:
|
| 339 |
1. **Upload** an image using the interface above
|
| 340 |
+
2. **Adjust** the confidence threshold slider to filter detections (default: 0.25 = 25%)
|
| 341 |
+
3. **View** the annotated image with bounding boxes and labels (RGB colors fixed)
|
| 342 |
+
4. **Analyze** the detection data in the table with confidence scores and coordinates
|
| 343 |
+
5. **Download** the results as a CSV file for further analysis
|
| 344 |
+
|
| 345 |
+
**Confidence Threshold Guide:**
|
| 346 |
+
- **0.01-0.20**: Very permissive - shows many detections, including uncertain ones
|
| 347 |
+
- **0.25-0.50**: Balanced - good mix of accuracy and detection count (recommended)
|
| 348 |
+
- **0.50-0.80**: Conservative - only high-confidence detections
|
| 349 |
+
- **0.80-1.00**: Very strict - only extremely confident detections
|
| 350 |
|
| 351 |
The confidence scores are rounded to 3 decimal places, and coordinates to 1 decimal place for clarity.
|
| 352 |
+
The CSV file includes all detection data with timestamps for easy tracking.
|
| 353 |
""",
|
| 354 |
theme=gr.themes.Soft(),
|
| 355 |
css="""
|
|
|
|
| 359 |
.output-image {
|
| 360 |
height: 400px !important;
|
| 361 |
}
|
| 362 |
+
.slider-container {
|
| 363 |
+
margin: 10px 0;
|
| 364 |
+
}
|
| 365 |
""",
|
| 366 |
examples=[
|
| 367 |
# Add example images if available
|
| 368 |
],
|
| 369 |
cache_examples=False,
|
| 370 |
+
flagging_mode="never"
|
| 371 |
)
|
| 372 |
|
| 373 |
return demo
|