ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
edge_dnn.cpp
Go to the documentation of this file.
1#include "edge_dnn.hpp"
2
4
5#include <algorithm>
6#include <array>
7#include <cctype>
8#include <chrono>
9#include <cmath>
10#include <filesystem>
11#include <fstream>
12#include <iostream>
13#include <iterator>
14#include <limits>
15#include <set>
16#include <stdexcept>
17#include <string_view>
18#include <vector>
19
20#include <opencv2/core/persistence.hpp>
21#include <opencv2/core/version.hpp>
22#include <opencv2/dnn.hpp>
23#include <opencv2/imgproc.hpp>
24
25namespace acmxvk::dnn {
26 namespace {
27
28 struct BackendState {
29 bool selected = false;
30 };
31
32 struct TimedOutput {
33 cv::Mat output;
34 double milliseconds = std::numeric_limits<double>::infinity();
35 };
36
37 [[nodiscard]] bool backendAvailable(cv::dnn::Backend backend,
38 cv::dnn::Target target) {
39 try {
40 const std::vector<cv::dnn::Target> available =
41 cv::dnn::getAvailableTargets(backend);
42 return std::find(available.begin(), available.end(), target) !=
43 available.end();
44 } catch (const cv::Exception &) {
45 return false;
46 }
47 }
48
49 void setCpuBackend(cv::dnn::Net &net) {
50 net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
51 net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
52 }
53
54 void setCudaBackend(cv::dnn::Net &net, bool fp16) {
55 net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
56 net.setPreferableTarget(fp16 ? cv::dnn::DNN_TARGET_CUDA_FP16
57 : cv::dnn::DNN_TARGET_CUDA);
58 }
59
60 [[nodiscard]] cv::Mat runForward(cv::dnn::Net &net,
61 const cv::Mat &blob,
62 const cv::String &input_name,
63 const cv::String &output_name) {
64 if (input_name.empty()) {
65 net.setInput(blob);
66 } else {
67 net.setInput(blob, input_name);
68 }
69 return output_name.empty() ? net.forward()
70 : net.forward(output_name);
71 }
72
74 cv::dnn::Net &net, const cv::Mat &blob,
75 const cv::String &input_name,
76 const cv::String &output_name) {
77 static_cast<void>(runForward(net, blob, input_name, output_name));
78
79 TimedOutput measured;
80 constexpr int TIMED_RUNS = 2;
81 const auto start = std::chrono::steady_clock::now();
82 for (int run = 0; run < TIMED_RUNS; ++run) {
83 measured.output =
84 runForward(net, blob, input_name, output_name);
85 }
86 measured.milliseconds =
87 std::chrono::duration<double, std::milli>(
88 std::chrono::steady_clock::now() - start)
89 .count() /
90 TIMED_RUNS;
91 return measured;
92 }
93
94 [[nodiscard]] cv::Mat selectBackendAndForward(
95 cv::dnn::Net &net, BackendState &state, const cv::Mat &blob,
96 const cv::String &input_name,
97 const cv::String &output_name) {
98 if (state.selected) {
99 return runForward(net, blob, input_name, output_name);
100 }
101 state.selected = true;
102
103 const bool fp16_available = backendAvailable(
104 cv::dnn::DNN_BACKEND_CUDA, cv::dnn::DNN_TARGET_CUDA_FP16);
105 const bool fp32_available = backendAvailable(
106 cv::dnn::DNN_BACKEND_CUDA, cv::dnn::DNN_TARGET_CUDA);
107 if (!fp16_available && !fp32_available) {
108 setCpuBackend(net);
109 std::cout << "acmxvk: DNN backend: CPU (CUDA unavailable)\n";
110 return runForward(net, blob, input_name, output_name);
111 }
112
113 setCpuBackend(net);
114 TimedOutput cpu =
115 benchmarkBackend(net, blob, input_name, output_name);
116 const bool use_fp16 = fp16_available;
117 try {
118 setCudaBackend(net, use_fp16);
119 TimedOutput cuda =
120 benchmarkBackend(net, blob, input_name, output_name);
121 if (cuda.milliseconds < cpu.milliseconds) {
122 std::cout << "acmxvk: DNN backend: CUDA "
123 << (use_fp16 ? "FP16" : "FP32") << " ("
124 << cuda.milliseconds << " ms vs CPU "
125 << cpu.milliseconds << " ms)\n";
126 return cuda.output;
127 }
128 setCpuBackend(net);
129 std::cout << "acmxvk: DNN backend: CPU (" << cpu.milliseconds
130 << " ms vs CUDA " << cuda.milliseconds << " ms)\n";
131 return cpu.output;
132 } catch (const cv::Exception &error) {
133 setCpuBackend(net);
134 std::cerr << "acmxvk: CUDA DNN benchmark failed; using CPU ("
135 << cpu.milliseconds << " ms): " << error.what()
136 << '\n';
137 return cpu.output;
138 }
139 }
140
141 [[nodiscard]] cv::String lastOutputName(const cv::dnn::Net &net) {
142 const std::vector<cv::String> names =
143 net.getUnconnectedOutLayersNames();
144 return names.empty() ? cv::String() : names.back();
145 }
146
147 [[nodiscard]] cv::Mat spatialPlane(const cv::Mat &output) {
148 if (output.dims == 4 && output.size[0] == 1 &&
149 output.size[1] == 1 && output.type() == CV_32F) {
150 return cv::Mat(output.size[2], output.size[3], CV_32F,
151 const_cast<float *>(output.ptr<float>(0, 0)));
152 }
153 if (output.dims == 2 && output.type() == CV_32F) {
154 return output;
155 }
156 return {};
157 }
158
159 void validateMapKeys(const cv::FileNode &node,
160 const std::set<std::string> &allowed,
161 std::string_view context) {
162 if (!node.isMap()) {
163 throw std::runtime_error(std::string(context) +
164 " must be a YAML mapping");
165 }
166 for (auto iterator = node.begin(); iterator != node.end();
167 ++iterator) {
168 const std::string name = (*iterator).name();
169 if (!allowed.contains(name)) {
170 throw std::runtime_error(std::string(context) +
171 " contains unsupported field '" +
172 name + "'");
173 }
174 }
175 }
176
177 [[nodiscard]] std::string readString(const cv::FileNode &node,
178 std::string_view context,
179 bool allow_empty = false) {
180 if (node.empty() || !node.isString()) {
181 throw std::runtime_error(std::string(context) +
182 " must be a string");
183 }
184 std::string value;
185 node >> value;
187 allow_empty);
188 return value;
189 }
190
191 [[nodiscard]] double readNumber(const cv::FileNode &node,
192 double default_value,
193 std::string_view context) {
194 if (node.empty()) {
195 return default_value;
196 }
197 if (!node.isInt() && !node.isReal()) {
198 throw std::runtime_error(std::string(context) +
199 " must be numeric");
200 }
201 const double value = node.real();
202 if (!std::isfinite(value)) {
203 throw std::runtime_error(std::string(context) +
204 " must be finite");
205 }
206 return value;
207 }
208
209 [[nodiscard]] int readInteger(const cv::FileNode &node,
210 int default_value,
211 std::string_view context) {
212 const double value = readNumber(node, default_value, context);
213 if (std::trunc(value) != value ||
214 value < std::numeric_limits<int>::min() ||
215 value > std::numeric_limits<int>::max()) {
216 throw std::runtime_error(std::string(context) +
217 " must be an integer");
218 }
219 return static_cast<int>(value);
220 }
221
222 [[nodiscard]] bool readBoolean(const cv::FileNode &node,
223 bool default_value,
224 std::string_view context) {
225 if (node.empty()) {
226 return default_value;
227 }
228 if (node.isInt() || node.isReal()) {
229 const double value = node.real();
230 if (value == 0.0) {
231 return false;
232 }
233 if (value == 1.0) {
234 return true;
235 }
236 } else if (node.isString()) {
237 std::string value;
238 node >> value;
239 std::transform(value.begin(), value.end(), value.begin(),
240 [](unsigned char character) {
241 return static_cast<char>(
242 std::tolower(character));
243 });
244 if (value == "true") {
245 return true;
246 }
247 if (value == "false") {
248 return false;
249 }
250 }
251 throw std::runtime_error(std::string(context) +
252 " must be true or false");
253 }
254
255 [[nodiscard]] std::string readYamlText(
256 const std::filesystem::path &path) {
257 input::validate_text_file(path, "ONNX YAML configuration");
258 std::ifstream stream(path, std::ios::binary);
259 if (!stream) {
260 throw std::runtime_error(
261 "unable to open ONNX YAML configuration: " +
262 path.string());
263 }
264 std::string text((std::istreambuf_iterator<char>(stream)),
265 std::istreambuf_iterator<char>());
266 if (!text.starts_with("%YAML")) {
267 const std::size_t first_content =
268 text.find_first_not_of(" \t\r\n");
269 const bool has_document_marker =
270 first_content != std::string::npos &&
271 text.compare(first_content, 3, "---") == 0;
272 text.insert(0, has_document_marker ? "%YAML:1.0\n"
273 : "%YAML:1.0\n---\n");
274 }
275 return text;
276 }
277
278 void validateTensorName(std::string_view name) {
279 if (name.empty()) {
280 return;
281 }
282 if (name.size() > 256U ||
283 !std::all_of(name.begin(), name.end(), [](char value) {
284 const auto character =
285 static_cast<unsigned char>(value);
286 return std::isalnum(character) != 0 || value == '_' ||
287 value == ':' || value == '/' || value == '.' ||
288 value == '-';
289 })) {
290 throw std::runtime_error(
291 "model.input contains unsupported characters");
292 }
293 }
294
295 [[nodiscard]] cv::Mat buildHardenedFloatAlpha(
296 const cv::Mat &image, const cv::Mat &mask, float black_point,
297 float white_point) {
298 constexpr int MAXIMUM_WORK_DIMENSION = 512;
299 const int image_max_dimension = std::max(image.cols, image.rows);
300 const double work_scale =
301 image_max_dimension > MAXIMUM_WORK_DIMENSION
302 ? static_cast<double>(MAXIMUM_WORK_DIMENSION) /
303 image_max_dimension
304 : 1.0;
305 const cv::Size work_size(
306 std::max(1, cvRound(image.cols * work_scale)),
307 std::max(1, cvRound(image.rows * work_scale)));
308
309 cv::Mat soft;
310 if (mask.channels() == 1) {
311 mask.convertTo(soft, CV_32F,
312 mask.depth() == CV_8U ? 1.0 / 255.0 : 1.0);
313 } else {
314 cv::Mat gray;
315 cv::cvtColor(mask, gray, cv::COLOR_BGR2GRAY);
316 gray.convertTo(soft, CV_32F,
317 gray.depth() == CV_8U ? 1.0 / 255.0 : 1.0);
318 }
319 if (soft.size() != work_size) {
320 cv::resize(soft, soft, work_size, 0, 0, cv::INTER_LINEAR);
321 }
322 cv::threshold(soft, soft, 1.0, 1.0, cv::THRESH_TRUNC);
323 cv::threshold(soft, soft, 0.0, 0.0, cv::THRESH_TOZERO);
324
325 cv::Mat binary;
326 cv::threshold(soft, binary, 0.5F, 255.0F, cv::THRESH_BINARY);
327 binary.convertTo(binary, CV_8U);
328 const auto scaled_kernel_size = [work_scale](int full_size) {
329 int size = std::max(1, cvRound(full_size * work_scale));
330 if ((size & 1) == 0) {
331 ++size;
332 }
333 return size;
334 };
335 const int open_size = scaled_kernel_size(3);
336 const int close_size = scaled_kernel_size(7);
337 const int erode_size = scaled_kernel_size(3);
338 if (open_size > 1) {
339 const cv::Mat kernel = cv::getStructuringElement(
340 cv::MORPH_ELLIPSE, cv::Size(open_size, open_size));
341 cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel);
342 }
343 if (close_size > 1) {
344 const cv::Mat kernel = cv::getStructuringElement(
345 cv::MORPH_ELLIPSE, cv::Size(close_size, close_size));
346 cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, kernel);
347 }
348
349 cv::Mat labels;
350 cv::Mat stats;
351 cv::Mat centroids;
352 const int label_count = cv::connectedComponentsWithStats(
353 binary, labels, stats, centroids, 8, CV_32S);
354 if (label_count > 1) {
355 int best_label = -1;
356 int best_area = 0;
357 for (int label = 1; label < label_count; ++label) {
358 const int area = stats.at<int>(label, cv::CC_STAT_AREA);
359 if (area > best_area) {
360 best_area = area;
361 best_label = label;
362 }
363 }
364 const int minimum_area =
365 (binary.cols * binary.rows) / 200;
366 if (best_label > 0 && best_area >= minimum_area) {
367 cv::compare(labels, best_label, binary, cv::CMP_EQ);
368 }
369 }
370 if (erode_size > 1) {
371 const cv::Mat kernel = cv::getStructuringElement(
372 cv::MORPH_ELLIPSE, cv::Size(erode_size, erode_size));
373 cv::erode(binary, binary, kernel);
374 }
375
376 cv::Mat silhouette;
377 binary.convertTo(silhouette, CV_32F, 1.0 / 255.0);
378 cv::multiply(soft, silhouette, soft);
379 cv::GaussianBlur(soft, soft, cv::Size(), 1.2 * work_scale);
380
381 const float range =
382 std::max(white_point - black_point, 1.0e-6F);
383 soft.convertTo(soft, CV_32F, 1.0F / range,
384 -black_point / range);
385 cv::threshold(soft, soft, 1.0, 1.0, cv::THRESH_TRUNC);
386 cv::threshold(soft, soft, 0.0, 0.0, cv::THRESH_TOZERO);
387 cv::pow(soft, 1.6, soft);
388 if (soft.size() != image.size()) {
389 cv::resize(soft, soft, image.size(), 0, 0, cv::INTER_LINEAR);
390 }
391 return soft;
392 }
393
394 } // namespace
395
397 cv::dnn::Net net;
398 BackendState backend;
399 cv::Size input_size{224, 224};
401 double scale = 1.0 / 255.0;
402 cv::Scalar mean{0.0, 0.0, 0.0};
403 bool swap_rb = true;
404 bool dynamic_shape = false;
410 bool multiple_outputs = false;
411 cv::String input_name;
412 cv::String output_name;
413 cv::Mat blob;
414 cv::Mat work;
415 cv::Mat converted;
416 cv::Mat smoothed;
417
418 explicit Impl(const std::string &configuration_path) {
419 const std::filesystem::path yaml_path(configuration_path);
420 if (!std::filesystem::is_regular_file(yaml_path)) {
421 throw std::runtime_error(
422 "ONNX configuration is not a regular file: " +
423 configuration_path);
424 }
425
426 const std::string yaml_text = readYamlText(yaml_path);
427 cv::FileStorage storage(yaml_text,
428 cv::FileStorage::READ |
429 cv::FileStorage::MEMORY |
430 cv::FileStorage::FORMAT_YAML);
431 if (!storage.isOpened()) {
432 throw std::runtime_error(
433 "unable to parse ONNX YAML configuration: " +
434 configuration_path);
435 }
436 const cv::FileNode root = storage.root();
437 validateMapKeys(root, {"model", "preprocessing", "postprocessing"},
438 "ONNX YAML root");
439
440 const cv::FileNode model = root["model"];
441 validateMapKeys(model, {"path", "input"}, "model");
442 const std::string configured_model_path =
443 readString(model["path"], "model.path");
444 std::string configured_input_name;
445 if (!model["input"].empty()) {
446 if (!model["input"].isString()) {
447 throw std::runtime_error("model.input must be a string");
448 }
449 model["input"] >> configured_input_name;
450 validateTensorName(configured_input_name);
451 }
452 input_name = configured_input_name;
453
454 std::filesystem::path model_path(configured_model_path);
455 if (model_path.is_relative()) {
456 model_path = yaml_path.parent_path() / model_path;
457 }
458 model_path = model_path.lexically_normal();
459 if (!std::filesystem::is_regular_file(model_path)) {
460 throw std::runtime_error(
461 "ONNX model referenced by YAML is not a regular file: " +
462 model_path.string());
463 }
464
465 const cv::FileNode preprocessing = root["preprocessing"];
466 if (!preprocessing.empty()) {
467 validateMapKeys(preprocessing,
468 {"width", "height", "scale", "swap_rb",
469 "mean", "dynamic", "alignment"},
470 "preprocessing");
471 input_size.width = readInteger(preprocessing["width"], 224,
472 "preprocessing.width");
473 input_size.height = readInteger(preprocessing["height"], 224,
474 "preprocessing.height");
475 scale = readNumber(preprocessing["scale"], 1.0 / 255.0,
476 "preprocessing.scale");
477 swap_rb = readBoolean(preprocessing["swap_rb"], true,
478 "preprocessing.swap_rb");
479 dynamic_shape = readBoolean(preprocessing["dynamic"], false,
480 "preprocessing.dynamic");
481 shape_alignment = readInteger(preprocessing["alignment"], 4,
482 "preprocessing.alignment");
483 const cv::FileNode mean_node = preprocessing["mean"];
484 if (!mean_node.empty()) {
485 if (!mean_node.isSeq() || mean_node.size() != 3U) {
486 throw std::runtime_error(
487 "preprocessing.mean must contain three numbers");
488 }
489 for (int index = 0; index < 3; ++index) {
490 mean[index] = readNumber(
491 mean_node[index], 0.0, "preprocessing.mean");
492 }
493 }
494 }
495 if (input_size.width < 0 || input_size.width > 16384 ||
496 input_size.height < 0 || input_size.height > 16384 ||
497 (!dynamic_shape &&
498 (input_size.width == 0 || input_size.height == 0))) {
499 throw std::runtime_error(
500 "preprocessing dimensions are outside the supported range");
501 }
502 if (std::abs(scale) > 1.0e6 ||
503 std::any_of(&mean[0], &mean[0] + 3, [](double value) {
504 return !std::isfinite(value) || std::abs(value) > 1.0e6;
505 })) {
506 throw std::runtime_error(
507 "preprocessing scale or mean is outside the supported range");
508 }
510 throw std::runtime_error(
511 "preprocessing.alignment must be between 1 and 1024");
512 }
513
515 dynamic_shape && input_size.width > 0 &&
516 input_size.height > 0 && input_size.width <= 256 &&
517 input_size.height <= 256;
518 const cv::FileNode postprocessing = root["postprocessing"];
519 if (!postprocessing.empty()) {
520 validateMapKeys(postprocessing, {"bilateral"},
521 "postprocessing");
522 const cv::FileNode bilateral = postprocessing["bilateral"];
523 if (!bilateral.empty() && bilateral.isMap()) {
524 validateMapKeys(
525 bilateral,
526 {"enabled", "diameter", "sigma_color", "sigma_space"},
527 "postprocessing.bilateral");
528 bilateral_smoothing = readBoolean(
529 bilateral["enabled"], bilateral_smoothing,
530 "postprocessing.bilateral.enabled");
531 bilateral_diameter = readInteger(
532 bilateral["diameter"], bilateral_diameter,
533 "postprocessing.bilateral.diameter");
534 bilateral_sigma_color = readNumber(
535 bilateral["sigma_color"], bilateral_sigma_color,
536 "postprocessing.bilateral.sigma_color");
537 bilateral_sigma_space = readNumber(
538 bilateral["sigma_space"], bilateral_sigma_space,
539 "postprocessing.bilateral.sigma_space");
540 } else if (!bilateral.empty()) {
541 bilateral_smoothing = readBoolean(
542 bilateral, bilateral_smoothing,
543 "postprocessing.bilateral");
544 }
545 }
547 bilateral_sigma_color < 0.0 ||
548 bilateral_sigma_color > 1.0e6 ||
549 bilateral_sigma_space < 0.0 ||
550 bilateral_sigma_space > 1.0e6) {
551 throw std::runtime_error(
552 "bilateral smoothing values are outside the supported range");
553 }
554 if ((bilateral_diameter & 1) == 0) {
556 }
557
558#if defined(CV_VERSION_MAJOR) && (CV_VERSION_MAJOR >= 5)
559 net = cv::dnn::readNetFromONNX(model_path.string(),
560 cv::dnn::ENGINE_CLASSIC);
561#else
562 net = cv::dnn::readNetFromONNX(model_path.string());
563#endif
564 if (net.empty()) {
565 throw std::runtime_error("generic ONNX model is empty");
566 }
567 const std::vector<cv::String> names =
568 net.getUnconnectedOutLayersNames();
569 multiple_outputs = names.size() > 1U;
570 output_name = names.empty() ? cv::String() : names.back();
571 }
572
573 [[nodiscard]] cv::Size resolveInputSize(
574 const cv::Size &source_size) const {
575 if (!dynamic_shape) {
576 return input_size;
577 }
578 int width = input_size.width;
579 int height = input_size.height;
580 if (width <= 0 && height <= 0) {
581 width = source_size.width;
582 height = source_size.height;
583 } else if (width <= 0) {
584 width = cvRound(height *
585 static_cast<double>(source_size.width) /
586 std::max(source_size.height, 1));
587 } else if (height <= 0) {
588 height = cvRound(width *
589 static_cast<double>(source_size.height) /
590 std::max(source_size.width, 1));
591 }
592 const auto align_dimension = [this](int value) {
593 value = std::max(value, shape_alignment);
594 return std::max(
596 cvRound(static_cast<double>(value) / shape_alignment) *
598 };
599 return {align_dimension(width), align_dimension(height)};
600 }
601
602 [[nodiscard]] const cv::Mat &smoothOutput(const cv::Mat &source) {
603 if (!bilateral_smoothing) {
604 return source;
605 }
606 cv::bilateralFilter(source, smoothed, bilateral_diameter,
609 return smoothed;
610 }
611
612 void process(const cv::Mat &image, cv::Mat &result) {
613 if (image.empty()) {
614 result.release();
615 return;
616 }
617 const cv::Size frame_input_size = resolveInputSize(image.size());
618 if (frame_input_size != active_input_size) {
619 if (!active_input_size.empty()) {
620 backend.selected = false;
621 }
622 active_input_size = frame_input_size;
623 }
624 cv::dnn::blobFromImage(image, blob, scale, frame_input_size, mean,
625 swap_rb, false, CV_32F);
626 const cv::Mat raw = selectBackendAndForward(
628
629 if (multiple_outputs) {
630 const cv::Mat plane = spatialPlane(raw);
631 if (plane.empty()) {
632 throw std::runtime_error(
633 "generic ONNX multi-output result has no float plane");
634 }
635 cv::exp(-plane, work);
636 cv::add(work, cv::Scalar::all(1.0), work);
637 cv::divide(1.0, work, work);
638 cv::normalize(work, converted, 0, 255, cv::NORM_MINMAX,
639 CV_8U);
640 const cv::Mat &display = smoothOutput(converted);
641 cv::resize(display, converted, image.size(), 0, 0,
642 cv::INTER_LINEAR);
643 cv::cvtColor(converted, result, cv::COLOR_GRAY2BGR);
644 return;
645 }
646 if (raw.dims != 4 || raw.size[0] != 1 ||
647 raw.type() != CV_32F) {
648 throw std::runtime_error(
649 "generic ONNX output must be a 1xCxHxW float tensor");
650 }
651 const int channels = raw.size[1];
652 const int height = raw.size[2];
653 const int width = raw.size[3];
654 if (channels == 1) {
655 const cv::Mat plane(
656 height, width, CV_32F,
657 const_cast<float *>(raw.ptr<float>(0, 0)));
658 cv::normalize(plane, converted, 0, 255, cv::NORM_MINMAX,
659 CV_8U);
660 const cv::Mat &display = smoothOutput(converted);
661 cv::resize(display, converted, image.size(), 0, 0,
662 cv::INTER_LINEAR);
663 cv::cvtColor(converted, result, cv::COLOR_GRAY2BGR);
664 return;
665 }
666 if (channels < 3) {
667 throw std::runtime_error(
668 "generic ONNX output has fewer than three color channels");
669 }
670 std::vector<cv::Mat> planes;
671 planes.reserve(3);
672 for (int channel = 0; channel < 3; ++channel) {
673 planes.emplace_back(
674 height, width, CV_32F,
675 const_cast<float *>(raw.ptr<float>(0, channel)));
676 }
677 cv::merge(planes, work);
678 cv::normalize(work, converted, 0, 255, cv::NORM_MINMAX, CV_8U);
679 const cv::Mat &display = smoothOutput(converted);
680 cv::resize(display, converted, image.size(), 0, 0,
681 cv::INTER_LINEAR);
682 cv::cvtColor(converted, result, cv::COLOR_RGB2BGR);
683 }
684 };
685
687 const std::string &configuration_path)
688 : impl(std::make_unique<Impl>(configuration_path)) {}
689
691
692 void GenericOnnxProcessor::process(const cv::Mat &image,
693 cv::Mat &result) {
694 impl->process(image, result);
695 }
696
698 cv::dnn::Net net;
699 BackendState backend;
700 cv::String output_name;
701 cv::Mat blob;
702 cv::Mat work;
703 cv::Mat edge;
704
705 explicit Impl(const std::string &model_path) {
706 if (!std::filesystem::is_regular_file(model_path)) {
707 throw std::runtime_error("edge model is not a regular file: " +
708 model_path);
709 }
710#if defined(CV_VERSION_MAJOR) && (CV_VERSION_MAJOR >= 5)
711 net = cv::dnn::readNetFromONNX(model_path,
712 cv::dnn::ENGINE_CLASSIC);
713#else
714 net = cv::dnn::readNetFromONNX(model_path);
715#endif
716 if (net.empty()) {
717 throw std::runtime_error("DexiNed ONNX model is empty");
718 }
719 output_name = lastOutputName(net);
720 }
721
722 void process(const cv::Mat &image, cv::Mat &result) {
723 if (image.empty()) {
724 result.release();
725 return;
726 }
727
728 cv::dnn::blobFromImage(image, blob, 1.0, cv::Size(512, 512),
729 cv::Scalar(103.5, 116.2, 123.6), false,
730 false, CV_32F);
731 const cv::Mat raw =
732 selectBackendAndForward(net, backend, blob, {}, output_name);
733 const cv::Mat plane = spatialPlane(raw);
734 if (plane.empty()) {
735 throw std::runtime_error(
736 "DexiNed output does not contain a float edge plane");
737 }
738
739 cv::exp(-plane, work);
740 cv::add(work, cv::Scalar::all(1.0), work);
741 cv::divide(1.0, work, work);
742 cv::normalize(work, edge, 0, 255, cv::NORM_MINMAX, CV_8U);
743 cv::resize(edge, result, image.size(), 0, 0, cv::INTER_LINEAR);
744 }
745 };
746
747 EdgeDetector::EdgeDetector(const std::string &model_path)
748 : impl(std::make_unique<Impl>(model_path)) {}
749
750 EdgeDetector::~EdgeDetector() = default;
751
752 void EdgeDetector::process(const cv::Mat &image, cv::Mat &result) {
753 impl->process(image, result);
754 }
755
757 cv::dnn::Net net;
758 BackendState backend;
759 const cv::Size input_size{192, 192};
760 const cv::String input_name{"x"};
761 const cv::String output_name{"save_infer_model/scale_0.tmp_1"};
762 cv::Size source_size;
763 cv::Mat blob;
764 cv::Mat logit;
765 cv::Mat probability;
768
769 explicit Impl(const std::string &model_path) {
770 if (!std::filesystem::is_regular_file(model_path)) {
771 throw std::runtime_error(
772 "human segmentation model is not a regular file: " +
773 model_path);
774 }
775#if defined(CV_VERSION_MAJOR) && (CV_VERSION_MAJOR >= 5)
776 net = cv::dnn::readNetFromONNX(model_path,
777 cv::dnn::ENGINE_CLASSIC);
778#else
779 net = cv::dnn::readNetFromONNX(model_path);
780#endif
781 if (net.empty()) {
782 throw std::runtime_error("PP-HumanSeg ONNX model is empty");
783 }
784 }
785
786 [[nodiscard]] cv::Mat infer(const cv::Mat &image) {
787 if (image.empty()) {
788 return {};
789 }
790 source_size = image.size();
791 cv::dnn::blobFromImage(image, blob, 1.0 / 127.5, input_size,
792 cv::Scalar(127.5, 127.5, 127.5), false,
793 false, CV_32F);
794 const cv::Mat output = selectBackendAndForward(
796 if (output.dims != 4 || output.size[0] != 1 ||
797 output.size[1] < 2 || output.type() != CV_32F) {
798 throw std::runtime_error(
799 "PP-HumanSeg output is not a two-channel float mask");
800 }
801
802 const int height = output.size[2];
803 const int width = output.size[3];
804 const cv::Mat background(
805 height, width, CV_32F,
806 const_cast<float *>(output.ptr<float>(0, 0)));
807 const cv::Mat foreground(
808 height, width, CV_32F,
809 const_cast<float *>(output.ptr<float>(0, 1)));
810 cv::subtract(foreground, background, logit);
811 cv::exp(-logit, probability);
812 cv::add(probability, cv::Scalar::all(1.0), probability);
813 cv::divide(1.0, probability, probability);
814 cv::resize(probability, resized_mask, source_size, 0, 0,
815 cv::INTER_CUBIC);
816 if (previous_mask.empty() ||
817 previous_mask.size() != resized_mask.size()) {
819 } else {
820 cv::addWeighted(resized_mask, 0.6, previous_mask, 0.4, 0.0,
822 }
823 return previous_mask;
824 }
825 };
826
827 HumanSegmenter::HumanSegmenter(const std::string &model_path)
828 : impl(std::make_unique<Impl>(model_path)) {}
829
831
832 cv::Mat HumanSegmenter::infer(const cv::Mat &image) {
833 return impl->infer(image);
834 }
835
836 cv::Mat hardenedAlphaMask(const cv::Mat &image, const cv::Mat &mask,
837 float black_point, float white_point) {
838 if (image.empty() || mask.empty()) {
839 return {};
840 }
841 cv::Mat alpha;
842 buildHardenedFloatAlpha(image, mask, black_point, white_point)
843 .convertTo(alpha, CV_8U, 255.0);
844 return alpha;
845 }
846
847 cv::Mat isolateBody(const cv::Mat &image, const cv::Mat &mask,
848 float black_point, float white_point) {
849 if (image.empty() || mask.empty()) {
850 return image.clone();
851 }
852 const cv::Mat alpha =
853 hardenedAlphaMask(image, mask, black_point, white_point);
854 cv::Mat alpha_bgr;
855 cv::cvtColor(alpha, alpha_bgr, cv::COLOR_GRAY2BGR);
856 cv::Mat output;
857 cv::multiply(image, alpha_bgr, output, 1.0 / 255.0, CV_8UC3);
858 return output;
859 }
860
861} // namespace acmxvk::dnn
GLsizei GLsizei GLenum void * binary
EdgeDetector(const std::string &model_path)
Definition edge_dnn.cpp:747
void process(const cv::Mat &image, cv::Mat &result)
Definition edge_dnn.cpp:752
std::unique_ptr< Impl > impl
Definition edge_dnn.hpp:38
std::unique_ptr< Impl > impl
Definition edge_dnn.hpp:23
void process(const cv::Mat &image, cv::Mat &result)
Definition edge_dnn.cpp:692
GenericOnnxProcessor(const std::string &configuration_path)
Definition edge_dnn.cpp:686
cv::Mat infer(const cv::Mat &image)
Definition edge_dnn.cpp:832
HumanSegmenter(const std::string &model_path)
Definition edge_dnn.cpp:827
std::unique_ptr< Impl > impl
Definition edge_dnn.hpp:53
cv::Mat selectBackendAndForward(cv::dnn::Net &net, BackendState &state, const cv::Mat &blob, const cv::String &input_name, const cv::String &output_name)
Definition edge_dnn.cpp:94
double readNumber(const cv::FileNode &node, double default_value, std::string_view context)
Definition edge_dnn.cpp:191
std::string readYamlText(const std::filesystem::path &path)
Definition edge_dnn.cpp:255
cv::Mat buildHardenedFloatAlpha(const cv::Mat &image, const cv::Mat &mask, float black_point, float white_point)
Definition edge_dnn.cpp:295
int readInteger(const cv::FileNode &node, int default_value, std::string_view context)
Definition edge_dnn.cpp:209
std::string readString(const cv::FileNode &node, std::string_view context, bool allow_empty=false)
Definition edge_dnn.cpp:177
cv::String lastOutputName(const cv::dnn::Net &net)
Definition edge_dnn.cpp:141
void setCudaBackend(cv::dnn::Net &net, bool fp16)
Definition edge_dnn.cpp:54
TimedOutput benchmarkBackend(cv::dnn::Net &net, const cv::Mat &blob, const cv::String &input_name, const cv::String &output_name)
Definition edge_dnn.cpp:73
bool backendAvailable(cv::dnn::Backend backend, cv::dnn::Target target)
Definition edge_dnn.cpp:37
bool readBoolean(const cv::FileNode &node, bool default_value, std::string_view context)
Definition edge_dnn.cpp:222
void validateMapKeys(const cv::FileNode &node, const std::set< std::string > &allowed, std::string_view context)
Definition edge_dnn.cpp:159
cv::Mat runForward(cv::dnn::Net &net, const cv::Mat &blob, const cv::String &input_name, const cv::String &output_name)
Definition edge_dnn.cpp:60
cv::Mat spatialPlane(const cv::Mat &output)
Definition edge_dnn.cpp:147
void validateTensorName(std::string_view name)
Definition edge_dnn.cpp:278
cv::Mat hardenedAlphaMask(const cv::Mat &image, const cv::Mat &mask, float black_point, float white_point)
Definition edge_dnn.cpp:836
cv::Mat isolateBody(const cv::Mat &image, const cv::Mat &mask, float black_point, float white_point)
Definition edge_dnn.cpp:847
void validate_text_file(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes, std::size_t maximum_line_bytes)
void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty)
void process(const cv::Mat &image, cv::Mat &result)
Definition edge_dnn.cpp:722
Impl(const std::string &model_path)
Definition edge_dnn.cpp:705
cv::Size resolveInputSize(const cv::Size &source_size) const
Definition edge_dnn.cpp:573
Impl(const std::string &configuration_path)
Definition edge_dnn.cpp:418
const cv::Mat & smoothOutput(const cv::Mat &source)
Definition edge_dnn.cpp:602
void process(const cv::Mat &image, cv::Mat &result)
Definition edge_dnn.cpp:612
Impl(const std::string &model_path)
Definition edge_dnn.cpp:769
cv::Mat infer(const cv::Mat &image)
Definition edge_dnn.cpp:786