SmartSpectra C++ SDK
Measure human vitals from video with SmartSpectra C++ SDK.
Loading...
Searching...
No Matches
foreground_container_impl.hpp
1
2// foreground_container_impl.h
3// Created by Greg on 4/29/2024.
4// Copyright (C) 2024 Presage Security, Inc.
5//
6// This program is free software; you can redistribute it and/or
7// modify it under the terms of the GNU Lesser General Public
8// License as published by the Free Software Foundation; either
9// version 3 of the License, or (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14// Lesser General Public License for more details.
15//
16// You should have received a copy of the GNU Lesser General Public License
17// along with this program; if not, write to the Free Software Foundation,
18// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
21#pragma once
22// === standard library includes (if any) ===
23#include <thread>
24// === third-party includes (if any) ===
25#include <mediapipe/framework/port/opencv_imgproc_inc.h>
26#include <mediapipe/framework/port/opencv_highgui_inc.h>
27#include <mediapipe/framework/formats/image_frame.h>
28#include <mediapipe/framework/formats/image_frame_opencv.h>
29#include <physiology/graph/stream_and_packet_names.h>
30// === local includes (if any) ===
31#include "foreground_container.hpp"
32#include "initialization.hpp"
33#include "image_transfer.hpp"
34#include "packet_helpers.hpp"
35#include "benchmarking.hpp"
36#include "keyboard_input.hpp"
37#include <smartspectra/video_source/factory.hpp>
38
39
41
42namespace pe = physiology::edge;
43namespace pi = platform_independence;
44namespace init = initialization;
45namespace ph = packet_helpers;
46namespace keys = keyboard_input;
47namespace it = image_transfer;
48namespace bench = benchmarking;
49using json = nlohmann::json;
50
51
52template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
54 SettingsType settings
55): Base(settings),
56 load_video(!this->settings.video_source.input_video_path.empty()),
57 video_source(nullptr), keep_grabbing_frames(false) {}
58
59template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
60std::string ForegroundContainer<TDeviceType, TOperationMode, TIntegrationMode>::GenerateGuiWindowName() {
61 return "Presage SmartSpectra C++ SDK "
62 "[device: " + pi::AbslUnparseFlag(TDeviceType) +
63 "; operation mode: " + settings::AbslUnparseFlag(TOperationMode) +
64 "; integration mode: " + settings::AbslUnparseFlag(TIntegrationMode) + "]";
65}
66
67template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
68const std::string ForegroundContainer<TDeviceType, TOperationMode, TIntegrationMode>::kWindowName = ForegroundContainer<
69 TDeviceType,
70 TOperationMode,
71 TIntegrationMode>::GenerateGuiWindowName();
79template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
80absl::Status ForegroundContainer<TDeviceType,
81 TOperationMode,
82 TIntegrationMode>::HandleOutputData(int64_t frame_timestamp) {
83 bool got_core_metrics_output;
84 physiology::MetricsBuffer metrics_buffer;
85 MP_RETURN_IF_ERROR(ph::GetPacketContentsIfAny(
86 metrics_buffer,
87 got_core_metrics_output,
88 this->core_metrics_poller.Get(),
89 pe::graph::output_streams::kMetricsBuffer,
90 this->settings.verbosity_level > 2
91 ));
92 if (got_core_metrics_output) {
93 MP_RETURN_IF_ERROR(this->OnCoreMetricsOutput(metrics_buffer, frame_timestamp));
94 if (TOperationMode == settings::OperationMode::Spot) {
95 // reset to start state
96 this->recording = false;
97 if (this->load_video) {
98 keep_grabbing_frames = false;
99 } else if (this->settings.video_source.auto_lock &&
100 this->video_source->SupportsExposureControls()) {
101 MP_RETURN_IF_ERROR(this->video_source->TurnOnAutoExposure());
102 }
103 } else {
104 MP_RETURN_IF_ERROR(this->ComputeCorePerformanceTelemetry(metrics_buffer));
105 }
106 }
107 // A separate outer if-clause used here to increase the likelihood of compiler optimizing this out
108 // when we're in spot mode.
109 if (TOperationMode == settings::OperationMode::Continuous) {
110 if (this->settings.enable_edge_metrics) {
111 bool got_edge_metrics_output;
112 do {
113 physiology::Metrics edge_metrics;
114 mediapipe::Timestamp timestamp;
115 MP_RETURN_IF_ERROR(ph::GetPacketContentsIfAny(
116 edge_metrics, got_edge_metrics_output, this->edge_metrics_poller.Get(),
117 pe::graph::output_streams::kEdgeMetrics, timestamp,
118 this->settings.verbosity_level > 2
119 ));
120 if (got_edge_metrics_output) {
121 MP_RETURN_IF_ERROR(this->OnEdgeMetricsOutput(edge_metrics, timestamp.Value()));
122 }
123 } while (got_edge_metrics_output);
124 }
125 }
126
127 return absl::OkStatus();
128}
129
137template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
139 MP_RETURN_IF_ERROR(this->core_metrics_poller.Initialize(this->graph, pe::graph::output_streams::kMetricsBuffer));
140 if (TOperationMode == settings::OperationMode::Spot) {
141 return absl::OkStatus();
142 } else {
143 return this->edge_metrics_poller.Initialize(this->graph, pe::graph::output_streams::kEdgeMetrics);
144 }
145}
146
147template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
149 LOG(INFO) << "Begin to initialize preprocessing container.";
150 MP_RETURN_IF_ERROR(Base::Initialize());
151 MP_ASSIGN_OR_RETURN(this->video_source, video_source::BuildVideoSource(this->settings.video_source));
152
153 // Attempt to set up camera tuning
154 if (this->settings.video_source.enable_camera_tuning) {
155 // Use provided tuning settings
156 this->camera_tuner =
157 std::make_unique<video_source::CameraTuner>(this->settings.video_source.tuner_settings);
158 auto status = this->camera_tuner->SetVideoSource(this->video_source);
159 if (status.ok()) {
160 this->tuning_enabled = true;
161 LOG(INFO) << "Camera tuning enabled.";
162 } else {
163 LOG(WARNING) << "Camera tuning disabled: " << status.message();
164 this->tuning_enabled = false;
165 }
166 } else {
167 this->tuning_enabled = false;
168 }
169
170 MP_RETURN_IF_ERROR(init::InitializeGui(this->settings, kWindowName));
171 // legacy behavior: assume user wants to start with recording=on when a video file is supplied.
172 if (this->load_video || this->settings.start_with_recording_on) {
173 this->recording = true;
174 } else {
175 // turn on auto-exposure on launch *before* recording (if it's supported by this video source)
176 if (this->settings.video_source.auto_lock && this->video_source->SupportsExposureControls()) {
177 auto status = this->video_source->TurnOnAutoExposure();
178 if (absl::IsUnavailable(status)) {
179 LOG(INFO)
180 << "Warning: video source does not support auto-exposure controls. Please try manual controls or address the video source code.";
181 }
182 }
183 }
184
185#ifdef WITH_VIDEO_OUTPUT
186 RET_CHECK(this->video_source->HasFrameDimensions());
187 cv::Size input_video_size(this->video_source->GetWidth(), this->video_source->GetHeight());
188 MP_RETURN_IF_ERROR(
189 init::InitializeVideoSink<TDeviceType>(
190 this->stream_writer,
191 input_video_size,
192 this->settings.video_sink.destination,
193 30,
194 this->settings.video_sink.mode
195 )
196 );
197#endif
198
199 LOG(INFO) << "Finish preprocessing container initialization.";
200 return absl::OkStatus();
201}
202
203template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
204void ForegroundContainer<TDeviceType,
205 TOperationMode,
206 TIntegrationMode>::ScrollPastTimeOffset() {
207 // skip the first settings.start_time_offset_ms milliseconds of video
208 if (this->settings.start_time_offset_ms > 0 && this->load_video) {
209 cv::Mat camera_frame_raw;
210 // get first frame
211 *(this->video_source) >> camera_frame_raw;
212 if (!camera_frame_raw.empty()) {
213 // calculate correct recording start time
214 int64_t frame_timestamp = this->video_source->GetFrameTimestamp();
215 int64_t recording_start_time = frame_timestamp + this->settings.start_time_offset_ms * 1e3;
216 // skip frames until recording time is reached
217 while ((frame_timestamp < recording_start_time) && !camera_frame_raw.empty()) {
218 *(this->video_source) >> camera_frame_raw;
219 frame_timestamp = this->video_source->GetFrameTimestamp();
220 }
221 }
222 }
223}
224
225template<platform_independence::DeviceType TDeviceType, settings::OperationMode TOperationMode, settings::IntegrationMode TIntegrationMode>
227 this->operation_context.Reset();
228 if (!this->initialized) {
229 return absl::PermissionDeniedError("Client not initialized.");
230 }
231 this->running = true;
232 LOG(INFO) << "Set up output pollers.";
233
234 //TODO: check that callbacks aren't nullptr (potentially, move the checks out into container base class and call
235 // from both here and background container's StartGraph, instead of duplicating the code that's already there.)
236
237 MP_ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller output_video_poller,
238 this->graph.AddOutputStreamPoller(pe::graph::output_streams::kOutputVideo));
239 MP_ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller status_code_poller,
240 this->graph.AddOutputStreamPoller(pe::graph::output_streams::kStatusCode));
241 MP_ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller blue_tooth_poller,
242 this->graph.AddOutputStreamPoller(pe::graph::output_streams::kBlueTooth));
243
244 // frame rate diagnostics
245 MP_ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller frame_sent_through_poller,
246 this->graph.AddOutputStreamPoller(pe::graph::output_streams::kFrameSentThrough));
247
248
249 MP_RETURN_IF_ERROR(this->InitializeOutputDataPollers());
250
251 MP_RETURN_IF_ERROR(this->operation_context.InitializePollers(this->graph));
252
253 LOG(INFO) << "Start running the calculator graph.";
254 MP_RETURN_IF_ERROR(this->graph.StartRun({}));
255
256 // Start tuning if enabled
257 if (this->tuning_enabled) {
258 MP_RETURN_IF_ERROR(this->camera_tuner->StartTuning());
259 LOG(INFO) << "Starting camera tuning...";
260 }
261
262 LOG(INFO) << "Start to grab and process frames.";
263 this->keep_grabbing_frames = true;
264
265 double blue_tooth;
266
267#ifdef BENCHMARK_CAMERA_CAPTURE
268 int64_t i_frame = 0;
269 std::chrono::duration<double> interval_capture_time(0.);
270 std::chrono::duration<double> interval_frame_time(0.);
271 int64 frame_interval = 30;
272#endif
273
274 //TODO: this function needs to be moved into VideoSourceInterface and implemented in related subclasses.
275 // This way, video sources such as CaptureVideoFileSource can do the scrolling, whereas other sources
276 // can ignore the command (still not sure what FileStreamVideoSource should do for scroll behavior).
277 this->ScrollPastTimeOffset();
278
279 physiology::StatusCode previous_status_code = physiology::StatusCode::PROCESSING_NOT_STARTED;
280
281 // loop over frames
282 while (this->keep_grabbing_frames) {
283 cv::Mat camera_frame_raw;
284#ifdef BENCHMARK_CAMERA_CAPTURE
285 auto frame_loop_start = std::chrono::high_resolution_clock::now();
286#endif
287 // Capture frame from camera or video.
288 *this->video_source >> camera_frame_raw;
289#ifdef WITH_VIDEO_OUTPUT
290 if (this->stream_writer.isOpened() && this->settings.video_sink.passthrough) {
291 this->stream_writer.write(camera_frame_raw);
292 }
293#endif
294#ifdef BENCHMARK_CAMERA_CAPTURE
295 auto frame_capture_end = std::chrono::high_resolution_clock::now();
296#endif
297 if (camera_frame_raw.empty()) {
298 LOG(INFO) << "Encountered empty frame: assuming end of video or stream reached.";
299 this->keep_grabbing_frames = false;
300 } else {
301 // === got new frame, now process it and handle output ===
302
303 // compute timestamp
304 int64_t frame_timestamp = this->video_source->GetFrameTimestamp();
305 auto mp_frame_timestamp = mediapipe::Timestamp(frame_timestamp);
306 this->AddFrameTimestampToBenchmarkingInfo(mp_frame_timestamp);
307
308 // === handle output
309 cv::Mat camera_frame;
310 cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
311
312 // Wrap Mat into an ImageFrame.
313 auto input_frame = absl::make_unique<mediapipe::ImageFrame>(
314 mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
315 mediapipe::ImageFrame::kDefaultAlignmentBoundary
316 );
317 cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
318 // transfer camera_frame data to input_frame
319 camera_frame.copyTo(input_frame_mat);
320
321 // Send recording state to the graph.
322 MP_RETURN_IF_ERROR(
323 this->graph
324 .AddPacketToInputStream(
325 pe::graph::input_streams::kRecording,
326 mediapipe::MakePacket<bool>(this->recording).At(mp_frame_timestamp)
327 )
328 );
329 // Send image packet into the graph.
330 MP_RETURN_IF_ERROR(
331 it::FeedFrameToGraph(std::move(input_frame), this->graph, this->device_context, frame_timestamp,
332 pe::graph::input_streams::kInputVideo)
333 );
334
335 // region ========================================== HANDLE GRAPH OUTPUT ===================================
336 // Get the graph video output packet, or stop if that fails.
337 mediapipe::Packet output_video_packet;
338 if (output_video_poller.QueueSize() > 0) {
339 if (!output_video_poller.Next(&output_video_packet)) break;
340 cv::Mat output_frame_rgb;
341 MP_RETURN_IF_ERROR(it::GetFrameFromPacket<TDeviceType>(output_frame_rgb,
342 this->device_context,
343 output_video_packet));
344
345 // Convert to BGR and display.
346 cv::cvtColor(output_frame_rgb, this->output_frame_bgr, cv::COLOR_RGB2BGR);
347
348 // Envoke Callback on the video
349 MP_RETURN_IF_ERROR(this->OnVideoOutput(this->output_frame_bgr, frame_timestamp));
350
351 // only display output window when we're not in headless mode.
352 if (!this->settings.headless) {
353 if(this->tuning_enabled && this->camera_tuner->IsTuning()){
354 this->output_frame_bgr = this->camera_tuner->ProcessFrameForDisplay(this->output_frame_bgr);
355 }
356 cv::imshow(kWindowName, this->output_frame_bgr);
357 }
358#ifdef WITH_VIDEO_OUTPUT
359 if (this->stream_writer.isOpened() && !this->settings.video_sink.passthrough) {
360 this->stream_writer.write(output_frame_bgr);
361 }
362#endif
363 }
364
365 bool got_status_code_packet;
366 physiology::StatusValue status_value;
367 MP_RETURN_IF_ERROR(ph::GetPacketContentsIfAny(
368 status_value, got_status_code_packet, status_code_poller, pe::graph::output_streams::kStatusCode,
369 this->settings.verbosity_level > 2
370 ));
371
372 if (got_status_code_packet){
373 this->status = status_value;
374
375 // Call OnStatusCode for every status update (regardless of change)
376 MP_RETURN_IF_ERROR(this->OnStatusCode(this->status));
377
378 // Call OnStatusChange only when status actually changes
379 if (this->status.value() != previous_status_code) {
380 MP_RETURN_IF_ERROR(this->OnStatusChange(this->status));
381 previous_status_code = this->status.value();
382 }
383
384 // Process frame for tuning if enabled
385 if (this->tuning_enabled && this->camera_tuner->IsTuning()) {
386 auto tuning_status = this->camera_tuner->ProcessFrame(this->status.value(), frame_timestamp);
387
388 if (!tuning_status.ok()) {
389 // Tuning failed - log warning and disable tuning, but continue running
390 LOG(WARNING) << "Camera tuning failed: " << tuning_status.message();
391 LOG(WARNING) << "Continuing without tuning - recording will be disabled until status is `OK`.";
392 this->tuning_enabled = false;
393 } else if (!this->camera_tuner->IsTuning()) {
394 // Tuning just completed successfully
395 LOG(INFO) << "Camera tuning complete!";
396 auto exp = this->camera_tuner->GetTunedExposure();
397 auto gain = this->camera_tuner->GetTunedGain();
398 if (exp.ok()) {
399 LOG(INFO) << " Tuned exposure: " << *exp;
400 }
401 if (gain.ok()) {
402 LOG(INFO) << " Tuned gain: " << *gain;
403 }
404 auto wb = this->camera_tuner->GetTunedWhiteBalance();
405 if (wb.ok()) {
406 LOG(INFO) << " Tuned white balance: " << *wb;
407 }
408 }
409 }
410 }
411
412 bool got_blue_tooth_packet;
413 MP_RETURN_IF_ERROR(ph::GetPacketContentsIfAny(
414 blue_tooth, got_blue_tooth_packet, blue_tooth_poller, pe::graph::output_streams::kBlueTooth,
415 this->settings.verbosity_level > 0
416 ));
417
418 bool operation_state_changed;
419 MP_RETURN_IF_ERROR(this->operation_context
420 .QueryPollers(operation_state_changed, this->settings.verbosity_level > 1));
421
422 bool got_frame_sent_through_packet;
423 bool frame_sent_through;
424 mediapipe::Timestamp frame_sent_through_timestamp;
425 MP_RETURN_IF_ERROR(ph::GetPacketContentsIfAny(
426 frame_sent_through, got_frame_sent_through_packet, frame_sent_through_poller,
427 pe::graph::output_streams::kFrameSentThrough, frame_sent_through_timestamp,
428 this->settings.verbosity_level > 4
429 ));
430 if(got_frame_sent_through_packet){
431 MP_RETURN_IF_ERROR(this->OnFrameSentThrough(frame_sent_through, frame_sent_through_timestamp.Value()));
432 }
433
434 MP_RETURN_IF_ERROR(this->HandleOutputData(frame_timestamp));
435
436 // endregion ===============================================================================================
437 if (this->settings.headless) {
438 if (!this->load_video) {
439 // if we loaded video, that means we started recording already.
440 // Otherwise, start recording iff status code is OK
441 if (!this->recording && this->status.value() == physiology::StatusCode::OK) {
442 if (this->settings.video_source.auto_lock && this->video_source->SupportsExposureControls()) {
443 return this->video_source->TurnOffAutoExposure();
444 }
445 this->recording = true;
446 LOG(INFO) << "====== Recording started after timestamp:" << frame_timestamp << " ======";
447 }
448 }
449 std::this_thread::sleep_for(std::chrono::milliseconds(this->settings.interframe_delay_ms));
450 } else {
451 MP_RETURN_IF_ERROR(keys::HandleKeyboardInput(
452 this->keep_grabbing_frames, this->recording, *(this->video_source), this->settings, this->status,
453 this->camera_tuner.get(), this->tuning_enabled
454 ));
455 }
456 }
457
458#ifdef BENCHMARK_CAMERA_CAPTURE
459 MP_RETURN_IF_ERROR(
460 bench::HandleCameraBenchmarking(
461 i_frame, interval_capture_time, interval_frame_time, frame_loop_start, frame_capture_end,
462 frame_interval, this->settings.interframe_delay_ms, this->settings.verbosity_level
463 )
464 );
465#endif
466 }
467
468 LOG(INFO) << "Shutting down.";
469 MP_RETURN_IF_ERROR(this->graph.CloseAllInputStreams());
470 MP_RETURN_IF_ERROR(this->graph.CloseAllPacketSources());
471#ifdef WITH_VIDEO_OUTPUT
472 if (this->stream_writer.isOpened()) {
473 this->stream_writer.release();
474 }
475#endif
476 MP_RETURN_IF_ERROR(this->graph.WaitUntilDone());
477 this->running = false;
478 return absl::OkStatus();
479}
480} // namespace presage::smartspectra::container
absl::Status ComputeCorePerformanceTelemetry(const physiology::MetricsBuffer &metrics_buffer)
Definition container_impl.hpp:251
virtual absl::Status Initialize()
Definition container_impl.hpp:68
void AddFrameTimestampToBenchmarkingInfo(const mediapipe::Timestamp &timestamp)
Definition container_impl.hpp:227
Convenience container with a built-in video source and optional GUI.
Definition foreground_container.hpp:46
virtual absl::Status InitializeOutputDataPollers()
Definition foreground_container_impl.hpp:138
ForegroundContainer(SettingsType settings)
Definition foreground_container_impl.hpp:53
virtual absl::Status Run()
Definition foreground_container_impl.hpp:226
virtual absl::Status HandleOutputData(int64_t frame_timestamp)
Definition foreground_container_impl.hpp:82
absl::Status Initialize() override
Definition foreground_container_impl.hpp:148
absl::StatusOr< std::shared_ptr< VideoSource > > BuildVideoSource(const VideoSourceSettings &settings)
Factory helper for constructing the appropriate VideoSource implementation based on the provided sett...
Definition factory.cpp:20
Definition background_container.cpp:10