Всем привет, на днях возникла необходимость использования камеры Xphase Pro без официального приложения, и я столкнулся с тем, что в интернете особо‑то этим никто не занимался. О том, что делать с файлами типа ori и о FFI на Rust читайте под катом.

О самой камере

Камера представляет собой многолинзовую вундервафлю, конкретно у меня в руках оказалась модель Xphase Pro X2, хвалебные оды которой можно прочитать в интернете. Я по камерам не специалист, и толком тут ничего сказать не могу — разрешение высокое, цветопередача вроде нормальная.

Перво‑наперво, разберемся с тем, что вообще можно сделать с камерой. Она хостит WI‑FI сеть с одноименным названием, спокойно к ней подключаемся. Тут у нас есть несколько вариантов действий — можно скачать приложение на телефон и все сделать в графическом интерфейсе, а можно обратиться к камере по апи на хост 192.168.6.1 Именно второй вариант я и выбрал, в силу независящих от меня обстоятельств.

API камеры

В апи по адресу 192.168.6.1:8080 у нас есть несколько методов, таких как:

  • get_list

  • get_file - этот метод расположен на отдельном 8081 порте

  • delete_file

  • do_capture

  • get_thumb

Я наивно полагал, что одним только апи получится реализовать данную задачу. С виду все кажется просто - вызвали do_capture, камера сделала снимок, посмотрели его имя в get_list, вывели тамбнейл через get_thumb, если захотели полный снимок - попросили get_file. И тут на последнем этапе мы можем обнаружить, что get_file возвращает нам вовсе не картинку, а странный файл с расширением ori.

Поначалу я подумал, что это какой-то формат, близкий к raw, но все оказалось куда сложнее. Ori - это некий бинарный файл, который хранит в себе несколько снимков, с каждой линзы. На каждую линзу несколько снимков с разной выдержкой. Можно ли вытащить эти снимки из ori? Можно, на гитхабе есть репозиторий, где Entropy512 написал соответствующий скрипт.

Однако что делать с этими снимками дальше? Собирать панораму вручную через Hugin? Использовать какой-нибудь feature extractor и сшивать скриптами? Неужели разработчики не подумали о каком-нибудь готовом решении, ведь в приложении они как-то это делают?..

Открываем номикон и кастуем черную магию

Итак, наконец мы переходим к практической части статьи. Разработчики предоставляют библиотеку libPanoMaker.so, в которой есть нужная нам функция - превращение ori в jpg.

Разберемся с функциями, которые нам предоставляет библиотека:

  • ProInitRawFileReader

  • ProUpdateRawFileReader

  • ProCleanRawFileReader

  • ProMakePanoramaBuf

На бумаге все выглядит просто - создаем структуру, хранящую буфер с данными, вызываем ProMakePanoramaBuf , и волшебным мановением руки все само получается. Но реальность, как всегда, куда сложнее.

Для начала следует понять, что мы не обойдемся без ProUpdateRawFileReader. Она указывает, на какую длину заполнен буфер. Дело в том, что ProInitRawFileReader только создает структуру, которая хранит в себе буфер ori файла и его максимальную длину. По умолчанию считается, что этот буфер пустой.

Сделано это потому, что разработчики позволяют параллельно читать ori файл с их камеры и выполнять склейку панорамы из той части буфера, которая заполнена сейчас. В моем случае ori файл уже лежал локально, поэтому мне было нужно лишь заполнить буфер, создать из него RawFileReader и вызвать ProUpdateRawFileReader, чтобы библиотека "поняла", что буфер полон нужной информации.

Ну что ж, с чего начинать? Во-первых, нам нужно сделать так, чтобы libPanoMaker.so линковалась к нашему Rust приложению. Можно положить эту либу в место, которое считывается как папка для библиотек, например /usr/local/lib, можно написать build.rs, который будет искать эту библиотеку где-то поблизости с исходниками, тут кому как удобнее и понятнее.

Когда мы уверены, что линковщик найдет нужную библиотеку, начинаем писать наш интерфейс. Во-первых, объявим функции, которые будем вызывать из со-шки:

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;
}

По сути дела, здесь мы переписываем объявление функций из С++ на Rust, заменяя сишные типы на типы из Rust FFI. Обратите внимание, что там, где используются указатели, после звездочки следует ключевое слово mut , если же мы хотим показать, что память неизменяемая (или, как пишут в номиконе, place expression), пишем const . В остальном же, это дело нехитрое - где в C++ принимается int, в Rust принимается c_int, остальные типы также по аналогии. Здесь, правда, есть нюанс, что c_void - это не то же самое, что и (), поэтому явно конвертировать одно в другое не выйдет.

Но мало прилинковать PanoMaker, помимо него нам нужен и libz (он же zlib), без которого ничего не заработает. Поэтому добавим ссылку на libz:

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

Почему я написал конкретную функцию, а не оставил блок extern "C" пустым? Дело в том, что если оставить его пустым, то компилятор будет выдавать ту же ошибку, как если бы libz не был прилинкован. Более того, мало объявить функцию из этой библиотеки, нужно ее еще и вызвать - подозреваю, это все потому, что итоговый исполняемый файл просто-напросто оптимизируется и не включает в себя неиспользованную ссылку. Поэтому перед тем как будем писать код, посвященный обработке ori, вызовем функцию из zlib:

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

Что ж, теперь все готово к нашему ритуалу. Начнем с ori файла, прочитаем его и заполним буфер:

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);

Теперь создадим RawFileReader. Вернемся чуть назад, к объявлению функции ProInitRawFileReader и вспомним, что она возвращает *mut c_void, иными словами, сырой указатель. Безопасно ли это? Конечно же нет! Поэтому весь следующий код будет 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);

Обратите внимание, что мы вызвали ProUpdateRawFileReader , чтобы указать, что буфер полон. Все что теперь остается - это выделить память под отладочную информацию.

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

Теперь можем вызывать заветную ProMakePanoramaBuf. В ней нужно обязательно указать папку, в которой будет итоговый файл, причем путь к папке должен заканчиваться на слэш / - это важно! Также указываем имя входного файла без расширения. Все остальные параметры - стандартные, согласно документации разработчика.

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()
);

И - последний штрих - очистим память, выделенную под RawFileReader и закроем unsafe-блок.

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

Что ж, вот и все, что нужно было сделать. В итоге в папке output мы увидим нужную нам панораму в формате jpg. Полный код будет выглядеть так:

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());
}

Итоги

Ну что ж, мы узнали немного про камеры Xphase, про формат ori и про FFI.

В первую очередь, статья, конечно, направлена на тех бедолаг, кто столкнется с той же проблемой, что и я - как превратить непонятный ori в понятный jpg.

Во вторую очередь, мне давно хотелось как-то повзаимодействовать с FFI в Rust и написать более-менее понятный гайд. Оказалось, что все не так уж и сложно. Примитивные типы, такие как i32, без проблем конвертируются в c_int, строки одним методом конвертируются в *mut c_uchar в комментариях подсказали, что это неправильный подход, строки в Rust не являются нуль-терминированными, поэтому надо либо заканчивать их на \0, либо использовать CString и CStr. Понимаю, что почти не затронул теоретическую часть вопроса, но вряд ли я бы в рамках статьи написал полнее и понятнее, чем в номиконе.

В комментариях можете поделиться вариантами build.rs и подсказать, почему обязательно нужно объявлять и вызывать функцию из zlib, чтобы библиотека прилинковалась.

Ссылку на используемую .so и документацию к камере прикреплять не стал, потому что это не реклама, и мне за эту статью никто не платил, а значит приукрашивать реальность я не буду. Попробуйте поискать это все самостоятельно, и сами поймете, о чем я - мягко говоря, ищется это все не с первого раза.

Комментарии (3)


  1. AnthonyMikh
    10.07.2023 23:14
    +3

    строки одним методом конвертируются в *mut c_uchar

    И конвертируются неправильно. Учитывая, что длины строковых параметров нигде не передаются, функция почти наверняка ожидает нуль-терминированные строки в качестве строковых параметров, а строки в Rust таковыми не являются.


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

    И где гарантия, что 300 байтов хватит для каких бы то ни было целей?


    Ещё: почему вы заводите safe_result только для того, чтобы присвоить в неё значение result?


    И откуда вы вообще взяли эту библиотеку? Поиск в Google по запросу "libPanoMaker" находит только эту статью, а запрос "Xphase Pro PanoMaker" выдаёт какие-то нерелевантные результаты.


    1. domix32
      10.07.2023 23:14

      а строки в Rust таковыми не являются.

      именно поэтому там и передаётся длина вторым параметром.


    1. khmheh Автор
      10.07.2023 23:14
      +3

      И конвертируются неправильно. Учитывая, что длины строковых параметров нигде не передаются, функция почти наверняка ожидает нуль-терминированные строки в качестве строковых параметров, а строки в Rust таковыми не являются.

      Вы правы, в интернетах пишут, что правильный путь - это использование CString и CStr, которые нуль-терминированы, честно говоря, я теперь удивлен, что у меня все заработало.

      И где гарантия, что 300 байтов хватит для каких бы то ни было целей?

      Об этом я не упомянул, в документации сказано, что этот параметр должен быть не менее 256 байт.

      Ещё: почему вы заводите safe_result только для того, чтобы присвоить в неё значение result?

      Изначально я это сделал потому, что result хранит значение внутри unsafe блока, а результат растовской функции мне хотелось возвращать из safe блока. Доступа к result снаружи unsafe не будет - другая область видимости. Можно переписать это вот так

      let safe_result: i32 = unsafe {
        <...>
        let result: c_int = ProMakePanoramaBuf( <...> );
        <...>
        result
      };

      И откуда вы вообще взяли эту библиотеку? Поиск в Google по запросу "libPanoMaker" находит только эту статью, а запрос "Xphase Pro PanoMaker" выдаёт какие-то нерелевантные результаты.

      Ну, я в последнем же абзаце об этом и написал, это очень сложно гуглится, что является лучшей антирекламой этой камеры. А так, вот.