Adding python files

This commit is contained in:
2025-12-05 09:50:50 +02:00
parent c6143cd11a
commit 6bd2b100ca
24 changed files with 3076 additions and 0 deletions

0
src/utils/__init__.py Normal file
View File

218
src/utils/config_manager.py Normal file
View File

@@ -0,0 +1,218 @@
"""
Configuration manager for the microscopy object detection application.
Handles loading, saving, and accessing application configuration.
"""
import yaml
from pathlib import Path
from typing import Any, Dict, Optional
from src.utils.logger import get_logger
logger = get_logger(__name__)
class ConfigManager:
"""Manages application configuration."""
def __init__(self, config_path: str = "config/app_config.yaml"):
"""
Initialize configuration manager.
Args:
config_path: Path to configuration file
"""
self.config_path = Path(config_path)
self.config: Dict[str, Any] = {}
self._load_config()
def _load_config(self) -> None:
"""Load configuration from YAML file."""
try:
if self.config_path.exists():
with open(self.config_path, "r") as f:
self.config = yaml.safe_load(f) or {}
logger.info(f"Configuration loaded from {self.config_path}")
else:
logger.warning(f"Configuration file not found: {self.config_path}")
self._create_default_config()
except Exception as e:
logger.error(f"Error loading configuration: {e}")
self._create_default_config()
def _create_default_config(self) -> None:
"""Create default configuration."""
self.config = {
"database": {"path": "data/detections.db"},
"image_repository": {
"base_path": "",
"allowed_extensions": [
".jpg",
".jpeg",
".png",
".tif",
".tiff",
".bmp",
],
},
"models": {
"default_base_model": "yolov8s.pt",
"models_directory": "data/models",
},
"training": {
"default_epochs": 100,
"default_batch_size": 16,
"default_imgsz": 640,
"default_patience": 50,
"default_lr0": 0.01,
},
"detection": {
"default_confidence": 0.25,
"default_iou": 0.45,
"max_batch_size": 100,
},
"visualization": {
"bbox_colors": {
"organelle": "#FF6B6B",
"membrane_branch": "#4ECDC4",
"default": "#00FF00",
},
"bbox_thickness": 2,
"font_size": 12,
},
"export": {"formats": ["csv", "json", "excel"], "default_format": "csv"},
"logging": {
"level": "INFO",
"file": "logs/app.log",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
}
self.save_config()
def save_config(self) -> bool:
"""
Save current configuration to file.
Returns:
True if successful, False otherwise
"""
try:
# Create directory if it doesn't exist
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, "w") as f:
yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
logger.info(f"Configuration saved to {self.config_path}")
return True
except Exception as e:
logger.error(f"Error saving configuration: {e}")
return False
def get(self, key: str, default: Any = None) -> Any:
"""
Get configuration value by key.
Args:
key: Configuration key (can use dot notation, e.g., 'database.path')
default: Default value if key not found
Returns:
Configuration value or default
"""
keys = key.split(".")
value = self.config
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
def set(self, key: str, value: Any) -> None:
"""
Set configuration value by key.
Args:
key: Configuration key (can use dot notation)
value: Value to set
"""
keys = key.split(".")
config = self.config
# Navigate to the nested dictionary
for k in keys[:-1]:
if k not in config:
config[k] = {}
config = config[k]
# Set the value
config[keys[-1]] = value
logger.debug(f"Configuration updated: {key} = {value}")
def get_section(self, section: str) -> Dict[str, Any]:
"""
Get entire configuration section.
Args:
section: Section name (e.g., 'database', 'training')
Returns:
Dictionary with section configuration
"""
return self.config.get(section, {})
def update_section(self, section: str, values: Dict[str, Any]) -> None:
"""
Update entire configuration section.
Args:
section: Section name
values: Dictionary with new values
"""
if section not in self.config:
self.config[section] = {}
self.config[section].update(values)
logger.debug(f"Configuration section updated: {section}")
def reload(self) -> None:
"""Reload configuration from file."""
self._load_config()
def get_database_path(self) -> str:
"""Get database path."""
return self.get("database.path", "data/detections.db")
def get_image_repository_path(self) -> str:
"""Get image repository base path."""
return self.get("image_repository.base_path", "")
def set_image_repository_path(self, path: str) -> None:
"""Set image repository base path."""
self.set("image_repository.base_path", path)
self.save_config()
def get_models_directory(self) -> str:
"""Get models directory path."""
return self.get("models.models_directory", "data/models")
def get_default_training_params(self) -> Dict[str, Any]:
"""Get default training parameters."""
return self.get_section("training")
def get_default_detection_params(self) -> Dict[str, Any]:
"""Get default detection parameters."""
return self.get_section("detection")
def get_bbox_colors(self) -> Dict[str, str]:
"""Get bounding box colors for different classes."""
return self.get("visualization.bbox_colors", {})
def get_allowed_extensions(self) -> list:
"""Get list of allowed image file extensions."""
return self.get(
"image_repository.allowed_extensions", [".jpg", ".jpeg", ".png"]
)

235
src/utils/file_utils.py Normal file
View File

@@ -0,0 +1,235 @@
"""
File utility functions for the microscopy object detection application.
"""
import os
from pathlib import Path
from typing import List, Optional
from src.utils.logger import get_logger
logger = get_logger(__name__)
def get_image_files(
directory: str,
allowed_extensions: Optional[List[str]] = None,
recursive: bool = False,
) -> List[str]:
"""
Get all image files in a directory.
Args:
directory: Directory path to search
allowed_extensions: List of allowed file extensions (e.g., ['.jpg', '.png'])
recursive: Whether to search recursively
Returns:
List of absolute paths to image files
"""
if allowed_extensions is None:
allowed_extensions = [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp"]
# Normalize extensions to lowercase
allowed_extensions = [ext.lower() for ext in allowed_extensions]
image_files = []
directory_path = Path(directory)
if not directory_path.exists():
logger.error(f"Directory does not exist: {directory}")
return image_files
try:
if recursive:
# Recursive search
for ext in allowed_extensions:
image_files.extend(directory_path.rglob(f"*{ext}"))
# Also search uppercase extensions
image_files.extend(directory_path.rglob(f"*{ext.upper()}"))
else:
# Top-level search only
for ext in allowed_extensions:
image_files.extend(directory_path.glob(f"*{ext}"))
# Also search uppercase extensions
image_files.extend(directory_path.glob(f"*{ext.upper()}"))
# Convert to absolute paths and sort
image_files = sorted([str(f.absolute()) for f in image_files])
logger.info(f"Found {len(image_files)} image files in {directory}")
except Exception as e:
logger.error(f"Error searching for images: {e}")
return image_files
def ensure_directory(directory: str) -> bool:
"""
Ensure a directory exists, create if it doesn't.
Args:
directory: Directory path
Returns:
True if directory exists or was created successfully
"""
try:
Path(directory).mkdir(parents=True, exist_ok=True)
return True
except Exception as e:
logger.error(f"Error creating directory {directory}: {e}")
return False
def get_relative_path(file_path: str, base_path: str) -> str:
"""
Get relative path from base path.
Args:
file_path: Absolute file path
base_path: Base directory path
Returns:
Relative path string
"""
try:
return str(Path(file_path).relative_to(base_path))
except ValueError:
# If file_path is not relative to base_path, return the filename
return Path(file_path).name
def validate_file_path(file_path: str, must_exist: bool = True) -> bool:
"""
Validate a file path.
Args:
file_path: Path to validate
must_exist: Whether the file must exist
Returns:
True if valid, False otherwise
"""
path = Path(file_path)
if must_exist and not path.exists():
logger.error(f"File does not exist: {file_path}")
return False
if must_exist and not path.is_file():
logger.error(f"Path is not a file: {file_path}")
return False
return True
def get_file_size(file_path: str) -> int:
"""
Get file size in bytes.
Args:
file_path: Path to file
Returns:
File size in bytes, or 0 if error
"""
try:
return Path(file_path).stat().st_size
except Exception as e:
logger.error(f"Error getting file size for {file_path}: {e}")
return 0
def format_file_size(size_bytes: int) -> str:
"""
Format file size in human-readable format.
Args:
size_bytes: Size in bytes
Returns:
Formatted string (e.g., "1.5 MB")
"""
for unit in ["B", "KB", "MB", "GB"]:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def create_unique_filename(directory: str, base_name: str, extension: str) -> str:
"""
Create a unique filename by adding a number suffix if file exists.
Args:
directory: Directory path
base_name: Base filename without extension
extension: File extension (with or without dot)
Returns:
Unique filename
"""
if not extension.startswith("."):
extension = "." + extension
directory_path = Path(directory)
filename = f"{base_name}{extension}"
file_path = directory_path / filename
if not file_path.exists():
return filename
# Add number suffix
counter = 1
while True:
filename = f"{base_name}_{counter}{extension}"
file_path = directory_path / filename
if not file_path.exists():
return filename
counter += 1
def is_image_file(
file_path: str, allowed_extensions: Optional[List[str]] = None
) -> bool:
"""
Check if a file is an image based on extension.
Args:
file_path: Path to file
allowed_extensions: List of allowed extensions
Returns:
True if file is an image
"""
if allowed_extensions is None:
allowed_extensions = [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp"]
extension = Path(file_path).suffix.lower()
return extension in [ext.lower() for ext in allowed_extensions]
def safe_filename(filename: str) -> str:
"""
Convert a string to a safe filename by removing/replacing invalid characters.
Args:
filename: Original filename
Returns:
Safe filename
"""
# Replace invalid characters
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, "_")
# Remove leading/trailing spaces and dots
filename = filename.strip(". ")
# Ensure filename is not empty
if not filename:
filename = "unnamed"
return filename

75
src/utils/logger.py Normal file
View File

@@ -0,0 +1,75 @@
"""
Logging configuration for the microscopy object detection application.
"""
import logging
import sys
from pathlib import Path
from typing import Optional
def setup_logging(
log_file: str = "logs/app.log",
level: str = "INFO",
log_format: Optional[str] = None,
) -> logging.Logger:
"""
Setup application logging.
Args:
log_file: Path to log file
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_format: Custom log format string
Returns:
Configured logger instance
"""
# Create logs directory if it doesn't exist
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
# Default format if none provided
if log_format is None:
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Convert level string to logging constant
numeric_level = getattr(logging, level.upper(), logging.INFO)
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(numeric_level)
# Remove existing handlers
root_logger.handlers.clear()
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(numeric_level)
console_formatter = logging.Formatter(log_format)
console_handler.setFormatter(console_formatter)
root_logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(numeric_level)
file_formatter = logging.Formatter(log_format)
file_handler.setFormatter(file_formatter)
root_logger.addHandler(file_handler)
# Log initial message
root_logger.info("Logging initialized")
return root_logger
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance for a specific module.
Args:
name: Logger name (typically __name__)
Returns:
Logger instance
"""
return logging.getLogger(name)