3#include "opencv_client.hpp"
10#include <linux/videodev2.h>
15namespace yodau::backend {
19 [[maybe_unused]]
bool is_capture_device(
const std::string& path) {
20 const int fd = ::open(path.c_str(), O_RDONLY | O_NONBLOCK);
25 v4l2_capability cap {};
26 const int rc = ::ioctl(fd, VIDIOC_QUERYCAP, &cap);
33 std::uint32_t caps = cap.capabilities;
34 if (caps & V4L2_CAP_DEVICE_CAPS) {
35 caps = cap.device_caps;
38 const bool capture = (caps & V4L2_CAP_VIDEO_CAPTURE)
39 || (caps & V4L2_CAP_VIDEO_CAPTURE_MPLANE);
41 const bool streaming = (caps & V4L2_CAP_STREAMING);
43 return capture && streaming;
48int opencv_client::local_index_from_path(
const std::string& path)
const {
49 const std::string pref =
"/dev/video";
50 if (path.rfind(pref, 0) != 0) {
54 const auto tail = path.substr(pref.size());
58 = std::from_chars(tail.data(), tail.data() + tail.size(), idx);
59 if (res.ec != std::errc() || res.ptr != tail.data() + tail.size()) {
66frame opencv_client::mat_to_frame(
const cv::Mat& m)
const {
70 f.stride =
static_cast<
int>(m.step);
71 f.ts = std::chrono::steady_clock::now();
73 if (m.channels() == 3 && m.type() == CV_8UC3) {
74 f.format = pixel_format::bgr24;
75 f.data.assign(m.data, m.data + m.total() * m.elemSize());
80 if (m.channels() == 1) {
81 cv::cvtColor(m, bgr, cv::COLOR_GRAY2BGR);
82 }
else if (m.channels() == 4) {
83 cv::cvtColor(m, bgr, cv::COLOR_BGRA2BGR);
85 m.convertTo(bgr, CV_8UC3);
88 f.format = pixel_format::bgr24;
89 f.stride =
static_cast<
int>(bgr.step);
90 f.data.assign(bgr.data, bgr.data + bgr.total() * bgr.elemSize());
94void opencv_client::daemon_start(
95 const stream& s,
const std::function<
void(frame&&)>& on_frame,
96 const std::stop_token& st
98 const auto path = s.get_path();
101 const auto idx = local_index_from_path(path);
108 if (!cap.isOpened()) {
113 while (!st.stop_requested()) {
114 if (!cap.read(m) || m.empty()) {
115 if (s.is_looping() && s.get_type() == stream_type::file) {
116 cap.set(cv::CAP_PROP_POS_FRAMES, 0);
122 auto f = mat_to_frame(m);
123 on_frame(std::move(f));
127float opencv_client::cross_z(
128 const point& a,
const point& b,
const point& c
130 const float abx = b.x - a.x;
131 const float aby = b.y - a.y;
132 const float acx = c.x - a.x;
133 const float acy = c.y - a.y;
134 return abx * acy - aby * acx;
137int opencv_client::orient(
138 const point& a,
const point& b,
const point& c
140 const float v = cross_z(a, b, c);
141 if (v > point::epsilon) {
144 if (v < -point::epsilon) {
150bool opencv_client::between(
float a,
float b,
float c)
const {
151 return (a <= c + point::epsilon && c <= b + point::epsilon)
152 || (b <= c + point::epsilon && c <= a + point::epsilon);
155bool opencv_client::on_segment(
156 const point& a,
const point& b,
const point& c
158 return orient(a, b, c) == 0 && between(a.x, b.x, c.x)
159 && between(a.y, b.y, c.y);
162bool opencv_client::segments_intersect(
163 const point& p1,
const point& p2,
const point& q1,
const point& q2
165 const int o1 = orient(p1, p2, q1);
166 const int o2 = orient(p1, p2, q2);
167 const int o3 = orient(q1, q2, p1);
168 const int o4 = orient(q1, q2, p2);
170 if (o1 != o2 && o3 != o4) {
174 if (o1 == 0 && on_segment(p1, p2, q1)) {
177 if (o2 == 0 && on_segment(p1, p2, q2)) {
180 if (o3 == 0 && on_segment(q1, q2, p1)) {
183 if (o4 == 0 && on_segment(q1, q2, p2)) {
190std::optional<point> opencv_client::segment_intersection(
191 const point& p1,
const point& p2,
const point& q1,
const point& q2
193 const float rpx = p2.x - p1.x;
194 const float rpy = p2.y - p1.y;
195 const float spx = q2.x - q1.x;
196 const float spy = q2.y - q1.y;
198 const float den = rpx * spy - rpy * spx;
199 if (std::abs(den) <= point::epsilon) {
203 const float qpx = q1.x - p1.x;
204 const float qpy = q1.y - p1.y;
206 const float t = (qpx * spy - qpy * spx) / den;
207 const float u = (qpx * rpy - qpy * rpx) / den;
209 if (t < -point::epsilon || t > 1.0f + point::epsilon) {
212 if (u < -point::epsilon || u > 1.0f + point::epsilon) {
217 out.x = p1.x + t * rpx;
218 out.y = p1.y + t * rpy;
222void opencv_client::add_motion_event(
223 std::vector<event>& out,
const std::string& stream_name,
224 const std::chrono::steady_clock::time_point ts,
const point& pos_pct
227 e.kind = event_kind::motion;
228 e.stream_name = stream_name;
231 out.push_back(std::move(e));
234void opencv_client::consider_hit(
235 bool& hit,
float& best_dist2, point& best_a, point& best_b, point& best_pos,
236 const point& cur_pos_pct,
const point& a,
const point& b,
const point& pos
238 const float dx = pos.x - cur_pos_pct.x;
239 const float dy = pos.y - cur_pos_pct.y;
240 const float d2 = dx * dx + dy * dy;
242 if (d2 < best_dist2) {
251void opencv_client::test_line_segment_against_contour(
252 bool& hit,
float& best_dist2, point& best_a, point& best_b, point& best_pos,
253 const point& cur_pos_pct,
const std::vector<point>& contour_pct,
254 const point& a,
const point& b
256 if (contour_pct.size() < 2) {
260 for (size_t j = 1; j < contour_pct.size(); ++j) {
261 const auto& c1 = contour_pct[j - 1];
262 const auto& c2 = contour_pct[j];
264 if (segments_intersect(a, b, c1, c2)) {
265 point ip = cur_pos_pct;
266 const auto inter = segment_intersection(a, b, c1, c2);
267 if (inter.has_value()) {
272 hit, best_dist2, best_a, best_b, best_pos, cur_pos_pct, a, b, ip
277 const auto& c_last = contour_pct.back();
278 const auto& c_first = contour_pct.front();
279 if (segments_intersect(a, b, c_last, c_first)) {
280 point ip = cur_pos_pct;
281 const auto inter = segment_intersection(a, b, c_last, c_first);
282 if (inter.has_value()) {
287 hit, best_dist2, best_a, best_b, best_pos, cur_pos_pct, a, b, ip
292void opencv_client::process_tripwire_for_line(
293 std::vector<event>& out,
const stream& s,
const line& l,
294 const point& prev_pos,
const point& cur_pos_pct,
295 const std::vector<point>& contour_pct,
296 const std::chrono::steady_clock::time_point now
298 const auto& pts = l.points;
299 if (pts.size() < 2) {
306 point best_pos = cur_pos_pct;
307 float best_dist2 = std::numeric_limits<
float>::max();
309 for (size_t i = 1; i < pts.size(); ++i) {
310 test_line_segment_against_contour(
311 hit, best_dist2, best_a, best_b, best_pos, cur_pos_pct, contour_pct,
316 if (l.closed && pts.size() > 2) {
317 test_line_segment_against_contour(
318 hit, best_dist2, best_a, best_b, best_pos, cur_pos_pct, contour_pct,
319 pts.back(), pts.front()
327 const float prev_side = cross_z(best_a, best_b, prev_pos);
328 const float cur_side = cross_z(best_a, best_b, cur_pos_pct);
330 std::string dir =
"flat";
331 if (prev_side <= 0.0f && cur_side > 0.0f) {
333 }
else if (prev_side >= 0.0f && cur_side < 0.0f) {
337 if (l.dir == tripwire_dir::neg_to_pos) {
338 if (dir !=
"neg_to_pos") {
341 }
else if (l.dir == tripwire_dir::pos_to_neg) {
342 if (dir !=
"pos_to_neg") {
347 const int tripwire_cooldown_ms = 1200;
348 const std::string key = s.get_name() +
"|" + l.name +
"|" + dir;
350 bool allow_tripwire =
true;
352 std::scoped_lock lock(mtx);
353 auto it = last_tripwire_by_key.find(key);
354 if (it != last_tripwire_by_key.end()) {
356 = std::chrono::duration_cast<std::chrono::milliseconds>(
360 if (dt < tripwire_cooldown_ms) {
361 allow_tripwire =
false;
365 if (allow_tripwire) {
366 last_tripwire_by_key[key] = now;
370 if (!allow_tripwire) {
375 t.kind = event_kind::tripwire;
376 t.stream_name = s.get_name();
377 t.line_name = l.name;
379 t.pos_pct = best_pos;
382 std::cerr <<
"tripwire stream=" << t.stream_name <<
" line=" << t.line_name
383 <<
" dir=" << dir << std::endl;
385 out.push_back(std::move(t));
388std::optional<size_t> opencv_client::find_largest_contour_index(
389 const std::vector<std::vector<cv::Point>>& contours
391 if (contours.empty()) {
395 double max_area = 0.0;
398 for (size_t i = 0; i < contours.size(); ++i) {
399 const double area = cv::contourArea(contours[i]);
400 if (area > max_area) {
410opencv_client::motion_processor(
const stream& s,
const frame& f) {
411 std::vector<event> out;
413 if (f.data.empty() || f.width <= 0 || f.height <= 0) {
418 f.height, f.width, CV_8UC3,
const_cast<std::uint8_t*>(f.data.data()),
419 static_cast<size_t>(f.stride)
423 cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
424 cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0.0);
428 std::scoped_lock lock(mtx);
429 auto it = prev_gray_by_stream.find(s.get_name());
430 if (it == prev_gray_by_stream.end()) {
431 prev_gray_by_stream.emplace(s.get_name(), gray.clone());
434 prev_gray = it->second;
435 it->second = gray.clone();
439 cv::absdiff(prev_gray, gray, diff);
440 cv::threshold(diff, diff, 25, 255, cv::THRESH_BINARY);
442 cv::erode(diff, diff, cv::Mat(), cv::Point(-1, -1), 1);
443 cv::dilate(diff, diff, cv::Mat(), cv::Point(-1, -1), 2);
445 std::vector<std::vector<cv::Point>> contours;
447 diff, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE
450 if (contours.empty()) {
454 const auto max_i_opt = find_largest_contour_index(contours);
455 if (!max_i_opt.has_value()) {
459 const size_t max_i = *max_i_opt;
460 const double max_area = cv::contourArea(contours[max_i]);
462 const double min_area = 0.001 *
static_cast<
double>(diff.rows * diff.cols);
463 if (max_area < min_area) {
467 std::vector<cv::Point> approx;
469 const double eps = 3.0;
470 cv::approxPolyDP(contours[max_i], approx, eps,
true);
473 std::vector<point> contour_pct;
474 contour_pct.reserve(approx.size());
476 for (
const auto& pt : approx) {
478 p.x =
static_cast<
float>(pt.x) * 100.0f /
static_cast<
float>(f.width);
479 p.y =
static_cast<
float>(pt.y) * 100.0f /
static_cast<
float>(f.height);
480 contour_pct.push_back(p);
490 bbox2f motion_box {};
491 bool motion_box_ok =
false;
493 if (!contour_pct.empty()) {
494 motion_box.min_x = 100.0f;
495 motion_box.min_y = 100.0f;
496 motion_box.max_x = 0.0f;
497 motion_box.max_y = 0.0f;
499 for (
const auto& p : contour_pct) {
500 if (p.x < motion_box.min_x) {
501 motion_box.min_x = p.x;
503 if (p.y < motion_box.min_y) {
504 motion_box.min_y = p.y;
506 if (p.x > motion_box.max_x) {
507 motion_box.max_x = p.x;
509 if (p.y > motion_box.max_y) {
510 motion_box.max_y = p.y;
514 motion_box_ok =
true;
517 const int nz = cv::countNonZero(diff);
518 const int total = diff.rows * diff.cols;
519 const double ratio = total > 0 ?
static_cast<
double>(nz) / total : 0.0;
525 const double min_ratio = 0.02;
526 const int cooldown_ms = 150;
528 if (ratio < min_ratio) {
532 const auto now = std::chrono::steady_clock::now();
534 std::scoped_lock lock(mtx);
535 auto it = last_emit_by_stream.find(s.get_name());
536 if (it != last_emit_by_stream.end()) {
538 = std::chrono::duration_cast<std::chrono::milliseconds>(
542 if (dt < cooldown_ms) {
546 last_emit_by_stream[s.get_name()] = now;
549 cv::Moments mm = cv::moments(contours[max_i]);
553 cx = mm.m10 / mm.m00;
554 cy = mm.m01 / mm.m00;
556 cx =
static_cast<
double>(f.width) * 0.5;
557 cy =
static_cast<
double>(f.height) * 0.5;
560 const point cur_pos_pct {
static_cast<
float>(cx * 100.0 / f.width),
561 static_cast<
float>(cy * 100.0 / f.height) };
564 bool has_prev =
false;
566 std::scoped_lock lock(mtx);
567 auto it = last_pos_by_stream.find(s.get_name());
568 if (it != last_pos_by_stream.end()) {
569 prev_pos = it->second;
572 last_pos_by_stream[s.get_name()] = cur_pos_pct;
576 const auto lines = s.lines_snapshot();
577 for (
const auto& lp : lines) {
582 const auto& pts = lp->points;
589 line_box.min_x = 100.0f;
590 line_box.min_y = 100.0f;
591 line_box.max_x = 0.0f;
592 line_box.max_y = 0.0f;
594 for (
const auto& p : pts) {
595 if (p.x < line_box.min_x) {
596 line_box.min_x = p.x;
598 if (p.y < line_box.min_y) {
599 line_box.min_y = p.y;
601 if (p.x > line_box.max_x) {
602 line_box.max_x = p.x;
604 if (p.y > line_box.max_y) {
605 line_box.max_y = p.y;
610 = !(line_box.max_x < motion_box.min_x
611 || line_box.min_x > motion_box.max_x);
614 = !(line_box.max_y < motion_box.min_y
615 || line_box.min_y > motion_box.max_y);
617 if (!(x_overlap && y_overlap)) {
622 process_tripwire_for_line(
623 out, s, *lp, prev_pos, cur_pos_pct, contour_pct, now
628 add_motion_event(out, s.get_name(), now, cur_pos_pct);
630 const int grid_step = 24;
631 const int max_bubbles = 80;
634 for (
int y = 0; y < diff.rows; y += grid_step) {
635 const std::uint8_t* row = diff.ptr<std::uint8_t>(y);
636 for (
int x = 0; x < diff.cols; x += grid_step) {
642 p.x =
static_cast<
float>(x) * 100.0f /
static_cast<
float>(f.width);
643 p.y =
static_cast<
float>(y) * 100.0f /
static_cast<
float>(f.height);
645 add_motion_event(out, s.get_name(), now, p);
648 if (bubbled >= max_bubbles) {
653 if (bubbled >= max_bubbles) {
661stream_manager::daemon_start_fn opencv_client::daemon_start_fn() {
663 const stream& s, std::function<
void(frame&&)> on_frame,
665 ) { daemon_start(s, on_frame, st); };
668stream_manager::frame_processor_fn opencv_client::frame_processor_fn() {
669 return [
this](
const stream& s,
const frame& f) {
670 return motion_processor(s, f);
675 opencv_client& global_opencv_client() {
676 static opencv_client inst;
681void opencv_daemon_start(
682 const stream& s,
const std::function<
void(frame&&)>& on_frame,
683 const std::stop_token& st
685 global_opencv_client().daemon_start(s, on_frame, st);
688std::vector<event> opencv_motion_processor(
const stream& s,
const frame& f) {
689 return global_opencv_client().motion_processor(s, f);