Adding python files
This commit is contained in:
0
src/utils/__init__.py
Normal file
0
src/utils/__init__.py
Normal file
218
src/utils/config_manager.py
Normal file
218
src/utils/config_manager.py
Normal 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
235
src/utils/file_utils.py
Normal 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
75
src/utils/logger.py
Normal 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)
|
||||
Reference in New Issue
Block a user