Всем привет, на днях возникла необходимость использования камеры 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 и документацию к камере прикреплять не стал, потому что это не реклама, и мне за эту статью никто не платил, а значит приукрашивать реальность я не буду. Попробуйте поискать это все самостоятельно, и сами поймете, о чем я - мягко говоря, ищется это все не с первого раза.