Giving Odin Vision
Tags: odin, opencv, c, computervision
Published: 2024-05-08 16:52:54

---
title: Giving Odin Vision
published: true
description: 
tags: odin, opencv, c, computervision
cover_image: https://raw.githubusercontent.com/yevhen-k/opencv-odin-demo/master/assets/logo.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-05-08 14:36 +0000
---

## Disclaimer

This post is about my experience with [Odin programming language](https://odin-lang.org/). So, I won't talk about its features and advantages and provide basic tutorials. There are plenty of materials on those topics.

## Prologue

Recently, I was browsing YouTube unconsciously and fell into a pit of recommendations about programming languages. Odin was one of them. Many famous tech YouTubers either talked about Odin or interviewed Ginger Bill (Odin's creator). So, I checked Odin's [overwiew](https://odin-lang.org/docs/overview/) page and found the language interesting to give it a try.

## Prerequisites
1. Familiarity with C/C++
2. Familiarity with OpenCV
3. Odin's Overview page has been read

## Choosing a Project

What kind of project I'd like to make? Hellope (Odin's "Hello World")? Or making a game? Why even a game you may ask. There are some YouTube tutorials and articles on making games with Odin. It's because Odin gives you the ability to use `raylib`, `sdl2`, `glfw`, `OpenGL`, `directx`, and even `vulkan` right out of the box with its [`Vendor Library Collection`](https://pkg.odin-lang.org/vendor/).

Given my experience with computer vision, I decided to make a project with the use of OpenCV.

## OpenCV Makes Odin See

**Project idea**: make a simple app that reads frames from a webcam and shows frames in a window.

This is the famous `VideoCapture` "Hello World" implemented many times in `C++` and `Python`:
- C++ example: https://docs.opencv.org/4.x/d8/dfe/classcv_1_1VideoCapture.html
- Python example: https://docs.opencv.org/4.x/dd/d43/tutorial_py_video_display.html

But how do I combine Odin and OpenCV to read camera frames? There is no such package in Odin that makes binds with OpenCV. At least I haven't found it.

Fortunately, Odin supports FFI and [bindings to C](https://odin-lang.org/news/binding-to-c/). Unfortunately, OpenCV doesn't expose C API.

So, here was my plan:
1. Make C wrappers for all necessary C++ functions
2. Compile wrapper lib to `.a` or `.so` file
3. Describe all wrapper functions in Odin foreign block
4. Implement app
5. Run app

### 1. Make C wrappers for all necessary C++ functions

To make different programming languages and OS able to talk with each other, they should share the same language. `C` is such a *lingua franca*. It might not be perfect, but it's better than nothing.

The goal of the section is to *express all C++ types and functionality in plane C*.

To implement all required functionality of the app we'll need the following functions and types:

- `cv::Mat` type to store read data
- `cv::imread()` reads an image from the file and returns `cv::Mat` object
- `empty()` method of `cv::Mat` object to check if it's empty
- `cv::namedWindow()` function to create a named window
- `cv::imshow()` function to show the image on named window
- `cv::destroyWindow()` function to destroy the window and release its resources
- `cv::waitKey()` function to wait for a pressed key
- `cv::imwrite()` function to write an image to a file
- `cv::VideoCapture` type to manage video camera capture
- `open()` method of `cv::VideoCapture` to open a camera for video capturing
- `release()` method of `cv::VideoCapture` to release and deallocate capture resources
- `isOpened()` method of `cv::VideoCapture` to check if capture is opened
- `read()` method of `cv::VideoCapture` to read a frame from the camera and store it in `cv::Mat` object

Let's make the corresponding header `imageprocessing.h` and implementation `imageprocessing.cpp` files.

```cpp
// `imageprocessing.h`

#ifndef IMPROC_H
#define IMPROC_H
#include <stdbool.h>

typedef void *Mat;
typedef void *VideoCapture;

#ifdef __cplusplus
extern "C" {
#endif

Mat cv_new_mat();

Mat cv_image_read(const char *file, int flags);

bool cv_mat_isempty(Mat mat);

void cv_named_window(const char *name);

void cv_image_show(const char *name, Mat img);

int cv_wait_key(int delay);

void cv_destroy_window(const char *name);

bool cv_image_write(const char *filename, Mat img);

void cv_free_mem(void *data);

VideoCapture cv_new_videocapture();

bool cv_videocapture_open(VideoCapture cap, int device_id, int api_id);

void cv_videocapture_release(VideoCapture cap);

bool cv_videocapture_isopened(VideoCapture cap);

bool cv_videocapture_read(VideoCapture cap, Mat frame);

#ifdef __cplusplus
}
#endif

#endif
```

```cpp
// `imageprocessing.cpp`
#include "imageprocessing.h"
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>

Mat cv_new_mat() { return new cv::Mat(); }

Mat cv_image_read(const char *file, int flags) {
  cv::Mat image = cv::imread(file, flags);
  return new cv::Mat(image);
}

bool cv_mat_isempty(Mat mat) {
  cv::Mat *m = static_cast<cv::Mat *>(mat);
  return m->empty();
}

void cv_named_window(const char *name) {
  cv::namedWindow(name);
}

void cv_image_show(const char *name, Mat img) {
  cv::Mat *image = static_cast<cv::Mat *>(img);
  std::string win_name{name};
  cv::imshow(name, *image);
}

int cv_wait_key(int delay) { return cv::waitKey(delay); }

void cv_destroy_window(const char *name) {
  cv::destroyWindow(name);
}

bool cv_image_write(const char *filename, Mat img) {
  cv::Mat *image = static_cast<cv::Mat *>(img);
  return cv::imwrite(filename, *image);
}

void cv_free_mem(void *data) { free(data); }

VideoCapture cv_new_videocapture() { return new cv::VideoCapture(); }

bool cv_videocapture_open(VideoCapture cap, int device_id, int api_id) {
  cv::VideoCapture *capture = static_cast<cv::VideoCapture *>(cap);
  return capture->open(device_id, api_id);
}

void cv_videocapture_release(VideoCapture cap) {
  cv::VideoCapture *capture = static_cast<cv::VideoCapture *>(cap);
  capture->release();
}

bool cv_videocapture_isopened(VideoCapture cap) {
  cv::VideoCapture *capture = static_cast<cv::VideoCapture *>(cap);
  return capture->isOpened();
}

bool cv_videocapture_read(VideoCapture cap, Mat frame) {
  cv::Mat *image = static_cast<cv::Mat *>(frame);
  cv::VideoCapture *capture = static_cast<cv::VideoCapture *>(cap);
  return capture->read(*image);
}
```

As you can see, we don't operate with OpenCV's `cv::Mat` and `cv::VideoCapture` types directly. Instead, we use the so-called opaque pointer pattern (see `typedef void *Mat;` and `typedef void *VideoCapture;`) that helps us encapsulate (hide) C++ details of implementation and use only C types.

### 2. Compile wrapper lib to `.a` or `.so` file

All operations below assume:
- you're running Linux
- OpenCV is installed

#### Compiling static library

```bash
$ gcc -c -Wall -Werror -fpic -I/usr/include/opencv4 -I/usr/include/opencv4/opencv2 `pkg-config --libs opencv4` -lstdc++ imageprocessing.cpp
$ ar rcs imageprocessing.a imageprocessing.o
```

#### Compiling shared library

```bash
$ gcc -c -Wall -Werror -fpic -I/usr/include/opencv4 -I/usr/include/opencv4/opencv2 `pkg-config --libs opencv4` -lstdc++ imageprocessing.cpp
$ gcc -shared -o libimageprocessing.so imageprocessing.o
```

### 3. Describe all wrapper functions in Odin foreign block

Odin has IMO a great feature that significantly simplifies usage of both static and shared libraries. Yes, I'm about `foreign import`! The `foreign import` block simply expects a path to the shared or static library. If I understood correctly, the path to the static library should be specified relative to the package, and the path to the shared library should be specified relative to a compiled executable.

Let's describe wrapped-in C functionality in Odin lang. In the example below I use linking with the static library in the `foreign import` block.

```go
// imageprocessing_bindings.odin

package imageprocessing

import "core:c"

when ODIN_OS == .Linux do foreign import cv "imageprocessing.a"
// when ODIN_OS == .Linux do foreign import cv "libimageprocessing.so"

Mat :: distinct rawptr
VideoCapture :: distinct rawptr

// Just copy-paste from https://docs.opencv.org/4.9.0/d8/d6a/group__imgcodecs__flags.html#ga61d9b0126a3e57d9277ac48327799c80
ImageReadModes :: enum c.int {
	IMREAD_UNCHANGED           = -1, //!< If set, return the loaded image as is (with alpha channel,
	//!< otherwise it gets cropped). Ignore EXIF orientation.
	IMREAD_GRAYSCALE           = 0, //!< If set, always convert image to the single channel
	//!< grayscale image (codec internal conversion).
	IMREAD_COLOR               = 1, //!< If set, always convert image to the 3 channel BGR color image.
	IMREAD_ANYDEPTH            = 2, //!< If set, return 16-bit/32-bit image when the input has the
	//!< corresponding depth, otherwise convert it to 8-bit.
	IMREAD_ANYCOLOR            = 4, //!< If set, the image is read in any possible color format.
	IMREAD_LOAD_GDAL           = 8, //!< If set, use the gdal driver for loading the image.
	IMREAD_REDUCED_GRAYSCALE_2 = 16, //!< If set, always convert image to the single channel grayscale
	//!< image and the image size reduced 1/2.
	IMREAD_REDUCED_COLOR_2     = 17, //!< If set, always convert image to the 3 channel BGR color image
	//!< and the image size reduced 1/2.
	IMREAD_REDUCED_GRAYSCALE_4 = 32, //!< If set, always convert image to the single channel grayscale
	//!< image and the image size reduced 1/4.
	IMREAD_REDUCED_COLOR_4     = 33, //!< If set, always convert image to the 3 channel BGR color image
	//!< and the image size reduced 1/4.
	IMREAD_REDUCED_GRAYSCALE_8 = 64, //!< If set, always convert image to the single channel grayscale
	//!< image and the image size reduced 1/8.
	IMREAD_REDUCED_COLOR_8     = 65, //!< If set, always convert image to the 3 channel BGR color image
	//!< and the image size reduced 1/8.
	IMREAD_IGNORE_ORIENTATION  = 128, //!< If set, do not rotate the image
	//!< according to EXIF's orientation flag.
}


// Just copy-paste https://docs.opencv.org/4.9.0/d4/d15/group__videoio__flags__base.html#ga023786be1ee68a9105bf2e48c700294d
VideoCaptureAPIs :: enum c.int {
	CAP_ANY           = 0, //!< Auto detect == 0
	CAP_VFW           = 200, //!< Video For Windows (obsolete, removed)
	CAP_V4L           = 200, //!< V4L/V4L2 capturing support
	CAP_V4L2          = CAP_V4L, //!< Same as CAP_V4L
	CAP_FIREWIRE      = 300, //!< IEEE 1394 drivers
	CAP_FIREWARE      = CAP_FIREWIRE, //!< Same value as CAP_FIREWIRE
	CAP_IEEE1394      = CAP_FIREWIRE, //!< Same value as CAP_FIREWIRE
	CAP_DC1394        = CAP_FIREWIRE, //!< Same value as CAP_FIREWIRE
	CAP_CMU1394       = CAP_FIREWIRE, //!< Same value as CAP_FIREWIRE
	CAP_QT            = 500, //!< QuickTime (obsolete, removed)
	CAP_UNICAP        = 600, //!< Unicap drivers (obsolete, removed)
	CAP_DSHOW         = 700, //!< DirectShow (via videoInput)
	CAP_PVAPI         = 800, //!< PvAPI, Prosilica GigE SDK
	CAP_OPENNI        = 900, //!< OpenNI (for Kinect)
	CAP_OPENNI_ASUS   = 910, //!< OpenNI (for Asus Xtion)
	CAP_ANDROID       = 1000, //!< MediaNDK (API Level 21+) and NDK Camera (API level 24+) for Android
	CAP_XIAPI         = 1100, //!< XIMEA Camera API
	CAP_AVFOUNDATION  = 1200, //!< AVFoundation framework for iOS (OS X Lion will have the same API)
	CAP_GIGANETIX     = 1300, //!< Smartek Giganetix GigEVisionSDK
	CAP_MSMF          = 1400, //!< Microsoft Media Foundation (via videoInput). See platform specific notes above.
	CAP_WINRT         = 1410, //!< Microsoft Windows Runtime using Media Foundation
	CAP_INTELPERC     = 1500, //!< RealSense (former Intel Perceptual Computing SDK)
	CAP_REALSENSE     = 1500, //!< Synonym for CAP_INTELPERC
	CAP_OPENNI2       = 1600, //!< OpenNI2 (for Kinect)
	CAP_OPENNI2_ASUS  = 1610, //!< OpenNI2 (for Asus Xtion and Occipital Structure sensors)
	CAP_OPENNI2_ASTRA = 1620, //!< OpenNI2 (for Orbbec Astra)
	CAP_GPHOTO2       = 1700, //!< gPhoto2 connection
	CAP_GSTREAMER     = 1800, //!< GStreamer
	CAP_FFMPEG        = 1900, //!< Open and record video file or stream using the FFMPEG library
	CAP_IMAGES        = 2000, //!< OpenCV Image Sequence (e.g. img_%02d.jpg)
	CAP_ARAVIS        = 2100, //!< Aravis SDK
	CAP_OPENCV_MJPEG  = 2200, //!< Built-in OpenCV MotionJPEG codec
	CAP_INTEL_MFX     = 2300, //!< Intel MediaSDK
	CAP_XINE          = 2400, //!< XINE engine (Linux)
	CAP_UEYE          = 2500, //!< uEye Camera API
	CAP_OBSENSOR      = 2600, //!< For Orbbec 3D-Sensor device/module (Astra+, Femto, Astra2, Gemini2, Gemini2L, Gemini2XL, Femto Mega) attention: Astra2, Gemini2, and Gemini2L cameras currently only support Windows and Linux kernel versions no higher than 4.15, and higher versions of Linux kernel may have exceptions.
}

@(default_calling_convention = "c")
foreign cv {
	cv_new_mat :: proc() -> Mat ---
	cv_image_read :: proc(file: cstring, flags: c.int) -> Mat ---
	cv_mat_isempty :: proc(mat: Mat) -> c.bool ---
	cv_named_window :: proc(name: cstring) ---
	cv_image_show :: proc(name: cstring, img: Mat) ---
	cv_wait_key :: proc(delay: c.int) -> c.int ---
	cv_destroy_window :: proc(name: cstring) ---
	cv_image_write :: proc(filename: cstring, img: Mat) -> c.bool ---
	cv_free_mem :: proc(data: rawptr) ---

	cv_new_videocapture :: proc() -> VideoCapture ---
	cv_videocapture_open :: proc(capture: VideoCapture, device_id, api_id: c.int) -> c.bool ---
	cv_videocapture_release :: proc(capture: VideoCapture) ---
	cv_videocapture_isopened :: proc(capture: VideoCapture) -> c.bool ---
	cv_videocapture_read :: proc(capture: VideoCapture, frame: Mat) -> c.bool ---
}
```

As you can see, `foreign import` points to the library where to look for symbols (functions), and the `foreign` block describes all functions with their signatures that could be found in the library with Odin lang. Each function declaration ends with `---` which means that the function body is located elsewhere (in a shared or static library in our case).

To "emulate" opaque types, I use `Mat :: distinct rawptr` construction, which is the same as `typedef void *Mat;` on `C`'s side.

Odin's types are compatible with C's types. The compatibility makes communication between Odin and C smooth. `"core:c"` package is especially helpful in the task of type compatibility with C. Here is a simple type conversion table between C types and corresponing Odin types.

| Odin Type | C Type       |
| --------- | ------------ |
| cstring   | const char * |
| c.int     | int          |
| c.bool    | bool         |
| rawptr    | void*        |

### 4. Implement app

Here is an implementation of the VideoCapture demo. As a bonus, you can find functionality to read and show images.

```go

package imageprocessing

import "core:c"
import "core:fmt"
import "core:os"


demo_read_image :: proc() {
	image_path :: "path/to/image.png"
	image := cv_image_read(image_path, cast(c.int)(ImageReadModes.IMREAD_COLOR))
	defer cv_free_mem(image)

	if cv_mat_isempty(image) {
		fmt.eprintfln("Error: can't find or open an image: %s", image_path)
		os.exit(1)
	}

	window_name :: "The Image"
	cv_named_window(window_name)
	defer cv_destroy_window(window_name)

	cv_image_show(window_name, image)
	cv_wait_key(0)
}

demo_read_camera :: proc() {

	frame := cv_new_mat()
	defer cv_free_mem(frame)

	capture := cv_new_videocapture()
	defer {
		cv_videocapture_release(capture)
		cv_free_mem(capture)
	}

	device_id: c.int = 0
	api_id: c.int = cast(c.int)VideoCaptureAPIs.CAP_ANY

	cv_videocapture_open(capture, device_id, api_id)

	window_name :: "Camera Video"
	cv_named_window(window_name)
	defer cv_destroy_window(window_name)

	if !cv_videocapture_isopened(capture) {
		fmt.eprintfln(
			"Error: can't open camera stream for device_id=%d and api_id=%d",
			device_id,
			api_id,
		)
		os.exit(1)
	}

	fmt.println(">>> Reading frames...")
	for {
		cv_videocapture_read(capture, frame)
		if cv_mat_isempty(frame) {
			fmt.eprintln("Error: empty frame... exiting")
			break
		}
		cv_image_show(window_name, frame)
		c := cv_wait_key(25)
		if c == 27 { 	// ESC key pressed
			break
		}
	}

}

main :: proc() {
	fmt.println(">>> OpenCV Odin Started...")
	// demo_read_image()
	demo_read_camera()

	fmt.println(">>> OpenCV Odin Ended...")
}

```

The code is quite self-descriptive. We have `main` proc. It is the starting point of the application. Moreover, all of Odin's code is a *word-for-word translation* from `C`. In the `main` proc, there are two functions:
- `demo_read_image()` for reading images (commented out)
- `demo_read_camera()` for reading camera frames

### 5. Run app

For both static and dynamic linking, the build is done with the following code:

```bash
odin build ../opencv-odin-demo -extra-linker-flags:"`pkg-config --libs opencv4` -lstdc++"
```

As you can see, the Odin compiler can interact with the linker and its flags.

This will generate `opencv-odin-demo` executable. And then you can run the app:

```bash
./opencv-odin-demo
```

That's it! It just works!

## Full Code

The code is available here: https://github.com/yevhen-k/opencv-odin-demo/

## Epilogue

Odin has convenient and neat features alongside its simplicity. Linking with FFI was easier than I thought before starting the project.

The code I gave most likely isn't idiomatic and good from Odin's perspective, but it gives you a scratch to start from. I hope it might help you to make your own OpenCV+Odin projects soon.

**PS**

Mom look! I'm a real programmer, not a frameworker!