1extern crate machine_vision_formats as formats;
20
21use std::time::{Duration, Instant};
22
23use ci2::{
24 AcquisitionMode, AutoMode, DynamicFrameWithInfo, HostTimingInfo, TriggerMode, TriggerSelector,
25};
26use flydra_mvg::FlydraMultiCameraSystem;
27use formats::PixFmt;
28use strand_dynamic_frame::DynamicFrameOwned;
29
30use braid_sim::Scenario;
31use braid_sim::scenario::BlobParams;
32use braid_sim::world::World;
33
34pub const SIM_SPEC_ENV: &str = "STRAND_CAM_SIM_SPEC";
36
37const SUPPORTED_PIXEL_FORMATS: [PixFmt; 7] = [
43 PixFmt::Mono8,
44 PixFmt::RGB8,
45 PixFmt::YUV422,
46 PixFmt::BayerRG8,
47 PixFmt::BayerGR8,
48 PixFmt::BayerGB8,
49 PixFmt::BayerBG8,
50];
51
52fn load_scenario() -> ci2::Result<Scenario> {
54 let path = std::env::var_os(SIM_SPEC_ENV).ok_or_else(|| {
55 ci2::Error::from(format!(
56 "the sim camera backend requires the {SIM_SPEC_ENV} environment variable \
57 to point at a sim.toml scenario file"
58 ))
59 })?;
60 let text = std::fs::read_to_string(&path)
61 .map_err(|e| ci2::Error::from(format!("reading {SIM_SPEC_ENV} ({path:?}): {e}")))?;
62 Scenario::from_toml_str(&text)
63 .map_err(|e| ci2::Error::from(format!("parsing {SIM_SPEC_ENV} ({path:?}): {e}")))
64}
65
66pub struct WrappedModule {}
67
68pub fn new_module() -> ci2::Result<WrappedModule> {
69 Ok(WrappedModule {})
70}
71
72pub struct SimTerminateGuard {}
75
76pub fn make_singleton_guard(
77 _module: &dyn ci2::CameraModule<CameraType = WrappedCamera, Guard = SimTerminateGuard>,
78) -> ci2::Result<SimTerminateGuard> {
79 Ok(SimTerminateGuard {})
80}
81
82impl<'a> ci2::CameraModule for &'a WrappedModule {
83 type CameraType = WrappedCamera;
84 type Guard = SimTerminateGuard;
85
86 fn name(self: &&'a WrappedModule) -> &'static str {
87 "sim"
88 }
89
90 fn camera_infos(self: &&'a WrappedModule) -> ci2::Result<Vec<Box<dyn ci2::CameraInfo>>> {
91 let scenario = load_scenario()?;
92 let infos = (0..scenario.cameras.count)
93 .map(|k| {
94 let ci: Box<dyn ci2::CameraInfo> = Box::new(SimCameraInfo::new(k));
95 ci
96 })
97 .collect();
98 Ok(infos)
99 }
100
101 fn camera(self: &mut &'a WrappedModule, name: &str) -> ci2::Result<Self::CameraType> {
102 WrappedCamera::new(name)
103 }
104
105 fn settings_file_extension(&self) -> &str {
106 "toml"
108 }
109}
110
111#[derive(Debug, Clone)]
112struct SimCameraInfo {
113 name: String,
114 serial: String,
115}
116
117impl SimCameraInfo {
118 fn new(k: usize) -> Self {
119 Self {
120 name: Scenario::camera_name(k),
121 serial: format!("{k}"),
122 }
123 }
124}
125
126impl ci2::CameraInfo for SimCameraInfo {
127 fn name(&self) -> &str {
128 &self.name
129 }
130 fn serial(&self) -> &str {
131 &self.serial
132 }
133 fn model(&self) -> &str {
134 "sim"
135 }
136 fn vendor(&self) -> &str {
137 "braid-sim"
138 }
139}
140
141pub struct WrappedCamera {
142 info: SimCameraInfo,
143 cam_name: String,
145 cam_index: usize,
147 seed: u64,
149 timing: braid_sim::scenario::TimingModel,
151 system: FlydraMultiCameraSystem<f64>,
153 world: World,
155 image_width: usize,
156 image_height: usize,
157 blob: BlobParams,
158 reported_fps: Option<f64>,
162 bg_warmup_frames: u32,
165 fps: f64,
170 frame_rate_enabled: bool,
173 pixel_format: PixFmt,
177 start: Option<Instant>,
179 start_datetime: Option<chrono::DateTime<chrono::Utc>>,
182 next_fno: usize,
184}
185
186fn _test_camera_is_send() {
187 fn implements<T: Send>() {}
189 implements::<WrappedCamera>();
190}
191
192impl WrappedCamera {
193 fn new(name: &str) -> ci2::Result<Self> {
194 let scenario = load_scenario()?;
195
196 let valid = (0..scenario.cameras.count).any(|k| Scenario::camera_name(k) == name);
198 if !valid {
199 return Err(ci2::Error::from(format!(
200 "unknown sim camera \"{name}\"; expected one of simcam0..simcam{}",
201 scenario.cameras.count.saturating_sub(1)
202 )));
203 }
204
205 let system = braid_sim::calibration::build_calibration(&scenario)
206 .map_err(|e| ci2::Error::from(format!("building sim calibration: {e}")))?;
207
208 let cam_index = Scenario::camera_index(name).ok_or_else(|| {
209 ci2::Error::from(format!("cannot parse camera index from \"{name}\""))
210 })?;
211 let info = SimCameraInfo {
212 name: name.to_string(),
213 serial: name.trim_start_matches("simcam").to_string(),
214 };
215
216 Ok(Self {
217 info,
218 cam_name: name.to_string(),
219 cam_index,
220 seed: scenario.seed,
221 timing: scenario.timing.clone(),
222 image_width: scenario.cameras.image_width,
223 image_height: scenario.cameras.image_height,
224 blob: scenario.blob.clone(),
225 reported_fps: scenario.reported_fps,
226 bg_warmup_frames: scenario.bg_warmup_frames,
227 fps: scenario.fps,
228 frame_rate_enabled: true,
229 pixel_format: PixFmt::Mono8,
230 start: None,
231 start_datetime: None,
232 next_fno: 0,
233 world: World::new(scenario),
234 system,
235 })
236 }
237
238 fn frame_period(&self) -> Duration {
240 Duration::from_secs_f64(1.0 / self.fps)
241 }
242
243 fn blobs_for_frame(&self, fno: usize) -> Vec<(f64, f64)> {
247 if (fno as u32) < self.bg_warmup_frames {
248 return Vec::new();
249 }
250 let t = (fno as u32 - self.bg_warmup_frames) as f64 / self.fps;
252 let obs = &self.world.scenario().observation;
253 let mut blobs: Vec<(f64, f64)> = self
254 .world
255 .state_at(t)
256 .iter()
257 .filter(|insect| !obs.is_suppressed(self.seed, self.cam_index, fno, insect.id))
258 .filter_map(|insect| {
259 braid_sim::projection::project_pixel(
260 &self.system,
261 &self.cam_name,
262 self.image_width,
263 self.image_height,
264 &insect.pos,
265 )
266 .map(|(x, y)| obs.jitter_pixel(self.seed, self.cam_index, fno, insect.id, x, y))
267 })
268 .collect();
269 blobs.extend(obs.clutter(
271 self.seed,
272 self.cam_index,
273 fno,
274 self.image_width,
275 self.image_height,
276 ));
277 blobs
278 }
279}
280
281impl ci2::CameraInfo for WrappedCamera {
282 fn name(&self) -> &str {
283 &self.info.name
284 }
285 fn serial(&self) -> &str {
286 &self.info.serial
287 }
288 fn model(&self) -> &str {
289 "sim"
290 }
291 fn vendor(&self) -> &str {
292 "braid-sim"
293 }
294}
295
296impl ci2::Camera for WrappedCamera {
297 fn command_execute(&self, _name: &str, _verify: bool) -> ci2::Result<()> {
299 Err(ci2::Error::FeatureNotPresent())
300 }
301 fn feature_bool(&self, _name: &str) -> ci2::Result<bool> {
302 Err(ci2::Error::FeatureNotPresent())
303 }
304 fn feature_bool_set(&self, _name: &str, _value: bool) -> ci2::Result<()> {
305 Err(ci2::Error::FeatureNotPresent())
306 }
307 fn feature_enum(&self, _name: &str) -> ci2::Result<String> {
308 Err(ci2::Error::FeatureNotPresent())
309 }
310 fn feature_enum_set(&self, _name: &str, _value: &str) -> ci2::Result<()> {
311 Err(ci2::Error::FeatureNotPresent())
312 }
313 fn feature_float(&self, _name: &str) -> ci2::Result<f64> {
314 Err(ci2::Error::FeatureNotPresent())
315 }
316 fn feature_float_set(&self, _name: &str, _value: f64) -> ci2::Result<()> {
317 Err(ci2::Error::FeatureNotPresent())
318 }
319 fn feature_int(&self, _name: &str) -> ci2::Result<i64> {
320 Err(ci2::Error::FeatureNotPresent())
321 }
322 fn feature_int_set(&self, _name: &str, _value: i64) -> ci2::Result<()> {
323 Err(ci2::Error::FeatureNotPresent())
324 }
325
326 fn node_map_load(&self, _settings: &str) -> ci2::Result<()> {
327 Err(ci2::Error::FeatureNotPresent())
328 }
329 fn node_map_save(&self) -> ci2::Result<String> {
330 Err(ci2::Error::FeatureNotPresent())
331 }
332
333 fn width(&self) -> ci2::Result<u32> {
334 Ok(self.image_width as u32)
335 }
336 fn height(&self) -> ci2::Result<u32> {
337 Ok(self.image_height as u32)
338 }
339
340 fn pixel_format(&self) -> ci2::Result<PixFmt> {
341 Ok(self.pixel_format)
342 }
343 fn possible_pixel_formats(&self) -> ci2::Result<Vec<PixFmt>> {
344 Ok(SUPPORTED_PIXEL_FORMATS.to_vec())
345 }
346 fn set_pixel_format(&mut self, pixel_format: PixFmt) -> ci2::Result<()> {
347 if SUPPORTED_PIXEL_FORMATS.contains(&pixel_format) {
348 self.pixel_format = pixel_format;
349 Ok(())
350 } else {
351 Err(ci2::Error::from(format!(
352 "sim backend does not support pixel format {pixel_format}; \
353 supported: {SUPPORTED_PIXEL_FORMATS:?}"
354 )))
355 }
356 }
357
358 fn exposure_time(&self) -> ci2::Result<f64> {
359 Err(ci2::Error::FeatureNotPresent())
360 }
361 fn exposure_time_range(&self) -> ci2::Result<(f64, f64)> {
362 Err(ci2::Error::FeatureNotPresent())
363 }
364 fn set_exposure_time(&mut self, _: f64) -> ci2::Result<()> {
365 Err(ci2::Error::FeatureNotPresent())
366 }
367 fn exposure_auto(&self) -> ci2::Result<AutoMode> {
368 Err(ci2::Error::FeatureNotPresent())
369 }
370 fn set_exposure_auto(&mut self, _: AutoMode) -> ci2::Result<()> {
371 Err(ci2::Error::FeatureNotPresent())
372 }
373
374 fn gain(&self) -> ci2::Result<f64> {
375 Err(ci2::Error::FeatureNotPresent())
376 }
377 fn gain_range(&self) -> ci2::Result<(f64, f64)> {
378 Err(ci2::Error::FeatureNotPresent())
379 }
380 fn set_gain(&mut self, _: f64) -> ci2::Result<()> {
381 Err(ci2::Error::FeatureNotPresent())
382 }
383 fn gain_auto(&self) -> ci2::Result<AutoMode> {
384 Err(ci2::Error::FeatureNotPresent())
385 }
386 fn set_gain_auto(&mut self, _: AutoMode) -> ci2::Result<()> {
387 Err(ci2::Error::FeatureNotPresent())
388 }
389
390 fn trigger_mode(&self) -> ci2::Result<TriggerMode> {
391 Err(ci2::Error::FeatureNotPresent())
392 }
393 fn set_trigger_mode(&mut self, _: TriggerMode) -> ci2::Result<()> {
394 Err(ci2::Error::FeatureNotPresent())
395 }
396
397 fn acquisition_frame_rate_enable(&self) -> ci2::Result<bool> {
401 Ok(self.frame_rate_enabled)
402 }
403 fn set_acquisition_frame_rate_enable(&mut self, value: bool) -> ci2::Result<()> {
404 self.frame_rate_enabled = value;
405 Ok(())
406 }
407 fn acquisition_frame_rate(&self) -> ci2::Result<f64> {
408 Ok(self.fps)
409 }
410 fn acquisition_frame_rate_range(&self) -> ci2::Result<(f64, f64)> {
411 Ok((1.0, 1000.0))
412 }
413 fn set_acquisition_frame_rate(&mut self, value: f64) -> ci2::Result<()> {
414 if value <= 0.0 {
415 return Err(ci2::Error::from("frame rate must be positive"));
416 }
417 self.fps = value;
418 Ok(())
419 }
420
421 fn trigger_selector(&self) -> ci2::Result<TriggerSelector> {
422 Err(ci2::Error::FeatureNotPresent())
423 }
424 fn set_trigger_selector(&mut self, _: TriggerSelector) -> ci2::Result<()> {
425 Err(ci2::Error::FeatureNotPresent())
426 }
427
428 fn acquisition_mode(&self) -> ci2::Result<AcquisitionMode> {
429 Err(ci2::Error::FeatureNotPresent())
430 }
431 fn set_acquisition_mode(&mut self, _: AcquisitionMode) -> ci2::Result<()> {
432 Err(ci2::Error::FeatureNotPresent())
433 }
434
435 fn acquisition_start(&mut self) -> ci2::Result<()> {
436 self.next_fno = 0;
437 self.start = Some(Instant::now());
438 self.start_datetime = Some(chrono::Utc::now());
439 Ok(())
440 }
441 fn acquisition_stop(&mut self) -> ci2::Result<()> {
442 self.start = None;
443 Ok(())
444 }
445
446 fn next_frame(&mut self) -> ci2::Result<DynamicFrameWithInfo> {
447 let fno = self.next_fno;
448 self.next_fno += 1;
449
450 if let (Some(start), true) = (self.start, self.frame_rate_enabled) {
456 let extra = self.timing.extra_delay_sec(self.seed, self.cam_index, fno);
457 let target = start + self.frame_period() * fno as u32 + Duration::from_secs_f64(extra);
458 let now = Instant::now();
459 if target > now {
460 std::thread::sleep(target - now);
461 }
462 }
463
464 let blobs = self.blobs_for_frame(fno);
465 let bg = self.blob.background;
470 let peak = self.blob.peak as f64;
471 let sigma = self.blob.sigma;
472 let (w, h) = (self.image_width, self.image_height);
473 let (buf, stride) = match self.pixel_format {
474 PixFmt::RGB8 => (
475 braid_sim::render::render_rgb8(w, h, bg, &blobs, peak, sigma),
476 w * 3,
477 ),
478 PixFmt::YUV422 => (
479 braid_sim::render::render_yuv422_uyvy(w, h, bg, &blobs, peak, sigma),
480 w * 2,
481 ),
482 _ => (
485 braid_sim::render::render_mono8(w, h, bg, &blobs, peak, sigma),
486 w,
487 ),
488 };
489
490 let image = DynamicFrameOwned::from_buf(
491 self.image_width as u32,
492 self.image_height as u32,
493 stride,
494 buf,
495 self.pixel_format,
496 )
497 .ok_or_else(|| ci2::Error::SingleFrameError("sim frame had invalid layout".into()))?;
498
499 let datetime = match (self.reported_fps, self.start_datetime) {
505 (Some(rfps), Some(base)) if rfps > 0.0 => {
506 base + chrono::Duration::nanoseconds((fno as f64 / rfps * 1e9) as i64)
507 }
508 _ => chrono::Utc::now(),
509 };
510
511 let device_timestamp = (fno as f64 / self.fps * 1e9) as u64;
516 let backend_data: Option<Box<dyn ci2::BackendData>> =
517 Some(Box::new(ci2_pylon_types::PylonExtra {
518 block_id: fno as u64,
519 device_timestamp,
520 }));
521
522 Ok(DynamicFrameWithInfo {
523 image: std::sync::Arc::new(image),
524 host_timing: HostTimingInfo { fno, datetime },
525 backend_data,
526 })
527 }
528}