How to turn an ori file into a jpeg panorama using Rust FFI

Hello everyone, the other day there was a need to use the Xphase Pro camera without an official application, and I was faced with the fact that no one on the Internet did this much. Read about what to do with files like ori and FFI on Rust under the cut.

About the camera itself

The camera is a multi-lens prodigy, specifically, I had the Xphase Pro X2 model in my hands, the laudatory odes of which can be read on the Internet. I’m not an expert on cameras, and I really can’t say anything here – the resolution is high, the color reproduction seems to be normal.

First things first, let’s figure out what can generally be done with the camera. It hosts a WI-FI network of the same name, we can easily connect to it. Here we have several options – you can download the application to your phone and do everything in the graphical interface, or you can access the camera via api on the host 192.168.6.1 It was the second option that I chose, due to circumstances beyond my control.

Camera API

In the api at 192.168.6.1:8080 we have several methods such as:

I naively thought that only api would be able to implement this task. In appearance, everything seems simple – they called do_capture, the camera took a picture, looked at its name in get_list, displayed the thumbnail through get_thumb, if they wanted a full picture, they asked for get_file. And here, at the last stage, we can find that get_file returns us not a picture at all, but a strange file with the ori extension.

At first I thought that this was some kind of format close to raw, but everything turned out to be much more complicated. Ori is a kind of binary file that stores several shots from each lens. For each lens, several shots with different shutter speeds. Is it possible to pull these pictures from ori? It is possible, on github there is a repository where Entropy512 wrote the corresponding script.

But what to do with these pictures next? Collect panorama manually through Hugin? Use some feature extractor and stitch with scripts? Did the developers not think of some ready-made solution, because they somehow do it in the application? ..

Open the nomicon and cast black magic

So, finally, we move on to the practical part of the article. Developers provide the library libPanoMaker.sowhich has the function we need – the transformation ori V jpg.

Let’s deal with the functions that the library provides us:

  • ProInitRawFileReader

  • ProUpdateRawFileReader

  • ProCleanRawFileReader

  • ProMakePanoramaBuf

On paper, everything looks simple – we create a structure that stores a buffer with data, call ProMakePanoramaBuf , and with a magical wave of the hand, everything turns out by itself. But the reality, as always, is much more complicated.

To begin with, we must understand that we cannot do without ProUpdateRawFileReader. It indicates how long the buffer is filled. The fact is that ProInitRawFileReader only creates a structure that stores the file’s ori buffer and its maximum length. By default, this buffer is considered empty.

This is done because the developers allow you to read the ori file from their camera in parallel and merge the panorama from the part of the buffer that is now full. In my case, the ori file was already local, so I only needed to fill the buffer, create from it RawFileReader and call ProUpdateRawFileReaderso that the library “understands” that the buffer is full of the required information.

Well, where to start? First, we need to make sure that libPanoMaker.so is linked to our Rust application. You can put this lib in a place that is read as a folder for libraries, for example /usr/local/lib you can write build.rs, which will look for this library somewhere nearby with the source code, here it is more convenient and understandable for someone.

When we are sure that the linker will find the required library, we start writing our interface. First, let’s declare the functions that we will call from the co-shki:

use std::ffi::{CString, c_char, c_uchar, c_int, c_double, c_void};

#[link(name = "PanoMaker")]
extern "C" { 
    fn ProInitRawFileReader(fileBuf: *mut c_uchar, fileSize: c_int) -> *mut c_void; 

    fn ProUpdateRawFileReader(hRawFileReader: *mut c_void, bufWrPos: c_int) -> c_int;

    fn ProCleanRawFileReader(hRawFileReader: *mut c_void) -> c_int;

    fn ProMakePanoramaBuf(threadNum: c_int, memType: c_int, hRawFileReader: *mut c_void, outputDir: *const c_uchar, fileNo: *const c_uchar, 
        hdrSel: c_int, outputType: c_int, colorMode: c_int, extendMode: c_int, outputJpgType: c_int, outputQuality: c_int, stitchMode: c_int, 
        gyroMode: c_int, templateMode: c_int, templateFileName: *const c_uchar, 
        logoAngle: c_double, luminance: c_double, contrastRatio: c_double, gammaMode: c_int, wbMode: c_int, wbConfB: c_double, wbConfG: c_double, 
        wbConfR: c_double, saturation: c_double, dbgData: *mut c_uchar) -> c_int;
}

In essence, here we are rewriting the declaration of functions from C ++ to Rust, replacing C-types with types from Rust FFI. Note that where pointers are used, the asterisk is followed by the keyword mut but if we want to show that the memory is immutable (or, as they say in nomycon, place expression), we write const . Otherwise, this is a simple matter – where in C ++ is it accepted intRust accepts c_int, other types are also by analogy. Here, however, there is a nuance that c_void – is not the same as ()so explicitly converting one to the other will not work.

But it is not enough to link PanoMaker, in addition to it, we also need libz (aka zlib), without which nothing will work. So let’s add a link to libz:

#[link(name = "z")]
extern "C" {
    fn zlibVersion() -> *mut c_char;
}

Why did I write a specific function and not leave a block extern "C" empty? The fact is that if you leave it empty, then the compiler will give the same error as if libz had not been linked. Moreover, it is not enough to declare a function from this library, you also need to call it – I suspect this is all because the final executable file is simply optimized and does not include an unused reference. Therefore, before we write the code dedicated to processing ori, we call the function from zlib:

unsafe { println!("{:?}", zlibVersion()); }

Well, now everything is ready for our ritual. Let’s start with the ori file, read it and fill the buffer:

let target_ori: &'static str = "input";
let mut file_buf: Vec<c_uchar> = vec![];

let mut ori_file = File::open(target_ori.to_string() + ".ori").expect("No ori file found!");
let fsize = ori_file.metadata().unwrap().len();
ori_file.read_to_end(&mut file_buf);

Now let’s create RawFileReader. Let’s go back a little to the function declaration ProInitRawFileReader and remember what it returns *mut c_void, in other words, a raw pointer. Is it safe? Of course not! Therefore, all of the following code will be unsafe.

unsafe { // <- открываем unsafe блок
let mut reader: *mut c_void = ProInitRawFileReader(file_buf.as_mut_ptr(), (fsize) as c_int);
ProUpdateRawFileReader(reader, (fsize) as c_int);

Please note that we have called ProUpdateRawFileReader to indicate that the buffer is full. All that remains now is to allocate memory for debugging information.

let mut dbg_data: Vec<c_uchar> = Vec::from([0;300]);

Now we can call the treasured ProMakePanoramaBuf. In it, you must specify the folder in which the final file will be, and the path to the folder must end with a slash / – it is important! We also specify the name of the input file without extension. All other parameters are standard, according to the developer’s documentation.

let result: c_int = ProMakePanoramaBuf(1, 0, reader, "output/".as_ptr(), target_ori.as_ptr(), 
  10, 0, 1, 0, 1, 70, 0, 0, 0, "".as_ptr(), -1., 1.2, 1.3, 1, 0, 1.0, 1.0, 1.0, 1.0, 
  dbg_data.as_mut_ptr()
);

And – the final touch – clear the memory allocated for RawFileReader and close the unsafe block.

ProCleanRawFileReader(reader);
} // <- закрыли unsafe-блок

Well, that was all that needed to be done. Finally in the folder output we will see the panorama we need in the format jpg. The complete code will look like this:

use std::ffi::{CString, c_char, c_uchar, c_int, c_double, c_void};
use std::fs::File;
use std::io::Read;

#[link(name = "z")]
extern "C" {
    fn zlibVersion() -> *mut c_char;
}
#[link(name = "PanoMaker")]
extern "C" { 
    fn ProInitRawFileReader(fileBuf: *mut c_uchar, fileSize: c_int) -> *mut c_void; 

    fn ProUpdateRawFileReader(hRawFileReader: *mut c_void, bufWrPos: c_int) -> c_int;

    fn ProCleanRawFileReader(hRawFileReader: *mut c_void) -> c_int;

    fn ProMakePanoramaBuf(threadNum: c_int, memType: c_int, hRawFileReader: *mut c_void, outputDir: *const c_uchar, fileNo: *const c_uchar, 
        hdrSel: c_int, outputType: c_int, colorMode: c_int, extendMode: c_int, outputJpgType: c_int, outputQuality: c_int, stitchMode: c_int, 
        gyroMode: c_int, templateMode: c_int, templateFileName: *const c_uchar, 
        logoAngle: c_double, luminance: c_double, contrastRatio: c_double, gammaMode: c_int, wbMode: c_int, wbConfB: c_double, wbConfG: c_double, 
        wbConfR: c_double, saturation: c_double, dbgData: *mut c_uchar) -> c_int;
}

fn process_panorama() -> i32 {
  unsafe { println!("{:?}", zlibVersion()); }
  let target_ori: &'static str = "input";
  let mut safe_result: i32 = 0;
  let mut file_buf: Vec<c_uchar> = vec![];
  
  let mut ori_file = File::open(target_ori.to_string() + ".ori").expect("No ori file found!");
  let fsize = ori_file.metadata().unwrap().len();
  ori_file.read_to_end(&mut file_buf);

  unsafe {
    let mut reader: *mut c_void = ProInitRawFileReader(file_buf.as_mut_ptr(), (fsize) as c_int);
    ProUpdateRawFileReader(reader, (fsize) as c_int);
    
    let mut dbg_data: Vec<c_uchar> = Vec::from([0;300]);
    let result: c_int = ProMakePanoramaBuf(1, 0, reader, "output/".as_ptr(), target_ori.as_ptr(), 10, 0, 1, 0, 1, 70, 0, 0, 0, "".as_ptr(), -1., 1.2, 1.3, 1, 0, 1.0, 1.0, 1.0, 1.0, dbg_data.as_mut_ptr());
    
    ProCleanRawFileReader(reader);
    safe_result = result;
  }

  return safe_result;
}

fn main() {
  println!("{}", process_panorama());
}

Results

Well, we learned a little about Xphase cameras, about the format ori and about FFI.

First of all, the article, of course, is aimed at those poor fellows who will face the same problem as me – how to turn an incomprehensible ori into understandable jpg.

Secondly, I have long wanted to somehow interact with FFI in Rust and write a more or less understandable guide. It turned out that everything is not so difficult. Primitive types like i32can be easily converted to c_intstrings are converted to *mut c_uchar. I understand that I almost did not touch on the theoretical part of the issue, but it is unlikely that I would have written more fully and more clearly within the framework of the article than in nomycon.

You can share your options in the comments. build.rs and suggest why it is necessary to declare and call a function from zlibto link the library.

I did not attach a link to the .so used and the documentation to the camera, because this is not an advertisement, and no one paid me for this article, which means I will not embellish reality. Try to search for all this on your own, and you yourself will understand what I mean – to put it mildly, this is not all searched for the first time.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *