Give the thermal picture the time its own frame arrived
Both frames of a waypoint used to take the colour frame's time, which made the thermal one look taken at a moment it was not: it is the newest frame the camera had, up to max_frame_age_ms old. Each file and CamEvent now has the time its own frame reached the host; the two of one waypoint share hdg/pit and are at most that age apart. The docs say to pair them by that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff89643a4f
commit
fc507f7407
|
|
@ -100,7 +100,8 @@ frame_timeout_ms = 5000
|
|||
|
||||
; --- A thermal camera (InfraTec TarisIR mini), beside the colour one ---------
|
||||
; It takes its picture at the same waypoints and intervals, right after the
|
||||
; colour camera, and both carry the same timestamp: RGB/<t>.jxl and TIR/<t>.jxl.
|
||||
; colour camera. Each file and CamEvent has the time its own frame arrived; the
|
||||
; two of one waypoint share hdg/pit and are at most max_frame_age_ms apart.
|
||||
; The counts are saved 16-bit and always lossless (jxl_distance must stay 0);
|
||||
; T[K] = count x 0.01 under profile 0, x 0.02 under profile 1, emissivity 1.
|
||||
; Its driver, id (the serial) and interface belong in hardware.ini; [Hardware]
|
||||
|
|
|
|||
|
|
@ -140,8 +140,9 @@ With a thermal camera the scheduler and the pipeline still see one
|
|||
the one the keep rule reads) and the thermal one after them. Its `trigger()`
|
||||
asks the colour source first; only if that delivered does it ask the thermal
|
||||
source, which hands over its newest frame if it is no older than
|
||||
`max_frame_age_ms` and otherwise says no at once. Every frame of one trigger
|
||||
takes the colour frame's timestamp. The colour camera failing stops the run as
|
||||
`max_frame_age_ms` and otherwise says no at once. Each frame keeps the time it
|
||||
reached the host, so the two of one waypoint differ by up to that age. The
|
||||
colour camera failing stops the run as
|
||||
before; the thermal one failing never does (`offline()` says why, Health
|
||||
`cams_offline` names it). Thermal frames are `Mono16` counts: the pipeline
|
||||
rotates them, takes `tmax`/`tmed`/hot spot on the rotated image
|
||||
|
|
|
|||
|
|
@ -1065,9 +1065,9 @@ firmware `ST` lines (per-axis state + encoder counts), parsed by
|
|||
|
||||
A TarisIR mini beside the colour camera takes its picture at the same waypoints
|
||||
and intervals: the colour camera is asked first, and only once it has delivered
|
||||
is the thermal one asked for its newest frame. Both frames carry the colour
|
||||
frame's timestamp, so `RGB/<t>.jxl` and `TIR/<t>.jxl` are a pair (and so are
|
||||
their CamEvents, see [mqtt-api.md](mqtt-api.md#thermal-camera-cam-tir)).
|
||||
is the thermal one asked for its newest frame. Each file and CamEvent has the
|
||||
time its own frame arrived: the two of one waypoint share `hdg`/`pit` and are at
|
||||
most `max_frame_age_ms` apart (see [mqtt-api.md](mqtt-api.md#thermal-camera-cam-tir)).
|
||||
|
||||
- **Best effort.** A thermal camera that is missing, unlicensed or has no fresh
|
||||
frame costs that picture its thermal image, nothing more: the scan goes on, the
|
||||
|
|
|
|||
|
|
@ -153,9 +153,12 @@ their payload is unchanged.
|
|||
| `tmed` | int/null | Median of the image, tenths of a °C (the anchor for a display window) |
|
||||
| `hotx`, `hoty` | number/null | Where the hottest pixel is: its centre, 0..1 across and down the image **as stored** (after `rotation_deg`), as for detection boxes |
|
||||
|
||||
- **Same `time` as the colour picture.** The thermal camera is asked right after
|
||||
the colour one, at the same waypoint, and its event and file take the colour
|
||||
frame's timestamp: `RGB/<time>.jxl` and `TIR/<time>.jxl` are one pair. A
|
||||
- **Its own `time`.** The thermal camera is asked right after the colour one, at
|
||||
the same waypoint, and hands over the newest frame it has. `time` (and the file
|
||||
name) is when that frame reached the tower computer, so it is up to
|
||||
`max_frame_age_ms` (400) off the colour picture's, earlier or later. The `RGB`
|
||||
and `TIR` event of one waypoint share `hdg` and `pit` (sampled together) and
|
||||
follow each other in the stream; pair them by that, not by an equal `time`. A
|
||||
picture the thermal camera had no fresh frame for has no `TIR` event.
|
||||
- **The file holds counts**, 16-bit single channel, lossless:
|
||||
`T[K] = count × 0.01` under `cal` 0 (`FF (-40-120C)`), `× 0.02` under `cal` 1
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ namespace fgc {
|
|||
// frame for the same waypoint. The others are best effort - a follower with
|
||||
// no fresh frame costs that picture its thermal image, nothing more.
|
||||
//
|
||||
// Every frame of one trigger carries the leader's timestamp, so the pair is
|
||||
// RGB/<t>.jxl and TIR/<t>.jxl with the same <t> in both CamEvents.
|
||||
// Every frame keeps its own time, when it reached the host: the thermal frame
|
||||
// is the newest the camera had, so its time is up to max_frame_age_ms off the
|
||||
// colour one, and that is what the file name and the CamEvent say.
|
||||
//
|
||||
// A follower that is briefly without a fresh frame (frameReady() false: the
|
||||
// thermal camera's NUC) holds the picture back for up to `follower_wait_ms`,
|
||||
|
|
@ -71,8 +72,6 @@ private:
|
|||
std::vector<int> counts_; // cameras each child is configured with
|
||||
std::vector<int> offsets_; // global index of each child's camera 0
|
||||
FrameCallback callback_;
|
||||
bool in_trigger_ = false;
|
||||
long long shot_ts_ = 0; // the leader's frame time, during a trigger
|
||||
int follower_wait_ms_;
|
||||
long long waiting_since_ = 0; // steady ms a picture began waiting for a follower
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,12 +76,9 @@ bool CompositeCameraSource::trigger() {
|
|||
<< " ms; taking the picture without it";
|
||||
}
|
||||
waiting_since_ = 0;
|
||||
in_trigger_ = true;
|
||||
shot_ts_ = 0;
|
||||
const bool taken = children_[0]->trigger();
|
||||
if (taken)
|
||||
for (size_t k = 1; k < children_.size(); ++k) children_[k]->trigger();
|
||||
in_trigger_ = false;
|
||||
return taken;
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +97,6 @@ void CompositeCameraSource::setFrameCallback(FrameCallback cb) {
|
|||
// The leader's frames pass through untouched (they can be 24 MB); its
|
||||
// camera 0 is global camera 0, so not even the index changes.
|
||||
children_[0]->setFrameCallback([this](const Frame& f) {
|
||||
if (in_trigger_ && shot_ts_ == 0) shot_ts_ = f.timestamp_ms;
|
||||
if (callback_) callback_(f);
|
||||
});
|
||||
for (size_t k = 1; k < children_.size(); ++k) {
|
||||
|
|
@ -109,7 +105,6 @@ void CompositeCameraSource::setFrameCallback(FrameCallback cb) {
|
|||
if (!callback_) return;
|
||||
Frame g = f;
|
||||
g.cam_id = f.cam_id + offset;
|
||||
if (in_trigger_ && shot_ts_ != 0) g.timestamp_ms = shot_ts_;
|
||||
callback_(g);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,12 +100,12 @@ TEST_CASE("the composite numbers the cameras leader first, and counts them all")
|
|||
CHECK(r.frames[2].cam_id == 2); // the thermal camera
|
||||
}
|
||||
|
||||
TEST_CASE("one trigger gives every frame the leader's time") {
|
||||
TEST_CASE("every frame keeps the time it arrived, the follower's too") {
|
||||
Rig r;
|
||||
REQUIRE(r.src->trigger());
|
||||
REQUIRE(r.frames.size() == 2);
|
||||
CHECK(r.frames[0].timestamp_ms == 1000);
|
||||
CHECK(r.frames[1].timestamp_ms == 1000); // not its own 5000
|
||||
CHECK(r.frames[1].timestamp_ms == 5000); // its own, not the leader's
|
||||
}
|
||||
|
||||
TEST_CASE("the followers are asked only once the leader has delivered") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue