Nim with OpenCV: 1
Building a Drift Detector in Nim
Simplicity of Python, speed of C and Extensibility of Lisp. Nim is a new contender in the programming language industry which has made bold claims. Learn about Nim by building a Drift Detector in OpenCV
Table of contents
Introduction
Traditionally, integrating C++
libraries with any other language has been a challenge mainly due to the fact that C++
code is mangled, unlike C
where names like function names or variable names are directly converted to symbol names in the compiled output, in C++
every name is mangled which allows for function overloading and namespacing.
Due to this fact (which allows for function overloading) we don't have a deterministic naming convention for calling some compiled C++
functions in some other languages. Instead of having manual bridges and cumbersome bindings, Nim
's unique approach to interoperability offers a refreshing solution. It is almost like we are templating C++
in Nim
, An Absolute Delight !!
Some Syntax
Nim
borrows a lot from python
's syntax like using indentation to define blocks and using whitespaces instead of brackets to define arguments.
# fibnonacci sequence generator in nim
proc fibonacci(n: int): seq[int] =
var fibSeq = @[0, 1]
for i in 2..n:
fibSeq.add(fibSeq[i - 1] + fibSeq[i - 2])
fibSeq
let n = 10
let fibSeq = fibonacci(n)
echo "Fibonacci sequence up to ", n, ":", fibSeq
Read more about the similarities and how a python
developer can transition to Nim
here
That's where all the similarities end, Nim
is compiled rather than being interpreted and is statically typed, but we won't be going too deep into the general syntax rather how we can use C++
in Nim
Pragma
Pragmas are Nim's method to give the compiler additional information, we use Pragmas extensively when interoping with C/C++ . Let's look at some important Pragmas
Link
{.link: "<path/to/library>".}
This Pragma specify the paths to the library that needs to be linked during compilation
Import Type Definition
type MyInteger {.importcpp: "long".} = int
In Nim, a type represents a set of values along with operations that can be performed on those values.
In this declaration:
MyInteger
is the name of the Nim type we are defining.importcpp: "long"
specifies thatMyInteger
corresponds to thelong
data type in C++.= int
indicates thatMyInteger
in Nim is represented as anint
, which is Nim's arbitrary-sized integer type.- The
.
at the end of the pragma{.importcpp: "long".}
signifies the end of the pragma declaration.
here we have established a mapping between the long
type of C++ and the int type in Nim
A more complex example
type myMat* {.importcpp: "cv::Mat", header: "<opencv2/core.hpp>",
byref.} = object
cols: int
rows: int
here we are interfacing the opencv
libraries and importing the cv::Mat
data type
Here's a breakdown of the declaration:
myMat
is the name of the Nim type we are defining.importcpp: "cv::Mat"
specifies thatmyMat
corresponds to thecv::Mat
class in C++, which is a fundamental structure in OpenCV for representing matrices upon which we do operations like rotation .- we used the
header
pragma here asheader: "<opencv2/core.hpp>"
which indicates the header file in which thecv::Mat
class is declared. . byref
denotes that instances ofmyMat
are passed by reference, meaning that they are accessed through a reference rather than being copied. This is important for efficiency when working with large data structures- it will become clear later what would happen if we don't pass the
byref
- it will become clear later what would happen if we don't pass the
Additionally, within the myMat
object:
cols
androws
are integer fields representing the number of columns and rows in the matrix, respectively. These fields provide access to the dimensions of the matrix within Nim code.
internally doing myMat.cols
in nim is translated to cv::Mat::some_matrix.cols
in C++
⚠️ we have only imported the data type and not the methods that are defined on this type including the constructor itself
Side Note: The
*
in nim doesn't represent a pointer object rather it is how we define a public symbol which can be accessed from different nim files, similiar to how we do pub in rust
The code won't work as it is because we are not linking against the opencv
libraries. Remember to link we use the link
pragma l
ike
{.link: "/opt/Headers/opencv-4.9.0/build/lib/libopencv_core.so.4.9.0".}
Importing Constructors
proc myMatConstructor*(cols: int, rows: int, types: int): myMat {.importcpp: "cv::Mat::Mat(@)",
header: "<opencv2/core.hpp>", constructor.}
-
proc myMatConstructor*
declares a procedure namedmyMatConstructor
. -
(cols: int, rows: int, types: int)
specifies the parameters that the constructor expects. These parameters represent the number of columns, rows, and the type of the matrix, respectively. -
: myMat
indicates that the constructor returns an instance of themyMat
type which we defined earlier -
{.importcpp: "cv::Mat::Mat(@)"}
specifies the C++ constructor that the Nim procedure corresponds to. In this case, it corresponds to the constructor of thecv::Mat
class in OpenCV, which initializes a matrix with the given dimensions and type.- the
@
symbol means that put whatever arguments were passed to procedure here
- the
-
header: "<opencv2/core.hpp>"
specifies the header file in which the C++ constructor is declared. This ensures that the necessary declarations are available to the Nim code. -
constructor
denotes that this procedure is a constructor, ensuring that it's called automatically when a new instance of themyMat
type is created.
Importing Procedures
proc at*(mat: myMat, x: int, y: int): int
{.importcpp: "at<uchar>", header: "<opencv2/core.hpp>".}
The at
procedure allows us to access elements of a matrix represented by the myMat
type which internally is mapped to the cv::Mat
type
-
proc at*(mat: myMat, x: int, y: int): int
: This defines a procedure namedat
that takes three parameters:mat
, which is an instance of themyMat
type representing the matrix, andx
andy
, which are the coordinates of the element we want to access. The procedure returns an integer value. -
{.importcpp: "at}
: This pragma specifies the C++ function or method that the Nim procedure corresponds to. In this case, it corresponds to theat
function/method in the C++ codebase, which is used to access elements of the matrix.- see how we have passed the C++
char
type inside the<>
we can change it to something else likeat<int>
- see how we have passed the C++
We see that we can write pure C++ inside the ""
after the .importcpp
pragma and wrap it in a nim procedure. You can have multiline C++ code inside those quotes and it will still work, that's what I love about nim It suprisingly just works
Note: inside the
header
field you have to pass the full path to the library, if you don't want to do that then you can tell the Nim compiler where to look for the headers. There are multiple ways but one way is by using a$project.nim.cfg
file like here and don't forget to enable the C++ backend in Nim by adding the command line argument or in the nimble config
Drift Detection
Let's put all this syntax used into making a Drift AKA Motion detector. Motion detection is a crucial component of computer vision systems that involves identifying and analyzing changes in the position or appearance of objects within a scene over time.
there are multiple ways of detecting motion
- Frame Differencing
- Optical Flow
- Background Subtraction
we will be focusing on Frame Differencing which is the easiest one to implement.
Consider a black and white image as an array of pixels, each with a value ranging from 0 to 255. When capturing two images, one at time 0 seconds and the other at 1 second, any motion present between the two instances results in a change in the pixel configuration. This change manifests as non-zero differences between corresponding pixels in the first and second images.
Consider two grayscale images represented as 2D arrays:
Image at time t (0 seconds):
[[100, 120, 110],
[105, 125, 115],
[95, 115, 105]]
Image at time t+1 (1 second):
[[100, 125, 110],
[105, 120, 115],
[95, 115, 105]]
Differences:
[[0, 5, 0],
[0, -5, 0],
[0, 0, 0]]
The matrix is not all zeroes so we have detected some motion We will build upon this Frame Differencing method our Motion Detector
Coding
For the input frame sequence let's use the webcam of our computer.
You can access it in opencv C++ using cv::VideoCapture::open(0)
which will use the default camera interface
First we define a type which holds the cv::VideoCapture
type
type myVideoCapture* {.importcpp: "cv::VideoCapture",
header: "<opencv2/videoio.hpp>".} = object
and now we define the open function for it
proc open(cap: myVideoCapture, index: int): bool {.importcpp: "cv::VideoCapture::open",
header: "<opencv2/videoio.hpp>".}
see how Nim handles the return value mapping from a C++ function to the Nim procedure , we didn't have to define what the function should return when some value X is returned from the C++ function, its piped to Nim bool
data type.
Now we use the cv::VideoCapture::read
method to read from our webcam.
proc read(cap: myVideoCapture, frame: myMat): bool {.importcpp: "cv::VideoCapture::read",
header: "<opencv2/videoio.hpp>".}
Let's test this code by doing
var cap: myVideoCapture
open(cap, 0)
var colored_frame: myMat
if read(cap, colored_frame):
imwrite(colored_frame,"output.png")
else:
{.fatal: "not able to read from capture device".}
discard
the fatal
pragma can be used to output errors, Nim provides a lot of pragmas that allow for customizability and flexibility
Anyways, if everything was implemented correctly the program will read a frame and then save it to a png file.
Now that we can read and save frames let's move onto doing the frame differencing.
The above operations have been using data structures such as cv::Mat
for image representation. While it's possible to perform the motion detection operations directly on cv::Mat
objects, working with Nim's native data structures like 2D arrays can often lead to cleaner and more readable code.
to convert a cv::Mat
into a Nim 2D array we do
proc at*(mat: myMat, x: int, y: int): int
{.importcpp: "at<uchar>", header: "<opencv2/core.hpp>".}
proc matToMatrix(mat: myMat): seq[seq[int]] =
var matrix: seq[seq[int]] = @[]
for i in 0..mat.rows-1:
var row: seq[int] = @[]
for j in 0..mat.cols-1:
row.add(at(mat, i, j))
matrix.add(row)
return matrix
- The
at
procedure allows us to access elements of a matrix represented by themyMat
type which internally is mapped to thecv::Mat
type seq
is Nim's vector representation- we initialises it using the
@[]
which initialises an empty 2D vector
- we initialises it using the
- We iterate the
cv::Mat
type and fill up the 2D sequence of ints
Ok so we got the 2D representation of the frame now what ? We take 2 frames taken one after the other and create a new difference frame out of it.
proc diffMatrix(f1: seq[seq[int]], f2: seq[seq[int]]): seq[seq[int]] =
var diff_matrix: seq[seq[int]] = @[]
for i in 0..f1.len-1:
var row: seq[int] = @[]
for j in 0..f1[0].len-1:
if(abs(f2[i][j] - f1[i][j]))>0:
row.add(abs(f2[i][j] - f1[i][j]))
else:
row.add(0)
diff_matrix.add(row)
return diff_matrix
simple enough eh ? we it
erate both the matrices and add the difference to the diff_matrix
, if there is none we just add a 0
now we just iterate the diff_matrix
and check if its all zeros or not
proc isMotion(img: seq[seq[int]]): bool =
for i in 0..img.len-1:
for j in 0..img[0].len-1:
if img[i][j] != 0:
return false
return true
and add some helper functions to output if we have detected motion or not
while true:
var frame1: myMat
discard read(cap, frame1)
os.sleep(100)
var frame2: myMat
discard read(cap, frame2)
var te1: seq[seq[int]] = matToMatrix(g1)
var te2: seq[seq[int]] = matToMatrix(g2)
var drift = diffMatrix(te2, te1)
if isMotion(drift):
echo "Motion Detected"
Voila !!! we have made a motion detector in Nim and interoping with C++
A Deeper Look
How is this happening ? if C++ does name mangling then how does Nim know everytime what the symbol name is going to be ? What sorcery is this 👾
Under the hood Nim is actually compiling the code we wrote to C++ and then interfaces with the other C++ normally. Pretty Cool isn't it !!
You can take a look at the C++ files generated by Nim inside /.cache/nim/$projectname(_r|_d)
directory
Final Regards
Nim really is a great language, the python like readability the speed of C/C++ and impressive interoperability makes it a really compelling language to learn and build projects in.
In Rust/ Zig or any other language you would have modify the C++ code and add a bunch of extern C
calls so that the name of that symbol isn't mangled but in Nim it just works.
Helpful links
- Source code available here