Download source/build-camstack-models.py from camstack/camstack-models: direct link, hf CLI and curl.
- Browser
- Download file 9.7 kB
-
https://huggingface.co/camstack/camstack-models/resolve/ff7efbf95d4dc14aafc63254ad1e48e5979d32aa/source/build-camstack-models.py
- Command line
-
hf download hf://camstack/camstack-models@ff7efbf95d4dc14aafc63254ad1e48e5979d32aa/source/build-camstack-models.py
-
curl -L -o build-camstack-models.py https://huggingface.co/camstack/camstack-models/resolve/ff7efbf95d4dc14aafc63254ad1e48e5979d32aa/source/build-camstack-models.py
9.7 kB
| #!/usr/bin/env python3 | |
| """ | |
| Build the camstack-* model set with batch-flexible CoreML packages. | |
| The pipeline's Python pool currently does N predicts iterated internally | |
| on a list-of-dicts (saves session setup, but each frame still costs ~9ms | |
| warm). A CoreML model exported with a flexible batch dim accepts a single | |
| stacked tensor of shape `(N, 3, H, W)` β one ANE dispatch per batch, | |
| amortising every fixed cost. | |
| This script produces (per model in MODELS): | |
| exported-models/camstack-<id>.onnx β ONNX with dynamic batch axis | |
| exported-models/camstack-<id>.mlpackage β CoreML with RangeDim batch (1..16) | |
| exported-models/camstack-<id>_openvino_model/ β OpenVINO IR with dynamic batch | |
| The dynamic axis is SAFE to export and must not be pinned here: the same | |
| artifact is loaded on CPU, iGPU, NPU and ANE nodes, so an export cannot know | |
| which target it will meet. The loader does β `_static_batch_required` / | |
| `_pin_batch_for_device` in `packages/addon-pipeline/python/inference_pool.py` | |
| reshape the graph to a static batch of 1 for the one target that refuses dynamic | |
| dims (the Intel NPU) and hand it over untouched everywhere else (D202). Pinning | |
| it here would instead delete the flexible axis the CoreML batched-ANE dispatch | |
| path depends on. | |
| Usage: | |
| python scripts/build-camstack-models.py # from the CamStack repository | |
| python build-camstack-models.py # from the mirror's source/ folder | |
| Environment: the packages in convert_lib's requirements.txt plus `ultralytics` | |
| (which also fetches the upstream `.pt` checkpoints). INT8 needs calibration | |
| images in CAMSTACK_CALIB_DIR (default ./calibration-images). | |
| """ | |
| import os | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| # convert_lib.py is the converter Model Studio runs at runtime. In the CamStack | |
| # repository it lives in packages/addon-model-studio/python/; the published copy | |
| # of this script (the `source/` folder of the camstack/camstack-models mirror) | |
| # ships it as a sibling file. A sibling wins, so the SAME file runs in both places. | |
| _HERE = Path(__file__).resolve().parent | |
| _CONVERT_LIB_DIRS = ( | |
| _HERE, | |
| _HERE.parent / 'packages' / 'addon-model-studio' / 'python', | |
| ) | |
| _CONVERT_LIB_DIR = next( | |
| (d for d in _CONVERT_LIB_DIRS if (d / 'convert_lib.py').is_file()), None | |
| ) | |
| if _CONVERT_LIB_DIR is None: | |
| sys.exit( | |
| 'convert_lib.py not found next to this script or in ' | |
| 'packages/addon-model-studio/python/' | |
| ) | |
| sys.path.insert(0, str(_CONVERT_LIB_DIR)) | |
| from convert_lib import export_openvino, quantize_openvino_int8, validate_openvino_ir | |
| # --- Config ------------------------------------------------------------ | |
| OUTPUT_DIR = Path("exported-models") | |
| MODELS = [ | |
| # (ultralytics-id, exported-name, image-size) | |
| ("yolov9t", "camstack-yolov9t", 640), | |
| ("yolov9s", "camstack-yolov9s", 640), | |
| ("yolo26n", "camstack-yolo26n", 640), | |
| ] | |
| # Directory of calibration images (e.g. a COCO val subset) for INT8. INT8 is | |
| # only produced when this dir exists + has images; otherwise it's skipped with | |
| # a note. Override via CAMSTACK_CALIB_DIR. | |
| CALIB_DIR = Path(os.environ.get("CAMSTACK_CALIB_DIR", "calibration-images")) | |
| BATCH_RANGE_LOWER = 1 | |
| BATCH_RANGE_UPPER = 16 | |
| BATCH_DEFAULT = 1 | |
| # --- Helpers ----------------------------------------------------------- | |
| def get_size_mb(path: Path) -> float: | |
| if path.is_file(): | |
| return path.stat().st_size / (1024 * 1024) | |
| return sum(f.stat().st_size for f in path.rglob("*") if f.is_file()) / (1024 * 1024) | |
| def ensure_dir(p: Path) -> Path: | |
| p.mkdir(parents=True, exist_ok=True) | |
| return p | |
| # --- Step 1: YOLO -> ONNX (dynamic batch) ------------------------------ | |
| def export_onnx(ultra_id: str, out_path: Path, imgsz: int) -> Path: | |
| """Export the Ultralytics .pt as ONNX with `dynamic=True`. | |
| `dynamic=True` produces an ONNX with `images` axis 0 marked dynamic | |
| (batch) so coremltools.convert can set RangeDim on it. | |
| """ | |
| from ultralytics import YOLO | |
| print(f" β loading {ultra_id}.pt (auto-download if missing)") | |
| model = YOLO(f"{ultra_id}.pt") | |
| print(f" β exporting ONNX (imgsz={imgsz}, dynamic batch)") | |
| onnx_src = model.export( | |
| format="onnx", | |
| imgsz=imgsz, | |
| dynamic=True, | |
| simplify=True, | |
| opset=17, | |
| ) | |
| if not onnx_src: | |
| raise RuntimeError(f"ONNX export returned no path for {ultra_id}") | |
| onnx_src = Path(onnx_src) | |
| if onnx_src != out_path: | |
| shutil.copy2(onnx_src, out_path) | |
| print(f" β {out_path.name} ({get_size_mb(out_path):.1f} MB)") | |
| return out_path | |
| # --- Step 2: ONNX -> CoreML (RangeDim batch) --------------------------- | |
| def export_coreml(ultra_id: str, out_path: Path, imgsz: int) -> Path: | |
| """Export the Ultralytics .pt as a CoreML mlpackage with flexible batch. | |
| Ultralytics' CoreML exporter handles the torch.jit.trace + coremltools | |
| pipeline internally. With `dynamic=True` + `batch=N`, it produces a | |
| model that accepts shape `(B, 3, imgsz, imgsz)` with B β [1..N]. | |
| `half=True` sets compute precision to FLOAT16 β required for ANE. | |
| """ | |
| from ultralytics import YOLO | |
| print(f" β loading {ultra_id}.pt for CoreML export") | |
| model = YOLO(f"{ultra_id}.pt") | |
| print( | |
| f" β exporting CoreML mlpackage (imgsz={imgsz}, dynamic batch up to {BATCH_RANGE_UPPER}, half=FP16)" | |
| ) | |
| src = model.export( | |
| format="coreml", | |
| imgsz=imgsz, | |
| dynamic=True, | |
| batch=BATCH_RANGE_UPPER, | |
| half=True, | |
| nms=False, | |
| ) | |
| if not src: | |
| raise RuntimeError(f"CoreML export returned no path for {ultra_id}") | |
| src = Path(src) | |
| if out_path.exists(): | |
| shutil.rmtree(out_path) | |
| if src.is_dir(): | |
| shutil.copytree(src, out_path) | |
| else: | |
| out_path.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src, out_path / src.name) | |
| print(f" β {out_path.name} ({get_size_mb(out_path):.1f} MB)") | |
| return out_path | |
| # --- Step 3: ONNX -> OpenVINO (dynamic batch IR) ----------------------- | |
| # export_openvino, quantize_openvino_int8, validate_openvino_ir are imported | |
| # from convert_lib.py (shared with the runtime; located above). | |
| # --- Step 4: Verification --------------------------------------------- | |
| def verify_coreml_batch(mlpackage_path: Path, imgsz: int) -> None: | |
| """Predict with N=1 then N=4 to confirm batch flexibility works.""" | |
| import coremltools as ct | |
| import numpy as np | |
| print(f" β loading {mlpackage_path.name} for verification") | |
| model = ct.models.MLModel(str(mlpackage_path)) | |
| # Find input name | |
| spec = model.get_spec() | |
| input_name = spec.description.input[0].name | |
| for batch in (1, 4): | |
| x = np.random.rand(batch, 3, imgsz, imgsz).astype(np.float32) | |
| out = model.predict({input_name: x}) | |
| out_keys = list(out.keys()) | |
| out_shape = out[out_keys[0]].shape if hasattr(out[out_keys[0]], "shape") else "?" | |
| print(f" β batch={batch} β output[{out_keys[0]}].shape={out_shape}") | |
| # --- Main -------------------------------------------------------------- | |
| def main() -> None: | |
| output = ensure_dir(OUTPUT_DIR) | |
| print(f"Output: {output.resolve()}") | |
| for ultra_id, name, imgsz in MODELS: | |
| print() | |
| print("=" * 70) | |
| print(f"Building {name} (source: {ultra_id}.pt, imgsz={imgsz}Γ{imgsz})") | |
| print("=" * 70) | |
| onnx_path = output / f"{name}.onnx" | |
| coreml_path = output / f"{name}.mlpackage" | |
| try: | |
| export_onnx(ultra_id, onnx_path, imgsz) | |
| except Exception as e: | |
| print(f" β ONNX export failed: {e}") | |
| continue | |
| try: | |
| export_coreml(ultra_id, coreml_path, imgsz) | |
| except Exception as e: | |
| print(f" β CoreML conversion failed: {e}") | |
| # OpenVINO IR variants β all converted from the ONNX via ov.convert_model | |
| # (NOT the Ultralytics openvino exporter). fp32 baseline + fp16 (GPU/NPU). | |
| try: | |
| out_xml_fp32 = output / f"{name}.xml" | |
| export_openvino(str(onnx_path), str(out_xml_fp32), fp16=False) | |
| validate_openvino_ir(str(out_xml_fp32)) | |
| out_xml_fp16 = output / f"{name}-fp16.xml" | |
| export_openvino(str(onnx_path), str(out_xml_fp16), fp16=True) | |
| validate_openvino_ir(str(out_xml_fp16)) | |
| except Exception as e: | |
| print(f" β OpenVINO conversion failed: {e}") | |
| # INT8 β only when calibration images are present. | |
| if CALIB_DIR.is_dir() and any(CALIB_DIR.iterdir()): | |
| try: | |
| out_xml_int8 = output / f"{name}-int8.xml" | |
| quantize_openvino_int8( | |
| str(onnx_path), str(out_xml_int8), str(CALIB_DIR), imgsz | |
| ) | |
| validate_openvino_ir(str(out_xml_int8)) | |
| except Exception as e: | |
| print(f" β OpenVINO INT8 quantization failed: {e}") | |
| else: | |
| print(f" β INT8 skipped β no calibration images in {CALIB_DIR} (set CAMSTACK_CALIB_DIR)") | |
| if coreml_path.exists(): | |
| try: | |
| verify_coreml_batch(coreml_path, imgsz) | |
| except Exception as e: | |
| print(f" β CoreML verification failed: {e}") | |
| # --- Summary --- | |
| print() | |
| print("=" * 70) | |
| print("Build complete. Files:") | |
| print("=" * 70) | |
| total = 0.0 | |
| for f in sorted(output.iterdir()): | |
| size = get_size_mb(f) | |
| total += size | |
| print(f" {f.name:50s} {size:8.1f} MB") | |
| print(" " + "β" * 60) | |
| print(f" {'Total':50s} {total:8.1f} MB") | |
| print() | |
| print("Upload to HF camstack/camstack-models when ready.") | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |