Making it installabel package and switching to segmentation mode

This commit is contained in:
2025-12-05 15:51:16 +02:00
parent 9011276584
commit 310e0b2285
20 changed files with 667 additions and 56 deletions

View File

@@ -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
View 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())

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -56,7 +56,7 @@ class ConfigManager:
],
},
"models": {
"default_base_model": "yolov8s.pt",
"default_base_model": "yolov8s-seg.pt",
"models_directory": "data/models",
},
"training": {