Skip to content

Commit 241e341

Browse files
authored
Fix timestamp (#877)
* Precise timestamps are now synced with the unix system time, also added more timestamps to the timestamp_us section of the footer.
1 parent eb565c0 commit 241e341

File tree

7 files changed

+141
-41
lines changed

7 files changed

+141
-41
lines changed

Backend/includes/data_struct/time_map.hh

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,76 @@
44
#include <cstdint>
55
#include <vector>
66

7+
/**
8+
* @class FrameTimeMap
9+
*
10+
* @brief Lock-free ring buffer that stores per-frame timing information.
11+
*
12+
* This class is used to associate frame IDs (monotonically increasing counters)
13+
* with their corresponding timestamps. It maintains three parallel rings:
14+
* - Synced timestamp (in Unix µs): host-synchronized frame time
15+
* - Camera timestamp (in µs since device boot, 0 if unavailable)
16+
* - Offset (in µs): applied offset between host and camera clock domains
17+
*
18+
* The constructor takes a capacity that must be a power of two; this allows
19+
* efficient wrapping using a bit mask instead of modulo arithmetic.
20+
*
21+
* Typical usage:
22+
* - The producer thread (camera acquisition) calls write_batch() once per
23+
* batch of frames. This writes N consecutive entries starting at base_id.
24+
* - Consumer threads (recorder, processing pipeline) call lookup_*() with
25+
* a frame_id to retrieve the stored timing information.
26+
*
27+
* Concurrency model:
28+
* - Single producer, multiple readers.
29+
* - Writes and reads use relaxed atomics since there is only one writer,
30+
* and eventual consistency is sufficient for time-stamping.
31+
*/
732
class FrameTimeMap
833
{
934
public:
1035
explicit FrameTimeMap(size_t capacity_pow2 = 1 << 20)
1136
: mask_(capacity_pow2 - 1)
12-
, ring_(capacity_pow2)
37+
, ring_synced_(capacity_pow2)
38+
, ring_cam_(capacity_pow2)
39+
, ring_off_(capacity_pow2)
1340
{
1441
}
1542

16-
// Write exact per-frame timestamps for a batch:
17-
inline void write_batch(uint64_t base_id, uint64_t ts0_us, uint64_t period_us, unsigned count)
43+
inline void write_batch(uint64_t base_id,
44+
uint64_t ts0_synced_us,
45+
uint64_t period_us,
46+
unsigned count,
47+
uint64_t ts0_cam_us,
48+
uint64_t offset_us)
1849
{
1950
for (unsigned i = 0; i < count; ++i)
2051
{
21-
ring_[(base_id + i) & mask_].store(ts0_us + uint64_t(i) * period_us, std::memory_order_relaxed);
52+
const uint64_t id = (base_id + i) & mask_;
53+
const uint64_t synced = ts0_synced_us + uint64_t(i) * period_us;
54+
const uint64_t cam = ts0_cam_us ? (ts0_cam_us + uint64_t(i) * period_us) : 0;
55+
ring_synced_[id].store(synced, std::memory_order_relaxed);
56+
ring_cam_[id].store(cam, std::memory_order_relaxed);
57+
ring_off_[id].store(offset_us, std::memory_order_relaxed);
2258
}
2359
}
2460

25-
// Lookup (relaxed is fine; recorder is a consumer)
26-
inline uint64_t lookup(uint64_t frame_id) const { return ring_[(frame_id)&mask_].load(std::memory_order_relaxed); }
61+
inline uint64_t lookup_synced(uint64_t frame_id) const
62+
{
63+
return ring_synced_[frame_id & mask_].load(std::memory_order_relaxed);
64+
}
65+
inline uint64_t lookup_camera(uint64_t frame_id) const
66+
{
67+
return ring_cam_[frame_id & mask_].load(std::memory_order_relaxed);
68+
}
69+
inline uint64_t lookup_offset(uint64_t frame_id) const
70+
{
71+
return ring_off_[frame_id & mask_].load(std::memory_order_relaxed);
72+
}
2773

2874
private:
2975
size_t mask_;
30-
std::vector<std::atomic<uint64_t>> ring_;
31-
};
76+
std::vector<std::atomic<uint64_t>> ring_synced_;
77+
std::vector<std::atomic<uint64_t>> ring_cam_;
78+
std::vector<std::atomic<uint64_t>> ring_off_;
79+
};

Backend/includes/io/output_file/output_holo_file.hh

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,18 @@ class OutputHoloFile : public OutputFrameFile, public HoloFile
6363
*
6464
* \param first_us Timestamp in microseconds of the first frame
6565
* \param last_us Timestamp in microseconds of the last frame
66+
* \param first_camera_us Camera timestamp in microseconds of the first frame
67+
* \param last_camera_us Camera timestamp in microseconds of the last frame
68+
* \param offset_us Offset in microseconds applied to the camera timestamps to obtain the system timestamps
6669
*/
67-
void set_session_timestamps_us(uint64_t first_us, uint64_t last_us)
70+
void set_session_timestamps_us(
71+
uint64_t first_us, uint64_t last_us, uint64_t first_camera_us, uint64_t last_camera_us, uint64_t offset_us)
6872
{
6973
session_first_ts_us_ = first_us;
7074
session_last_ts_us_ = last_us;
75+
session_first_camera_ts_us_ = first_camera_us;
76+
session_last_camera_ts_us_ = last_camera_us;
77+
session_offset_us_ = offset_us;
7178
has_session_ts_ = true;
7279
}
7380

@@ -92,5 +99,8 @@ class OutputHoloFile : public OutputFrameFile, public HoloFile
9299
bool has_session_ts_ = false;
93100
uint64_t session_first_ts_us_ = 0;
94101
uint64_t session_last_ts_us_ = 0;
102+
uint64_t session_first_camera_ts_us_ = 0;
103+
uint64_t session_last_camera_ts_us_ = 0;
104+
uint64_t session_offset_us_ = 0;
95105
};
96106
} // namespace holovibes::io_files

Backend/sources/io/output_file/output_holo_file.cc

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ void OutputHoloFile::export_compute_settings(int input_fps, size_t contiguous)
126126
{"ExposureTime", exposure_time}};
127127
}
128128

129+
// Get current date and time for the record timestamp
129130
auto now = std::chrono::system_clock::now();
130131
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
131132
std::tm local_tm = *std::localtime(&now_c);
@@ -134,22 +135,32 @@ void OutputHoloFile::export_compute_settings(int input_fps, size_t contiguous)
134135
ss << std::put_time(&local_tm, "%Y-%m-%d %H:%M:%S");
135136
std::string record_timestamp = ss.str();
136137

138+
// Precise timestamps (us)
137139
uint64_t first_ts_us = has_session_ts_ ? session_first_ts_us_ : 0;
138140
uint64_t last_ts_us = has_session_ts_ ? session_last_ts_us_ : 0;
139141
uint64_t duration_us = (has_session_ts_ && last_ts_us >= first_ts_us) ? (last_ts_us - first_ts_us) : 0;
142+
uint64_t first_camera_ts_us = has_session_ts_ ? session_first_camera_ts_us_ : 0;
143+
uint64_t last_camera_ts_us = has_session_ts_ ? session_last_camera_ts_us_ : 0;
144+
uint64_t offset_us = has_session_ts_ ? session_offset_us_ : 0;
140145

141146
// Build the info JSON without top-level camera_fps
142-
auto j_fi = nlohmann::json{
143-
{"pixel_pitch", {{"x", api.input.get_pixel_size()}, {"y", api.input.get_pixel_size()}}},
144-
{"input_fps", api.input.can_get_camera_fps() ? camera_fps : input_fps},
145-
{"camera_fps", camera_fps}, // camera frames per second
146-
{"eye_type", api.record.get_recorded_eye()},
147-
{"contiguous", contiguous},
148-
{"holovibes_version", __HOLOVIBES_VERSION__},
149-
{"camera", camera_info},
150-
{"file_create_timestamp", file_creation_timestamp_},
151-
{"file_record_timestamp", record_timestamp},
152-
{"timestamps_us", {{"first", first_ts_us}, {"last", last_ts_us}, {"duration", duration_us}}}};
147+
auto j_fi =
148+
nlohmann::json{{"pixel_pitch", {{"x", api.input.get_pixel_size()}, {"y", api.input.get_pixel_size()}}},
149+
{"input_fps", api.input.can_get_camera_fps() ? camera_fps : input_fps},
150+
{"camera_fps", camera_fps}, // camera frames per second
151+
{"eye_type", api.record.get_recorded_eye()},
152+
{"contiguous", contiguous},
153+
{"holovibes_version", __HOLOVIBES_VERSION__},
154+
{"camera", camera_info},
155+
{"file_create_timestamp", file_creation_timestamp_},
156+
{"file_record_timestamp", record_timestamp},
157+
{"timestamps_us",
158+
{{"first", first_ts_us},
159+
{"last", last_ts_us},
160+
{"duration", duration_us},
161+
{"camera_first", first_camera_ts_us},
162+
{"camera_last", last_camera_ts_us},
163+
{"offset", offset_us}}}};
153164

154165
meta_data_ = nlohmann::json{{"compute_settings", api.settings.compute_settings_to_json()}, {"info", j_fi}};
155166
}

Backend/sources/thread/camera_frame_read_worker.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,14 @@ void CameraFrameReadWorker::enqueue_loop(const camera::CapturedFramesDescriptor&
6868
// assign a contiguous ID range for this batch
6969
const uint64_t base_id = next_frame_id_.fetch_add(total, std::memory_order_relaxed);
7070

71-
// fill the time map for RAW 1->1
72-
g_time_map.write_batch(base_id, captured_fd.first_frame_timestamp_us, captured_fd.frame_period_us, total);
71+
// store timestamps in global time map
72+
g_time_map.write_batch(base_id,
73+
captured_fd.first_frame_timestamp_us, // ts0_synced (Unix µs)
74+
captured_fd.frame_period_us,
75+
total,
76+
captured_fd.camera_timestamp_us, // ts0_cam (boot µs) or 0 if not available with this camera
77+
captured_fd.frame_offset_us // offset applied (µs)
78+
);
7379

7480
// enqueue region1 with IDs
7581
if (captured_fd.count1 > 0)

Backend/sources/thread/frame_record_worker.cc

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ void FrameRecordWorker::run()
221221
if (API.record.get_record_mode() == RecordMode::RAW)
222222
{
223223
this_id = g_record_id_queue.pop_one_blocking();
224-
uint64_t this_ts = g_time_map.lookup(this_id);
224+
uint64_t this_ts = g_time_map.lookup_synced(this_id);
225225
if (!first_id)
226226
{
227227
first_id = this_id;
@@ -285,20 +285,29 @@ void FrameRecordWorker::run()
285285

286286
if (API.record.get_record_mode() == RecordMode::RAW && first_id.has_value())
287287
{
288-
const uint64_t first_ts_us = g_time_map.lookup(*first_id);
289-
const uint64_t last_ts_us = g_time_map.lookup(last_id);
288+
const uint64_t first_ts_us = g_time_map.lookup_synced(*first_id);
289+
const uint64_t last_ts_us = g_time_map.lookup_synced(last_id);
290290

291291
LOG_INFO("Record timestamps (us): first={} last={}", first_ts_us, last_ts_us);
292292

293293
const uint64_t duration_us = (last_ts_us >= first_ts_us) ? (last_ts_us - first_ts_us) : 0;
294294

295+
const uint64_t first_camera_ts_us = g_time_map.lookup_camera(*first_id);
296+
const uint64_t last_camera_ts_us = g_time_map.lookup_camera(last_id);
297+
298+
const uint64_t offset_us = g_time_map.lookup_offset(last_id);
299+
295300
LOG_INFO("Record duration: {} us ({} ms, {:.3f} s)",
296301
duration_us,
297302
duration_us / 1000,
298303
static_cast<double>(duration_us) / 1'000'000.0);
299304
if (auto* holo = dynamic_cast<io_files::OutputHoloFile*>(output_frame_file))
300305
{
301-
holo->set_session_timestamps_us(first_ts_us, last_ts_us);
306+
holo->set_session_timestamps_us(first_ts_us,
307+
last_ts_us,
308+
first_camera_ts_us,
309+
last_camera_ts_us,
310+
offset_us);
302311
}
303312
}
304313

Camera/CamerasPhantom/camera_phantom_interface.cc

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@
1616

1717
#include <windows.h>
1818

19-
static inline uint64_t qpc_now_us()
19+
static inline uint64_t unix_now_us()
2020
{
21-
LARGE_INTEGER qpc, freq;
22-
QueryPerformanceCounter(&qpc);
23-
QueryPerformanceFrequency(&freq);
24-
// Cast to long double to avoid overflow in the division
25-
long double us = (long double)qpc.QuadPart * 1e6L / (long double)freq.QuadPart;
26-
return (uint64_t)us;
21+
FILETIME ft;
22+
#if defined(NTDDI_WIN8) && (NTDDI_VERSION >= NTDDI_WIN8)
23+
GetSystemTimePreciseAsFileTime(&ft);
24+
#else
25+
GetSystemTimeAsFileTime(&ft);
26+
#endif
27+
28+
ULARGE_INTEGER uli;
29+
uli.LowPart = ft.dwLowDateTime;
30+
uli.HighPart = ft.dwHighDateTime;
31+
32+
// FILETIME is 100-ns ticks since 1601-01-01; convert to Unix epoch µs
33+
static constexpr uint64_t EPOCH_DIFF_US = 11644473600000000ULL; // 1601->1970
34+
uint64_t us = (uli.QuadPart / 10ULL); // 100ns -> µs
35+
return us - EPOCH_DIFF_US;
2736
}
2837

2938
struct SoftClockAlign
@@ -33,19 +42,17 @@ struct SoftClockAlign
3342

3443
void observe(uint64_t t_cam_us, uint64_t t_host_us)
3544
{
36-
// t_host_us >= acquisition_time + latency >= t_cam_us + latency'
37-
// Therefore (t_host - t_cam) is an upper bound of the real offset.
45+
// Upper bound candidate
3846
uint64_t cand = (t_host_us > t_cam_us) ? (t_host_us - t_cam_us) : 0;
3947

40-
// Keep the minimum offset seen so far
48+
// Try to update the minimum offset
4149
uint64_t cur = offset_min_us.load(std::memory_order_relaxed);
4250
while (cand < cur && !offset_min_us.compare_exchange_weak(cur, cand))
4351
{ /* spin */
4452
}
4553

46-
// [DEBUG] Log the current offset estimate
47-
// std::cout << "[SoftClockAlign] Candidate offset=" << cand
48-
// << " us, current_min=" << offset_min_us.load(std::memory_order_relaxed) << " us" << std::endl;
54+
uint64_t min_off = offset_min_us.load(std::memory_order_relaxed);
55+
uint64_t extra_latency = (cand >= min_off) ? (cand - min_off) : 0;
4956
}
5057

5158
uint64_t to_host_time(uint64_t t_cam_us) const
@@ -289,13 +296,13 @@ void CameraPhantomInt::shutdown_camera() { return; }
289296
CapturedFramesDescriptor CameraPhantomInt::get_frames()
290297
{
291298
// 1) Host timestamp right BEFORE waiting (lower bound)
292-
uint64_t host_before_us = qpc_now_us();
299+
uint64_t host_before_us = unix_now_us();
293300

294301
// 2) Blocking wait for a buffer
295302
auto buffer = Euresys::ScopedBuffer(*(grabber_->available_grabbers_[0]));
296303

297304
// 3) Host timestamp right AFTER the buffer (upper bound, better for offset)
298-
uint64_t host_after_us = qpc_now_us();
305+
uint64_t host_after_us = unix_now_us();
299306

300307
// Multi-grabber support
301308
unsigned int nb_grabbers = params_.at<unsigned int>("NbGrabbers");
@@ -330,6 +337,9 @@ CapturedFramesDescriptor CameraPhantomInt::get_frames()
330337
ret.frame_period_us = period_us;
331338
ret.has_hw_timestamp = (cam_ts_us != 0);
332339

340+
ret.frame_offset_us = (ret.has_hw_timestamp) ? (synced_ts_us - cam_ts_us) : 0;
341+
ret.camera_timestamp_us = cam_ts_us;
342+
333343
return ret;
334344
}
335345

Camera/include/icamera.hh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ struct CapturedFramesDescriptor
6464
/*! \brief Nominal period between consecutive frames in this batch (us). If unknown, leave 0. */
6565
uint64_t frame_period_us = 0;
6666

67+
/*! \brief Frame offset between system timestamps and camera timestamps (us). If unknown, leave 0. */
68+
uint64_t frame_offset_us = 0;
69+
70+
/*! \brief Camera timestamp of the first frame in region1, if available. 0 otherwise. */
71+
uint64_t camera_timestamp_us = 0;
72+
6773
/*! \brief True if first_frame_timestamp_us came from the camera/grabber. */
6874
bool has_hw_timestamp = false;
6975

0 commit comments

Comments
 (0)