import argparse import json import sys from pathlib import Path import numpy as np import torch import torch.nn as nn FILE = Path(__file__).resolve() ROOT = FILE.parents[1] if str(ROOT) not in sys.path: sys.path.append(str(ROOT)) from models.common import DetectMultiBackend from export import attempt_load from utils.general import LOGGER def analyze_checkpoint(weights_path): """Analyze checkpoint file structure to understand size.""" import torch ckpt = torch.load(weights_path, map_location='cpu') LOGGER.info(f"\n{'='*80}") LOGGER.info(f"CHECKPOINT ANALYSIS: {Path(weights_path).name}") LOGGER.info(f"{'='*80}") # Check what's in the checkpoint if isinstance(ckpt, dict): LOGGER.info(f"Checkpoint keys: {list(ckpt.keys())}") # Analyze each component size total_params = 0 for key in ckpt.keys(): if key in ['model', 'ema']: model_obj = ckpt[key] if hasattr(model_obj, 'parameters'): params = sum(p.numel() for p in model_obj.parameters()) param_size_mb = params * 4 / (1024**2) # FP32 = 4 bytes total_params += params LOGGER.info(f" - {key}: {params:,} parameters ({param_size_mb:.2f}MB if FP32)") LOGGER.info(f"Total parameters: {total_params:,}") LOGGER.info(f"Theoretical FP32 size: {total_params * 4 / (1024**2):.2f}MB") # Check actual file size import os actual_size = os.path.getsize(weights_path) / (1024**2) compression_ratio = (total_params * 4 / (1024**2)) / actual_size if actual_size > 0 else 0 LOGGER.info(f"Actual file size: {actual_size:.2f}MB") LOGGER.info(f"Compression ratio: {compression_ratio:.2f}x") LOGGER.info(f"{'='*80}\n") def load_model_lightweight(weights_path, device='cpu'): """Load model in the most lightweight way possible (state_dict only). This avoids loading EMA weights and other training artifacts that can double the size. """ import torch from models.yolo import Model LOGGER.info(f"Loading model with lightweight method: {weights_path}") ckpt = torch.load(weights_path, map_location=device) # Prefer regular model over EMA (EMA weights can be larger) if 'model' in ckpt: model = ckpt['model'] elif 'ema' in ckpt: LOGGER.warning("Only EMA weights available, using them (may increase size)") model = ckpt['ema'] else: raise ValueError(f"No model found in checkpoint: {weights_path}") # Convert to float32 and eval mode model = model.float().eval() # Remove training-specific attributes to reduce memory if hasattr(model, 'hyp'): delattr(model, 'hyp') if hasattr(model, 'gr'): delattr(model, 'gr') return model class Model_Merged(nn.Module): def __init__(self, model_roi0, model_roi1): super(Model_Merged, self).__init__() self.model_roi0 = model_roi0 self.model_roi1 = model_roi1 self.model_roi0.model[-1].export_raw = True # set export=True for Detect head self.model_roi1.model[-1].export_raw = True # set export=True for Detect # 确保两个模型都设置为eval模式 self.model_roi0.eval() self.model_roi1.eval() self.stride = model_roi0.stride # assuming both models have the same stride self.names = model_roi0.names # assuming both models have the same class names def forward(self, x_roi0, x_roi1): with torch.no_grad(): out_roi0 = self.model_roi0(x_roi0) out_roi1 = self.model_roi1(x_roi1) return out_roi0 + out_roi1 def merge_models(roi0_model_path, roi1_model_path, save_path, use_lightweight=False): """Load and merge two models, ensuring minimal memory footprint. Issues with large model size: 1. attempt_load may load EMA weights which are larger 2. fuse=True can sometimes increase size due to optimization overhead 3. The checkpoint contains training metadata Args: use_lightweight: If True, use custom lightweight loading (experimental) """ import os # Report original model sizes size_roi0 = os.path.getsize(roi0_model_path) / (1024 * 1024) # MB size_roi1 = os.path.getsize(roi1_model_path) / (1024 * 1024) # MB LOGGER.info(f"Original model sizes: ROI0={size_roi0:.2f}MB, ROI1={size_roi1:.2f}MB, Total={size_roi0+size_roi1:.2f}MB") # Load the two models - try to minimize size if use_lightweight: LOGGER.info("Using lightweight model loading (experimental)") model_roi0 = load_model_lightweight(roi0_model_path, device='cpu') model_roi1 = load_model_lightweight(roi1_model_path, device='cpu') else: LOGGER.info(f"Loading ROI0 model: {roi0_model_path}") model_roi0 = attempt_load(roi0_model_path, device='cpu', fuse=False) # fuse=False to avoid overhead LOGGER.info(f"Loading ROI1 model: {roi1_model_path}") model_roi1 = attempt_load(roi1_model_path, device='cpu', fuse=False) # Create merged model merged_model = Model_Merged(model_roi0, model_roi1) return merged_model if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--roi0_model_path', type=str, default='release/yolov5s-30w/roi0_last.pt', help='Path to the ROI0 model file') parser.add_argument('--roi1_model_path', type=str, default='release/yolov5s-30w/roi1_last.pt', help='Path to the ROI1 model file') parser.add_argument('--save_dir', type=str, default='release/yolov5s-30w', help='Path to save the merged model file') parser.add_argument('--imgsz', nargs=2, type=int, default=[352, 704], metavar=('H', 'W'), help='Input image size (height width), e.g. --imgsz 352 704') parser.add_argument('--opset', type=int, default=11, help='ONNX opset version (11, 12, 13, 17, etc.)') parser.add_argument('--skip-export', action='store_true', help='Skip exporting to TorchScript/ONNX') parser.add_argument('--lightweight', action='store_true', help='Use lightweight model loading (may reduce export size)') parser.add_argument('--export-separate', action='store_true', help='Export two models separately instead of merged (saves space)') parser.add_argument('--analyze', action='store_true', help='Analyze checkpoint structure and exit') args = parser.parse_args() # Analyze checkpoint if requested if args.analyze: analyze_checkpoint(args.roi0_model_path) if args.roi0_model_path != args.roi1_model_path: analyze_checkpoint(args.roi1_model_path) sys.exit(0) if not Path(args.save_dir).exists(): Path(args.save_dir).mkdir(parents=True, exist_ok=True) merged_model = merge_models(args.roi0_model_path, args.roi1_model_path, args.save_dir, args.lightweight) merged_model.eval() h, w = args.imgsz im0 = torch.randn(1, 3, h, w) # example input for roi0 im1 = torch.randn(1, 3, h, w) output = merged_model(im0, im1) print(f"Merged model created successfully.") # Important note about model size LOGGER.info("=" * 80) LOGGER.info("MODEL SIZE EXPLANATION:") LOGGER.info("The merged model contains TWO complete and independent models.") LOGGER.info("Expected export size: ~2x original size (≈100MB for 2x25MB models)") LOGGER.info("This is NORMAL because the two models have different trained weights.") if args.export_separate: LOGGER.info("Using --export-separate to save space by exporting separately.") LOGGER.info("=" * 80) if args.skip_export: LOGGER.info("Skipping export (--skip-export flag set)") sys.exit(0) # Option 1: Export separately (recommended for saving space) if args.export_separate: LOGGER.info("Exporting models SEPARATELY to save space...") # Export ROI0 model save_path_roi0_ts = Path(args.save_dir) / "roi0_model.torchscript" save_path_roi0_onnx = Path(args.save_dir) / "roi0_model.onnx" try: LOGGER.info(f"Exporting ROI0 to TorchScript: {save_path_roi0_ts}") ts0 = torch.jit.trace(merged_model.model_roi0, im0, strict=False) ts0.save(str(save_path_roi0_ts)) size_ts0 = save_path_roi0_ts.stat().st_size / (1024 * 1024) LOGGER.info(f"ROI0 TorchScript export success: {size_ts0:.2f}MB") LOGGER.info(f"Exporting ROI0 to ONNX: {save_path_roi0_onnx}") torch.onnx.export( merged_model.model_roi0, im0, str(save_path_roi0_onnx), verbose=False, opset_version=args.opset, do_constant_folding=True, input_names=["input"], output_names=["output"] ) size_onnx0 = save_path_roi0_onnx.stat().st_size / (1024 * 1024) LOGGER.info(f"ROI0 ONNX export success: {size_onnx0:.2f}MB") except Exception as e: LOGGER.error(f"ROI0 export failed: {e}") # Export ROI1 model save_path_roi1_ts = Path(args.save_dir) / "roi1_model.torchscript" save_path_roi1_onnx = Path(args.save_dir) / "roi1_model.onnx" try: LOGGER.info(f"Exporting ROI1 to TorchScript: {save_path_roi1_ts}") ts1 = torch.jit.trace(merged_model.model_roi1, im1, strict=False) ts1.save(str(save_path_roi1_ts)) size_ts1 = save_path_roi1_ts.stat().st_size / (1024 * 1024) LOGGER.info(f"ROI1 TorchScript export success: {size_ts1:.2f}MB") LOGGER.info(f"Exporting ROI1 to ONNX: {save_path_roi1_onnx}") torch.onnx.export( merged_model.model_roi1, im1, str(save_path_roi1_onnx), verbose=False, opset_version=args.opset, do_constant_folding=True, input_names=["input"], output_names=["output"] ) size_onnx1 = save_path_roi1_onnx.stat().st_size / (1024 * 1024) LOGGER.info(f"ROI1 ONNX export success: {size_onnx1:.2f}MB") except Exception as e: LOGGER.error(f"ROI1 export failed: {e}") LOGGER.info("=" * 80) LOGGER.info("SEPARATE EXPORT COMPLETE") LOGGER.info("Use these files for inference by loading both models separately") LOGGER.info("=" * 80) sys.exit(0) # Option 2: Export merged model (default, but larger) save_path_ts = Path(args.save_dir) / "merged_model.torchscript" try: LOGGER.info(f"Exporting to TorchScript: {save_path_ts}") # Use torch.jit.trace with tuple of inputs for multi-input models ts = torch.jit.trace(merged_model, (im0, im1), strict=False) ts.save(str(save_path_ts)) # Report file size size_ts = save_path_ts.stat().st_size / (1024 * 1024) # MB LOGGER.info(f"TorchScript export success: {save_path_ts} (Size: {size_ts:.2f}MB)") except Exception as e: LOGGER.error(f"TorchScript export failed: {e}") # Export to ONNX save_path_onnx = Path(args.save_dir) / "merged_model.onnx" try: # Check PyTorch version to determine compatible opset version import torch torch_version = torch.__version__ LOGGER.info(f"PyTorch version: {torch_version}") # Use opset 11 or 13 for better compatibility, or accept the default (18) # opset 11: widely supported by TensorRT, OpenVINO, ONNX Runtime # opset 13: better operator coverage # opset 17+: required for newer PyTorch versions opset = args.opset # Use command line argument LOGGER.info(f"Exporting to ONNX with opset_version={opset}: {save_path_onnx}") torch.onnx.export( merged_model, (im0, im1), # tuple of example inputs str(save_path_onnx), verbose=False, opset_version=opset, do_constant_folding=True, input_names=["roi0_input", "roi1_input"], # specify names for two inputs output_names=["roi0_output_2d_8x", "roi0_output_2d_16x", "roi0_output_2d_32x", "roi0_output_3d_8x", "roi0_output_3d_16x", "roi0_output_3d_32x", "roi1_output_2d_8x", "roi1_output_2d_16x", "roi1_output_2d_32x", "roi1_output_3d_8x", "roi1_output_3d_16x", "roi1_output_3d_32x"], # specify names for two outputs dynamic_axes={ "roi0_input": {0: "batch"}, "roi1_input": {0: "batch"}, "roi0_output": {0: "batch"}, "roi1_output": {0: "batch"} } if False else None # set to True to enable dynamic batch size ) LOGGER.info(f"ONNX export success: {save_path_onnx}") # Report file size before simplification size_onnx_before = save_path_onnx.stat().st_size / (1024 * 1024) # MB LOGGER.info(f"ONNX model size before simplification: {size_onnx_before:.2f}MB") # Optional: simplify ONNX model try: import onnx import onnxslim # Skip simplification for multi-input models as it may increase size LOGGER.warning("Skipping ONNX simplification for multi-input model (often increases size)") LOGGER.info("If you still want to try simplification, use: onnxslim input.onnx output.onnx") # Uncomment below to force simplification (not recommended for multi-input models) # LOGGER.info("Simplifying ONNX model with onnxslim...") # model_onnx = onnx.load(str(save_path_onnx)) # model_onnx = onnxslim.slim(model_onnx) # onnx.save(model_onnx, str(save_path_onnx)) # # # Report size after simplification # size_onnx_after = save_path_onnx.stat().st_size / (1024 * 1024) # MB # reduction = size_onnx_before - size_onnx_after # LOGGER.info(f"ONNX simplification success: {size_onnx_after:.2f}MB (reduced by {reduction:.2f}MB)") except ImportError: LOGGER.warning("onnxslim not installed, skipping simplification. Install with: pip install onnxslim") except Exception as e: LOGGER.warning(f"ONNX simplification failed: {e}") except Exception as e: LOGGER.error(f"ONNX export failed: {e}")