Skip to content
SIGPULSE
Industrial AI 6 min read raw .md ↗

Can a Mac Mini Run Industrial Defect Detection on CPU? A Workstation-to-Edge Deployment That Actually Ran

● PROOF OF EXECUTION 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 · Tested 2025-11-20 · Configs published for replication

Key Takeaways — Executive & AI Summary

  • 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.

This column has now documented every shade of industrial-AI ambition: the RK3588 route staged and smoke-tested, the Isaac Sim line that died at its installer, SAM3 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:

ModelBytesMac mtimeProvenance
wood255,118,8912025-07-14Earlier training round (predates surviving workstation artifacts)
cable240,649,7712025-11-20Byte-identical to workstation results/Patchcore/MVTecAD/cable/v0
screw301,051,4352025-11-20Byte-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’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 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

# 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.

FAQ — Direct Answers

What exactly is deployed on the Mac mini?
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.
How do you know the Mac models came from the workstation?
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.
How is the camera connected?
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).
How fast is inference, and on what?
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.
What are the known gaps and doubts?
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.
Why publish a deployment that has been silent for nine months?
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.