Getting models
Some of what scalacv can do needs nothing but the library: contours, colour masks, optical flow, ArUco
markers, QR codes. The rest — face detection, face recognition, body pose, hand landmarks, background
segmentation, any neural network at all — needs a model file: a .onnx file holding the
weights somebody else trained. scalacv ships none of them.
An ONNX file (Open Neural Network Exchange — the interchange format the training frameworks export to)
is the whole model: its graph and its numbers, in one file. OpenCV's dnn module reads it and runs the
forward pass on plain CPU. Nothing about that is hard; the hard part, and the part every other page on
this site hands off to you, is getting the file onto disk and knowing it is the file you meant.
That is what this page is for: how to fetch a model, how to prove it is intact, where to put it, and which model each capability actually wants.
Models.fetch(spec, dir) downloads a ModelSpec into dir, checks its SHA-256, and hands back the
path. It tries mirrors in order, accepts file:// as well as https://, writes to a temp file and only
moves it into place once it verifies, and is idempotent — a correct file already on disk is returned
without touching the network. Two specs ship ready-made: FaceDetect.modelSpec and
FaceRecognizer.modelSpec.
What ships in the jars, and what does not
Three different kinds of payload get confused with each other. Only one of them is a download:
| Payload | In the jars? | How you get it |
|---|---|---|
OpenCV native libraries (.so / .dylib / .dll) | yes, in the bytedeco classifier jars | extracted on the first OpenCv.load() — see the native cache |
| Haar / LBP cascade XML | yes, in the same jars | Cascades.load(CascadeName.FrontalFaceAlt) — no network, no download |
| ONNX models (YuNet, SFace, pose, hand, segmentation, your own) | no | this page |
So the cascades are the one detector family that works on a machine with no internet access and no provisioning step at all. They are also markedly less accurate than YuNet — that is the trade, and Object detection spells it out.
The windows-x86_64 bytedeco jar ships an empty share/ directory and no cascade XML at all, so
Cascades.load returns a Left(CvError.LoadFailed) there — and says so in those words. On Windows you
ship the XML with your own application and call Cascades.loadFrom("path/to/haarcascade_frontalface_alt.xml").
Every other platform needs nothing.
The two fetch paths
There are exactly two, and they behave identically in every way that matters. The difference is only how much you have to write down.
| You want | Call | Verifies | Where the pinned values live |
|---|---|---|---|
| the YuNet face detector | FaceDetect.downloadModel(dir) | exact byte size, then SHA-256 | FaceDetect.ModelSizeBytes, FaceDetect.ModelSha256 |
| anything else | Models.fetch(spec, dir) | SHA-256, when the spec pins one | the ModelSpec you (or scalacv) wrote |
FaceDetect.downloadModel — the one dedicated one-liner
import java.nio.file.Path
val yunetPath: Either[CvError, Path] = FaceDetect.downloadModel(Path.of("models"))
The argument (the parameter is called into) is a directory, created if it is absent; the file name
is fixed at face_detection_yunet_2023mar.onnx, which is what makes the "is it already here and still
correct?" check possible at all. Give it a directory that only your application writes to.
Models.fetch — the general form
Models.fetch takes a ModelSpec and does the same thing for any model:
import java.nio.file.Path
val sfacePath: Either[CvError, Path] = Models.fetch(FaceRecognizer.modelSpec, Path.of("models"))
Prefer this one as soon as you fetch more than one model — the loop over several specs is then the same three lines rather than one special case plus a loop.
What "verified" means, step by step
Both paths run the same sequence, and each step exists because of a failure mode that has actually happened to somebody:
- Is it already here? If
into/<fileName>is a regular file and it still hashes to the pinned value, that path is returned immediately. No network, no re-download. This is what makes it safe to call at every start-up. - Create the directory if it does not exist. A failure here is a
Left(CvError.LoadFailed(dir, "could not create the download directory: …")). - Try each URL in order. The first one that downloads and verifies wins. Every failure is
collected, so if all of them fail you get one
Leftlisting what each mirror did. - Download to a temp file beside the target, not to the target. The temp file is a sibling named
.model-<random>.part, so the final move stays inside one filesystem and is as close to atomic as the filesystem allows. - Verify the temp file, before anything else looks at those bytes.
- Move it onto the target — and only then. An interrupted run, a full disk or a killed process can therefore never leave a half-written model for the next start-up to load. A model is executable content fetched from a host nobody here controls; the checksum is the supply-chain gate, and it is checked before OpenCV ever sees the file.
- Delete the temp file on the way out, whatever happened.
Models.fetch gives each mirror a 15-second connect budget and a 60-second request budget, and
follows HTTP redirects. FaceDetect.downloadModel uses 20 s and 120 s — it predates the generic
downloader and keeps its own, roomier numbers. Mirrors are tried in order, so a mirror that hangs costs
you its timeout and then the next one is tried. Do not wrap either call in your own retry loop; the
retry across mirrors is already there.
Writing a ModelSpec for your own model
A ModelSpec is three things and nothing else:
| Field | Type | What it is |
|---|---|---|
fileName | String | the fixed name the file gets on disk. This is the cache key — see the danger below |
urls | Seq[String] | the mirrors to try, in order. http://, https:// and file:// all work |
sha256 | Option[String] | the pinned digest, lower- or upper-case. Some for ModelSpec(...), None for ModelSpec.unverified(...) |
ModelSpec has a private constructor and two named builders, so the verifying form is the one you reach
for by default and the unverified form is something you have to ask for by name:
ModelSpec(fileName: String, urls: Seq[String], sha256: String): ModelSpec // verifying — the default
ModelSpec.unverified(fileName: String, urls: Seq[String]): ModelSpec // no integrity check
Both reject an empty fileName and an empty urls with an IllegalArgumentException at construction —
a spec with no source is a programmer error, not a runtime failure to handle.
The recipe, end to end
-
Pick your source. A release asset, a model-zoo URL, an object-storage path, a file you already have. Anything reachable by URL.
-
Download it once, by hand, and look at it. Is it the size the publisher says? Is it actually ONNX and not, say, an HTML "sign in to continue" page?
-
Hash it.
sha256sum my-model.onnx # Linuxshasum -a 256 my-model.onnx # macOScertutil -hashfile my-model.onnx SHA256 # Windows -
Write the spec, pasting that digest in.
-
Call
Models.fetchat start-up and gate readiness on theRight.
import java.nio.file.Path
val myModel = ModelSpec(
fileName = "yolov8n-v3.onnx",
urls = Seq(
"file:///opt/models/yolov8n-v3.onnx", // the local copy, if the image has one…
"https://models.example.com/yolov8n-v3.onnx" // …otherwise the network
),
sha256 = "0000000000000000000000000000000000000000000000000000000000000000" // paste yours here
)
val myModelPath: Either[CvError, Path] = Models.fetch(myModel, Path.of("models"))
The order of urls is the order they are tried, so putting a file:// first means a machine that
already has the model never opens a socket, while a machine that does not falls through to the network —
the same code, the same verification, on both.
Prove it to yourself, with no download
You do not need a network or a real model to see the whole mechanism work: file:// is just another
source, so a few bytes in a temporary directory make a complete, honest demonstration. Everything in this
section really runs when this page is built.
import java.nio.file.Files
import java.security.MessageDigest
// Stand in for "a model file I already have": a handful of bytes in a temp directory.
// Files.write returns the path it wrote, which is exactly the path we want to point a spec at.
val sourceDir = Files.createTempDirectory("scalacv-docs-source")
val sourceFile =
Files.write(sourceDir.resolve("pretend-model.onnx"), "not a network, but it hashes like one".getBytes)
// Exactly the digest `sha256sum` would print for that file.
val pretendSha =
MessageDigest
.getInstance("SHA-256")
.digest(Files.readAllBytes(sourceFile))
.map(b => f"$b%02x")
.mkString
val pretendSpec =
ModelSpec(fileName = "pretend-model.onnx", urls = Seq(sourceFile.toUri.toString), sha256 = pretendSha)
Fetch it into a fresh directory:
val modelDir = Files.createTempDirectory("scalacv-docs-models")
val fetched = Models.fetch(pretendSpec, modelDir)
fetched.map(_.getFileName.toString)
// res4: Either[CvError, String] = Right("pretend-model.onnx")
And now the two properties the rest of this page keeps claiming. Idempotent — the second call finds the file, re-hashes it, and returns the same answer without reading the source again:
Models.fetch(pretendSpec, modelDir) == fetched
// res5: Boolean = true
Verified — the same bytes under a wrong pinned hash are refused:
val tamperedSpec = ModelSpec("pretend-model.onnx", Seq(sourceFile.toUri.toString), "00" * 32)
val rejectedDir = Files.createTempDirectory("scalacv-docs-rejected")
Models.fetch(tamperedSpec, rejectedDir).isLeft
// res6: Boolean = true
That Left is a CvError.LoadFailed whose details read
could not be downloaded from any source. followed by one indented line per mirror — here,
checksum mismatch: got <the real digest>, expected 0000…. Nothing was moved into rejectedDir: the
temp file is deleted and the target is never created.
file:// and air-gapped deployments
Because file:// is a first-class source and the hash is still checked, an offline deployment is not
a special case in the code — it is a spec whose only URL is local:
val offlineSpec = ModelSpec(
fileName = "face_detection_yunet_2023mar.onnx",
urls = Seq("file:///opt/models/face_detection_yunet_2023mar.onnx"),
sha256 = FaceDetect.ModelSha256
)
Bake the file into the container image (or mount it read-only), list only the file:// URL, and keep the
pinned digest. You get a real integrity gate — a corrupted layer or a mounted volume pointing at the
wrong file fails at start-up with a named error instead of misbehaving at inference time. See
Deploying to production and the native cache for where this
sits in a container build.
Opting out of the checksum
Sometimes a model has no published digest. ModelSpec.unverified(fileName, urls) builds a spec with no
integrity check at all:
val noChecksum = ModelSpec.unverified("some-model.onnx", Seq("https://models.example.com/some-model.onnx"))
It is a deliberate, named opt-out: the word unverified is right there in the diff, so nobody can reach
this behaviour by accident. It also costs more than you would expect:
unverified disables cache invalidation, not just integrityModels.fetch keys its cache on into.resolve(spec.fileName) and nothing else, and the "is the file
on disk still good?" test is "does it match the pinned hash?". With no hash pinned, that test is
vacuously true for any file already sitting at that path.
The consequences, in order of how much they hurt:
- Same name, same hash → no network. What you want.
- Same name, new hash → the stale file fails verification, the model is re-downloaded and replaces it. This is how a genuine model upgrade rolls out.
- Same name,
unverified→ whatever is on disk is served forever. The URL is never contacted again. Ship a new model under the old file name and every replica that has ever run keeps answering with last quarter's weights, silently, with no error anywhere.
The fix is a one-word habit: put the version in the file name — "yolov8n-v3.onnx", not
"yolov8n.onnx". Two versions then coexist on disk, a rollback is a config change rather than a cache
purge, and a rolling deploy never has two replicas fighting over one path. Do that whether or not you pin
a hash. See the native cache for the deployment side of the same story.
The two built-in specs
scalacv pins two models itself, because two of its APIs are useless without them. Both carry a checksum, so you never write these by hand:
val yunetSpec: ModelSpec = FaceDetect.modelSpec // YuNet — face detection
val sfaceSpec: ModelSpec = FaceRecognizer.modelSpec // SFace — face recognition
(yunetSpec.fileName, FaceDetect.ModelSizeBytes, yunetSpec.urls.size)
// res9: Tuple3[String, Long, Int] = (
// "face_detection_yunet_2023mar.onnx",
// 232589L,
// 2
// )
(sfaceSpec.fileName, sfaceSpec.sha256)
// res10: Tuple2[String, Option[String]] = (
// "face_recognition_sface_2021dec.onnx",
// Some("0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79")
// )
| YuNet | SFace | |
|---|---|---|
| What it does | finds faces, with five landmarks each | turns an aligned face into a 128-value embedding |
| File | face_detection_yunet_2023mar.onnx | face_recognition_sface_2021dec.onnx |
| Size | 232,589 bytes (232 kB) | ~37 MB |
| Pinned SHA-256 | 8f2383e4…52fa4 (FaceDetect.ModelSha256) | 0ba9fbfa…4e79 (FaceRecognizer.modelSpec.sha256) |
| Mirrors in the spec | 2 (a commit-pinned URL, then main) | 1 |
| Fetch it with | FaceDetect.downloadModel(dir) or Models.fetch(FaceDetect.modelSpec, dir) | Models.fetch(FaceRecognizer.modelSpec, dir) |
| Then load it with | FaceDetect.create(path, inputSize) | FaceRecognizer.load(path) |
| Guide | Object detection | Face recognition |
YuNet's first mirror pins the exact commit that last touched the file, so those bytes cannot change under
you; the second follows main and exists only so that a repository reorganisation degrades to a fallback
instead of an outage. Neither is trusted — the checksum decides.
The Git-LFS trap
This one is worth reading once even if you never touch the OpenCV Zoo, because the failure looks like a successful download.
Large files in a Git repository are usually stored with Git LFS (Large File Storage): the repository
itself contains a ~131-byte pointer text file, and the real bytes live on a separate media host. The
OpenCV Zoo keeps its .onnx files this way. If you fetch such a file from a raw.githubusercontent.com
URL, you get HTTP 200 and the pointer, not the model. Nothing errors. You have a 131-byte file called
something.onnx that OpenCV then refuses to load as a network, with a message about the graph rather
than about the download.
Two defences, both already in the code:
FaceDetect.ModelUrlsusesmedia.githubusercontent.com/media/…, the host that serves the real object, rather than therawhost that serves the pointer. (The pointer does at least carry anoid sha256:line — which is where the pinned digest was cross-checked from.)FaceDetectadditionally pinsModelSizeBytesand checks the size before the hash, so a 131-byte pointer is reported as "expected 232589 bytes … but got 131 — the download is truncated, or the server answered with something that is not the model" rather than as an opaque digest mismatch.
ModelSpec has no size field, so the generic path cannot make that distinction. A pointer, an HTML
error page and a corrupted transfer all surface the same way:
checksum mismatch: got <digest of whatever arrived>, expected <the pinned digest>
If you see that, look at the file size on disk first. A three-digit byte count means you got a pointer or an error page, not a corrupt model.
FaceRecognizer.modelSpec's single URL is of the form https://github.com/opencv/opencv_zoo/raw/main/….
GitHub redirects that form to the media host for LFS objects and Models.fetch follows redirects, so it
normally lands on the real 37 MB file. If it ever does not — a checksum mismatch on a suspiciously small
file — fetch it by hand from the media host and point a local spec at your copy:
val sfaceFallback = ModelSpec(
fileName = "face_recognition_sface_2021dec.onnx",
urls = Seq(
"file:///opt/models/face_recognition_sface_2021dec.onnx",
"https://media.githubusercontent.com/media/opencv/opencv_zoo/main/" +
"models/face_recognition_sface/face_recognition_sface_2021dec.onnx"
),
sha256 = "0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79"
)
Which model do I need?
The other guides on this site tell you what to do with a model. This table tells you what to look for
and how to configure it. The three blob parameters are the ones that go wrong silently: get them wrong
and the model runs, produces numbers, and is quietly worse rather than broken — see
blobFromImage for why.
| Capability | Model family to look for | Typical inputSize | scaleFactor / mean / swapRB | What decodes the output |
|---|---|---|---|---|
| Face detection | YuNet, from the OpenCV Zoo — spec built in | the frame size you expect, e.g. Size(320, 320) | handled inside FaceDetectorYN | FaceDetect.create → image.faces(detector) |
| Face recognition | SFace, from the OpenCV Zoo — spec built in | fixed by the model | handled inside FaceRecognizerSF | FaceRecognizer.load → rec.embed(image, face) |
| Body pose, regression | a MoveNet-style single-pose ONNX export | Size(192, 192) (MoveNet Lightning) | 1.0 / 255 / Scalar(0, 0, 0) / true | PoseEstimator.decode(out, size, KeypointLayout.Regression, PoseTopology.CocoBody17) |
| Body pose, heatmap | an OpenPose-style export | whatever the model documents | whatever the model documents | PoseEstimator.decode(out, size, KeypointLayout.Heatmap, …) |
| Hand landmarks | a MediaPipe hand-landmark export, converted to ONNX | Size(224, 224) | 1.0 / 255 / Scalar(0, 0, 0) / true | PoseEstimator.decode(…, PoseTopology.Hand21), then GestureRecognizer |
| Selfie segmentation | MediaPipe selfie-segmentation, MODNet or U²-Net, as ONNX | Size(256, 256) | 1.0 / 255 / Scalar(0, 0, 0) / true | Segmenter.decodeMask(out, size) — accepts [1, 1, H, W] and [1, 2, H, W] |
| Classification, generic detection, depth, … | any ONNX export | whatever the model documents | whatever the model documents | none — read the tensor yourself with Dnn |
Notes on that table, because a table cannot carry caveats:
swapRBis the one to double-check. Almost every published model was trained on RGB, while OpenCV decodes images to BGR.Dnn.blobFromImagedefaults it tofalse(that is OpenCV's own default and silently disagreeing with upstream documentation would be worse), while the one-call helpersImage.estimatePoseandImage.segmentdefault it totrue(that is what their models want). If your keypoints are plausible but consistently a bit off, this is the first thing to flip.inputSizefor YuNet is not a constraint.FaceDetect.createrequires one, butFaceDetect.detectre-sets it on every frame, so any image size works. It still matters, because YuNet's anchors are laid out for it — pass the size of the frames you actually expect.- A topology is not in the file. A model's tensor says "17 keypoints"; it does not say which one is
the left wrist.
PoseTopologysupplies that, and itssizemust match the model's keypoint count ordecodefails with a named error. See Decoding a model's output.
Where each of these is used in anger: Pose estimation, Gesture & sign recognition, Video conferencing, Face recognition, Deep learning.
MediaPipe ships TFLite, and OpenCV does not read it
This is the single most common dead end on the way to a working pose or segmentation pipeline, so here it is once, in full.
Google's MediaPipe publishes the obvious models for body pose, hand landmarks and selfie segmentation
— and publishes them as TFLite (.tflite), the TensorFlow Lite format. scalacv runs models through
OpenCV's dnn module, and scalacv exposes only the ONNX importer (why).
A .tflite file will not load. There is no flag for this.
Two ways forward:
-
Convert it.
tf2onnxreads TFLite and writes ONNX:pip install tf2onnxpython -m tf2onnx.convert --tflite hand_landmark.tflite --output hand_landmark.onnx --opset 13Then hash the result and pin it in a
ModelSpecexactly like any other file. Note that the digest is now yours: a conversion is not byte-reproducible across tool versions, so pin the artefact you actually tested, and give it a versioned file name. -
Find something already exported. Plenty of pose, hand and segmentation networks are published as ONNX directly — MoveNet exports, MODNet, U²-Net, the OpenCV Zoo. If a model was trained in PyTorch it almost certainly has an ONNX export somewhere, because
torch.onnx.exportis one line.
Whichever route you take, the conversion is a build-time step. Do not try to convert at runtime; fetch the converted artefact like any other model.
Sanity-checking a model you have never run
You have a .onnx file. Before wiring it into a pipeline, ask it two questions: what are its outputs
called, and what shape is the tensor it produces. The shape is what tells you which decoder in the
table above applies.
/** Runs one grey probe frame through a model and prints what came back. A diagnostic, not a pipeline. */
def describeOutput(modelPath: String, inputSize: Size): Either[CvError, Unit] =
Dnn.fromOnnx(modelPath).map { managedNet =>
managedNet.use { net =>
// The blob *names* you may pass as `outputName` to Dnn.forward. For an ONNX import these are the
// graph's declared outputs, not the `onnx_node!…` layer names.
println(s"outputs: ${net.getUnconnectedOutLayersNames()}")
// A mid-grey frame is enough: we are asking about the tensor's shape, not its values.
val probe = Image.blank(inputSize.width.toInt, inputSize.height.toInt, Scalar(128, 128, 128))
try
Dnn
.blobFromImage(probe.mat, scaleFactor = 1.0 / 255, size = Some(inputSize), swapRB = true)
.use { blob =>
Dnn.forward(net, blob).use { out =>
val shape = (0 until out.dims()).map(i => out.size(i))
println(s"output: ${out.dims()} dims, shape ${shape.mkString("[", ", ", "]")}")
}
}
finally probe.close()
}
}
val checked: Either[CvError, Unit] = describeOutput("models/unknown.onnx", Size(256, 256))
Read the printed shape against this:
| Printed shape | What it is | Decode with |
|---|---|---|
[1, 1, K, 3] | K keypoints as (y, x, score), normalised to [0, 1] | KeypointLayout.Regression with a topology whose size is K |
[1, K, H, W] | one heatmap plane per keypoint | KeypointLayout.Heatmap |
[1, 1, H, W] | one foreground-probability plane | Segmenter.decodeMask |
[1, 2, H, W] | background / foreground; the last channel is the person | Segmenter.decodeMask |
[1, N] | N class scores | your own arg-max — see Reading the output |
[1, N, 15]-ish rows | detection boxes | your own decode plus NMS |
If the shape does not match what you expected, the usual causes are: the wrong model, the wrong
inputSize (a model with a fixed input rejects a differently-shaped blob), or a multi-output graph where
you need to name the output rather than take the last layer.
PoseEstimator.decode and Segmenter.decodeMask both validate the tensor before they reshape it, and
raise a CvError.NativeCall that says what it expected and what it got — for example "expected 51 values
(K=17 keypoints × (y, x, score)) but the model produced 63. This is not the regression pose model this
topology decodes." That is a much better starting point than OpenCV's raw total-size exception, and it is
almost always either the wrong KeypointLayout or a topology whose size does not match the network.
When a fetch fails
Every failure is a Left(CvError.LoadFailed(resource, details)) — a value, not a thrown exception, so it
composes with the rest of the error model. These are the messages you will actually see,
and what each one means:
| Message fragment | Where from | What it means |
|---|---|---|
could not create the download directory: | both paths | into is not writable, or a file already exists at that path |
could not be downloaded from any source. | Models.fetch | every mirror failed; one indented line per URL follows |
could not be downloaded from any known mirror. | FaceDetect.downloadModel | the same thing, phrased differently — grep for both |
HTTP 404 from <url> | Models.fetch | wrong path on the mirror — any status ≥ 400 is reported in this form |
HTTP 404 (no URL in the text) | FaceDetect.downloadModel | the same, but it accepts only 200 once redirects have been followed |
checksum mismatch: got …, expected … | Models.fetch | an LFS pointer, an HTML error page, the wrong version, or a corrupt transfer |
expected 232589 bytes … but got 131 | FaceDetect.downloadModel | the size pre-check fired: you got a pointer or an error page |
SHA-256 mismatch for … Refusing to load an unverified model. | FaceDetect.downloadModel | right size, wrong bytes |
| a timeout message from the JDK HTTP client | both paths | the mirror was too slow for the request budget — see below. FaceDetect.downloadModel prefixes it with the exception's class name; Models.fetch reports the message alone |
there is no readable file at this path. | FaceDetect.create | the loader's own pre-check: you never fetched the model, or you passed the directory instead of the file |
no such file — supply the SFace ONNX model path | FaceRecognizer.load | the same, for SFace |
Diagnosis order, when it is a checksum mismatch: size first, then content. ls -l the file (or read
the byte count out of the message). Three digits means a pointer or an error page. Roughly the right size
but the wrong digest means the publisher moved the file under the same name, and your pin is now stale —
which is exactly the situation the pin exists to surface.
What the downloader will not do for you
Models.fetch is a thin, deliberate wrapper over the JDK's HttpClient. It is not a download manager, and
being honest about the edges saves you a bad afternoon:
- No authentication. It sends a bare
GETwith no headers. A model behind a token, a private bucket or an artifact registry cannot be fetched with it. Download it with a tool that can authenticate, put it on disk, and point afile://spec at it — you keep the checksum gate that way. - No proxy configuration surface. Whatever the JDK's
HttpClientdoes by default in your environment is what you get; there is no knob here. - No resume, and no progress reporting. Each mirror gets one request with a 60-second budget
(120 s for
FaceDetect.downloadModel) — that is a budget for the whole exchange, not an idle timeout. A 37 MB model on a slow link can exhaust it, at which point every mirror reports a timeout and you get aLeftthat looks like an outage but is a bandwidth problem. If that is your situation, fetch the file withcurl -C -or your package pipeline and use afile://spec. - No size limit. The response body is written to the temp file before anything inspects it, so a mirror that serves ten gigabytes writes ten gigabytes to your disk and then fails the checksum. Point specs at hosts you trust.
- No size pre-check in the generic path. Only
FaceDetect.downloadModelpins a byte count;ModelSpechas no size field. - No locking between processes. Two processes fetching the same spec into the same directory each write their own temp file and then move it onto the target; with the same pinned hash that is harmless. With different content under the same file name it is a race — which is the other reason to put the version in the file name.
- No conversion and no model validation. It moves bytes and checks a digest. Whether those bytes are
a network OpenCV can import is discovered by
Dnn.fromOnnx/FaceDetect.create/FaceRecognizer.load, each of which returns its ownLeft.
Fail fast at boot
The place to fetch models is start-up, next to OpenCv.load(), with readiness gated on the result. Both
calls are idempotent and cheap once satisfied, so this costs nothing after the first run:
import java.nio.file.Path
/** Everything that must be on disk before this process serves a request. */
def warmUp(into: Path): Either[CvError, (Path, Path)] =
OpenCv.load() // extract + load the natives; idempotent and thread-safe
for
detector <- Models.fetch(FaceDetect.modelSpec, into)
embedder <- Models.fetch(FaceRecognizer.modelSpec, into)
yield (detector, embedder)
val ready: Boolean = warmUp(Path.of("/var/lib/myapp/models")).isRight
A process that starts, passes its health check, and only discovers at the first request that the model directory is empty is the failure mode this avoids. See Deploying to production.
Why nothing is vendored
The models are fetched rather than committed, and that is a licensing decision rather than a size one.
YuNet is MIT-licensed (Shiqi Yu). Shipping a copy inside this repository — or inside a published jar —
would oblige scalacv to reproduce that notice and carry that obligation on behalf of everyone who
redistributes the jar. Keeping the model a runtime download keeps the obligation where it belongs: with
whoever actually redistributes the weights. The same reasoning applies to every other model, and it is
recorded in THIRD-PARTY.md.
The practical consequence for you: when you redistribute an application that bundles a model, you inherit that model's licence, not scalacv's. Read it before you bake the file into an image. Some model zoos are MIT or Apache-2.0 and ask only for a notice; some are non-commercial; some are research-only. The download is where that decision belongs to you, which is exactly why it is a download.
This repository ships no photographs or video clips either, for the same licensing reason — which is why
every runnable example on this site draws its input with Image.blank(...).drawRect(...) instead of
reading a file.
Next
- Deep learning (DNN) — load the model you just fetched, blob a frame, run it.
- Object detection — YuNet end to end, the model scalacv fetches for you.
- Face recognition — SFace, and what to do with an embedding.
- Pose estimation and Gesture & sign recognition — the two capabilities that need a converted model most often.
- Video conferencing — selfie segmentation and the mask it produces.
- The native cache and Deploying to production — where models live in a container, and how to warm everything before the first request.
- Run a neural network (ONNX) — the tutorial this page unblocks.