A.R.G.U.S 2.1.0
Adaptive Real-Time Guardian for Unsafe Situations
Loading...
Searching...
No Matches
CameraCapture.cpp
Go to the documentation of this file.
1
6#include "CameraCapture.hpp"
7#include "CppTimerStdFuncCallback.h"
8
9#include <algorithm>
10#include <chrono>
11#include <cctype>
12#include <condition_variable>
13#include <cstdint>
14#include <cstdlib>
15#include <functional>
16#include <iostream>
17#include <memory>
18#include <mutex>
19#include <string>
20#include <vector>
21
22#if defined(ARGUS_HAVE_LIBCAM2OPENCV)
23#include <libcam2opencv.h>
24#endif
25
26namespace {
27
28constexpr int kTargetFrameWidth = 640;
29constexpr int kTargetFrameHeight = 480;
30constexpr int kTargetFrameRate = 30;
31constexpr std::chrono::milliseconds kEmptyFrameRetryBackoff{20};
32
33bool waitForRetryBackoff(std::chrono::nanoseconds delay) {
34 if (delay.count() <= 0) {
35 return true;
36 }
37
38 std::mutex mutex;
39 std::condition_variable condition;
40 bool fired = false;
41
42 CppTimerCallback timer;
43 timer.registerEventCallback([&]() {
44 std::lock_guard<std::mutex> lock(mutex);
45 fired = true;
46 condition.notify_one();
47 });
48
49 try {
50 timer.startns(static_cast<long>(delay.count()), ONESHOT);
51 } catch (...) {
52 return false;
53 }
54
55 std::unique_lock<std::mutex> lock(mutex);
56 condition.wait(lock, [&]() { return fired; });
57 timer.stop();
58 return true;
59}
60
61std::vector<std::string> buildLibcameraPipelines() {
62 // Ordered from stricter caps to more permissive fallback.
63 return {
64 "libcamerasrc ! video/x-raw,format=NV12,width=640,height=480,framerate=30/1 "
65 "! videoconvert ! video/x-raw,format=BGR "
66 "! appsink drop=true max-buffers=1 sync=false",
67 "libcamerasrc ! video/x-raw,width=640,height=480,framerate=30/1 "
68 "! videoconvert ! video/x-raw,format=BGR "
69 "! appsink drop=true max-buffers=1 sync=false",
70 "libcamerasrc ! videoconvert ! video/x-raw,format=BGR "
71 "! appsink drop=true max-buffers=1 sync=false"};
72}
73
74bool tryOpen(cv::VideoCapture& cap,
75 const std::string& label,
76 const std::function<bool()>& opener) {
77 if (!opener()) {
78 std::cerr << "[CameraCapture] Open failed via " << label << std::endl;
79 return false;
80 }
81
82 std::cout << "[CameraCapture] Opened via " << label;
83 if (cap.isOpened()) {
84 std::cout << " (backend: " << cap.getBackendName() << ")";
85 }
86 std::cout << std::endl;
87 return true;
88}
89
90bool isLibcamerifyActive() {
91 const char* ld_preload = std::getenv("LD_PRELOAD");
92 if (ld_preload == nullptr) {
93 return false;
94 }
95
96 const std::string preload(ld_preload);
97 return preload.find("v4l2-compat") != std::string::npos ||
98 preload.find("libcamerify") != std::string::npos ||
99 preload.find("libcamera") != std::string::npos;
100}
101
102std::string buildV4l2DevicePath(int camera_index) {
103 return "/dev/video" + std::to_string(camera_index);
104}
105
106const char* backendPreferenceToString(CameraCapture::BackendPreference preference) {
107 switch (preference) {
109 return "auto";
111 return "opencv";
113 return "libcamera2opencv";
114 }
115 return "unknown";
116}
117
118CameraCapture::BackendPreference parseBackendPreference(const char* raw_value,
119 bool* recognised = nullptr) {
120 if (recognised != nullptr) {
121 *recognised = true;
122 }
123
124 if (raw_value == nullptr) {
125 if (recognised != nullptr) {
126 *recognised = false;
127 }
129 }
130
131 std::string value(raw_value);
132 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
133 return static_cast<char>(std::tolower(c));
134 });
135
136 if (value == "auto") {
138 }
139 if (value == "opencv" || value == "videocapture" || value == "opencvvideocapture") {
141 }
142 if (value == "libcamera2opencv" || value == "libcamera" ||
143 value == "cam2opencv") {
145 }
146
147 if (recognised != nullptr) {
148 *recognised = false;
149 }
151}
152
153CameraCapture::BackendPreference effectiveBackendPreference(
154 CameraCapture::BackendPreference requested_preference) {
155 if (requested_preference != CameraCapture::BackendPreference::Auto) {
156 return requested_preference;
157 }
158
159 bool recognised = false;
160 const auto env_preference =
161 parseBackendPreference(std::getenv("ARGUS_CAMERA_BACKEND"), &recognised);
162 if (recognised) {
163 return env_preference;
164 }
165
167}
168
169cv::Mat normaliseCapturedFrame(const cv::Mat& frame) {
170 if (frame.empty()) {
171 return {};
172 }
173
174 if (frame.cols == kTargetFrameWidth && frame.rows == kTargetFrameHeight) {
175 return frame.clone();
176 }
177
178 cv::Mat resized;
179 cv::resize(frame,
180 resized,
181 cv::Size(kTargetFrameWidth, kTargetFrameHeight),
182 0.0,
183 0.0,
184 cv::INTER_LINEAR);
185 return resized;
186}
187
188} // namespace
189
191public:
192 virtual ~CameraCaptureBackend() = default;
193
194 virtual bool isOpen() const noexcept = 0;
195 virtual bool waitForNextFrame(FrameEvent& output_event) = 0;
196 virtual std::string backendName() const = 0;
197 virtual std::string backendImplementation() const = 0;
198 virtual bool setFocusPosition(float position) { return false; }
199 virtual float getFocusPosition() const { return -1.0f; }
200};
201
203public:
204 explicit OpenCvVideoCaptureBackend(int camera_index) {
205 const bool libcamerify_active = isLibcamerifyActive();
206 if (libcamerify_active) {
207 std::cout << "[CameraCapture] libcamerify detected via LD_PRELOAD. "
208 "Enforcing V4L2-first camera open policy."
209 << std::endl;
210 }
211
212 bool opened = false;
213 if (libcamerify_active) {
214 const std::string device_path = buildV4l2DevicePath(camera_index);
215 opened = tryOpen(cap_, "V4L2 device path " + device_path, [&]() {
216 return cap_.open(device_path, cv::CAP_V4L2);
217 });
218 if (!opened) {
219 opened = tryOpen(cap_, "V4L2 index " + std::to_string(camera_index), [&]() {
220 return cap_.open(camera_index, cv::CAP_V4L2);
221 });
222 }
223 } else {
224 opened = tryOpen(cap_, "V4L2 index " + std::to_string(camera_index), [&]() {
225 return cap_.open(camera_index, cv::CAP_V4L2);
226 });
227 if (!opened) {
228 opened = tryOpen(cap_,
229 "default backend index " + std::to_string(camera_index),
230 [&]() { return cap_.open(camera_index); });
231 }
232 }
233
234 if (!opened && !libcamerify_active) {
235 const std::vector<std::string> pipelines = buildLibcameraPipelines();
236 for (std::size_t index = 0; index < pipelines.size() && !opened; ++index) {
237 const std::string label =
238 "GStreamer/libcamerasrc pipeline " + std::to_string(index + 1);
239 opened = tryOpen(cap_, label, [&]() {
240 return cap_.open(pipelines[index], cv::CAP_GSTREAMER);
241 });
242 }
243 }
244
245 if (!opened) {
246 if (libcamerify_active) {
247 std::cerr << "ERROR: Cannot open camera in libcamerify mode "
248 "(tried /dev/videoN and V4L2 index only)."
249 << std::endl;
250 } else {
251 std::cerr
252 << "ERROR: Cannot open camera. Tried V4L2, default backend, and libcamerasrc."
253 << std::endl;
254 }
255 }
256
257 if (cap_.isOpened() && !cap_.set(cv::CAP_PROP_BUFFERSIZE, 1)) {
258 std::cerr
259 << "[CameraCapture] Warning: CAP_PROP_BUFFERSIZE unsupported by backend."
260 << std::endl;
261 }
262
263 if (cap_.isOpened()) {
264 (void)cap_.set(cv::CAP_PROP_FRAME_WIDTH, kTargetFrameWidth);
265 (void)cap_.set(cv::CAP_PROP_FRAME_HEIGHT, kTargetFrameHeight);
266 (void)cap_.set(cv::CAP_PROP_FPS, kTargetFrameRate);
267 (void)cap_.set(cv::CAP_PROP_READ_TIMEOUT_MSEC, 2000);
268 }
269 }
270
272 if (cap_.isOpened()) {
273 cap_.release();
274 }
275 }
276
277 bool isOpen() const noexcept override {
278 return cap_.isOpened();
279 }
280
281 bool waitForNextFrame(FrameEvent& output_event) override {
282 if (!cap_.isOpened()) {
283 return false;
284 }
285
286 constexpr int kMaxReadAttempts = 4;
287 output_event.image_data.release();
288 for (int attempt = 0; attempt < kMaxReadAttempts; ++attempt) {
289 if (!cap_.read(output_event.image_data)) {
290 output_event.image_data.release();
291 }
292
293 if (!output_event.image_data.empty()) {
294 break;
295 }
296
297 (void)waitForRetryBackoff(kEmptyFrameRetryBackoff);
298 }
299
300 if (output_event.image_data.empty()) {
301 std::cerr << "WARNING: Empty frame captured (backend: ";
302 if (cap_.isOpened()) {
303 std::cerr << cap_.getBackendName();
304 } else {
305 std::cerr << "closed";
306 }
307 std::cerr << ")." << std::endl;
308 return false;
309 }
310
311 const auto steady_now = std::chrono::steady_clock::now();
312 const auto now = std::chrono::system_clock::now();
313 output_event.capture_timestamp = steady_now;
314 output_event.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
315 now.time_since_epoch())
316 .count();
317 return true;
318 }
319
320 std::string backendName() const override {
321 if (!cap_.isOpened()) {
322 return "CLOSED";
323 }
324 return cap_.getBackendName();
325 }
326
327 std::string backendImplementation() const override {
328 return "OpenCV VideoCapture";
329 }
330
331private:
332 cv::VideoCapture cap_;
333};
334
335#if defined(ARGUS_HAVE_LIBCAM2OPENCV)
336class Libcamera2OpenCvBackend final : public CameraCaptureBackend {
337public:
338 explicit Libcamera2OpenCvBackend(int camera_index, float focus_position = -1.0f) {
339 Libcam2OpenCVSettings settings;
340 settings.cameraIndex = static_cast<unsigned int>(std::max(camera_index, 0));
341 settings.framerate = kTargetFrameRate;
342 settings.lensPosition = focus_position;
343 current_focus_position_ = focus_position;
344
345 camera_.registerCallback([this](const cv::Mat& frame,
346 const libcamera::ControlList&) {
347 std::lock_guard<std::mutex> lock(mutex_);
348 latest_frame_ = normaliseCapturedFrame(frame);
349 latest_capture_timestamp_ = std::chrono::steady_clock::now();
350 const auto now = std::chrono::system_clock::now();
351 latest_timestamp_ms_ =
352 std::chrono::duration_cast<std::chrono::milliseconds>(
353 now.time_since_epoch())
354 .count();
355 ++frame_sequence_;
356 condition_.notify_one();
357 });
358
359 try {
360 camera_.start(settings);
361 open_ = true;
362 std::cout << "[CameraCapture] Opened via libcamera2opencv callback backend"
363 << std::endl;
364 } catch (const std::exception& exception) {
365 last_error_ = exception.what();
366 std::cerr << "[CameraCapture] libcamera2opencv start failed: "
367 << last_error_ << std::endl;
368 safeStop();
369 } catch (...) {
370 last_error_ = "unknown libcamera2opencv exception";
371 std::cerr << "[CameraCapture] libcamera2opencv start failed: "
372 << last_error_ << std::endl;
373 safeStop();
374 }
375 }
376
377 ~Libcamera2OpenCvBackend() override {
378 safeStop();
379 }
380
381 bool isOpen() const noexcept override {
382 return open_;
383 }
384
385 bool waitForNextFrame(FrameEvent& output_event) override {
386 if (!open_) {
387 return false;
388 }
389
390 std::unique_lock<std::mutex> lock(mutex_);
391 const bool ready = condition_.wait_for(
392 lock,
393 std::chrono::milliseconds(2000),
394 [&]() { return frame_sequence_ > delivered_sequence_ || !open_; });
395
396 if (!ready || (!open_ && frame_sequence_ <= delivered_sequence_)) {
397 std::cerr << "WARNING: Empty frame captured (backend: libcamera2opencv)."
398 << std::endl;
399 return false;
400 }
401
402 output_event.image_data = latest_frame_.clone();
403 output_event.capture_timestamp = latest_capture_timestamp_;
404 output_event.timestamp_ms = latest_timestamp_ms_;
405 delivered_sequence_ = frame_sequence_;
406 return !output_event.image_data.empty();
407 }
408
409 std::string backendName() const override {
410 return open_ ? "LIBCAMERA2OPENCV" : "CLOSED";
411 }
412
413 std::string backendImplementation() const override {
414 return "libcamera2opencv";
415 }
416
417 bool setFocusPosition(float position) override {
418 std::lock_guard<std::mutex> lock(mutex_);
419 current_focus_position_ = position;
420 return true; // Focus change will apply on next frame
421 }
422
423 float getFocusPosition() const override {
424 std::lock_guard<std::mutex> lock(mutex_);
425 return current_focus_position_;
426 }
427
428private:
429 void safeStop() noexcept {
430 if (open_) {
431 try {
432 camera_.stop();
433 } catch (...) {
434 }
435 }
436
437 {
438 std::lock_guard<std::mutex> lock(mutex_);
439 open_ = false;
440 }
441 condition_.notify_all();
442 }
443
444 Libcam2OpenCV camera_;
445 mutable std::mutex mutex_;
446 std::condition_variable condition_;
447 cv::Mat latest_frame_;
448 long long latest_timestamp_ms_{0};
449 std::chrono::steady_clock::time_point latest_capture_timestamp_{};
450 std::uint64_t frame_sequence_{0};
451 std::uint64_t delivered_sequence_{0};
452 bool open_{false};
453 std::string last_error_;
454 float current_focus_position_{-1.0f};
455};
456#endif
457
458std::unique_ptr<CameraCaptureBackend> makeCameraBackend(CameraCapture::Options options) {
459 const auto effective_preference =
460 effectiveBackendPreference(options.backend_preference);
461
462 switch (effective_preference) {
464#if defined(ARGUS_HAVE_LIBCAM2OPENCV)
465 {
466 auto backend =
467 std::make_unique<Libcamera2OpenCvBackend>(options.camera_index, options.focus_position);
468 if (backend->isOpen()) {
469 return backend;
470 }
471 std::cerr << "[CameraCapture] libcamera2opencv backend unavailable; "
472 "falling back to OpenCV VideoCapture."
473 << std::endl;
474 }
475#endif
476 [[fallthrough]];
478 auto backend = std::make_unique<OpenCvVideoCaptureBackend>(options.camera_index);
479 if (backend->isOpen()) {
480 return backend;
481 }
482 return nullptr;
483 }
485#if defined(ARGUS_HAVE_LIBCAM2OPENCV)
486 {
487 auto backend =
488 std::make_unique<Libcamera2OpenCvBackend>(options.camera_index, options.focus_position);
489 if (backend->isOpen()) {
490 return backend;
491 }
492 return nullptr;
493 }
494#else
495 std::cerr << "[CameraCapture] requested backend '"
496 << backendPreferenceToString(effective_preference)
497 << "' is not available in this build. "
498 "Configure with -DARGUS_ENABLE_LIBCAMERA2OPENCV=ON "
499 "and install the required libcamera/turbojpeg dependencies."
500 << std::endl;
501 return nullptr;
502#endif
503 }
504 return nullptr;
505}
506
508 : CameraCapture(Options{camera_index, BackendPreference::Auto}) {}
509
511 : backend_preference_(options.backend_preference), current_focus_position_(options.focus_position) {
512 backend_ = makeCameraBackend(options);
513}
514
516
518 if (!backend_) {
519 return false;
520 }
521 return backend_->waitForNextFrame(output_event);
522}
523
524std::string CameraCapture::backendName() const {
525 if (!backend_) {
526 return "CLOSED";
527 }
528 return backend_->backendName();
529}
530
532 if (!backend_) {
533 return "UNAVAILABLE";
534 }
535 return backend_->backendImplementation();
536}
537
539 return backend_preference_;
540}
541
543 // Clamp position to valid range
544 position = std::max(-1.0f, std::min(1.0f, position));
545 current_focus_position_ = position;
546
547 if (!backend_) {
548 return false;
549 }
550 return backend_->setFocusPosition(position);
551}
552
554 if (!backend_) {
555 return current_focus_position_;
556 }
557 return backend_->getFocusPosition();
558}
std::unique_ptr< CameraCaptureBackend > makeCameraBackend(CameraCapture::Options options)
Camera frame acquisition interface for ARGUS.
virtual std::string backendImplementation() const =0
virtual bool isOpen() const noexcept=0
virtual float getFocusPosition() const
virtual bool waitForNextFrame(FrameEvent &output_event)=0
virtual std::string backendName() const =0
virtual bool setFocusPosition(float position)
virtual ~CameraCaptureBackend()=default
Camera capture facade with pluggable backend implementations.
BackendPreference backendPreference() const noexcept
Return requested backend preference.
BackendPreference
Backend selection policy.
@ Auto
Auto-select backend based on platform/runtime constraints.
@ OpenCvVideoCapture
Force OpenCV VideoCapture backend.
@ Libcamera2OpenCv
Force libcamera2opencv callback backend.
bool setFocusPosition(float position)
Set camera focus position when backend/hardware supports it.
std::string backendName() const
Return active backend name.
float getFocusPosition() const
Get current requested focus position.
bool waitForNextFrame(FrameEvent &output_event)
Wait for the next captured frame.
~CameraCapture()
Release camera hardware resources.
std::string backendImplementation() const
Return active backend implementation family.
CameraCapture(int camera_index=0)
Construct using explicit camera index.
OpenCvVideoCaptureBackend(int camera_index)
std::string backendImplementation() const override
std::string backendName() const override
bool isOpen() const noexcept override
bool waitForNextFrame(FrameEvent &output_event) override
Construction options for camera capture.
float focus_position
Focus request: -1.0 autofocus, 0.0 closest, 1.0 farthest.
int camera_index
Camera index passed to backend.
BackendPreference backend_preference
Requested backend policy.
Single captured frame and its timing metadata.
long long timestamp_ms
Epoch-style timestamp for log compatibility.
cv::Mat image_data
Captured image.
std::chrono::steady_clock::time_point capture_timestamp
Monotonic capture time.