Skip to main content

Coming from OpenCV (Java or Python)

Already know OpenCV from Python or the Java bindings? Then you know the operations — this page maps them onto scalacv and calls out the three things scalacv does differently (on purpose). The short version: same OpenCV underneath, but ownership is explicit, constants are typed, and expected failures are values.

import scalacv.*

OpenCv.load()

Three differences worth internalising first

1. Ownership is explicit, and that's a feature. In Python the GC (and cv2's own refcounting) hides native memory; in Java you either leak or call release() by hand. scalacv makes it a handle: an Image / Managed frees exactly once, and a scope (Image.reading, Managed.use, Camera.using) does it for you. You get Python's convenience and deterministic freeing. (Why it matters: the JVM GC can't see off-heap pressure, so "just let it collect" leaks — the numbers are stark.)

2. Constants are typed enums, not ints. No more cv2.COLOR_BGR2GRAY magic numbers or cv.CV_8UC3 you can pass to the wrong argument. ColorConversion.BgrToGray, Threshold.Binary, BorderType.Reflect101 — the compiler checks them.

3. Expected failures are Either, not empty Mats or exceptions. cv2.imread returns None/an empty array on a missing file and you find out three calls later. Image.read returns Left(CvError.DecodeFailed) you can't forget to check. Programmer mistakes still throw; see the error model.

Also unchanged, so no surprise: colour order is still BGR, and coordinates are still (x, y) from the top-left. See Image basics.

Idiom map

OpenCV-Pythonscalacv (high-level)
img = cv2.imread("p.jpg")Image.read("p.jpg")Either[CvError, Image]
cv2.imwrite("o.png", img)image.write("o.png")
g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)image.gray (or image.convert(ColorConversion.BgrToGray))
cv2.GaussianBlur(img, (5,5), 0)image.gaussianBlur(Size(5, 5)) (or image.blur(2))
cv2.Canny(img, 80, 160)image.canny(80, 160)
_, t = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)image.threshold(128)
cv2.resize(img, (w, h))image.resize(w, h)
img[y0:y1, x0:x1] (ROI copy)image.crop(Rect(x0, y0, w, h)) (independent copy)
cnts, _ = cv2.findContours(...)image.contours()Seq[Contour]
cv2.rectangle(img, ...)image.drawRect(Rect(...), Scalar.Green)
cap = cv2.VideoCapture(0)Camera.open(0) / Video.open(0)
manual img.release()scope it (Image.reading, Managed.use) — or image.close()

The escape hatch: you never lose the raw API

scalacv wraps the official org.opencv.* Java API, and it's one method away at all times. Two moves cover everything.

Borrow the raw Mat for any org.opencv.* call scalacv doesn't wrap. image.mat hands you the underlying org.opencv.core.Mat; the Image keeps ownership, so read from it, pass it to any OpenCV function — just don't release it:

import org.opencv.imgproc.Imgproc

val img = Image.blank(64, 64, Scalar.White)
val grey = img.gray // a transform: it consumes `img`, so `grey` is now the live handle

val corners = new org.opencv.core.Mat() // a raw Mat we allocated, so ours to release
try
// Call a raw Imgproc function scalacv doesn't surface, on the borrowed Mat:
Imgproc.cornerHarris(grey.mat, corners, 2, 3, 0.04) // `grey.mat` is BORROWED — never release it
finally
corners.release()
grey.close() // frees the greyscale image; `img` was already spent by `.gray`

Note which value gets closed. image.mat borrows, but .gray is a transform, so it consumes the Image you called it on and hands back a new one. Writing img.gray.mat on one line would spend img and then throw the greyscale Image away without a name — leaving nothing to close and leaking its Mat, while the later img.close() silently did nothing (releasing a spent handle is a no-op). Give the result a name and close that.

Adopt a raw Mat produced by some OpenCV call back into the managed world with Image.wrap(Managed(mat)) — from then on it's owned and scoped like anything else:

val raw = new org.opencv.core.Mat(48, 48, org.opencv.core.CvType.CV_8UC3)
// ... a raw OpenCV call fills `raw` ...
val adopted: Image = Image.wrap(Managed(raw)) // now managed; close()/Using frees it
adopted.close()

The mid-level extension methods on Mat (mat.cvtColor(...), mat.canny(...), mat.findContours()) are the same operations as the Image methods, one tier down — use them when you're already holding a raw Mat.

Gotchas that bite migrants

You can't reuse a value after a transform

Image has move semantics: image.gray consumes image. In Python g = cv2.cvtColor(img, ...) leaves img usable; here image is spent. To use one image two ways, take image.copy first. This is what makes a chain leak-free.

val im = Image.blank(8, 8)
val g = im.gray // consumes im
im.width // throws — im was spent (in Python this would just work)
// java.lang.IllegalStateException: this Mat has already been released or consumed — using it now would crash the JVM from native code. A high-level Image is spent by any transform (gray/blur/…) or terminal (write/bytes/close); call `.copy` before the first use if you need it twice. Run with -Dscalacv.trackOwnership=true to record where it was consumed.
// at scalacv.Managed.spentError(Managed.scala:55)
// at scalacv.Managed.get(Managed.scala:68)
// at scalacv.Image.width(Image.scala:63)
// at repl.MdocSession$MdocApp.$init$$$anonfun$1(opencv-java.md:62)
  • imread doesn't throw and doesn't return null — it returns Left(DecodeFailed). Pattern-match or flatMap the Either; you can't accidentally run a pipeline on nothing.
  • A detector isn't a plain object you dropCascadeClassifier, a DNN Net, and 180-odd other types have no public release(); scalacv frees them through a safe bridge when you close() the Managed. Don't new them and forget them (that leaks in Python/Java too — here it's just visible).
  • Threading — a Mat/detector shared across threads is a C++ data race, exactly as in Python/Java. See Concurrency.
  • No NumPy view tricks — there's no img[::2, ::2] stride slicing. crop returns an independent copy; for channel/row work drop to the raw Mat.

Next