OpenShot Library | libopenshot 1.1.1
Loading...
Searching...
No Matches
CVObjectDetection.cpp
Go to the documentation of this file.
1
10// Copyright (c) 2008-2019 OpenShot Studios, LLC
11//
12// SPDX-License-Identifier: LGPL-3.0-or-later
13
14#include <fstream>
15#include <iomanip>
16#include <iostream>
17#include <algorithm>
18
19#include "CVObjectDetection.h"
20#include "Exceptions.h"
21#include "Logger.h"
22
23#define int64 int64_t
24#define uint64 uint64_t
25#include <opencv2/core/ocl.hpp>
26#undef uint64
27#undef int64
28#include "objdetectdata.pb.h"
29#include <google/protobuf/util/time_util.h>
30
31using namespace std;
32using namespace openshot;
33using google::protobuf::util::TimeUtil;
34
35namespace {
36
37bool LooksLikeTransposedYoloOutput(const cv::Mat& out, size_t classCount)
38{
39 // YOLO26 segmentation exports without end-to-end postprocessing use
40 // [1, attributes, candidates], e.g. [1, 116, 8400]:
41 // 4 box channels + class scores + optional mask coefficients.
42 return out.dims == 3 && out.size[0] == 1 && out.size[1] >= 4 &&
43 out.size[2] > out.size[1] &&
44 (classCount == 0 || out.size[1] >= 4 + static_cast<int>(classCount));
45}
46
47cv::Rect ScaledXYWHBox(
48 float centerX,
49 float centerY,
50 float width,
51 float height,
52 const cv::Size& frameDims,
53 int inputWidth,
54 int inputHeight)
55{
56 if (centerX <= 1.0f && centerY <= 1.0f && width <= 1.0f && height <= 1.0f) {
57 centerX *= static_cast<float>(frameDims.width);
58 width *= static_cast<float>(frameDims.width);
59 centerY *= static_cast<float>(frameDims.height);
60 height *= static_cast<float>(frameDims.height);
61 } else {
62 const float xFactor = static_cast<float>(frameDims.width) / static_cast<float>(inputWidth);
63 const float yFactor = static_cast<float>(frameDims.height) / static_cast<float>(inputHeight);
64 centerX *= xFactor;
65 width *= xFactor;
66 centerY *= yFactor;
67 height *= yFactor;
68 }
69
70 float left = centerX - width / 2.0f;
71 float top = centerY - height / 2.0f;
72 float right = centerX + width / 2.0f;
73 float bottom = centerY + height / 2.0f;
74
75 left = std::max(0.0f, std::min(left, static_cast<float>(frameDims.width - 1)));
76 top = std::max(0.0f, std::min(top, static_cast<float>(frameDims.height - 1)));
77 right = std::max(0.0f, std::min(right, static_cast<float>(frameDims.width)));
78 bottom = std::max(0.0f, std::min(bottom, static_cast<float>(frameDims.height)));
79
80 return cv::Rect(
81 static_cast<int>(left),
82 static_cast<int>(top),
83 std::max(0, static_cast<int>(right - left)),
84 std::max(0, static_cast<int>(bottom - top)));
85}
86
87std::vector<uint32_t> EncodeBinaryMaskRLE(const std::vector<uint8_t>& mask)
88{
89 std::vector<uint32_t> rle;
90 if (mask.empty())
91 return rle;
92
93 uint8_t current = 0;
94 uint32_t count = 0;
95 for (uint8_t value : mask) {
96 value = value ? 1 : 0;
97 if (value == current) {
98 ++count;
99 } else {
100 rle.push_back(count);
101 current = value;
102 count = 1;
103 }
104 }
105 rle.push_back(count);
106 return rle;
107}
108
109cv::Mat DecodeBinaryMaskRLE(const CVObjectMaskData& mask)
110{
111 cv::Mat image(mask.height, mask.width, CV_8UC1, cv::Scalar(0));
112 if (!mask.HasData())
113 return image;
114
115 const int total = mask.width * mask.height;
116 int offset = 0;
117 bool value = false;
118 uint8_t* data = image.ptr<uint8_t>();
119 for (uint32_t count : mask.rle) {
120 const int end = std::min(total, offset + static_cast<int>(count));
121 if (value) {
122 std::fill(data + offset, data + end, static_cast<uint8_t>(1));
123 }
124 offset = end;
125 value = !value;
126 if (offset >= total)
127 break;
128 }
129 return image;
130}
131
132CVObjectMaskData TransformMaskToBox(
133 const CVObjectMaskData& sourceMask,
134 const cv::Rect_<float>& sourceBox,
135 const cv::Rect_<float>& targetBox,
136 const cv::Size& frameDims)
137{
138 CVObjectMaskData result;
139 if (!sourceMask.HasData() || sourceBox.width <= 0.0f || sourceBox.height <= 0.0f ||
140 targetBox.width <= 0.0f || targetBox.height <= 0.0f ||
141 frameDims.width <= 0 || frameDims.height <= 0) {
142 return result;
143 }
144
145 const float scaleX = sourceMask.width / static_cast<float>(frameDims.width);
146 const float scaleY = sourceMask.height / static_cast<float>(frameDims.height);
147 const cv::Rect_<float> sourceMaskBox(
148 sourceBox.x * scaleX,
149 sourceBox.y * scaleY,
150 sourceBox.width * scaleX,
151 sourceBox.height * scaleY);
152 const cv::Rect_<float> targetMaskBox(
153 targetBox.x * scaleX,
154 targetBox.y * scaleY,
155 targetBox.width * scaleX,
156 targetBox.height * scaleY);
157 if (sourceMaskBox.width <= 0.0f || sourceMaskBox.height <= 0.0f)
158 return result;
159
160 const double xScale = targetMaskBox.width / sourceMaskBox.width;
161 const double yScale = targetMaskBox.height / sourceMaskBox.height;
162 cv::Mat transform = (cv::Mat_<double>(2, 3) <<
163 xScale, 0.0, targetMaskBox.x - xScale * sourceMaskBox.x,
164 0.0, yScale, targetMaskBox.y - yScale * sourceMaskBox.y);
165
166 cv::Mat source = DecodeBinaryMaskRLE(sourceMask);
167 cv::Mat transformed;
168 cv::warpAffine(
169 source, transformed, transform, source.size(),
170 cv::INTER_NEAREST, cv::BORDER_CONSTANT, cv::Scalar(0));
171 if (cv::countNonZero(transformed) == 0)
172 return result;
173
174 result.width = sourceMask.width;
175 result.height = sourceMask.height;
176 result.rle = EncodeBinaryMaskRLE(
177 std::vector<uint8_t>(transformed.data, transformed.data + transformed.total()));
178 return result;
179}
180
181CVObjectMaskData BuildMaskFromPrototype(
182 const cv::Mat& prototype,
183 const std::vector<float>& coefficients,
184 const cv::Rect& box,
185 const cv::Size& frameDims)
186{
187 CVObjectMaskData result;
188 if (prototype.dims != 4 || prototype.size[0] != 1 ||
189 prototype.size[1] != static_cast<int>(coefficients.size()))
190 return result;
191
192 const int channels = prototype.size[1];
193 const int maskHeight = prototype.size[2];
194 const int maskWidth = prototype.size[3];
195 const int maskPixels = maskWidth * maskHeight;
196 const float* protoData = reinterpret_cast<const float*>(prototype.data);
197
198 const int left = std::max(0, static_cast<int>(box.x * maskWidth / static_cast<float>(frameDims.width)));
199 const int top = std::max(0, static_cast<int>(box.y * maskHeight / static_cast<float>(frameDims.height)));
200 const int right = std::min(maskWidth, static_cast<int>((box.x + box.width) * maskWidth / static_cast<float>(frameDims.width)));
201 const int bottom = std::min(maskHeight, static_cast<int>((box.y + box.height) * maskHeight / static_cast<float>(frameDims.height)));
202 if (left >= right || top >= bottom)
203 return result;
204
205 std::vector<uint8_t> binary(maskPixels, 0);
206 for (int y = top; y < bottom; ++y) {
207 for (int x = left; x < right; ++x) {
208 const int pixel = y * maskWidth + x;
209 float value = 0.0f;
210 for (int channel = 0; channel < channels; ++channel) {
211 value += coefficients[channel] * protoData[channel * maskPixels + pixel];
212 }
213 binary[pixel] = value > 0.0f ? 1 : 0;
214 }
215 }
216
217 result.width = maskWidth;
218 result.height = maskHeight;
219 result.rle = EncodeBinaryMaskRLE(binary);
220 return result;
221}
222
223std::string LoadONNXModel(std::string modelPath, cv::dnn::Net *net)
224{
225#if CV_VERSION_MAJOR < 4 || (CV_VERSION_MAJOR == 4 && CV_VERSION_MINOR < 3)
226 return std::string("Failed to load ONNX model: YOLO requires OpenCV 4.3.0 or newer. "
227 "This OpenCV build is ") + CV_VERSION + ".";
228#else
229 try {
230 cv::dnn::Net loaded_net = cv::dnn::readNetFromONNX(modelPath);
231 if (net) {
232 *net = loaded_net;
233 }
234 return "";
235 } catch (const cv::Exception& e) {
236 std::string error_text = std::string("Failed to load ONNX model: ") + e.what();
237 if (error_text.find("Unsupported data type: FLOAT16") != std::string::npos) {
238 error_text = "Failed to load ONNX model: FLOAT16 is not supported by this OpenCV build. "
239 "Please use an FP32 ONNX model.";
240 }
241 return error_text;
242 } catch (const std::exception& e) {
243 return std::string("Failed to load ONNX model: ") + e.what();
244 } catch (...) {
245 return "Failed to load ONNX model: unknown error";
246 }
247#endif
248}
249
250}
251
252CVObjectDetection::CVObjectDetection(std::string processInfoJson, ProcessingController &processingController)
253: processingController(&processingController), processingDevice("CPU"), inpWidth(640), inpHeight(640), generateMasks(true){
254 confThreshold = 0.10;
255 nmsThreshold = 0.1;
256 SetJson(processInfoJson);
257}
258
259std::string CVObjectDetection::ValidateONNXModel(std::string modelPath)
260{
261 return LoadONNXModel(modelPath, nullptr);
262}
263
264void CVObjectDetection::setProcessingDevice(){
265 const std::string requestedDevice = processingDevice;
266 if (processingDevice == "CPU") {
267 net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
268 net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
269 Logger::Instance()->Log("Object Detection DNN device: requested CPU, selected CPU");
270 return;
271 }
272
273 if(processingDevice == "GPU" || processingDevice == "GPU_AUTO" || processingDevice == "GPU_CUDA"){
274 try {
275 const std::vector<cv::dnn::Target> targets = cv::dnn::getAvailableTargets(cv::dnn::DNN_BACKEND_CUDA);
276 if (std::find(targets.begin(), targets.end(), cv::dnn::DNN_TARGET_CUDA) != targets.end()) {
277 net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
278 net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
279 Logger::Instance()->Log("Object Detection DNN device: requested " + requestedDevice + ", selected CUDA");
280 return;
281 }
282 } catch (const cv::Exception&) {
283 }
284 }
285
286 if(processingDevice == "GPU_OPENCL"){
287 try {
288 const std::vector<cv::dnn::Target> targets = cv::dnn::getAvailableTargets(cv::dnn::DNN_BACKEND_OPENCV);
289 if (std::find(targets.begin(), targets.end(), cv::dnn::DNN_TARGET_OPENCL) != targets.end()) {
290 cv::ocl::setUseOpenCL(true);
291 net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
292 net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL);
293 Logger::Instance()->Log("Object Detection DNN device: requested " + requestedDevice + ", selected OpenCL");
294 return;
295 }
296 } catch (const cv::Exception&) {
297 }
298 }
299
300 processingDevice = "CPU";
301 net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
302 net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
303 Logger::Instance()->Log("Object Detection DNN device: requested " + requestedDevice + ", selected CPU");
304}
305
306void CVObjectDetection::detectObjectsClip(openshot::Clip &video, size_t _start, size_t _end, bool process_interval)
307{
308
309 start = _start; end = _end;
310
311 video.Open();
312
313 if(error){
314 return;
315 }
316
317 processingController->SetError(false, "");
318
319 if(modelPath.empty()) {
320 processingController->SetError(true, "Missing path to YOLO ONNX model file");
321 error = true;
322 return;
323 }
324 if(classesFile.empty()) {
325 processingController->SetError(true, "Missing path to class name file");
326 error = true;
327 return;
328 }
329
330 std::ifstream model_file(modelPath);
331 if(!model_file.good()){
332 processingController->SetError(true, "Incorrect path to YOLO ONNX model file");
333 error = true;
334 return;
335 }
336 std::ifstream classes_file(classesFile);
337 if(!classes_file.good()){
338 processingController->SetError(true, "Incorrect path to class name file");
339 error = true;
340 return;
341 }
342
343 // Load names of classes
344 classNames.clear();
345 std::string line;
346 while (std::getline(classes_file, line)) classNames.push_back(line);
347
348 // Load the network
349 std::string error_text = LoadONNXModel(modelPath, &net);
350 if (!error_text.empty()) {
351 processingController->SetError(true, error_text);
352 error = true;
353 return;
354 }
355 setProcessingDevice();
356
357 size_t frame_number;
358 if(!process_interval || end <= 1 || end-start == 0){
359 // Get total number of frames in video
360 start = (int)(video.Start() * video.Reader()->info.fps.ToFloat());
361 end = (int)(video.End() * video.Reader()->info.fps.ToFloat());
362 }
363
364 for (frame_number = start; frame_number <= end; frame_number++)
365 {
366 // Stop the feature tracker process
367 if(processingController->ShouldStop()){
368 return;
369 }
370
371 std::shared_ptr<openshot::Frame> f = video.GetFrame(frame_number);
372
373 // Grab OpenCV Mat image
374 cv::Mat cvimage = f->GetImageCV();
375
376 DetectObjects(cvimage, frame_number);
377
378 // Update progress
379 processingController->SetProgress(uint(100*(frame_number-start)/(end-start)));
380
381 }
382}
383
384void CVObjectDetection::DetectObjects(const cv::Mat &frame, size_t frameId){
385 // Get frame as OpenCV Mat
386 cv::Mat blob;
387
388 // Create a 4D blob from the frame.
389 cv::dnn::blobFromImage(frame, blob, 1/255.0, cv::Size(inpWidth, inpHeight), cv::Scalar(0,0,0), true, false);
390
391 std::vector<cv::Mat> outs;
392 try {
393 // Sets the input to the network
394 net.setInput(blob);
395 // Runs the forward pass to get output of the output layers
396 net.forward(outs, getOutputsNames(net));
397 } catch (const cv::Exception& e) {
398 processingController->SetError(true, std::string("Object detection inference failed: ") + e.what());
399 error = true;
400 return;
401 }
402
403 // Remove the bounding boxes with low confidence
404 postprocess(frame.size(), outs, frameId);
405
406}
407
408
409// Remove the bounding boxes with low confidence using non-maxima suppression
410void CVObjectDetection::postprocess(const cv::Size &frameDims, const std::vector<cv::Mat>& outs, size_t frameId)
411{
412 std::vector<int> classIds;
413 std::vector<float> confidences;
414 std::vector<cv::Rect> boxes;
415 std::vector<std::vector<ClassScore>> detectionClassScores;
416 std::vector<CVObjectMaskData> detectionMasks;
417 std::vector<int> objectIds;
418 const int maxClassCandidates = 5;
419
420 for (size_t i = 0; i < outs.size(); ++i) {
421 cv::Mat det = outs[i];
422
423 if (LooksLikeTransposedYoloOutput(det, classNames.size())) {
424 const int attributes = det.size[1];
425 const int candidates = det.size[2];
426 const int classCount = !classNames.empty()
427 ? static_cast<int>(classNames.size())
428 : attributes - 4;
429 const int maskCoefficientCount = attributes - 4 - classCount;
430 const cv::Mat* prototype = nullptr;
431 if (generateMasks && maskCoefficientCount > 0) {
432 auto prototypeIt = std::find_if(outs.begin(), outs.end(),
433 [maskCoefficientCount](const cv::Mat& out) {
434 return out.dims == 4 && out.size[0] == 1 && out.size[1] == maskCoefficientCount;
435 });
436 if (prototypeIt != outs.end()) {
437 prototype = &(*prototypeIt);
438 }
439 }
440 const float* data = reinterpret_cast<const float*>(det.data);
441
442 for (int candidateIndex = 0; candidateIndex < candidates; ++candidateIndex) {
443 std::vector<ClassScore> rowClassScores;
444 rowClassScores.reserve(maxClassCandidates);
445
446 for (int classIndex = 0; classIndex < classCount; ++classIndex) {
447 const float classConfidence = data[(4 + classIndex) * candidates + candidateIndex];
448 if (rowClassScores.size() < static_cast<size_t>(maxClassCandidates)) {
449 rowClassScores.emplace_back(classIndex, classConfidence);
450 std::sort(rowClassScores.begin(), rowClassScores.end(),
451 [](const ClassScore& a, const ClassScore& b) { return a.score > b.score; });
452 } else if (classConfidence > rowClassScores.back().score) {
453 rowClassScores.back() = ClassScore(classIndex, classConfidence);
454 std::sort(rowClassScores.begin(), rowClassScores.end(),
455 [](const ClassScore& a, const ClassScore& b) { return a.score > b.score; });
456 }
457 }
458
459 if (rowClassScores.empty() || rowClassScores.front().score <= confThreshold) {
460 continue;
461 }
462
463 cv::Rect box = ScaledXYWHBox(
464 data[candidateIndex],
465 data[candidates + candidateIndex],
466 data[2 * candidates + candidateIndex],
467 data[3 * candidates + candidateIndex],
468 frameDims, inpWidth, inpHeight);
469 if (box.width <= 0 || box.height <= 0) {
470 continue;
471 }
472
473 classIds.push_back(rowClassScores.front().classId);
474 confidences.push_back(rowClassScores.front().score);
475 boxes.push_back(box);
476 detectionClassScores.push_back(rowClassScores);
477 if (prototype) {
478 std::vector<float> coefficients;
479 coefficients.reserve(maskCoefficientCount);
480 for (int coefficientIndex = 0; coefficientIndex < maskCoefficientCount; ++coefficientIndex) {
481 coefficients.push_back(data[(4 + classCount + coefficientIndex) * candidates + candidateIndex]);
482 }
483 detectionMasks.push_back(BuildMaskFromPrototype(*prototype, coefficients, box, frameDims));
484 } else {
485 detectionMasks.push_back({});
486 }
487 }
488 continue;
489 }
490
491 // YOLOv5-style ONNX output is usually [1, num_boxes, num_classes + 5].
492 if (det.dims == 3) {
493 det = det.reshape(1, det.size[1]);
494 }
495 if (det.dims != 2 || det.cols < 6) {
496 continue;
497 }
498
499 const float xFactor = static_cast<float>(frameDims.width) / static_cast<float>(inpWidth);
500 const float yFactor = static_cast<float>(frameDims.height) / static_cast<float>(inpHeight);
501
502 float* data = reinterpret_cast<float*>(det.data);
503 for (int j = 0; j < det.rows; ++j, data += det.cols) {
504 std::vector<ClassScore> rowClassScores;
505 rowClassScores.reserve(maxClassCandidates);
506 int classScoresEnd = det.cols;
507 if (!classNames.empty()) {
508 classScoresEnd = std::min(det.cols, 5 + static_cast<int>(classNames.size()));
509 }
510 for (int classIndex = 5; classIndex < classScoresEnd; ++classIndex) {
511 const float classConfidence = data[classIndex] * data[4];
512 if (rowClassScores.size() < static_cast<size_t>(maxClassCandidates)) {
513 rowClassScores.emplace_back(classIndex - 5, classConfidence);
514 std::sort(rowClassScores.begin(), rowClassScores.end(),
515 [](const ClassScore& a, const ClassScore& b) { return a.score > b.score; });
516 } else if (classConfidence > rowClassScores.back().score) {
517 rowClassScores.back() = ClassScore(classIndex - 5, classConfidence);
518 std::sort(rowClassScores.begin(), rowClassScores.end(),
519 [](const ClassScore& a, const ClassScore& b) { return a.score > b.score; });
520 }
521 }
522 if (rowClassScores.empty()) {
523 continue;
524 }
525
526 float confidence = rowClassScores.front().score;
527
528 if (confidence > confThreshold) {
529 int centerX = 0;
530 int centerY = 0;
531 int width = 0;
532 int height = 0;
533
534 if (data[0] > 1.0f || data[1] > 1.0f || data[2] > 1.0f || data[3] > 1.0f) {
535 centerX = static_cast<int>(data[0] * xFactor);
536 centerY = static_cast<int>(data[1] * yFactor);
537 width = static_cast<int>(data[2] * xFactor);
538 height = static_cast<int>(data[3] * yFactor);
539 } else {
540 centerX = static_cast<int>(data[0] * frameDims.width);
541 centerY = static_cast<int>(data[1] * frameDims.height);
542 width = static_cast<int>(data[2] * frameDims.width);
543 height = static_cast<int>(data[3] * frameDims.height);
544 }
545
546 int left = centerX - width / 2;
547 int top = centerY - height / 2;
548
549 classIds.push_back(rowClassScores.front().classId);
550 confidences.push_back(confidence);
551 boxes.push_back(cv::Rect(left, top, width, height));
552 detectionClassScores.push_back(rowClassScores);
553 detectionMasks.push_back({});
554 }
555 }
556 }
557
558 // Perform non maximum suppression to eliminate redundant overlapping boxes with
559 // lower confidences
560 std::vector<int> indices;
561 cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
562
563 // Pass boxes to SORT algorithm
564 std::vector<cv::Rect> sortBoxes;
565 std::vector<float> sortConfidences;
566 std::vector<int> sortClassIds;
567 std::vector<std::vector<ClassScore>> sortClassScores;
568 std::vector<CVObjectMaskData> sortMasks;
569 for(auto index : indices) {
570 sortBoxes.push_back(boxes[index]);
571 sortConfidences.push_back(confidences[index]);
572 sortClassIds.push_back(classIds[index]);
573 sortClassScores.push_back(detectionClassScores[index]);
574 sortMasks.push_back(index < static_cast<int>(detectionMasks.size()) ? detectionMasks[index] : CVObjectMaskData());
575 }
576 sort.update(sortBoxes, frameId, sqrt(pow(frameDims.width,2) + pow(frameDims.height, 2)), sortConfidences, sortClassIds, sortClassScores);
577
578 // Clear data vectors
579 boxes.clear(); confidences.clear(); classIds.clear(); objectIds.clear();
580 std::vector<CVObjectMaskData> masks;
581 // Get SORT predicted boxes
582 for(auto TBox : sort.frameTrackingResult){
583 if(TBox.frame == frameId){
584 boxes.push_back(TBox.box);
585 confidences.push_back(TBox.confidence);
586 classIds.push_back(TBox.classId);
587 objectIds.push_back(TBox.id);
588 CVObjectMaskData mask;
589 double bestIoU = 0.0;
590 for (size_t maskIndex = 0; maskIndex < sortMasks.size(); ++maskIndex) {
591 if (!sortMasks[maskIndex].HasData() || sortClassIds[maskIndex] != TBox.classId)
592 continue;
593 double score = SortTracker::GetIOU(cv::Rect_<float>(sortBoxes[maskIndex]), TBox.box);
594 if (score > bestIoU) {
595 bestIoU = score;
596 mask = sortMasks[maskIndex];
597 }
598 }
599 if (mask.HasData()) {
600 recentObjectMasks[TBox.id] = CVTrackedMaskData{frameId, mask, TBox.box};
601 } else {
602 const auto recentMask = recentObjectMasks.find(TBox.id);
603 if (recentMask != recentObjectMasks.end() &&
604 frameId > recentMask->second.frameId &&
605 frameId - recentMask->second.frameId <= 5) {
606 mask = TransformMaskToBox(
607 recentMask->second.mask,
608 recentMask->second.box,
609 TBox.box,
610 frameDims);
611 if (mask.HasData()) {
612 recentObjectMasks[TBox.id] = CVTrackedMaskData{frameId, mask, TBox.box};
613 }
614 }
615 }
616 masks.push_back(mask);
617 }
618 }
619
620 // Remove boxes based on controids distance
621 for(uint i = 0; i<boxes.size(); i++){
622 for(uint j = i+1; j<boxes.size(); j++){
623 int xc_1 = boxes[i].x + (int)(boxes[i].width/2), yc_1 = boxes[i].y + (int)(boxes[i].height/2);
624 int xc_2 = boxes[j].x + (int)(boxes[j].width/2), yc_2 = boxes[j].y + (int)(boxes[j].height/2);
625
626 if(fabs(xc_1 - xc_2) < 10 && fabs(yc_1 - yc_2) < 10){
627 if(classIds[i] == classIds[j]){
628 if(confidences[i] >= confidences[j]){
629 boxes.erase(boxes.begin() + j);
630 classIds.erase(classIds.begin() + j);
631 confidences.erase(confidences.begin() + j);
632 objectIds.erase(objectIds.begin() + j);
633 masks.erase(masks.begin() + j);
634 break;
635 }
636 else{
637 boxes.erase(boxes.begin() + i);
638 classIds.erase(classIds.begin() + i);
639 confidences.erase(confidences.begin() + i);
640 objectIds.erase(objectIds.begin() + i);
641 masks.erase(masks.begin() + i);
642 i = 0;
643 break;
644 }
645 }
646 }
647 }
648 }
649
650 // Remove boxes based in IOU score
651 for(uint i = 0; i<boxes.size(); i++){
652 for(uint j = i+1; j<boxes.size(); j++){
653
654 if( iou(boxes[i], boxes[j])){
655 if(classIds[i] == classIds[j]){
656 if(confidences[i] >= confidences[j]){
657 boxes.erase(boxes.begin() + j);
658 classIds.erase(classIds.begin() + j);
659 confidences.erase(confidences.begin() + j);
660 objectIds.erase(objectIds.begin() + j);
661 masks.erase(masks.begin() + j);
662 break;
663 }
664 else{
665 boxes.erase(boxes.begin() + i);
666 classIds.erase(classIds.begin() + i);
667 confidences.erase(confidences.begin() + i);
668 objectIds.erase(objectIds.begin() + i);
669 masks.erase(masks.begin() + i);
670 i = 0;
671 break;
672 }
673 }
674 }
675 }
676 }
677
678 // Normalize boxes coordinates
679 std::vector<cv::Rect_<float>> normalized_boxes;
680 for(auto box : boxes){
681 cv::Rect_<float> normalized_box;
682 normalized_box.x = (box.x)/(float)frameDims.width;
683 normalized_box.y = (box.y)/(float)frameDims.height;
684 normalized_box.width = (box.width)/(float)frameDims.width;
685 normalized_box.height = (box.height)/(float)frameDims.height;
686 normalized_boxes.push_back(normalized_box);
687 }
688
689 detectionsData[frameId] = CVDetectionData(classIds, confidences, normalized_boxes, frameId, objectIds, masks);
690}
691
692// Compute IOU between 2 boxes
693bool CVObjectDetection::iou(cv::Rect pred_box, cv::Rect sort_box){
694 // Determine the (x, y)-coordinates of the intersection rectangle
695 int xA = std::max(pred_box.x, sort_box.x);
696 int yA = std::max(pred_box.y, sort_box.y);
697 int xB = std::min(pred_box.x + pred_box.width, sort_box.x + sort_box.width);
698 int yB = std::min(pred_box.y + pred_box.height, sort_box.y + sort_box.height);
699
700 // Compute the area of intersection rectangle
701 int interArea = std::max(0, xB - xA + 1) * std::max(0, yB - yA + 1);
702
703 // Compute the area of both the prediction and ground-truth rectangles
704 int boxAArea = (pred_box.width + 1) * (pred_box.height + 1);
705 int boxBArea = (sort_box.width + 1) * (sort_box.height + 1);
706
707 // Compute the intersection over union by taking the intersection
708 float iou = interArea / (float)(boxAArea + boxBArea - interArea);
709
710 // If IOU is above this value the boxes are very close (probably a variation of the same bounding box)
711 if(iou > 0.5)
712 return true;
713 return false;
714}
715
716// Get the names of the output layers
717std::vector<cv::String> CVObjectDetection::getOutputsNames(const cv::dnn::Net& net)
718{
719 //Get the indices of the output layers, i.e. the layers with unconnected outputs
720 std::vector<int> outLayers = net.getUnconnectedOutLayers();
721
722 //get the names of all the layers in the network
723 std::vector<cv::String> layersNames = net.getLayerNames();
724
725 // Get the names of the output layers in names
726 std::vector<cv::String> names;
727 names.resize(outLayers.size());
728 for (size_t i = 0; i < outLayers.size(); ++i)
729 names[i] = layersNames[outLayers[i] - 1];
730 return names;
731}
732
734 // Check if the stabilizer info for the requested frame exists
735 if ( detectionsData.find(frameId) == detectionsData.end() ) {
736
737 return CVDetectionData();
738 } else {
739
740 return detectionsData[frameId];
741 }
742}
743
744void CVObjectDetection::NormalizeTrackedClasses()
745{
746 struct ClassEvidence {
747 float confidenceSum = 0.0f;
748 size_t count = 0;
749 };
750
751 std::map<int, std::map<int, ClassEvidence>> objectClassEvidence;
752 for (const auto& frameData : detectionsData) {
753 const CVDetectionData& detections = frameData.second;
754 const size_t detectionCount = std::min(detections.objectIds.size(), detections.classIds.size());
755 for (size_t i = 0; i < detectionCount; ++i) {
756 const float confidence = i < detections.confidences.size() ? detections.confidences[i] : 1.0f;
757 ClassEvidence& evidence = objectClassEvidence[detections.objectIds[i]][detections.classIds[i]];
758 evidence.confidenceSum += confidence;
759 ++evidence.count;
760 }
761 }
762
763 std::map<int, int> dominantClassByObject;
764 for (const auto& objectEvidence : objectClassEvidence) {
765 const int objectId = objectEvidence.first;
766 int bestClassId = -1;
767 ClassEvidence bestEvidence;
768 for (const auto& classEvidence : objectEvidence.second) {
769 const int classId = classEvidence.first;
770 const ClassEvidence& evidence = classEvidence.second;
771 if (bestClassId < 0 ||
772 evidence.confidenceSum > bestEvidence.confidenceSum ||
773 (evidence.confidenceSum == bestEvidence.confidenceSum && evidence.count > bestEvidence.count)) {
774 bestClassId = classId;
775 bestEvidence = evidence;
776 }
777 }
778 if (bestClassId >= 0) {
779 dominantClassByObject[objectId] = bestClassId;
780 }
781 }
782
783 for (auto& frameData : detectionsData) {
784 CVDetectionData& detections = frameData.second;
785 const size_t detectionCount = std::min(detections.objectIds.size(), detections.classIds.size());
786 for (size_t i = 0; i < detectionCount; ++i) {
787 const auto dominantClass = dominantClassByObject.find(detections.objectIds[i]);
788 if (dominantClass != dominantClassByObject.end()) {
789 detections.classIds[i] = dominantClass->second;
790 }
791 }
792 }
793}
794
796 if(protobuf_data_path.empty()) {
797 cerr << "Missing path to object detection protobuf data file." << endl;
798 return false;
799 }
800
801 NormalizeTrackedClasses();
802
803 // Create tracker message
804 pb_objdetect::ObjDetect objMessage;
805
806 //Save class names in protobuf message
807 for(int i = 0; i<classNames.size(); i++){
808 std::string* className = objMessage.add_classnames();
809 className->assign(classNames.at(i));
810 }
811
812 // Iterate over all frames data and save in protobuf message
813 for(std::map<size_t,CVDetectionData>::iterator it=detectionsData.begin(); it!=detectionsData.end(); ++it){
814 CVDetectionData dData = it->second;
815 AddFrameDataToProto(objMessage.add_frame(), dData);
816 }
817
818 // Add timestamp
819 *objMessage.mutable_last_updated() = TimeUtil::SecondsToTimestamp(time(NULL));
820
821 {
822 // Write the new message to disk.
823 std::fstream output(protobuf_data_path, ios::out | ios::trunc | ios::binary);
824 if (!objMessage.SerializeToOstream(&output)) {
825 cerr << "Failed to write protobuf message." << endl;
826 return false;
827 }
828 }
829
830 // Delete all global objects allocated by libprotobuf.
831 google::protobuf::ShutdownProtobufLibrary();
832
833 return true;
834
835}
836
837// Add frame object detection into protobuf message.
838void CVObjectDetection::AddFrameDataToProto(pb_objdetect::Frame* pbFrameData, CVDetectionData& dData) {
839
840 // Save frame number and rotation
841 pbFrameData->set_id(dData.frameId);
842
843 for(size_t i = 0; i < dData.boxes.size(); i++){
844 pb_objdetect::Frame_Box* box = pbFrameData->add_bounding_box();
845
846 // Save bounding box data
847 box->set_x(dData.boxes.at(i).x);
848 box->set_y(dData.boxes.at(i).y);
849 box->set_w(dData.boxes.at(i).width);
850 box->set_h(dData.boxes.at(i).height);
851 box->set_classid(dData.classIds.at(i));
852 box->set_confidence(dData.confidences.at(i));
853 box->set_objectid(dData.objectIds.at(i));
854
855 if (i < dData.masks.size() && dData.masks.at(i).HasData()) {
856 pb_objdetect::Frame_Box_Mask* mask = box->mutable_mask();
857 mask->set_width(dData.masks.at(i).width);
858 mask->set_height(dData.masks.at(i).height);
859 for (uint32_t count : dData.masks.at(i).rle) {
860 mask->add_rle(count);
861 }
862 }
863 }
864}
865
866// Load JSON string into this object
867void CVObjectDetection::SetJson(const std::string value) {
868 // Parse JSON string into JSON objects
869 try
870 {
871 const Json::Value root = openshot::stringToJson(value);
872 // Set all values that match
873
874 SetJsonValue(root);
875 }
876 catch (const std::exception& e)
877 {
878 // Error parsing JSON (or missing keys)
879 // throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
880 std::cout<<"JSON is invalid (missing keys or invalid data types)"<<std::endl;
881 }
882}
883
884// Load Json::Value into this object
885void CVObjectDetection::SetJsonValue(const Json::Value root) {
886
887 // Set data from Json (if key is found)
888 if (!root["protobuf_data_path"].isNull()){
889 protobuf_data_path = (root["protobuf_data_path"].asString());
890 }
891
892 if (!root["processing-device"].isNull()){
893 processingDevice = (root["processing-device"].asString());
894 }
895 if (!root["processing_device"].isNull()){
896 processingDevice = (root["processing_device"].asString());
897 }
898 if (!root["class-names"].isNull()){
899 classesFile = (root["class-names"].asString());
900 }
901 if (!root["classes_file"].isNull()){
902 classesFile = (root["classes_file"].asString());
903 }
904 if (!root["model"].isNull()){
905 modelPath = (root["model"].asString());
906 }
907 if (!root["model_path"].isNull()){
908 modelPath = (root["model_path"].asString());
909 }
910 if (!root["input-width"].isNull()){
911 inpWidth = root["input-width"].asInt();
912 }
913 if (!root["input_width"].isNull()){
914 inpWidth = root["input_width"].asInt();
915 }
916 if (!root["input-height"].isNull()){
917 inpHeight = root["input-height"].asInt();
918 }
919 if (!root["input_height"].isNull()){
920 inpHeight = root["input_height"].asInt();
921 }
922 if (!root["confidence-threshold"].isNull()){
923 confThreshold = root["confidence-threshold"].asFloat();
924 }
925 if (!root["confidence_threshold"].isNull()){
926 confThreshold = root["confidence_threshold"].asFloat();
927 }
928 if (!root["nms-threshold"].isNull()){
929 nmsThreshold = root["nms-threshold"].asFloat();
930 }
931 if (!root["nms_threshold"].isNull()){
932 nmsThreshold = root["nms_threshold"].asFloat();
933 }
934 if (!root["generate-masks"].isNull()){
935 generateMasks = root["generate-masks"].asBool();
936 }
937 if (!root["generate_masks"].isNull()){
938 generateMasks = root["generate_masks"].asBool();
939 }
940}
941
942/*
943||||||||||||||||||||||||||||||||||||||||||||||||||
944 ONLY FOR MAKE TEST
945||||||||||||||||||||||||||||||||||||||||||||||||||
946*/
947
948// Load protobuf data file
950 if(protobuf_data_path.empty()) {
951 cerr << "Missing path to object detection protobuf data file." << endl;
952 return false;
953 }
954
955 // Create tracker message
956 pb_objdetect::ObjDetect objMessage;
957
958 {
959 // Read the existing tracker message.
960 fstream input(protobuf_data_path, ios::in | ios::binary);
961 if (!objMessage.ParseFromIstream(&input)) {
962 cerr << "Failed to parse protobuf message." << endl;
963 return false;
964 }
965 }
966
967 // Make sure classNames and detectionsData are empty
968 classNames.clear(); detectionsData.clear();
969
970 // Get all classes names and assign a color to them
971 for(int i = 0; i < objMessage.classnames_size(); i++){
972 classNames.push_back(objMessage.classnames(i));
973 }
974
975 // Iterate over all frames of the saved message
976 for (size_t i = 0; i < objMessage.frame_size(); i++) {
977 // Create protobuf message reader
978 const pb_objdetect::Frame& pbFrameData = objMessage.frame(i);
979
980 // Get frame Id
981 size_t id = pbFrameData.id();
982
983 // Load bounding box data
984 const google::protobuf::RepeatedPtrField<pb_objdetect::Frame_Box > &pBox = pbFrameData.bounding_box();
985
986 // Construct data vectors related to detections in the current frame
987 std::vector<int> classIds;
988 std::vector<float> confidences;
989 std::vector<cv::Rect_<float>> boxes;
990 std::vector<int> objectIds;
991 std::vector<CVObjectMaskData> masks;
992
993 for(int i = 0; i < pbFrameData.bounding_box_size(); i++){
994 // Get bounding box coordinates
995 float x = pBox.Get(i).x(); float y = pBox.Get(i).y();
996 float w = pBox.Get(i).w(); float h = pBox.Get(i).h();
997 // Create OpenCV rectangle with the bouding box info
998 cv::Rect_<float> box(x, y, w, h);
999
1000 // Get class Id (which will be assign to a class name) and prediction confidence
1001 int classId = pBox.Get(i).classid(); float confidence = pBox.Get(i).confidence();
1002 // Get object Id
1003 int objectId = pBox.Get(i).objectid();
1004
1005 // Push back data into vectors
1006 boxes.push_back(box); classIds.push_back(classId); confidences.push_back(confidence);
1007 objectIds.push_back(objectId);
1008 CVObjectMaskData mask;
1009 if (pBox.Get(i).has_mask()) {
1010 mask.width = pBox.Get(i).mask().width();
1011 mask.height = pBox.Get(i).mask().height();
1012 for (int rleIndex = 0; rleIndex < pBox.Get(i).mask().rle_size(); ++rleIndex) {
1013 mask.rle.push_back(pBox.Get(i).mask().rle(rleIndex));
1014 }
1015 }
1016 masks.push_back(mask);
1017 }
1018
1019 // Assign data to object detector map
1020 detectionsData[id] = CVDetectionData(classIds, confidences, boxes, id, objectIds, masks);
1021 }
1022
1023 // Delete all global objects allocated by libprotobuf.
1024 google::protobuf::ShutdownProtobufLibrary();
1025
1026 return true;
1027}
Header file for CVObjectDetection class.
Header file for all Exception classes.
void SetError(bool err, std::string message)
void update(std::vector< cv::Rect > detection, int frame_count, double image_diagonal, std::vector< float > confidences, std::vector< int > classIds, std::vector< std::vector< ClassScore > > classScores={})
Definition sort.cpp:152
static double GetIOU(cv::Rect_< float > bb_test, cv::Rect_< float > bb_gt)
Definition sort.cpp:90
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
CVObjectDetection(std::string processInfoJson, ProcessingController &processingController)
void detectObjectsClip(openshot::Clip &video, size_t start=0, size_t end=0, bool process_interval=false)
void AddFrameDataToProto(pb_objdetect::Frame *pbFrameData, CVDetectionData &dData)
CVDetectionData GetDetectionData(size_t frameId)
static std::string ValidateONNXModel(std::string modelPath)
std::map< size_t, CVDetectionData > detectionsData
void SetJson(const std::string value)
Load JSON string into this object.
bool SaveObjDetectedData()
Protobuf Save and Load methods.
float Start() const
Get start position (in seconds) of clip (trim start of video)
Definition ClipBase.h:91
This class represents a clip (used to arrange readers on the timeline)
Definition Clip.h:89
void Open() override
Open the internal reader.
Definition Clip.cpp:393
float End() const override
Get end position (in seconds) of clip (trim end of video), which can be affected by the time curve.
Definition Clip.cpp:429
std::shared_ptr< openshot::Frame > GetFrame(int64_t clip_frame_number) override
Get an openshot::Frame object for a specific frame number of this clip. The image size and number of ...
Definition Clip.cpp:464
void Reader(openshot::ReaderBase *new_reader)
Set the current reader.
Definition Clip.cpp:346
void Log(std::string message, Level level=LevelDebug)
Definition Logger.cpp:146
static Logger * Instance()
Definition Logger.cpp:55
This namespace is the default namespace for all code in the openshot library.
const Json::Value stringToJson(const std::string value)
Definition Json.cpp:16
std::vector< int > objectIds
std::vector< cv::Rect_< float > > boxes
std::vector< CVObjectMaskData > masks
std::vector< int > classIds
std::vector< float > confidences
std::vector< uint32_t > rle