Making it installabel package and switching to segmentation mode
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Microscopy Object Detection Application
|
||||
|
||||
A desktop application for detecting and segmenting organelles and membrane
|
||||
branching structures in microscopy images using YOLOv8-seg.
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "Your Name"
|
||||
__email__ = "your.email@example.com"
|
||||
__license__ = "MIT"
|
||||
|
||||
# Package metadata
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"__author__",
|
||||
"__email__",
|
||||
"__license__",
|
||||
]
|
||||
|
||||
61
src/cli.py
Normal file
61
src/cli.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Command-line interface for microscopy object detection application.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from src import __version__
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Microscopy Object Detection Application - CLI Interface",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Launch GUI
|
||||
microscopy-detect-gui
|
||||
|
||||
# Show version
|
||||
microscopy-detect --version
|
||||
|
||||
# Get help
|
||||
microscopy-detect --help
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"microscopy-object-detection {__version__}",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--gui",
|
||||
action="store_true",
|
||||
help="Launch the GUI application (same as microscopy-detect-gui)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.gui:
|
||||
# Launch GUI
|
||||
try:
|
||||
from main import main as gui_main
|
||||
|
||||
gui_main()
|
||||
except Exception as e:
|
||||
print(f"Error launching GUI: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Show help if no arguments provided
|
||||
parser.print_help()
|
||||
print("\nTo launch the GUI, use: microscopy-detect-gui")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,7 +6,7 @@ Handles all database operations including CRUD operations, queries, and exports.
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional, Tuple, Any
|
||||
from typing import List, Dict, Optional, Tuple, Any, Union
|
||||
from pathlib import Path
|
||||
import csv
|
||||
import hashlib
|
||||
@@ -56,7 +56,7 @@ class DatabaseManager:
|
||||
model_name: str,
|
||||
model_version: str,
|
||||
model_path: str,
|
||||
base_model: str = "yolov8s.pt",
|
||||
base_model: str = "yolov8s-seg.pt",
|
||||
training_params: Optional[Dict] = None,
|
||||
metrics: Optional[Dict] = None,
|
||||
) -> int:
|
||||
@@ -243,6 +243,7 @@ class DatabaseManager:
|
||||
class_name: str,
|
||||
bbox: Tuple[float, float, float, float], # (x_min, y_min, x_max, y_max)
|
||||
confidence: float,
|
||||
segmentation_mask: Optional[List[List[float]]] = None,
|
||||
metadata: Optional[Dict] = None,
|
||||
) -> int:
|
||||
"""
|
||||
@@ -254,6 +255,7 @@ class DatabaseManager:
|
||||
class_name: Detected object class
|
||||
bbox: Bounding box coordinates (normalized 0-1)
|
||||
confidence: Detection confidence score
|
||||
segmentation_mask: Polygon coordinates for segmentation [[x1,y1], [x2,y2], ...]
|
||||
metadata: Additional metadata
|
||||
|
||||
Returns:
|
||||
@@ -265,8 +267,8 @@ class DatabaseManager:
|
||||
x_min, y_min, x_max, y_max = bbox
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO detections (image_id, model_id, class_name, x_min, y_min, x_max, y_max, confidence, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO detections (image_id, model_id, class_name, x_min, y_min, x_max, y_max, confidence, segmentation_mask, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
image_id,
|
||||
@@ -277,6 +279,7 @@ class DatabaseManager:
|
||||
x_max,
|
||||
y_max,
|
||||
confidence,
|
||||
json.dumps(segmentation_mask) if segmentation_mask else None,
|
||||
json.dumps(metadata) if metadata else None,
|
||||
),
|
||||
)
|
||||
@@ -302,8 +305,8 @@ class DatabaseManager:
|
||||
bbox = det["bbox"]
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO detections (image_id, model_id, class_name, x_min, y_min, x_max, y_max, confidence, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO detections (image_id, model_id, class_name, x_min, y_min, x_max, y_max, confidence, segmentation_mask, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
det["image_id"],
|
||||
@@ -314,6 +317,11 @@ class DatabaseManager:
|
||||
bbox[2],
|
||||
bbox[3],
|
||||
det["confidence"],
|
||||
(
|
||||
json.dumps(det.get("segmentation_mask"))
|
||||
if det.get("segmentation_mask")
|
||||
else None
|
||||
),
|
||||
(
|
||||
json.dumps(det.get("metadata"))
|
||||
if det.get("metadata")
|
||||
@@ -385,9 +393,11 @@ class DatabaseManager:
|
||||
detections = []
|
||||
for row in cursor.fetchall():
|
||||
det = dict(row)
|
||||
# Parse JSON metadata
|
||||
# Parse JSON fields
|
||||
if det.get("metadata"):
|
||||
det["metadata"] = json.loads(det["metadata"])
|
||||
if det.get("segmentation_mask"):
|
||||
det["segmentation_mask"] = json.loads(det["segmentation_mask"])
|
||||
detections.append(det)
|
||||
|
||||
return detections
|
||||
@@ -538,6 +548,7 @@ class DatabaseManager:
|
||||
"x_max",
|
||||
"y_max",
|
||||
"confidence",
|
||||
"segmentation_mask",
|
||||
"detected_at",
|
||||
]
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
@@ -545,6 +556,11 @@ class DatabaseManager:
|
||||
|
||||
for det in detections:
|
||||
row = {k: det[k] for k in fieldnames if k in det}
|
||||
# Convert segmentation mask list to JSON string for CSV
|
||||
if row.get("segmentation_mask") and isinstance(
|
||||
row["segmentation_mask"], list
|
||||
):
|
||||
row["segmentation_mask"] = json.dumps(row["segmentation_mask"])
|
||||
writer.writerow(row)
|
||||
|
||||
return True
|
||||
@@ -580,6 +596,7 @@ class DatabaseManager:
|
||||
class_name: str,
|
||||
bbox: Tuple[float, float, float, float],
|
||||
annotator: str,
|
||||
segmentation_mask: Optional[List[List[float]]] = None,
|
||||
verified: bool = False,
|
||||
) -> int:
|
||||
"""Add manual annotation."""
|
||||
@@ -589,10 +606,20 @@ class DatabaseManager:
|
||||
x_min, y_min, x_max, y_max = bbox
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO annotations (image_id, class_name, x_min, y_min, x_max, y_max, annotator, verified)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO annotations (image_id, class_name, x_min, y_min, x_max, y_max, segmentation_mask, annotator, verified)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(image_id, class_name, x_min, y_min, x_max, y_max, annotator, verified),
|
||||
(
|
||||
image_id,
|
||||
class_name,
|
||||
x_min,
|
||||
y_min,
|
||||
x_max,
|
||||
y_max,
|
||||
json.dumps(segmentation_mask) if segmentation_mask else None,
|
||||
annotator,
|
||||
verified,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
@@ -5,7 +5,7 @@ These dataclasses represent the database entities.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Tuple
|
||||
from typing import Optional, Dict, Tuple, List
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -46,6 +46,9 @@ class Detection:
|
||||
class_name: str
|
||||
bbox: Tuple[float, float, float, float] # (x_min, y_min, x_max, y_max)
|
||||
confidence: float
|
||||
segmentation_mask: Optional[
|
||||
List[List[float]]
|
||||
] # List of polygon coordinates [[x1,y1], [x2,y2], ...]
|
||||
detected_at: datetime
|
||||
metadata: Optional[Dict]
|
||||
|
||||
@@ -58,6 +61,9 @@ class Annotation:
|
||||
image_id: int
|
||||
class_name: str
|
||||
bbox: Tuple[float, float, float, float] # (x_min, y_min, x_max, y_max)
|
||||
segmentation_mask: Optional[
|
||||
List[List[float]]
|
||||
] # List of polygon coordinates [[x1,y1], [x2,y2], ...]
|
||||
annotator: str
|
||||
created_at: datetime
|
||||
verified: bool
|
||||
|
||||
@@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS detections (
|
||||
x_max REAL NOT NULL CHECK(x_max >= 0 AND x_max <= 1),
|
||||
y_max REAL NOT NULL CHECK(y_max >= 0 AND y_max <= 1),
|
||||
confidence REAL NOT NULL CHECK(confidence >= 0 AND confidence <= 1),
|
||||
segmentation_mask TEXT, -- JSON string of polygon coordinates [[x1,y1], [x2,y2], ...]
|
||||
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata TEXT, -- JSON string for additional metadata
|
||||
FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE,
|
||||
@@ -52,6 +53,7 @@ CREATE TABLE IF NOT EXISTS annotations (
|
||||
y_min REAL NOT NULL CHECK(y_min >= 0 AND y_min <= 1),
|
||||
x_max REAL NOT NULL CHECK(x_max >= 0 AND x_max <= 1),
|
||||
y_max REAL NOT NULL CHECK(y_max >= 0 AND y_max <= 1),
|
||||
segmentation_mask TEXT, -- JSON string of polygon coordinates [[x1,y1], [x2,y2], ...]
|
||||
annotator TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
verified BOOLEAN DEFAULT 0,
|
||||
|
||||
@@ -121,7 +121,7 @@ class ConfigDialog(QDialog):
|
||||
models_layout.addRow("Models Directory:", self.models_dir_edit)
|
||||
|
||||
self.base_model_edit = QLineEdit()
|
||||
self.base_model_edit.setPlaceholderText("yolov8s.pt")
|
||||
self.base_model_edit.setPlaceholderText("yolov8s-seg.pt")
|
||||
models_layout.addRow("Default Base Model:", self.base_model_edit)
|
||||
|
||||
models_group.setLayout(models_layout)
|
||||
@@ -232,7 +232,7 @@ class ConfigDialog(QDialog):
|
||||
self.config_manager.get("models.models_directory", "data/models")
|
||||
)
|
||||
self.base_model_edit.setText(
|
||||
self.config_manager.get("models.default_base_model", "yolov8s.pt")
|
||||
self.config_manager.get("models.default_base_model", "yolov8s-seg.pt")
|
||||
)
|
||||
|
||||
# Training settings
|
||||
|
||||
@@ -159,7 +159,7 @@ class DetectionTab(QWidget):
|
||||
|
||||
# Add base model option
|
||||
base_model = self.config_manager.get(
|
||||
"models.default_base_model", "yolov8s.pt"
|
||||
"models.default_base_model", "yolov8s-seg.pt"
|
||||
)
|
||||
self.model_combo.addItem(
|
||||
f"Base Model ({base_model})", {"id": 0, "path": base_model}
|
||||
@@ -256,7 +256,7 @@ class DetectionTab(QWidget):
|
||||
if model_id == 0:
|
||||
# Create database entry for base model
|
||||
base_model = self.config_manager.get(
|
||||
"models.default_base_model", "yolov8s.pt"
|
||||
"models.default_base_model", "yolov8s-seg.pt"
|
||||
)
|
||||
model_id = self.db_manager.add_model(
|
||||
model_name="Base Model",
|
||||
|
||||
@@ -87,6 +87,7 @@ class InferenceEngine:
|
||||
"class_name": det["class_name"],
|
||||
"bbox": tuple(bbox_normalized),
|
||||
"confidence": det["confidence"],
|
||||
"segmentation_mask": det.get("segmentation_mask"),
|
||||
"metadata": {"class_id": det["class_id"]},
|
||||
}
|
||||
detection_records.append(record)
|
||||
@@ -160,6 +161,7 @@ class InferenceEngine:
|
||||
conf: float = 0.25,
|
||||
bbox_thickness: int = 2,
|
||||
bbox_colors: Optional[Dict[str, str]] = None,
|
||||
draw_masks: bool = True,
|
||||
) -> tuple:
|
||||
"""
|
||||
Detect objects and return annotated image.
|
||||
@@ -169,6 +171,7 @@ class InferenceEngine:
|
||||
conf: Confidence threshold
|
||||
bbox_thickness: Thickness of bounding boxes
|
||||
bbox_colors: Dictionary mapping class names to hex colors
|
||||
draw_masks: Whether to draw segmentation masks (if available)
|
||||
|
||||
Returns:
|
||||
Tuple of (detections, annotated_image_array)
|
||||
@@ -189,12 +192,8 @@ class InferenceEngine:
|
||||
bbox_colors = {}
|
||||
default_color = self._hex_to_bgr(bbox_colors.get("default", "#00FF00"))
|
||||
|
||||
# Draw bounding boxes
|
||||
# Draw detections
|
||||
for det in detections:
|
||||
# Get absolute coordinates
|
||||
bbox_abs = det["bbox_absolute"]
|
||||
x1, y1, x2, y2 = [int(v) for v in bbox_abs]
|
||||
|
||||
# Get color for this class
|
||||
class_name = det["class_name"]
|
||||
color_hex = bbox_colors.get(
|
||||
@@ -202,7 +201,33 @@ class InferenceEngine:
|
||||
)
|
||||
color = self._hex_to_bgr(color_hex)
|
||||
|
||||
# Draw box
|
||||
# Draw segmentation mask if available and requested
|
||||
if draw_masks and det.get("segmentation_mask"):
|
||||
mask_normalized = det["segmentation_mask"]
|
||||
if mask_normalized and len(mask_normalized) > 0:
|
||||
# Convert normalized coordinates to absolute pixels
|
||||
mask_points = np.array(
|
||||
[
|
||||
[int(pt[0] * width), int(pt[1] * height)]
|
||||
for pt in mask_normalized
|
||||
],
|
||||
dtype=np.int32,
|
||||
)
|
||||
|
||||
# Create a semi-transparent overlay
|
||||
overlay = img.copy()
|
||||
cv2.fillPoly(overlay, [mask_points], color)
|
||||
# Blend with original image (30% opacity)
|
||||
cv2.addWeighted(overlay, 0.3, img, 0.7, 0, img)
|
||||
|
||||
# Draw mask contour
|
||||
cv2.polylines(img, [mask_points], True, color, bbox_thickness)
|
||||
|
||||
# Get absolute coordinates for bounding box
|
||||
bbox_abs = det["bbox_absolute"]
|
||||
x1, y1, x2, y2 = [int(v) for v in bbox_abs]
|
||||
|
||||
# Draw bounding box
|
||||
cv2.rectangle(img, (x1, y1), (x2, y2), color, bbox_thickness)
|
||||
|
||||
# Prepare label
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = get_logger(__name__)
|
||||
class YOLOWrapper:
|
||||
"""Wrapper for YOLOv8 model operations."""
|
||||
|
||||
def __init__(self, model_path: str = "yolov8s.pt"):
|
||||
def __init__(self, model_path: str = "yolov8s-seg.pt"):
|
||||
"""
|
||||
Initialize YOLO model.
|
||||
|
||||
@@ -282,6 +282,10 @@ class YOLOWrapper:
|
||||
boxes = result.boxes
|
||||
image_path = str(result.path)
|
||||
orig_shape = result.orig_shape # (height, width)
|
||||
height, width = orig_shape
|
||||
|
||||
# Check if this is a segmentation model with masks
|
||||
has_masks = hasattr(result, "masks") and result.masks is not None
|
||||
|
||||
for i in range(len(boxes)):
|
||||
# Get normalized coordinates
|
||||
@@ -299,6 +303,33 @@ class YOLOWrapper:
|
||||
float(v) for v in boxes.xyxy[i].cpu().numpy()
|
||||
], # Absolute pixels
|
||||
}
|
||||
|
||||
# Extract segmentation mask if available
|
||||
if has_masks:
|
||||
try:
|
||||
# Get the mask for this detection
|
||||
mask_data = result.masks.xy[
|
||||
i
|
||||
] # Polygon coordinates in absolute pixels
|
||||
|
||||
# Convert to normalized coordinates
|
||||
if len(mask_data) > 0:
|
||||
mask_normalized = []
|
||||
for point in mask_data:
|
||||
x_norm = float(point[0]) / width
|
||||
y_norm = float(point[1]) / height
|
||||
mask_normalized.append([x_norm, y_norm])
|
||||
detection["segmentation_mask"] = mask_normalized
|
||||
else:
|
||||
detection["segmentation_mask"] = None
|
||||
except Exception as mask_error:
|
||||
logger.warning(
|
||||
f"Error extracting mask for detection {i}: {mask_error}"
|
||||
)
|
||||
detection["segmentation_mask"] = None
|
||||
else:
|
||||
detection["segmentation_mask"] = None
|
||||
|
||||
detections.append(detection)
|
||||
|
||||
return detections
|
||||
|
||||
@@ -56,7 +56,7 @@ class ConfigManager:
|
||||
],
|
||||
},
|
||||
"models": {
|
||||
"default_base_model": "yolov8s.pt",
|
||||
"default_base_model": "yolov8s-seg.pt",
|
||||
"models_directory": "data/models",
|
||||
},
|
||||
"training": {
|
||||
|
||||
Reference in New Issue
Block a user