Geometry & typed values
Every OpenCV call speaks in coordinates, sizes, boxes and pixel values. scalacv gives those a small family of
immutable Scala value types — Point, Point3, Size, Rect, Scalar — plus a set of typed enums
that stand in for OpenCV's raw int constants. These are the vocabulary the rest of the library is written
in: a detector hands back Seq[Rect], a drawing call takes a Scalar and a Thickness, a resize takes an
Interpolation. Learn them once here and every other page reads more easily.
Everything on this page is a plain Scala value: no natives are touched, so the snippets simply run and print their results.
Why value types
org.opencv.core.Point and its siblings are mutable Java objects with public fields, and a
Seq[org.opencv.core.Rect] handed back from a detector is really a set of live handles into native memory.
Keep one around after the Mat it came from is freed and you are reading a dangling object.
scalacv copies at the boundary instead. A Rect is four ints in an ordinary case class, so a Seq[Rect] from
object detection stays valid — and stays yours — long after the source image is
released. Copying is cheap and it turns detector output into ordinary immutable data you can pattern-match, put
in a Map, or send across threads without a second thought (see concurrency).
val boxes = Seq(Rect(10, 10, 40, 30), Rect(80, 12, 25, 25))
// boxes: Seq[Rect] = List(
// Rect(x = 10, y = 10, width = 40, height = 30),
// Rect(x = 80, y = 12, width = 25, height = 25)
// )
boxes.sortBy(-_.area).map(_.topLeft)
// res1: Seq[Point] = List(
// Point(x = 10.0, y = 10.0),
// Point(x = 80.0, y = 12.0)
// )
Here is the whole family at a glance:
| Type | Fields | Element type | Guards its invariant? |
|---|---|---|---|
Point | x, y | Double | no |
Point3 | x, y, z | Double | no |
Size | width, height | Double | yes — no negative extent |
Rect | x, y, width, height | Int | yes — no negative extent |
Scalar | v0–v3 | Double | no |
Point
A 2-D coordinate, origin top-left, x right and y down. The fields are Double, matching OpenCV, so the
sub-pixel positions that feature and contour work produce survive intact.
Point(12, 8)
// res2: Point = Point(x = 12.0, y = 8.0)
Point3
A point in 3-D space — a model coordinate for AR / pose work, in the same units you give a
marker's side length (metres is the usual choice). z points out of the marker plane toward the camera.
Point3(0.05, 0.05, 0.0)
// res3: Point3 = Point3(x = 0.05, y = 0.05, z = 0.0)
You rarely build these by hand except to describe a known 3-D object (the corners of a marker, a calibration rig); pose estimation and marker AR are where they show up.
Size
Width and height, and one of the two types here that guards its invariant: a negative extent is rejected at construction (a zero extent is allowed — an empty size is meaningful).
Size(640, 480)
// res4: Size = Size(width = 640.0, height = 480.0)
Size(-1, 10) // require fails: a Size cannot be negative
// java.lang.IllegalArgumentException: requirement failed: a Size cannot be negative: -1.0x10.0
// at scala.Predef$.require(Predef.scala:337)
// at scalacv.Size.<init>(Geometry.scala:29)
// at scalacv.Size$.apply(Geometry.scala:28)
// at repl.MdocSession$MdocApp.$init$$$anonfun$3(geometry.md:50)
Size is what resize/gaussianBlur kernels take; the fields are Double because OpenCV's cv::Size is,
though for pixel dimensions you will pass whole numbers.
Rect
An axis-aligned box — x, y, width, height — with three derived values you keep reaching for. The origin
(x, y) may be negative (a region of interest can extend past the top-left of the image); the extent may not.
val r = Rect(10, 10, 40, 30)
// r: Rect = Rect(x = 10, y = 10, width = 40, height = 30)
r.area
// res5: Long = 1200L
(r.topLeft, r.bottomRight)
// res6: Tuple2[Point, Point] = (
// Point(x = 10.0, y = 10.0),
// Point(x = 50.0, y = 40.0)
// )
area: Long—width * height. It is aLong, not anInt, because the product overflows a signedIntpast roughly a 46340-pixel side — an easy limit to hit on a full-frame ROI of a large image.topLeft: Point— the corner(x, y).bottomRight: Point—(x + width, y + height), one past the last enclosed pixel.
Rect is what crop takes and what the detectors return, so it is the type you will see most
often. Note the corner is (x, y) and the size is (width, height) — a different shape from Point/Size,
which trips people up until it doesn't.
Scalar
A pixel value: up to four channel components v0–v3, with the trailing ones defaulting to 0. The important
thing to internalise is the channel order — OpenCV Mats are BGR by default, not RGB. The named constants
are ordered accordingly.
Scalar.Red // (0, 0, 255) — blue and green zero, red full
// res7: Scalar = Scalar(v0 = 0.0, v1 = 0.0, v2 = 255.0, v3 = 0.0)
(Scalar.Black, Scalar.White, Scalar.Green, Scalar.Blue)
// res8: Tuple4[Scalar, Scalar, Scalar, Scalar] = (
// Scalar(v0 = 0.0, v1 = 0.0, v2 = 0.0, v3 = 0.0),
// Scalar(v0 = 255.0, v1 = 255.0, v2 = 255.0, v3 = 0.0),
// Scalar(v0 = 0.0, v1 = 255.0, v2 = 0.0, v3 = 0.0),
// Scalar(v0 = 255.0, v1 = 0.0, v2 = 0.0, v3 = 0.0)
// )
So the "red" you pass to a drawing call is Scalar(0, 0, 255); write Scalar(255, 0, 0) and you
get blue. Build your own for any value — a single component works for a grey/1-channel image, and the fourth
channel is the alpha of a BGRA image:
Scalar(200) // mid-grey for a single-channel Mat
// res9: Scalar = Scalar(v0 = 200.0, v1 = 0.0, v2 = 0.0, v3 = 0.0)
Scalar(0, 0, 255, 128) // semi-transparent red in a BGRA image
// res10: Scalar = Scalar(v0 = 0.0, v1 = 0.0, v2 = 255.0, v3 = 128.0)
This is the single most common colour bug. If your reds come out blue, you almost certainly passed
Scalar(255, 0, 0) thinking in RGB. Use the named Scalar.Red/Scalar.Blue/… constants where you can, and
remember the order is B, G, R (, A).
Like the others, Scalar is copied out of the native object at the boundary, so the value you hold is never
a view onto a Mat that might change or be freed underneath you.
Typed enums: no raw int constants
OpenCV's Java API is a wall of bare integers — Imgproc.COLOR_BGR2GRAY, Imgproc.INTER_LINEAR,
Imgproc.RETR_TREE. They are untyped, unchecked, and trivially swappable: nothing stops you passing a
line-type constant where a font was wanted. scalacv's public API takes typed enums instead, and only converts
to the underlying int at the last moment. That int is always available as .cvValue if you need to drop to
a raw org.opencv.* call — the escape hatch is a door, not a wall.
ColorConversion.BgrToGray.cvValue
// res11: Int = 6
(Interpolation.Linear.cvValue, LineType.AntiAliased.cvValue, Font.Simplex.cvValue)
// res12: Tuple3[Int, Int, Int] = (1, 16, 0)
The true enumerations
Most of these are genuine enumerations — a value is exactly one of the cases, and .cvValue is a plain Int:
| Enum | Stands in for | Used by |
|---|---|---|
ColorConversion | Imgproc.COLOR_* | cvtColor / gray |
Interpolation | Imgproc.INTER_* | resize and warps |
LineType | Imgproc.LINE_* | drawing |
Font | Imgproc.FONT_HERSHEY_* | putText / drawText |
ContourRetrieval | Imgproc.RETR_* | findContours |
ContourApproximation | Imgproc.CHAIN_APPROX_* | findContours |
BorderType | Core.BORDER_* | padded image processing |
Flip | flip codes 1/0/-1 | transforms — mirror |
Rotation | Core.ROTATE_* | transforms — lossless quarter-turns |
MorphShape | Imgproc.MORPH_RECT/ELLIPSE/CROSS | erode / dilate / morphology |
MorphOp | Imgproc.MORPH_OPEN/CLOSE/… | compound morphology |
AdaptiveMethod | Imgproc.ADAPTIVE_THRESH_* | adaptive threshold |
Colormap | Imgproc.COLORMAP_* | false-colour image processing |
Because they are Scala 3 enums you get the usual perks — exhaustive match, .values, and a name for every
case:
ContourRetrieval.values.map(c => c.toString -> c.cvValue).toList
// res13: List[Tuple2[String, Int]] = List(
// ("External", 0),
// ("List", 1),
// ("CComp", 2),
// ("Tree", 3)
// )
Interpolation.values.map(_.toString).toList
// res14: List[String] = List("Nearest", "Linear", "Cubic", "Area", "Lanczos4")
Flip is Horizontal / Vertical / Both — the visible result — rather than OpenCV's axis-centric flip code
1 / 0 / -1, which nobody remembers. Rotation is Clockwise / CounterClockwise / Half. The typed
name is chosen so the call site reads as what it does; .cvValue still gives you the raw code.
The structured types: ImreadFlags and Threshold
Not every OpenCV flag family is a single choice, but the two that are not need different shapes — one is a genuine bitmask, the other only looks like one.
ImreadFlags — a decode color (how many channels / what depth) with an optional scale (decode at
reduced resolution) and an ignoreOrientation flag (skip the EXIF rotation). OpenCV's IMREAD_* constants
look OR-able but are not: each IMREAD_REDUCED_* value already bakes in its colour bit and IMREAD_UNCHANGED
is -1, so OR-ing a colour with a reduced-size flag silently decodes the wrong image. So the (color, scale)
pair maps totally onto exactly one named constant rather than composing, and only ignoreOrientation (bit
128) is a real independent flag OR-ed on top:
ImreadFlags(ImreadColor.Color, ImreadScale.Half, ignoreOrientation = true).cvValue
// res15: Int = 145
Reduced-size decode exists only for Grayscale and Color, and Unchanged can carry no extra bit at all —
the combinations OpenCV has no constant for are rejected at construction rather than quietly OR-ed into
something else:
ImreadFlags(ImreadColor.Unchanged, ImreadScale.Half) // require fails: -1 admits no reduction
// java.lang.IllegalArgumentException: requirement failed: Unchanged has no reduced-size decode; only Grayscale and Color support Half
// at scala.Predef$.require(Predef.scala:337)
// at scalacv.ImreadFlags.<init>(Enums.scala:267)
// at scalacv.ImreadFlags$.apply(Enums.scala:260)
// at repl.MdocSession$MdocApp.$init$$$anonfun$6(geometry.md:140)
The common cases have ready-made constants:
(ImreadFlags.Color.cvValue, ImreadFlags.Grayscale.cvValue, ImreadFlags.Unchanged.cvValue)
// res16: Tuple3[Int, Int, Int] = (1, 0, -1)
ImreadScale is a small enum in its own right — the fraction of full resolution to decode at, cheaper than a
full read followed by a resize because the codec skips the discarded detail:
ImreadScale.values.map(s => s.toString -> s.denom).toList
// res17: List[Tuple2[String, Int]] = List(
// ("Full", 1),
// ("Half", 2),
// ("Quarter", 4),
// ("Eighth", 8)
// )
Threshold — a Mode (binary, truncate, to-zero, …) OR-ed with at most one automatic-threshold Auto
modifier (Otsu or Triangle). The modifier is an Option, because the two auto methods are mutually exclusive
and most calls use neither:
Threshold(Threshold.Mode.Binary).cvValue // a fixed threshold
// res18: Int = 0
val t = Threshold.otsu() // Binary | THRESH_OTSU — let OpenCV pick the level
// t: Threshold = Threshold(mode = Binary, auto = Some(Otsu))
(t.cvValue, t.computesThreshold)
// res19: Tuple2[Int, Boolean] = (8, true)
computesThreshold is true exactly when an Auto is present — that is when OpenCV computes the level itself
and the value it returns (surfaced as ThresholdResult) actually means something, rather than echoing back the
fixed number you supplied. See image processing for threshold in action.
Neither of these is a single enum on purpose, for opposite reasons. Threshold is a real bitmask:
THRESH_BINARY | THRESH_OTSU is a combination, not an alternative, and a single-choice enum could not express
it without also admitting nonsense like a bare THRESH_MASK leaking into the public API. ImreadFlags is the
reverse trap — its constants look OR-able but are not, so instead of exposing bits it maps a small structured
value totally onto one named constant and rejects the combinations OpenCV cannot represent.
Thickness — the drawing companion
One more typed value lives with drawing but belongs to the same family: Thickness, which is
either a Stroke(pixels) outline or the Filled sentinel. OpenCV encodes "filled" as a thickness of -1, a
value ordinary arithmetic can produce by accident and which aborts native code if handed to a line or text.
Splitting the two into distinct types means the mistake stops compiling instead of crashing:
(Thickness.Default.cvValue, Thickness.Stroke(3).cvValue, Thickness.Filled.cvValue)
// res20: Tuple3[Int, Int, Int] = (1, 3, -1)
Thickness.Stroke(0) // require fails: a stroke must be at least one pixel wide
// java.lang.IllegalArgumentException: requirement failed: a stroke must be at least one pixel wide, got 0
// at scala.Predef$.require(Predef.scala:337)
// at scalacv.Thickness$Stroke.<init>(Draw.scala:42)
// at scalacv.Thickness$Stroke$.apply(Draw.scala:41)
// at repl.MdocSession$MdocApp.$init$$$anonfun$8(geometry.md:188)
Next
- The Image API — where
Rect,Scalar, and the enums are consumed - Drawing —
Scalar,Thickness,Font,LineTypein action - Image processing —
ColorConversion,Threshold,BorderType, morphology