Move the panorama keypoint detector onto the engine, and probe with a detector

XFeat's two exports are a Keypoints role now; the crate no longer names
tract, and the app compiles TensorRT engines for both ahead of the
first merge. The probe picks the smallest *detector* rather than the
smallest file: the tablet's first run chose the 112 KB eye classifier,
which has no int8 form, and reported the Hexagon as failed for want of
one.
This commit is contained in:
2026-09-19 16:05:08 +02:00
parent 76bc5652d7
commit 7a436e2549
10 changed files with 157 additions and 130 deletions
+6 -5
View File
@@ -12,11 +12,12 @@ thiserror.workspace = true
log.workspace = true
# Inference for the learned keypoint detector, on the same footing as
# `dr-segment`: `ort` is the API, tract is the engine, and both are optional
# so that the geometry — matching, the rotation solve, the projections — is a
# dependency-free crate that tests without a model.
# `dr-segment`: `ort` is the API, `dr-inference-engine` decides what runs
# it (docs/inference.md), and both are optional so that the geometry —
# matching, the rotation solve, the projections — is a dependency-free crate
# that tests without a model.
ort = { workspace = true, optional = true }
ort-tract = { workspace = true, optional = true }
dr-inference-engine = { workspace = true, optional = true }
ndarray = { workspace = true, optional = true }
[dev-dependencies]
@@ -30,7 +31,7 @@ default = ["xfeat", "embedded-model"]
# The XFeat detector (FR-MRG-8). Off, the crate has no model and no runtime,
# and `Detector` has no implementation — a build that only wants the geometry.
xfeat = ["dep:ort", "dep:ort-tract", "dep:ndarray"]
xfeat = ["dep:ort", "dep:dr-inference-engine", "dep:ndarray"]
# Compile the weights into the binary, for the same reason `dr-segment` does:
# Android hands the app no path to read a model from (ARCH §6.9).
+10
View File
@@ -63,3 +63,13 @@ pub enum PanoError {
#[error("inference: {0}")]
Inference(#[source] ort::Error),
}
#[cfg(feature = "xfeat")]
impl From<dr_inference_engine::Error> for PanoError {
fn from(e: dr_inference_engine::Error) -> Self {
match e {
dr_inference_engine::Error::Inference(e) => PanoError::Inference(e),
dr_inference_engine::Error::Io(e) => PanoError::ModelRead(e),
}
}
}
+22 -28
View File
@@ -2,10 +2,11 @@
//! The XFeat detector — the network under tract, and the decoder after it.
//!
//! Apache-2.0 weights (`models/LICENCE.md`), exported at a fixed shape by
//! `tools/export-xfeat.sh` and loaded through the same `ort`-over-tract
//! backend `dr-segment` and `dr-face` use, so this adds no runtime and no C
//! to the tree. ~300 ms per frame on the reference desktop, ~400 ms on the
//! tablet (S15.2, S15.4).
//! `tools/export-xfeat.sh` and loaded through the same `dr-inference-engine`
//! `dr-segment` and `dr-face` use, so this adds no runtime and no C to the
//! tree; what runs it is the device's business (docs/inference.md). ~300 ms
//! per frame on tract on the reference desktop, ~400 ms on the tablet
//! (S15.2, S15.4).
use crate::features::{decode_xfeat, DecodeOptions, Features, XFeatMaps, DESCRIPTOR_LEN};
use crate::image::Gray;
@@ -30,11 +31,18 @@ const EMBEDDED_PORTRAIT: &[u8] = include_bytes!("../../../models/keypoints/xfeat
/// A loaded detector: the network at both shapes.
pub struct XFeat {
landscape: ort::session::Session,
portrait: ort::session::Session,
landscape: dr_inference_engine::Model,
portrait: dr_inference_engine::Model,
pub options: DecodeOptions,
}
/// The bytes of both exports compiled into the binary, for whoever compiles
/// engines ahead of the first request (docs/inference.md §6).
#[cfg(feature = "embedded-model")]
pub fn embedded_model_bytes() -> [&'static [u8]; 2] {
[EMBEDDED_LANDSCAPE, EMBEDDED_PORTRAIT]
}
impl XFeat {
/// The weights compiled into the binary.
#[cfg(feature = "embedded-model")]
@@ -53,16 +61,10 @@ impl XFeat {
}
pub fn from_bytes(landscape: &[u8], portrait: &[u8]) -> Result<Self, PanoError> {
install_backend();
let session = |bytes: &[u8]| {
ort::session::Session::builder()
.map_err(PanoError::Inference)?
.commit_from_memory(bytes)
.map_err(PanoError::Inference)
};
use dr_inference_engine::{Form, Role};
Ok(XFeat {
landscape: session(landscape)?,
portrait: session(portrait)?,
landscape: dr_inference_engine::open(Role::Keypoints, Form::F32, landscape)?,
portrait: dr_inference_engine::open(Role::Keypoints, Form::F32, portrait)?,
options: DecodeOptions::default(),
})
}
@@ -75,11 +77,13 @@ impl XFeat {
/// so a caller that already scaled a frame to a proxy maps them on with
/// the scale it used and nothing else.
pub fn detect(&mut self, image: &Gray) -> Result<Features, PanoError> {
let ((in_w, in_h), session) = if image.height > image.width {
(INPUT_PORTRAIT, &mut self.portrait)
let ((in_w, in_h), model) = if image.height > image.width {
(INPUT_PORTRAIT, &self.portrait)
} else {
(INPUT_LANDSCAPE, &mut self.landscape)
(INPUT_LANDSCAPE, &self.landscape)
};
let acquired = model.acquire()?;
let mut session = acquired.lock();
let (fitted, scale) = image.fitted(in_w, in_h);
let padded = fitted.padded(in_w, in_h);
@@ -145,13 +149,3 @@ impl XFeat {
Ok(features)
}
}
fn install_backend() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
// False if another crate installed it first, which is fine: there is
// one backend compiled in for it to have chosen.
let _ = ort::set_api(ort_tract::api());
});
}