""" MicroGhost-Thermal: Configuration Module (V2) =============================================== Central configuration for MicroGhost V2 dual-branch architecture. V2 Changes: - Dual independent branches (RGB + Thermal) with separate weights - EnergyGate fusion at Scale 2 - BiFusion Neck (replaces FPN) - 3 anchors per cell - Phase-based training with CMM - Accuracy-first design (optimize for ESP32 later) """ import os import torch # ============================================================================ # DEVICE CONFIGURATION # ============================================================================ DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' # ============================================================================ # DATASET CONFIGURATION # ============================================================================ _PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) DATASET_ROOT = os.environ.get('MICROGHOST_DATA_ROOT', os.path.join(_PROJECT_DIR, 'data')) # Per-dataset subdirectory names under DATASET_ROOT DATASET_SUBDIRS = { 'llvip': 'llvip', 'forestpersons': 'ForestPersons', 'forestpersonsir': 'ForestPersonsIR', 'camod3fd': 'camo-m3fd', # Legacy (loaders kept for backward compat, not used in V2 training) 'kaist': 'kaist-multispectral', 'flirv2': 'FLIR_ADAS_v2', } # HuggingFace dataset identifiers (for auto-download) HUGGINGFACE_DATASETS = { 'forestpersons': 'etri/ForestPersons', 'forestpersonsir': 'etri/ForestPersonsIR', } def get_dataset_path(dataset_name): print(f'[DEBUG] Resolving path for {dataset_name}...') """Resolve dataset root path (env override or default under DATASET_ROOT).""" env_key = f'MICROGHOST_{dataset_name.upper()}_PATH' if env_key in os.environ: return os.environ[env_key] subdir = DATASET_SUBDIRS.get(dataset_name, dataset_name) path = os.path.join(DATASET_ROOT, subdir) if os.path.isdir(path): print(f'[DEBUG] Fallback path for {dataset_name}: {path}') return path alt = os.path.join(_PROJECT_DIR, subdir.upper() if dataset_name == 'llvip' else subdir) if os.path.isdir(alt): return alt # Kaggle Auto-Discovery (Makes symlinks optional on Kaggle) kaggle_input = '/kaggle/input' if os.path.exists(kaggle_input): for root, dirs, _ in os.walk(kaggle_input): if root.count(os.sep) - kaggle_input.count(os.sep) > 6: continue lower_root = root.lower() lower_dirs = [d.lower() for d in dirs] if dataset_name == 'llvip' and 'llvip' in lower_root: if 'infrared' in lower_dirs or 'visible' in lower_dirs: print(f'[DEBUG] Found {dataset_name} at {root}') return root if dataset_name == 'camod3fd' and ('m3fd' in lower_root or 'camo' in lower_root): if 'train' in lower_dirs or 'images' in lower_dirs: print(f'[DEBUG] Found {dataset_name} at {root}') return root if dataset_name == 'forestpersons' and 'forest' in lower_root and 'ir' not in lower_root.split('_')[-1] and 'ir' not in lower_root.split('/')[-1]: if 'images' in lower_dirs or 'annotations' in lower_dirs or 'train.json' in os.listdir(root): print(f'[DEBUG] Found {dataset_name} at {root}') return root if dataset_name == 'forestpersonsir' and 'forest' in lower_root and ('ir' in lower_root.split('_')[-1] or 'ir' in lower_root.split('/')[-1]): if 'images' in lower_dirs or 'annotations' in lower_dirs or 'train.json' in os.listdir(root): print(f'[DEBUG] Found {dataset_name} at {root}') return root return path # Supported dataset configurations DATASET_CONFIGS = { 'llvip': { 'name': 'LLVIP', 'description': 'Low-Light Vision Infrared-Visible Paired Dataset', 'infrared_train': 'infrared/train', 'infrared_test': 'infrared/test', 'visible_train': 'visible/train', 'visible_test': 'visible/test', 'annotations_train': 'Annotations', 'annotations_test': 'Annotations', 'annotation_format': 'voc_xml', 'classes': ['Pedestrian'], 'image_ext': ['.jpg', '.png', '.jpeg'], 'native_resolution': (1280, 1024), 'modality': 'paired', # synchronized RGB+Thermal }, 'forestpersons': { 'name': 'ForestPersons (RGB)', 'description': 'Under-canopy SAR person detection — RGB only', 'huggingface_id': 'etri/ForestPersons', 'annotation_format': 'yolo_txt', 'classes': ['person'], 'image_ext': ['.jpg', '.png', '.jpeg'], 'native_resolution': (640, 480), 'modality': 'rgb_only', # CMM-RXTO: thermal zeroed }, 'forestpersonsir': { 'name': 'ForestPersons IR (Thermal)', 'description': 'Under-canopy SAR person detection — Thermal/IR only', 'huggingface_id': 'etri/ForestPersonsIR', 'annotation_format': 'yolo_txt', 'classes': ['person'], 'image_ext': ['.jpg', '.png', '.jpeg'], 'native_resolution': (640, 480), 'modality': 'thermal_only', # CMM-ROTX: RGB zeroed }, 'camod3fd': { 'name': 'CAMO M3FD', 'description': 'Cross-spectral camouflaged pedestrian detection (mask→box)', 'train_imgs': 'train/Imgs', 'train_thermal': 'train/Thermal', 'train_gt': 'train/GT', 'val_imgs': 'val/Imgs', 'val_thermal': 'val/Thermal', 'val_gt': 'val/GT', 'annotation_format': 'mask_to_box', # pixel masks → bounding boxes 'classes': ['People', 'Car', 'Bus', 'Motorcycle', 'Lamp', 'Truck'], 'image_ext': ['.jpg', '.png', '.jpeg'], 'native_resolution': (640, 480), 'modality': 'paired', }, # Legacy datasets (loaders available, not in V2 training pipeline) 'kaist': { 'name': 'KAIST Multispectral Pedestrian', 'description': 'KAIST — urban driving, NOT used in V2 training', 'image_pattern': 'set{:02d}/V{:03d}/lwir/I{:05d}.png', 'annotation_format': 'kaist_txt', 'classes': ['person', 'cyclist', 'people'], 'native_resolution': (640, 480), 'modality': 'paired', }, 'flirv2': { 'name': 'FLIR ADAS v2', 'description': 'FLIR Thermal v2 — automotive, NOT used in V2 training', 'annotation_format': 'coco_json', 'classes': ['person', 'bike'], 'native_resolution': (640, 512), 'modality': 'paired', }, } # Active dataset (change this based on what you upload to Kaggle) ACTIVE_DATASET = 'llvip' # ============================================================================ # MODEL INPUT CONFIGURATION # ============================================================================ INPUT_WIDTH = 160 INPUT_HEIGHT = 128 INPUT_SIZE = (INPUT_HEIGHT, INPUT_WIDTH) # (128, 160) INPUT_CHANNELS = 4 # 3 (RGB) + 1 (Thermal) # ============================================================================ # TARGET CLASSES # ============================================================================ CLASS_MAP = { 'background': 0, 'person_visible': 1, 'person_camouflaged': 2, 'vehicle_boat': 3 } NUM_CLASSES = len(CLASS_MAP) # 4 # Anchor configuration NUM_ANCHORS = 3 # Default anchor aspect ratios (h/w) DEFAULT_ANCHOR_RATIOS = [1.2, 2.3, 3.5] # Default anchor sizes (relative to image, sqrt of area) DEFAULT_ANCHOR_SIZES = [0.108, 0.145, 0.180] # Grid sizes SMALL_GRID_H = INPUT_HEIGHT // 8 # 16 SMALL_GRID_W = INPUT_WIDTH // 8 # 20 LARGE_GRID_H = INPUT_HEIGHT // 16 # 8 LARGE_GRID_W = INPUT_WIDTH // 16 # 10 # Detection thresholds (split for different use cases) CONFIDENCE_THRESHOLD = 0.25 # Default for inference (conservative) EVAL_CONFIDENCE_THRESHOLD = 0.10 # For mAP evaluation (high recall) NMS_IOU_THRESHOLD = 0.35 # V2: tighter NMS for adjacent person detection MAX_DETECTIONS = 10 # Log clamp range for bbox encoding/decoding LOG_CLAMP_MIN = -4.5 LOG_CLAMP_MAX = 4.5 # Objectness metric threshold for recall computation OBJ_METRIC_THRESHOLD = 0.25 # Post-decode filters MIN_BOX_WIDTH_NORM = 0.03 MIN_BOX_HEIGHT_NORM = 0.05 MIN_BOX_AREA_NORM = 0.002 CORNER_FILTER_X = 0.12 CORNER_FILTER_Y = 0.10 CORNER_MAX_WIDTH = 0.10 # ============================================================================ # V1 MODEL ARCHITECTURE (kept for backward compatibility) # ============================================================================ RGB_STEM_CHANNELS = 16 THERMAL_STEM_CHANNELS = 16 STEM_CHANNELS = RGB_STEM_CHANNELS + THERMAL_STEM_CHANNELS # 32 SCALE1_CHANNELS = 32 SCALE2_CHANNELS = 48 SCALE3_CHANNELS = 96 FPN_CHANNELS = 64 CLASSIFIER_HIDDEN_DIM = 64 EXPAND_RATIO = 3 # ============================================================================ # V2 MODEL ARCHITECTURE (Dual-Branch, Accuracy-First) # ============================================================================ # Per-branch channel widths (each branch processes one modality independently) V2_STEM_CHANNELS = 16 # Per-branch stem output V2_SCALE1_CHANNELS = 24 # Per-branch Scale 1 (Ghost Bottleneck) V2_SCALE2_CHANNELS = 32 # Per-branch Scale 2 → EnergyGate V2_SCALE3_CHANNELS = 48 # Per-branch Scale 3 → BiFusion Neck V2_BIFUSION_CHANNELS = 48 # BiFusion Neck output (feeds heads + classifier) V2_CLASSIFIER_HIDDEN_DIM = 64 # Classifier FC hidden dim # Expand ratio sweet spot: 4 balances accuracy vs size # (3 = lean/V1, 6 = heavy/maximum capacity) V2_EXPAND_RATIO = 4 # ============================================================================ # TRAINING CONFIGURATION # ============================================================================ BATCH_SIZE = 8 LEARNING_RATE = 1e-3 WEIGHT_DECAY = 5e-4 EPOCHS = 50 PATIENCE = 10 NUM_WORKERS = 0 # Loss weights BBOX_WEIGHT = 3.0 OBJ_WEIGHT = 7.0 CLASS_WEIGHT = 0.5 # V2 auxiliary loss weights CONTRAST_WEIGHT = 0.1 # TFDet-style contrast loss (Phase 2+) GATE_REG_WEIGHT = 0.05 # EnergyGate entropy regularizer (Phase 3) # Learning rate schedule LR_SCHEDULER = 'cosine_warm_restarts' LR_T0 = 10 LR_T_MULT = 2 LR_MIN = 1e-6 # Data split VAL_RATIO = 0.2 RANDOM_SEED = 42 # Debugging / Testing DEBUG_MODE = False # Set to True to run only 1 batch per epoch and 1 epoch total for testing # ============================================================================ # V2 PHASE TRAINING CONFIGURATION # ============================================================================ TRAINING_PHASES = { 1: { 'name': 'Human Shape Foundation (LLVIP)', 'datasets': {'llvip': 1.0}, 'epochs': 50, 'lr': 1e-3, 'cmm_enabled': False, 'cmm_alpha': 0.0, 'contrast_loss': False, 'gate_regularizer': False, 'freeze_layers': [], 'batch_size': 32, }, 2: { 'name': 'Jungle Domain Transfer (ForestPersons + LLVIP)', 'datasets': { 'forestpersons': 0.40, # CMM-RXTO (thermal zeroed) 'forestpersonsir': 0.40, # CMM-ROTX (RGB zeroed) 'llvip': 0.20, # Replay (prevents forgetting) }, 'epochs': 60, 'lr': 2e-4, 'cmm_enabled': True, 'cmm_alpha_start': 0.3, 'cmm_alpha_end': 0.5, 'contrast_loss': True, 'gate_regularizer': False, 'freeze_layers': [], 'batch_size': 32, }, 3: { 'name': 'Full Mix Polish', 'datasets': { 'llvip': 0.34, 'forestpersons': 0.33, 'forestpersonsir': 0.33, }, 'epochs': 25, 'lr': 1e-5, 'cmm_enabled': True, 'cmm_alpha': 0.5, 'contrast_loss': True, 'gate_regularizer': True, 'freeze_layers': [], 'batch_size': 32, }, } # ============================================================================ # DEPLOYMENT CONSTRAINTS (ESP32-S3 with 8MB PSRAM) — optimize later # ============================================================================ ESP32_S3 = { 'sram_kb': 512, 'psram_mb': 8, 'flash_mb': 8, 'clock_mhz': 240, 'max_model_flash_kb': 4000, 'max_arena_sram_kb': 4000, 'target_fps': 10, 'target_model_fp16_kb': 2048, 'target_model_int8_kb': 2048, } # ============================================================================ # ALERT CONFIGURATION # ============================================================================ ALERT_CONFIG = { 'include_confidence': True, 'include_bbox': True, 'include_temperature': True, 'include_timestamp': True, 'include_intrusion_count': True, 'compact_format': True, } # ============================================================================ # FILE PATHS # ============================================================================ MODEL_SAVE_DIR = 'checkpoints' BEST_MODEL_V1_PATH = os.path.join(MODEL_SAVE_DIR, 'best_microghost_thermal_v1.pth') BEST_MODEL_V2_PATH = os.path.join(MODEL_SAVE_DIR, 'best_microghost_thermal_v3.pth') BEST_MODEL_PATH = BEST_MODEL_V2_PATH LAST_MODEL_PATH = os.path.join(MODEL_SAVE_DIR, 'last_microghost_thermal.pth') EXPORT_DIR = 'exports' ONNX_PATH = os.path.join(EXPORT_DIR, 'microghost_thermal.onnx') TFLITE_PATH = os.path.join(EXPORT_DIR, 'microghost_thermal.tflite') TFLITE_FP16_PATH = os.path.join(EXPORT_DIR, 'microghost_thermal_fp16.tflite') LOG_DIR = 'logs' def print_config(): """Print current configuration summary.""" print("=" * 65) print(" MicroGhost-Thermal Configuration (V2)") print("=" * 65) print(f" Device: {DEVICE}") print(f" Input: {INPUT_SIZE}×{INPUT_CHANNELS}") print(f" Classes: {NUM_CLASSES} → {list(CLASS_MAP.keys())}") print(f" Anchors/cell: {NUM_ANCHORS}") print(f" Grids: {SMALL_GRID_H}×{SMALL_GRID_W} (small), " f"{LARGE_GRID_H}×{LARGE_GRID_W} (large)") print(f" V2 Branch: {V2_STEM_CHANNELS}→{V2_SCALE1_CHANNELS}→" f"{V2_SCALE2_CHANNELS}→{V2_SCALE3_CHANNELS}") print(f" V2 BiFusion: {V2_BIFUSION_CHANNELS}") print(f" V2 Expand Ratio: {V2_EXPAND_RATIO}") print(f" Batch size: {BATCH_SIZE}") print(f" Learning rate: {LEARNING_RATE}") print(f" Epochs: {EPOCHS} (patience={PATIENCE})") print(f" Active dataset: {ACTIVE_DATASET}") print(f" Log clamp: [{LOG_CLAMP_MIN}, {LOG_CLAMP_MAX}]") print(f" NMS IoU: {NMS_IOU_THRESHOLD}") print(f" Conf (inference): {CONFIDENCE_THRESHOLD}") print(f" Conf (eval): {EVAL_CONFIDENCE_THRESHOLD}") print("=" * 65) if __name__ == '__main__': print_config()