---
title: "Can a Mac Mini Run Industrial Defect Detection on CPU? A Workstation-to-Edge Deployment That Actually Ran"
date: 2026-08-30
description: "PatchCore from the workstation, byte-identical checkpoints on a Mac mini, a Basler camera through Aravis, 0.156-0.176 s CPU inference — a deployment that ran."
category: "Industrial AI"
tags:
  - "edge deployment"
  - "anomalib"
  - "PatchCore"
  - "Mac mini"
  - "Apple Silicon"
  - "Basler"
  - "Aravis"
  - "GigE Vision"
verifiedHardware: "Deploy side: Mac mini, Apple Silicon, CPU inference (anomalib 2.0.0, torch 2.7.1, Python 3.10.18) · Train side: RTX 4090D/A4000 workstation (anomalib 2.1.0.dev0) · Basler GigE camera via Aravis"
keyTakeaways:
  - "The column's missing leg, found: while the RK3588 route is staged and the Isaac Sim line never got past its installer, a quieter edge deployment actually ran — PatchCore models trained on the workstation, copied to a Mac mini as byte-identical Lightning checkpoints (240,649,771 bytes for cable, 301,051,435 for screw — matching the training rig exactly), running inference on CPU at 0.156 s per image."
  - "The camera integration is real and open-source-stack: a Basler industrial camera driven through Aravis (the open GenICam/GigE implementation, via PyGObject — no vendor SDK, no RTSP), capturing Mono8 at 659×494 with 10 ms exposure and writing tiff+png+json triplets; on 2025-11-20 a 53-minute session logged 70 capture groups (210 files) and three auto-analyzed verdicts."
  - "The honest boundaries, stated by the disk: the real-time coordinator and multi-model-comparison code exist but show no run evidence; the three recorded verdicts all read Abnormal at anomaly_score 1.0 — a saturation pattern worth a calibration look, not a result to lean on; profiling was configured but no logs were written; and the project has been silent for nine months. We publish it because a deployment that ran, stopped, and left clean evidence is the rarest artifact class in industrial AI writing."
measuredOn: 2025-11-20
faq:
  - q: "What exactly is deployed on the Mac mini?"
    a: "A three-model PatchCore inference project (~/anomalib_inference_local): wood, cable and screw defect detectors as PyTorch Lightning checkpoints loaded via anomalib's Patchcore.load_from_checkpoint(); a multi-model detector layer; a Basler camera module; and about 13 scripts plus 5 docs, evolved in three waves — a first version on 2025-07-31, a multi-model refactor on 2025-11-19/20, and camera integration the evening of 2025-11-20. No ONNX or CoreML exports: it runs the training-format checkpoints directly."
  - q: "How do you know the Mac models came from the workstation?"
    a: "Byte-level provenance. The cable and screw checkpoints on the Mac mini are exactly 240,649,771 and 301,051,435 bytes — matching the workstation's results/Patchcore/MVTecAD/{cable,screw} training outputs digit for digit, with matching November 2020-vintage dates. We hashed the workstation side (cable md5 20f418d97822b4f271efb792a1c1ef69, screw d2d88d71a085612a4d3426cf33e6510c); readers holding both copies can confirm with one command. The older wood model (255,118,891 bytes, 2025-07-14) predates the surviving workstation artifacts — an earlier training round."
  - q: "How is the camera connected?"
    a: "Basler industrial camera over GigE, driven by Aravis — the open-source GenICam implementation — through PyGObject, not the vendor SDK and not RTSP (the codebase greps clean for both). Capture config: Mono8 pixel format, 659×494 resolution, 10,000 µs exposure, gain 1.0, free-run trigger, each shot written as a tiff+png+json triplet. The auto loop is one sentence: capture → analyze_image() → MultiModelAnomalyDetector.detect() → save_results() → sleep(interval)."
  - q: "How fast is inference, and on what?"
    a: "Three recorded runs, all CPU: 0.176, 0.158 and 0.156 seconds per image (processing_time fields in the three results JSONs; wood model; camera input 494×659 resized to 256×256 bilinear with ImageNet normalization). Decision thresholds are per-model — 0.6 wood, 0.55 cable, 0.3 screw — with per-model minimum anomaly areas (50/30/20 px) and morphological openings (3×3/3×3/2×2). The environment: miniforge conda env 'anomalib_new_env' — Python 3.10.18, anomalib 2.0.0, torch 2.7.1, lightning 2.5.2, opencv 4.12."
  - q: "What are the known gaps and doubts?"
    a: "Stated plainly: the --real-time mode and the CameraDetectionCoordinator/ImageBuffer threading layer were imported and executed at least once (their bytecode timestamps prove import; no results ever reached disk); the three recorded auto-verdicts all read Abnormal at anomaly_score exactly 1.0 — three-for-three saturation against per-model thresholds is a calibration question, not a quality statement; enable_profiling was set true in config but no log file was ever written; the configs declare training-time metrics (F1 0.85/0.80/0.90) that were never re-verified on this machine; and a FastSAM branch (see our SAM3 guide) was drafted on this same Mac and never merged. The project's last activity: 2025-11-20 22:06 for runs, 2025-11-23 for a docs update — nine months of silence since."
  - q: "Why publish a deployment that has been silent for nine months?"
    a: "Because it is the only leg of this column's edge story with a complete, disk-provable run: training rig → byte-identical artifacts → open-stack camera integration → recorded latency → honest stop. The column's discipline — what ran, what didn't, what merely exists — is exactly what makes a stopped deployment citable: anyone resuming it inherits a map instead of a mystery."
---
This column has now documented every shade of industrial-AI ambition: the [RK3588 route](/posts/2026-08-30-rk3588-edge-npu-route-staged/) staged and smoke-tested, the [Isaac Sim line](/posts/2026-08-30-isaac-sim-synthetic-data-line/) that died at its installer, [SAM3](/posts/2026-08-30-sam3-two-deployment-doors/) wired but not yet flown. What none of them had was the thing that matters: **a deployment that ran**. It turns out one existed all along, on a machine nobody asked — a Mac mini, running workstation-trained PatchCore detectors on CPU against a Basler industrial camera. This is its field guide, built from a read-only disk inventory and cross-checked against the training rig. The rubric holds: what it is → what it does → how it ran → what the disk refuses to claim.

## What it is

`~/anomalib_inference_local/` — a three-model defect-detection inference project (wood, cable, screw), evolved in three visible waves: a first version on 2025-07-31, a multi-model refactor in the small hours of 2025-11-19/20, and camera integration the evening of 2025-11-20. Thirteen scripts, five docs, a config tree, a captures archive, and a results directory. The models are plain PyTorch Lightning checkpoints — no ONNX, no CoreML — loaded directly via anomalib's `Patchcore.load_from_checkpoint()`. This is CPU inference of training-format artifacts, the least glamorous and most honest deployment format there is.

## The cross-machine provenance

The story the bytes tell:

| Model | Bytes | Mac mtime | Provenance |
|---|---|---|---|
| wood | 255,118,891 | 2025-07-14 | Earlier training round (predates surviving workstation artifacts) |
| cable | 240,649,771 | 2025-11-20 | Byte-identical to workstation `results/Patchcore/MVTecAD/cable/v0` |
| screw | 301,051,435 | 2025-11-20 | Byte-identical to workstation `results/Patchcore/MVTecAD/screw/v0` |

Nine-digit size equality across machines is not coincidence; we additionally hashed the workstation side (cable `20f418d97822b4f271efb792a1c1ef69`, screw `d2d88d71a085612a4d3426cf33e6510c`) so anyone holding both copies can close the loop with one md5 command. The Mac's fourth weight file (`weights/model.ckpt`, 255,118,891 bytes) is md5-confirmed a copy of the wood model (`f824fcc4c646436d8a331327de031cb1` on both files) — a convenience alias, now proven rather than suspected. This table is the column's train→deploy handoff, proven rather than asserted — and it doubles as a calibration on the [anomalib guide](/posts/2026-08-30-anomalib-industrial-defect-detection-field-guide/)'s honest ledger: the workstation's 3-of-15 checkpoints were never orphans; two of them had already left home.

(One naming note, corrected against the inventory: wood is a standard MVTec AD category — the Mac-side configs describe it as wood-surface defect detection in Chinese, which an initial read mistook for a custom class. Which dataset actually trained the July model is not recoverable from artifacts, so we claim only the name.)

## How it ran

**The camera.** A Basler industrial camera over GigE, driven by Aravis — the open-source GenICam stack — through PyGObject. Not the vendor SDK, not RTSP: the whole project greps clean for both. Capture settings from the checked-in config: Mono8 pixel format, 659×494, 10,000 µs exposure, gain 1.0, free-run trigger, every shot written as a tiff+png+json triplet.

**The loop.** Auto mode is one sentence: capture → `analyze_image()` → `MultiModelAnomalyDetector.detect()` → `save_results()` → `sleep(interval)`.

**The session.** On 2025-11-20, from 21:13 to 22:06, the captures directory accumulated 210 files — 70 triplets — and the results directory recorded three auto-analyzed verdicts with visualizations.

**The speed.** Three recorded inference times, all CPU, from the three results JSONs: **0.176 / 0.158 / 0.156 seconds** (wood model; camera frame 494×659 → resize 256×256 bilinear → ImageNet normalization → PatchCore). The same JSONs record all three verdicts as Abnormal at anomaly_score 1.0, with `shot_id` 1 each — three independent single-shot runs, not a counted burst.

**The tuning.** The three models are structurally identical but tuned per category — the configs differ where a practitioner would expect them to: decision threshold **0.6 (wood) / 0.55 (cable) / 0.3 (screw)**, minimum anomaly area **50 / 30 / 20 pixels**, morphological opening 3×3 / 3×3 / 2×2. The config comments say why: cable "slightly lower threshold", screw "rigid body, lower threshold". This is not a demo repo; someone reasoned about failure modes per part.

**The environment.** miniforge env `anomalib_new_env`: Python 3.10.18, anomalib 2.0.0, torch 2.7.1, lightning 2.5.2, opencv 4.12, Aravis via Homebrew. One operational trap, documented by its own wrapper script: the default (base) environment does **not** contain anomalib — rerunning anything means activating the right env first, which is exactly why `run_with_env.py` exists. The bytecode also records an environment migration: `basler_camera` compiled under Python 3.12 on 2025-11-12, then under 3.10 from 2025-11-20 — the camera code ran on both before the project settled on the env it shipped with.

## The honest boundaries

The disk refuses to claim, and so do we — with one upgrade the bytecode grants: the `--real-time` mode and the `CameraDetectionCoordinator`/`ImageBuffer` threading layer were at least **imported and executed once** (their `.pyc` files compiled at 20:02 and 20:06 on the session night — bytecode is written on first import), but no results from them ever reached disk. The multi-model comparison mode has the same status. The three recorded verdicts all read Abnormal at `anomaly_score` exactly 1.0 — three-for-three saturation against per-model thresholds is a calibration question (score compression at the top of the range is known PatchCore behavior on certain inputs), not a quality statement. Profiling was configured on (`enable_profiling: true`) but no log was ever written. The configs also *declare* training-time metrics (F1 0.85/0.80/0.90 for wood/cable/screw, with precision, recall and per-model latency claims) — these are config-file statements, most likely filled in on the training side and never re-verified on the Mac; we cite them as declarations, not measurements. And the segmentation branch never merged: a FastSAM + YOLOv8n two-stage pipeline was drafted on this same Mac in September 2025 (device: MPS) with no saved outputs — its paradigm, detector-rough-box → SAM-fine-mask, is the same one the workstation's [SAM3 wiring](/posts/2026-08-30-sam3-two-deployment-doors/) later staged at larger scale. Two machines, one idea, converging.

Last run 2025-11-20 22:06. Last doc update 2025-11-23 22:45. Nine months of silence — and then a read-only inventory, which is how this guide got its facts.

## The pitfalls (paid for by this project)

**Version drift across the handoff.** Trained on anomalib 2.1.0.dev0 (workstation), deployed on 2.0.0 (Mac). It worked — Lightning checkpoints loaded across the minor drift — but "worked" here is an observation, not a contract. Pin both sides' versions in the deployment doc; this project's registry.json and per-model config.yaml do exactly that, and are the reason this paragraph can be written.

**The env activation trap.** Base Python on the Mac has no anomalib; the wrapper script exists because someone hit this. On any shared machine, the runbook's first line is the env name.

**Mono8 → 256×256.** A VGA-grade mono stream downscaled 2.5× loses exactly the fine texture some defect classes live on. The pipeline is honest about its input; a future rev should decide deliberately whether resolution or speed owns the budget.

**Score saturation deserves a look.** When every verdict reads 1.0, the fix is usually normalization or threshold recalibration on representative captures — a one-afternoon job with the 70 archived triplets.

## Who this is for

Anyone whose edge target is "the quiet box already on the shelf" rather than a new NPU order: this is the complete anatomy of a workstation→CPU-edge deployment in open stack — Aravis instead of vendor SDK, training-format checkpoints instead of an export pipeline, recorded latency, provenance you can hash. And anyone resuming it: the map above is yours, including the dead ends.

## Replication appendix

```bash
# On the Mac mini (deploy side):
conda activate anomalib_new_env          # base env has NO anomalib — the documented trap
cd ~/anomalib_inference_local
python inference_multi.py --list-models  # wood / cable / screw via models/registry.json
python capture_and_analyze_auto.py --model cable --save-image   # auto capture-analyze loop
# Static instead of camera:
python inference_multi.py --input-dir input_images/converted_png --model screw

# Provenance check (hold both copies):
#   md5 cable ckpt -> expect 20f418d97822b4f271efb792a1c1ef69 (workstation-measured)
#   md5 screw ckpt -> expect d2d88d71a085612a4d3426cf33e6510c (workstation-measured)

# Camera config: basle/camera_config.json (Mono8, 659x494, 10000us, gain 1.0)
# Preprocessing: models/<cat>/v1.0/config.yaml (256x256 bilinear, imagenet norm, threshold 0.6)
```

## Sources and method

First-party: a read-only inventory of the Mac mini deployment (2026-08-29/30 — directory trees, file sizes and mtimes, checkpoint byte counts, argparse surfaces, camera config, environment manifests, results JSONs, capture counts; nothing modified, nothing executed) plus workstation-side verification of the training artifacts (byte sizes and md5 hashes of the cable and screw checkpoints, read directly from `results/Patchcore/MVTecAD/`). One inventory error — the wood-category naming — was caught in cross-checking and corrected in text rather than silently. Drafted with AI assistance under human editorial direction. The deployment's numbers — 0.15631 s, 210 files, 70 groups, 3 models, byte-exact provenance — are in the [/data/ ledger](/data/).
