use std::collections::VecDeque;
use std::time::{Duration, Instant};
use orbbec::pipeline::{Config, FrameType, Frameset, Pipeline, StreamType};
use orbbec::{AlignFilter, BoundingBox, ColorFrame, Context, DepthFrame};
const HUE_MIN: f32 = 75.0;
const HUE_MAX: f32 = 145.0;
const SAT_MIN: f32 = 0.30;
const VAL_MIN: f32 = 0.20;
const STEP: u32 = 2;
const MIN_COMPONENT: u32 = 200;
const MIN_SUPPORT: f32 = 0.2;
const WINDOW_LEN: usize = 7;
const WINDOW_MIN_VALID: usize = 4;
const WINDOW_MIN_CONSISTENT: usize = 3;
const WINDOW_TOL_M: f32 = 0.10;
const SMOOTHING: f32 = 0.3;
fn main() {
let mut hue_min = HUE_MIN;
let mut hue_max = HUE_MAX;
let mut min_component = MIN_COMPONENT;
let args: Vec<String> = std::env::args().collect();
for w in args.windows(2) {
if w[0] == "--hue-min" {
hue_min = w[1].parse().expect("bad --hue-min");
}
if w[0] == "--hue-max" {
hue_max = w[1].parse().expect("bad --hue-max");
}
if w[0] == "--min-size" {
min_component = w[1].parse().expect("bad --min-size");
}
}
let ctx = Context::new().expect("failed to create context");
let devices = ctx.query_devices().expect("failed to enumerate devices");
assert!(!devices.is_empty(), "no Orbbec device connected");
let pipeline0 = Pipeline::new().expect("failed to create pipeline");
let color_profiles = pipeline0
.stream_profiles(StreamType::Color)
.expect("color profiles");
let color_profile = [22, 31, 23, 25, 0]
.iter()
.find_map(|fmt| {
color_profiles
.match_video(Some(1280), Some(720), Some(*fmt), Some(30))
.ok()
.flatten()
})
.expect("no uncompressed 1280x720@30 color profile");
let mut config = Config::new().expect("failed to create config");
config
.enable_stream_with_profile(&color_profile)
.expect("failed to enable color profile");
config
.enable_stream(StreamType::Depth)
.expect("failed to enable depth stream");
let mut pipeline = Pipeline::new().expect("failed to create pipeline");
pipeline
.enable_frame_sync()
.expect("failed to enable frame sync");
let frames = pipeline
.start_capture(Some(&config))
.expect("failed to start pipeline");
let align = AlignFilter::new().expect("failed to create align filter");
let mut smoother = Smoother::new();
println!(
"tracking hue {hue_min:.0}..{hue_max:.0} degrees on {}x{} RGB",
color_profile.width(),
color_profile.height()
);
println!("{:>9} {:>9} {:>9} {:>14}", "dist(cm)", "avg(cm)", "support", "bbox");
let mut last_render = Instant::now();
loop {
match frames.recv_timeout(Duration::from_millis(1000)) {
Ok(frameset) => {
if frameset.frame(FrameType::Color).is_none()
|| frameset.frame(FrameType::Depth).is_none()
{
continue;
}
align
.set_align_target(&frameset, StreamType::Color)
.expect("set align target");
let aligned = match align.process(&frameset) {
Ok(Some(f)) => f,
_ => continue,
};
let aligned = Frameset::from_frame(aligned);
let Some(depth) = aligned.frame(FrameType::Depth) else {
continue;
};
let Some(depth) = DepthFrame::try_new(depth) else {
continue;
};
let Some(color) = frameset.frame(FrameType::Color) else {
continue;
};
let Some(color) = ColorFrame::try_new(color) else {
eprintln!("unexpected color format, wanted uncompressed");
break;
};
let bbox = largest_blob(&color, hue_min, hue_max, min_component);
let Some(bbox) = bbox else {
smoother.update(None);
if last_render.elapsed() >= Duration::from_millis(200) {
println!("{:>9} {:>9} {:>9} {:>14}", "no-block", "-", "-", "-");
last_render = Instant::now();
}
continue;
};
let (m, support) = depth
.box_distance(&bbox)
.filter(|(_, s)| *s >= MIN_SUPPORT)
.unwrap_or((0.0, 0.0));
let confirmed = smoother.update((m > 0.0).then_some(m));
let dist_cm = confirmed.map(|m| m * 100.0);
let avg_cm = smoother.avg().map(|m| m * 100.0);
if last_render.elapsed() >= Duration::from_millis(200) {
match (dist_cm, avg_cm) {
(Some(d), Some(a)) => println!(
"{d:>9.1} {a:>9.1} {:>8.1}% {:>14}",
support * 100.0,
format!("{}x{}@{},{}", bbox.w, bbox.h, bbox.x, bbox.y)
),
_ => println!("{:>9} {:>9} {:>9} {:>14}", "o-r", "-", "-", "-"),
}
last_render = Instant::now();
}
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
eprintln!("timed out waiting for frameset");
break;
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
pipeline.stop().expect("failed to stop pipeline");
}
fn largest_blob(
color: &ColorFrame,
hue_min: f32,
hue_max: f32,
min_component: u32,
) -> Option<BoundingBox> {
let w = color.width();
let h = color.height();
let gw = w / STEP;
let gh = h / STEP;
if gw == 0 || gh == 0 {
return None;
}
let mut visited = vec![false; (gw * gh) as usize];
let mut best: Option<(u32, BoundingBox)> = None;
for gy in 0..gh {
for gx in 0..gw {
let i = (gy * gw + gx) as usize;
if visited[i] {
continue;
}
visited[i] = true;
if !is_target(color, gx * STEP, gy * STEP, hue_min, hue_max) {
continue;
}
let mut stack = vec![(gx, gy)];
let (mut minx, mut maxx, mut miny, mut maxy) = (gx, gx, gy, gy);
let mut area = 0u32;
while let Some((cx, cy)) = stack.pop() {
area += 1;
minx = minx.min(cx);
maxx = maxx.max(cx);
miny = miny.min(cy);
maxy = maxy.max(cy);
for (nx, ny) in [(cx + 1, cy), (cx.wrapping_sub(1), cy), (cx, cy + 1), (cx, cy.wrapping_sub(1))]
{
if nx >= gw || ny >= gh {
continue;
}
let ni = (ny * gw + nx) as usize;
if visited[ni] {
continue;
}
visited[ni] = true;
if is_target(color, nx * STEP, ny * STEP, hue_min, hue_max) {
stack.push((nx, ny));
}
}
}
if area >= min_component {
let bbox = BoundingBox::new(
minx * STEP,
miny * STEP,
(maxx - minx + 1) * STEP,
(maxy - miny + 1) * STEP,
);
if best.as_ref().is_none_or(|(a, _)| area > *a) {
best = Some((area, bbox));
}
}
}
}
best.map(|(_, b)| b)
}
fn is_target(color: &ColorFrame, x: u32, y: u32, hue_min: f32, hue_max: f32) -> bool {
let Some((r, g, b)) = color.pixel_rgb(x, y) else {
return false;
};
let (h, s, v) = rgb_to_hsv(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
h >= hue_min && h <= hue_max && s >= SAT_MIN && v >= VAL_MIN
}
fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let d = max - min;
let h = if d <= 0.0 {
0.0
} else if max == r {
60.0 * (((g - b) / d) % 6.0)
} else if max == g {
60.0 * ((b - r) / d + 2.0)
} else {
60.0 * ((r - g) / d + 4.0)
};
let h = if h < 0.0 { h + 360.0 } else { h };
let s = if max <= 0.0 { 0.0 } else { d / max };
(h, s, max)
}
struct Smoother {
window: VecDeque<Option<f32>>,
avg: Option<f32>,
}
impl Smoother {
fn new() -> Self {
Self {
window: VecDeque::with_capacity(WINDOW_LEN),
avg: None,
}
}
fn update(&mut self, m: Option<f32>) -> Option<f32> {
self.window.push_back(m);
while self.window.len() > WINDOW_LEN {
self.window.pop_front();
}
let valid: Vec<f32> = self.window.iter().filter_map(|v| *v).collect();
let confirmed = if valid.len() >= WINDOW_MIN_VALID {
let mut sorted = valid.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = sorted[sorted.len() / 2];
let consistent = valid
.iter()
.filter(|&&v| (v - median).abs() <= WINDOW_TOL_M)
.count();
(consistent >= WINDOW_MIN_CONSISTENT).then_some(median)
} else {
None
};
self.avg = Some(match (self.avg, confirmed) {
(Some(a), Some(m)) => a + SMOOTHING * (m - a),
(_, Some(m)) => m,
_ => return None,
});
confirmed
}
fn avg(&self) -> Option<f32> {
self.avg
}
}