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 ---------
|
; --- A thermal camera (InfraTec TarisIR mini), beside the colour one ---------
|
||||||
; It takes its picture at the same waypoints and intervals, right after the
|
; 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);
|
; 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.
|
; 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]
|
; 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()`
|
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
|
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
|
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
|
`max_frame_age_ms` and otherwise says no at once. Each frame keeps the time it
|
||||||
takes the colour frame's timestamp. The colour camera failing stops the run as
|
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
|
before; the thermal one failing never does (`offline()` says why, Health
|
||||||
`cams_offline` names it). Thermal frames are `Mono16` counts: the pipeline
|
`cams_offline` names it). Thermal frames are `Mono16` counts: the pipeline
|
||||||
rotates them, takes `tmax`/`tmed`/hot spot on the rotated image
|
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
|
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
|
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
|
is the thermal one asked for its newest frame. Each file and CamEvent has the
|
||||||
frame's timestamp, so `RGB/<t>.jxl` and `TIR/<t>.jxl` are a pair (and so are
|
time its own frame arrived: the two of one waypoint share `hdg`/`pit` and are at
|
||||||
their CamEvents, see [mqtt-api.md](mqtt-api.md#thermal-camera-cam-tir)).
|
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
|
- **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
|
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) |
|
| `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 |
|
| `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
|
- **Its own `time`.** The thermal camera is asked right after the colour one, at
|
||||||
the colour one, at the same waypoint, and its event and file take the colour
|
the same waypoint, and hands over the newest frame it has. `time` (and the file
|
||||||
frame's timestamp: `RGB/<time>.jxl` and `TIR/<time>.jxl` are one pair. A
|
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.
|
picture the thermal camera had no fresh frame for has no `TIR` event.
|
||||||
- **The file holds counts**, 16-bit single channel, lossless:
|
- **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
|
`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
|
// frame for the same waypoint. The others are best effort - a follower with
|
||||||
// no fresh frame costs that picture its thermal image, nothing more.
|
// 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
|
// Every frame keeps its own time, when it reached the host: the thermal frame
|
||||||
// RGB/<t>.jxl and TIR/<t>.jxl with the same <t> in both CamEvents.
|
// 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
|
// 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`,
|
// 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> counts_; // cameras each child is configured with
|
||||||
std::vector<int> offsets_; // global index of each child's camera 0
|
std::vector<int> offsets_; // global index of each child's camera 0
|
||||||
FrameCallback callback_;
|
FrameCallback callback_;
|
||||||
bool in_trigger_ = false;
|
|
||||||
long long shot_ts_ = 0; // the leader's frame time, during a trigger
|
|
||||||
int follower_wait_ms_;
|
int follower_wait_ms_;
|
||||||
long long waiting_since_ = 0; // steady ms a picture began waiting for a follower
|
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";
|
<< " ms; taking the picture without it";
|
||||||
}
|
}
|
||||||
waiting_since_ = 0;
|
waiting_since_ = 0;
|
||||||
in_trigger_ = true;
|
|
||||||
shot_ts_ = 0;
|
|
||||||
const bool taken = children_[0]->trigger();
|
const bool taken = children_[0]->trigger();
|
||||||
if (taken)
|
if (taken)
|
||||||
for (size_t k = 1; k < children_.size(); ++k) children_[k]->trigger();
|
for (size_t k = 1; k < children_.size(); ++k) children_[k]->trigger();
|
||||||
in_trigger_ = false;
|
|
||||||
return taken;
|
return taken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +97,6 @@ void CompositeCameraSource::setFrameCallback(FrameCallback cb) {
|
||||||
// The leader's frames pass through untouched (they can be 24 MB); its
|
// 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.
|
// camera 0 is global camera 0, so not even the index changes.
|
||||||
children_[0]->setFrameCallback([this](const Frame& f) {
|
children_[0]->setFrameCallback([this](const Frame& f) {
|
||||||
if (in_trigger_ && shot_ts_ == 0) shot_ts_ = f.timestamp_ms;
|
|
||||||
if (callback_) callback_(f);
|
if (callback_) callback_(f);
|
||||||
});
|
});
|
||||||
for (size_t k = 1; k < children_.size(); ++k) {
|
for (size_t k = 1; k < children_.size(); ++k) {
|
||||||
|
|
@ -109,7 +105,6 @@ void CompositeCameraSource::setFrameCallback(FrameCallback cb) {
|
||||||
if (!callback_) return;
|
if (!callback_) return;
|
||||||
Frame g = f;
|
Frame g = f;
|
||||||
g.cam_id = f.cam_id + offset;
|
g.cam_id = f.cam_id + offset;
|
||||||
if (in_trigger_ && shot_ts_ != 0) g.timestamp_ms = shot_ts_;
|
|
||||||
callback_(g);
|
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
|
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;
|
Rig r;
|
||||||
REQUIRE(r.src->trigger());
|
REQUIRE(r.src->trigger());
|
||||||
REQUIRE(r.frames.size() == 2);
|
REQUIRE(r.frames.size() == 2);
|
||||||
CHECK(r.frames[0].timestamp_ms == 1000);
|
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") {
|
TEST_CASE("the followers are asked only once the leader has delivered") {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue