DEV Community

Cover image for Giving Odin Vision
Yevhen Krasnokutsky
Yevhen Krasnokutsky

Posted on

Giving Odin Vision

Disclaimer

This post is about my experience with Odin programming language. 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 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.

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:

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. 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.

// `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
Enter fullscreen mode Exit fullscreen mode
// `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);
}
Enter fullscreen mode Exit fullscreen mode

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

$ 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
Enter fullscreen mode Exit fullscreen mode

Compiling shared library

$ 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
Enter fullscreen mode Exit fullscreen mode

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.

// 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 ---
}
Enter fullscreen mode Exit fullscreen mode

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.


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...")
}

Enter fullscreen mode Exit fullscreen mode

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:

odin build ../opencv-odin-demo -extra-linker-flags:"`pkg-config --libs opencv4` -lstdc++"
Enter fullscreen mode Exit fullscreen mode

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:

./opencv-odin-demo
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (1)