From 463cd098116d46dd79086d0b0f3cfa7890bda65f Mon Sep 17 00:00:00 2001 From: Maksim Shabunin Date: Wed, 14 Jun 2023 22:21:38 +0300 Subject: [PATCH] Merge pull request #23666 from mshabunin:barcode-move Moved barcode from opencv_contrib #23666 Merge with https://github.com/opencv/opencv_contrib/pull/3497 ##### TODO - [x] Documentation (bib) - [x] Tutorial (references) - [x] Sample app (refactored) - [x] Java (test passes) - [x] Python (test passes) - [x] Build without DNN --- .../others/barcode_detect_and_decode.markdown | 76 +++ doc/tutorials/others/images/barcode_book.jpg | Bin 0 -> 29334 bytes .../others/images/barcode_book_res.jpg | Bin 0 -> 31818 bytes .../others/introduction_to_svm.markdown | 2 +- .../others/table_of_content_other.markdown | 1 + doc/tutorials/others/traincascade.markdown | 2 +- modules/objdetect/doc/objdetect.bib | 29 + .../objdetect/include/opencv2/objdetect.hpp | 2 + .../include/opencv2/objdetect/barcode.hpp | 65 +++ .../misc/java/test/BarcodeDetectorTest.java | 50 ++ .../misc/python/test/test_barcode_detector.py | 33 ++ modules/objdetect/perf/perf_barcode.cpp | 114 ++++ modules/objdetect/src/barcode.cpp | 374 +++++++++++++ .../src/barcode_decoder/abs_decoder.cpp | 118 ++++ .../src/barcode_decoder/abs_decoder.hpp | 99 ++++ .../common/hybrid_binarizer.cpp | 195 +++++++ .../common/hybrid_binarizer.hpp | 22 + .../barcode_decoder/common/super_scale.cpp | 77 +++ .../barcode_decoder/common/super_scale.hpp | 69 +++ .../src/barcode_decoder/common/utils.cpp | 36 ++ .../src/barcode_decoder/common/utils.hpp | 26 + .../src/barcode_decoder/ean13_decoder.cpp | 92 ++++ .../src/barcode_decoder/ean13_decoder.hpp | 31 ++ .../src/barcode_decoder/ean8_decoder.cpp | 79 +++ .../src/barcode_decoder/ean8_decoder.hpp | 32 ++ .../src/barcode_decoder/upcean_decoder.cpp | 290 ++++++++++ .../src/barcode_decoder/upcean_decoder.hpp | 67 +++ .../src/barcode_detector/bardetect.cpp | 510 ++++++++++++++++++ .../src/barcode_detector/bardetect.hpp | 62 +++ modules/objdetect/src/precomp.hpp | 3 + modules/objdetect/test/test_barcode.cpp | 127 +++++ samples/cpp/barcode.cpp | 223 ++++++++ 32 files changed, 2904 insertions(+), 2 deletions(-) create mode 100644 doc/tutorials/others/barcode_detect_and_decode.markdown create mode 100644 doc/tutorials/others/images/barcode_book.jpg create mode 100644 doc/tutorials/others/images/barcode_book_res.jpg create mode 100644 modules/objdetect/include/opencv2/objdetect/barcode.hpp create mode 100644 modules/objdetect/misc/java/test/BarcodeDetectorTest.java create mode 100644 modules/objdetect/misc/python/test/test_barcode_detector.py create mode 100644 modules/objdetect/perf/perf_barcode.cpp create mode 100644 modules/objdetect/src/barcode.cpp create mode 100644 modules/objdetect/src/barcode_decoder/abs_decoder.cpp create mode 100644 modules/objdetect/src/barcode_decoder/abs_decoder.hpp create mode 100644 modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.cpp create mode 100644 modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.hpp create mode 100644 modules/objdetect/src/barcode_decoder/common/super_scale.cpp create mode 100644 modules/objdetect/src/barcode_decoder/common/super_scale.hpp create mode 100644 modules/objdetect/src/barcode_decoder/common/utils.cpp create mode 100644 modules/objdetect/src/barcode_decoder/common/utils.hpp create mode 100644 modules/objdetect/src/barcode_decoder/ean13_decoder.cpp create mode 100644 modules/objdetect/src/barcode_decoder/ean13_decoder.hpp create mode 100644 modules/objdetect/src/barcode_decoder/ean8_decoder.cpp create mode 100644 modules/objdetect/src/barcode_decoder/ean8_decoder.hpp create mode 100644 modules/objdetect/src/barcode_decoder/upcean_decoder.cpp create mode 100644 modules/objdetect/src/barcode_decoder/upcean_decoder.hpp create mode 100644 modules/objdetect/src/barcode_detector/bardetect.cpp create mode 100644 modules/objdetect/src/barcode_detector/bardetect.hpp create mode 100644 modules/objdetect/test/test_barcode.cpp create mode 100644 samples/cpp/barcode.cpp diff --git a/doc/tutorials/others/barcode_detect_and_decode.markdown b/doc/tutorials/others/barcode_detect_and_decode.markdown new file mode 100644 index 0000000000..edfe9b8c10 --- /dev/null +++ b/doc/tutorials/others/barcode_detect_and_decode.markdown @@ -0,0 +1,76 @@ +Barcode Recognition {#tutorial_barcode_detect_and_decode} +=================== + +@tableofcontents + +@prev_tutorial{tutorial_traincascade} +@next_tutorial{tutorial_introduction_to_svm} + +| | | +| -: | :- | +| Compatibility | OpenCV >= 4.8 | + +Goal +---- + +In this chapter we will familiarize with the barcode detection and decoding methods available in OpenCV. + +Basics +---- + +Barcode is major technique to identify commodity in real life. A common barcode is a pattern of parallel lines arranged by black bars and white bars with vastly different reflectivity. Barcode recognition is to scan the barcode in the horizontal direction to get a string of binary codes composed of bars of different widths and colors, that is, the code information of the barcode. The content of barcode can be decoded by matching with various barcode encoding methods. Currently, we support EAN-8, EAN-13, UPC-A and UPC-E standards. + +See https://en.wikipedia.org/wiki/Universal_Product_Code and https://en.wikipedia.org/wiki/International_Article_Number + +Related papers: @cite Xiangmin2015research , @cite kass1987analyzing , @cite bazen2002systematic + +Code example +------------ + +### Main class +Several algorithms were introduced for barcode recognition. + +While coding, we firstly need to create a cv::barcode::BarcodeDetector object. It has mainly three member functions, which will be introduced in the following. + +#### Initialization + +Optionally user can construct barcode detector with super resolution model which should be downloaded from https://github.com/WeChatCV/opencv_3rdparty/tree/wechat_qrcode (`sr.caffemodel`, `sr.prototxt`). + +@snippet cpp/barcode.cpp initialize + +We need to create variables to store the outputs. + +@snippet cpp/barcode.cpp output + +#### Detecting + +cv::barcode::BarcodeDetector::detect method uses an algorithm based on directional coherence. First, we compute the average squared gradients of every pixel, @cite bazen2002systematic . Then we divide an image into square patches and compute the **gradient orientation coherence** and **mean gradient direction** of each patch. Then, we connect all patches that have **high gradient orientation coherence** and **similar gradient direction**. At this stage we use multiscale patches to capture the gradient distribution of multi-size barcodes, and apply non-maximum suppression to filter duplicate proposals. At last, we use cv::minAreaRect to bound the ROI, and output the corners of the rectangles. + +Detect codes in the input image, and output the corners of detected rectangles: + +@snippet cpp/barcode.cpp detect + +#### Decoding + +cv::barcode::BarcodeDetector::decode method first super-scales the image (_optionally_) if it is smaller than threshold, sharpens the image and then binaries it by OTSU or local binarization. Then it reads the contents of the barcode by matching the similarity of the specified barcode pattern. + +#### Detecting and decoding + +cv::barcode::BarcodeDetector::detectAndDecode combines `detect` and `decode` in a single call. A simple example below shows how to use this function: + +@snippet cpp/barcode.cpp detectAndDecode + +Visualize the results: + +@snippet cpp/barcode.cpp visualize + +Results +------- + +Original image: + +![image](images/barcode_book.jpg) + +After detection: + +![image](images/barcode_book_res.jpg) diff --git a/doc/tutorials/others/images/barcode_book.jpg b/doc/tutorials/others/images/barcode_book.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5b467d58ea9e98758e9876b307c759ddacee4b49 GIT binary patch literal 29334 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 literal 0 HcmV?d00001 diff --git a/doc/tutorials/others/images/barcode_book_res.jpg b/doc/tutorials/others/images/barcode_book_res.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b672f48727fda9282200bc490222a7a82f480ec2 GIT binary patch literal 31818 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 BLIVH* literal 0 HcmV?d00001 diff --git a/doc/tutorials/others/introduction_to_svm.markdown b/doc/tutorials/others/introduction_to_svm.markdown index b74d989acd..39d09d7542 100644 --- a/doc/tutorials/others/introduction_to_svm.markdown +++ b/doc/tutorials/others/introduction_to_svm.markdown @@ -3,7 +3,7 @@ Introduction to Support Vector Machines {#tutorial_introduction_to_svm} @tableofcontents -@prev_tutorial{tutorial_traincascade} +@prev_tutorial{tutorial_barcode_detect_and_decode} @next_tutorial{tutorial_non_linear_svms} | | | diff --git a/doc/tutorials/others/table_of_content_other.markdown b/doc/tutorials/others/table_of_content_other.markdown index a004df63e2..b4bbf62777 100644 --- a/doc/tutorials/others/table_of_content_other.markdown +++ b/doc/tutorials/others/table_of_content_other.markdown @@ -8,6 +8,7 @@ Other tutorials (ml, objdetect, photo, stitching, video) {#tutorial_table_of_con - video. @subpage tutorial_optical_flow - objdetect. @subpage tutorial_cascade_classifier - objdetect. @subpage tutorial_traincascade +- objdetect. @subpage tutorial_barcode_detect_and_decode - ml. @subpage tutorial_introduction_to_svm - ml. @subpage tutorial_non_linear_svms - ml. @subpage tutorial_introduction_to_pca diff --git a/doc/tutorials/others/traincascade.markdown b/doc/tutorials/others/traincascade.markdown index e4f75252cd..5cbacdb4cb 100644 --- a/doc/tutorials/others/traincascade.markdown +++ b/doc/tutorials/others/traincascade.markdown @@ -4,7 +4,7 @@ Cascade Classifier Training {#tutorial_traincascade} @tableofcontents @prev_tutorial{tutorial_cascade_classifier} -@next_tutorial{tutorial_introduction_to_svm} +@next_tutorial{tutorial_barcode_detect_and_decode} Introduction ------------ diff --git a/modules/objdetect/doc/objdetect.bib b/modules/objdetect/doc/objdetect.bib index 394eff8537..f3623732d5 100644 --- a/modules/objdetect/doc/objdetect.bib +++ b/modules/objdetect/doc/objdetect.bib @@ -18,3 +18,32 @@ year = {2016}, month = {October} } + +@mastersthesis{Xiangmin2015research, + title={Research on Barcode Recognition Technology In a Complex Background}, + author={Xiangmin, Wang}, + year={2015}, + school={Huazhong University of Science and Technology} +} + +@article{bazen2002systematic, + title={Systematic methods for the computation of the directional fields and singular points of fingerprints}, + author={Bazen, Asker M and Gerez, Sabih H}, + journal={IEEE transactions on pattern analysis and machine intelligence}, + volume={24}, + number={7}, + pages={905--919}, + year={2002}, + publisher={IEEE} +} + +@article{kass1987analyzing, + title={Analyzing oriented patterns}, + author={Kass, Michael and Witkin, Andrew}, + journal={Computer vision, graphics, and image processing}, + volume={37}, + number={3}, + pages={362--385}, + year={1987}, + publisher={Elsevier} +} diff --git a/modules/objdetect/include/opencv2/objdetect.hpp b/modules/objdetect/include/opencv2/objdetect.hpp index 68b8d86e69..8b3cd7c97b 100644 --- a/modules/objdetect/include/opencv2/objdetect.hpp +++ b/modules/objdetect/include/opencv2/objdetect.hpp @@ -103,6 +103,7 @@ using a Boosted Cascade of Simple Features. IEEE CVPR, 2001. The paper is availa @defgroup objdetect_hog HOG (Histogram of Oriented Gradients) descriptor and object detector + @defgroup objdetect_barcode Barcode detection and decoding @defgroup objdetect_qrcode QRCode detection and encoding @defgroup objdetect_dnn_face DNN-based face detection and recognition Check @ref tutorial_dnn_face "the corresponding tutorial" for more details. @@ -863,5 +864,6 @@ public: #include "opencv2/objdetect/detection_based_tracker.hpp" #include "opencv2/objdetect/face.hpp" #include "opencv2/objdetect/charuco_detector.hpp" +#include "opencv2/objdetect/barcode.hpp" #endif diff --git a/modules/objdetect/include/opencv2/objdetect/barcode.hpp b/modules/objdetect/include/opencv2/objdetect/barcode.hpp new file mode 100644 index 0000000000..958490a422 --- /dev/null +++ b/modules/objdetect/include/opencv2/objdetect/barcode.hpp @@ -0,0 +1,65 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_OBJDETECT_BARCODE_HPP +#define OPENCV_OBJDETECT_BARCODE_HPP + +#include +#include + +namespace cv { +namespace barcode { + +//! @addtogroup objdetect_barcode +//! @{ + +class CV_EXPORTS_W_SIMPLE BarcodeDetector : public cv::GraphicalCodeDetector +{ +public: + /** @brief Initialize the BarcodeDetector. + */ + CV_WRAP BarcodeDetector(); + /** @brief Initialize the BarcodeDetector. + * + * Parameters allow to load _optional_ Super Resolution DNN model for better quality. + * @param prototxt_path prototxt file path for the super resolution model + * @param model_path model file path for the super resolution model + */ + CV_WRAP BarcodeDetector(const std::string &prototxt_path, const std::string &model_path); + ~BarcodeDetector(); + + /** @brief Decodes barcode in image once it's found by the detect() method. + * + * @param img grayscale or color (BGR) image containing bar code. + * @param points vector of rotated rectangle vertices found by detect() method (or some other algorithm). + * For N detected barcodes, the dimensions of this array should be [N][4]. + * Order of four points in vector is bottomLeft, topLeft, topRight, bottomRight. + * @param decoded_info UTF8-encoded output vector of string or empty vector of string if the codes cannot be decoded. + * @param decoded_type vector strings, specifies the type of these barcodes + * @return true if at least one valid barcode have been found + */ + CV_WRAP bool decodeWithType(InputArray img, + InputArray points, + CV_OUT std::vector &decoded_info, + CV_OUT std::vector &decoded_type) const; + + /** @brief Both detects and decodes barcode + + * @param img grayscale or color (BGR) image containing barcode. + * @param decoded_info UTF8-encoded output vector of string(s) or empty vector of string if the codes cannot be decoded. + * @param decoded_type vector of strings, specifies the type of these barcodes + * @param points optional output vector of vertices of the found barcode rectangle. Will be empty if not found. + * @return true if at least one valid barcode have been found + */ + CV_WRAP bool detectAndDecodeWithType(InputArray img, + CV_OUT std::vector &decoded_info, + CV_OUT std::vector &decoded_type, + OutputArray points = noArray()) const; +}; +//! @} + +}} // cv::barcode:: + +#endif // OPENCV_OBJDETECT_BARCODE_HPP diff --git a/modules/objdetect/misc/java/test/BarcodeDetectorTest.java b/modules/objdetect/misc/java/test/BarcodeDetectorTest.java new file mode 100644 index 0000000000..92dfef667a --- /dev/null +++ b/modules/objdetect/misc/java/test/BarcodeDetectorTest.java @@ -0,0 +1,50 @@ +package org.opencv.test.barcode; + +import java.util.List; +import org.opencv.core.Mat; +import org.opencv.objdetect.BarcodeDetector; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.test.OpenCVTestCase; +import java.util.ArrayList; + +public class BarcodeDetectorTest extends OpenCVTestCase { + + private final static String ENV_OPENCV_TEST_DATA_PATH = "OPENCV_TEST_DATA_PATH"; + private String testDataPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + testDataPath = System.getenv(ENV_OPENCV_TEST_DATA_PATH); + if (testDataPath == null) + throw new Exception(ENV_OPENCV_TEST_DATA_PATH + " has to be defined!"); + } + + public void testDetectAndDecode() { + Mat img = Imgcodecs.imread(testDataPath + "/cv/barcode/multiple/4_barcodes.jpg"); + assertFalse(img.empty()); + BarcodeDetector detector = new BarcodeDetector(); + assertNotNull(detector); + List < String > infos = new ArrayList< String >(); + List < String > types = new ArrayList< String >(); + + boolean result = detector.detectAndDecodeWithType(img, infos, types); + assertTrue(result); + assertEquals(infos.size(), 4); + assertEquals(types.size(), 4); + final String[] correctResults = {"9787122276124", "9787118081473", "9787564350840", "9783319200064"}; + for (int i = 0; i < 4; i++) { + assertEquals(types.get(i), "EAN_13"); + result = false; + for (int j = 0; j < 4; j++) { + if (correctResults[j].equals(infos.get(i))) { + result = true; + break; + } + } + assertTrue(result); + } + + } +} diff --git a/modules/objdetect/misc/python/test/test_barcode_detector.py b/modules/objdetect/misc/python/test/test_barcode_detector.py new file mode 100644 index 0000000000..e4c297951f --- /dev/null +++ b/modules/objdetect/misc/python/test/test_barcode_detector.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +''' +=============================================================================== +Barcode detect and decode pipeline. +=============================================================================== +''' +import os +import numpy as np +import cv2 as cv + +from tests_common import NewOpenCVTests + +class barcode_detector_test(NewOpenCVTests): + + def test_detect(self): + img = cv.imread(os.path.join(self.extraTestDataPath, 'cv/barcode/multiple/4_barcodes.jpg')) + self.assertFalse(img is None) + detector = cv.barcode_BarcodeDetector() + retval, corners = detector.detect(img) + self.assertTrue(retval) + self.assertEqual(corners.shape, (4, 4, 2)) + + def test_detect_and_decode(self): + img = cv.imread(os.path.join(self.extraTestDataPath, 'cv/barcode/single/book.jpg')) + self.assertFalse(img is None) + detector = cv.barcode_BarcodeDetector() + retval, decoded_info, decoded_type, corners = detector.detectAndDecodeWithType(img) + self.assertTrue(retval) + self.assertTrue(len(decoded_info) > 0) + self.assertTrue(len(decoded_type) > 0) + self.assertEqual(decoded_info[0], "9787115279460") + self.assertEqual(decoded_type[0], "EAN_13") + self.assertEqual(corners.shape, (1, 4, 2)) diff --git a/modules/objdetect/perf/perf_barcode.cpp b/modules/objdetect/perf/perf_barcode.cpp new file mode 100644 index 0000000000..b960518a1e --- /dev/null +++ b/modules/objdetect/perf/perf_barcode.cpp @@ -0,0 +1,114 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "perf_precomp.hpp" +#include "opencv2/objdetect/barcode.hpp" + +namespace opencv_test{namespace{ + +typedef ::perf::TestBaseWithParam< tuple > Perf_Barcode_multi; +typedef ::perf::TestBaseWithParam< tuple > Perf_Barcode_single; + +PERF_TEST_P_(Perf_Barcode_multi, detect) +{ + const string root = "cv/barcode/multiple/"; + const string name_current_image = get<0>(GetParam()); + const cv::Size sz = get<1>(GetParam()); + const string image_path = findDataFile(root + name_current_image); + + Mat src = imread(image_path); + ASSERT_FALSE(src.empty()) << "Can't read image: " << image_path; + cv::resize(src, src, sz); + + vector< Point > corners; + auto bardet = barcode::BarcodeDetector(); + bool res = false; + TEST_CYCLE() + { + res = bardet.detectMulti(src, corners); + } + SANITY_CHECK_NOTHING(); + ASSERT_TRUE(res); +} + +PERF_TEST_P_(Perf_Barcode_multi, detect_decode) +{ + const string root = "cv/barcode/multiple/"; + const string name_current_image = get<0>(GetParam()); + const cv::Size sz = get<1>(GetParam()); + const string image_path = findDataFile(root + name_current_image); + + Mat src = imread(image_path); + ASSERT_FALSE(src.empty()) << "Can't read image: " << image_path; + cv::resize(src, src, sz); + + vector decoded_info; + vector decoded_type; + vector< Point > corners; + auto bardet = barcode::BarcodeDetector(); + bool res = false; + TEST_CYCLE() + { + res = bardet.detectAndDecodeWithType(src, decoded_info, decoded_type, corners); + } + SANITY_CHECK_NOTHING(); + ASSERT_TRUE(res); +} + +PERF_TEST_P_(Perf_Barcode_single, detect) +{ + const string root = "cv/barcode/single/"; + const string name_current_image = get<0>(GetParam()); + const cv::Size sz = get<1>(GetParam()); + const string image_path = findDataFile(root + name_current_image); + + Mat src = imread(image_path); + ASSERT_FALSE(src.empty()) << "Can't read image: " << image_path; + cv::resize(src, src, sz); + + vector< Point > corners; + auto bardet = barcode::BarcodeDetector(); + bool res = false; + TEST_CYCLE() + { + res = bardet.detectMulti(src, corners); + } + SANITY_CHECK_NOTHING(); + ASSERT_TRUE(res); +} + +PERF_TEST_P_(Perf_Barcode_single, detect_decode) +{ + const string root = "cv/barcode/single/"; + const string name_current_image = get<0>(GetParam()); + const cv::Size sz = get<1>(GetParam()); + const string image_path = findDataFile(root + name_current_image); + + Mat src = imread(image_path); + ASSERT_FALSE(src.empty()) << "Can't read image: " << image_path; + cv::resize(src, src, sz); + + vector decoded_info; + vector decoded_type; + vector< Point > corners; + auto bardet = barcode::BarcodeDetector(); + bool res = false; + TEST_CYCLE() + { + res = bardet.detectAndDecodeWithType(src, decoded_info, decoded_type, corners); + } + SANITY_CHECK_NOTHING(); + ASSERT_TRUE(res); +} + +INSTANTIATE_TEST_CASE_P(/*nothing*/, Perf_Barcode_multi, + testing::Combine( + testing::Values("4_barcodes.jpg"), + testing::Values(cv::Size(2041, 2722), cv::Size(1361, 1815), cv::Size(680, 907)))); +INSTANTIATE_TEST_CASE_P(/*nothing*/, Perf_Barcode_single, + testing::Combine( + testing::Values("book.jpg", "bottle_1.jpg", "bottle_2.jpg"), + testing::Values(cv::Size(480, 360), cv::Size(640, 480), cv::Size(800, 600)))); + +}} //namespace diff --git a/modules/objdetect/src/barcode.cpp b/modules/objdetect/src/barcode.cpp new file mode 100644 index 0000000000..549ea84a0a --- /dev/null +++ b/modules/objdetect/src/barcode.cpp @@ -0,0 +1,374 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "precomp.hpp" +#include +#include +#include "barcode_decoder/ean13_decoder.hpp" +#include "barcode_decoder/ean8_decoder.hpp" +#include "barcode_detector/bardetect.hpp" +#include "barcode_decoder/common/super_scale.hpp" +#include "barcode_decoder/common/utils.hpp" +#include "graphical_code_detector_impl.hpp" + +using std::string; +using std::vector; +using std::make_shared; +using std::array; +using std::shared_ptr; +using std::dynamic_pointer_cast; + +namespace cv { +namespace barcode { + +//================================================================================================== + +static bool checkBarInputImage(InputArray img, Mat &gray) +{ + CV_Assert(!img.empty()); + CV_CheckDepthEQ(img.depth(), CV_8U, ""); + if (img.cols() <= 40 || img.rows() <= 40) + { + return false; // image data is not enough for providing reliable results + } + int incn = img.channels(); + CV_Check(incn, incn == 1 || incn == 3 || incn == 4, ""); + if (incn == 3 || incn == 4) + { + cvtColor(img, gray, COLOR_BGR2GRAY); + } + else + { + gray = img.getMat(); + } + return true; +} + +static void updatePointsResult(OutputArray points_, const vector &points) +{ + if (points_.needed()) + { + int N = int(points.size() / 4); + if (N > 0) + { + Mat m_p(N, 4, CV_32FC2, (void *) &points[0]); + int points_type = points_.fixedType() ? points_.type() : CV_32FC2; + m_p.reshape(2, points_.rows()).convertTo(points_, points_type); // Mat layout: N x 4 x 2cn + } + else + { + points_.release(); + } + } +} + +inline const array, 2> &getDecoders() +{ + //indicate Decoder + static const array, 2> decoders{ + shared_ptr(new Ean13Decoder()), shared_ptr(new Ean8Decoder())}; + return decoders; +} + +//================================================================================================== + +class BarDecode +{ +public: + void init(const vector &bar_imgs_); + + const vector &getDecodeInformation() + { return result_info; } + + bool decodeMultiplyProcess(); + +private: + vector bar_imgs; + vector result_info; +}; + +void BarDecode::init(const vector &bar_imgs_) +{ + bar_imgs = bar_imgs_; +} + +bool BarDecode::decodeMultiplyProcess() +{ + static float constexpr THRESHOLD_CONF = 0.6f; + result_info.clear(); + result_info.resize(bar_imgs.size()); + parallel_for_(Range(0, int(bar_imgs.size())), [&](const Range &range) { + for (int i = range.start; i < range.end; i++) + { + Mat bin_bar; + Result max_res; + float max_conf = -1.f; + bool decoded = false; + for (const auto &decoder:getDecoders()) + { + if (decoded) + { break; } + for (const auto binary_type : binary_types) + { + binarize(bar_imgs[i], bin_bar, binary_type); + auto cur_res = decoder->decodeROI(bin_bar); + if (cur_res.second > max_conf) + { + max_res = cur_res.first; + max_conf = cur_res.second; + if (max_conf > THRESHOLD_CONF) + { + // code decoded + decoded = true; + break; + } + } + } //binary types + } //decoder types + + result_info[i] = max_res; + } + }); + return !result_info.empty(); +} + +//================================================================================================== +// Private class definition and implementation (pimpl) + +struct BarcodeImpl : public GraphicalCodeDetector::Impl +{ +public: + shared_ptr sr; + bool use_nn_sr = false; + +public: + //================= + // own methods + BarcodeImpl() = default; + vector initDecode(const Mat &src, const vector> &points) const; + bool decodeWithType(InputArray img, + InputArray points, + vector &decoded_info, + vector &decoded_type) const; + bool detectAndDecodeWithType(InputArray img, + vector &decoded_info, + vector &decoded_type, + OutputArray points_) const; + + //================= + // implement interface + ~BarcodeImpl() CV_OVERRIDE {} + bool detect(InputArray img, OutputArray points) const CV_OVERRIDE; + string decode(InputArray img, InputArray points, OutputArray straight_code) const CV_OVERRIDE; + string detectAndDecode(InputArray img, OutputArray points, OutputArray straight_code) const CV_OVERRIDE; + bool detectMulti(InputArray img, OutputArray points) const CV_OVERRIDE; + bool decodeMulti(InputArray img, InputArray points, vector& decoded_info, OutputArrayOfArrays straight_code) const CV_OVERRIDE; + bool detectAndDecodeMulti(InputArray img, vector& decoded_info, OutputArray points, OutputArrayOfArrays straight_code) const CV_OVERRIDE; +}; + +// return cropped and scaled bar img +vector BarcodeImpl::initDecode(const Mat &src, const vector> &points) const +{ + vector bar_imgs; + for (auto &corners : points) + { + Mat bar_img; + cropROI(src, bar_img, corners); +// sharpen(bar_img, bar_img); + // empirical settings + if (bar_img.cols < 320 || bar_img.cols > 640) + { + float scale = 560.0f / static_cast(bar_img.cols); + sr->processImageScale(bar_img, bar_img, scale, use_nn_sr); + } + bar_imgs.emplace_back(bar_img); + } + return bar_imgs; +} + +bool BarcodeImpl::decodeWithType(InputArray img, + InputArray points, + vector &decoded_info, + vector &decoded_type) const +{ + Mat inarr; + if (!checkBarInputImage(img, inarr)) + { + return false; + } + CV_Assert(points.size().width > 0); + CV_Assert((points.size().width % 4) == 0); + vector> src_points; + Mat bar_points = points.getMat(); + bar_points = bar_points.reshape(2, 1); + for (int i = 0; i < bar_points.size().width; i += 4) + { + vector tempMat = bar_points.colRange(i, i + 4); + if (contourArea(tempMat) > 0.0) + { + src_points.push_back(tempMat); + } + } + CV_Assert(!src_points.empty()); + vector bar_imgs = initDecode(inarr, src_points); + BarDecode bardec; + bardec.init(bar_imgs); + bardec.decodeMultiplyProcess(); + const vector info = bardec.getDecodeInformation(); + decoded_info.clear(); + decoded_type.clear(); + bool ok = false; + for (const auto &res : info) + { + if (res.isValid()) + { + ok = true; + } + + decoded_info.emplace_back(res.result); + decoded_type.emplace_back(res.typeString()); + } + return ok; +} + +bool BarcodeImpl::detectAndDecodeWithType(InputArray img, + vector &decoded_info, + vector &decoded_type, + OutputArray points_) const +{ + Mat inarr; + if (!checkBarInputImage(img, inarr)) + { + points_.release(); + return false; + } + vector points; + bool ok = this->detect(inarr, points); + if (!ok) + { + points_.release(); + return false; + } + updatePointsResult(points_, points); + decoded_info.clear(); + decoded_type.clear(); + ok = decodeWithType(inarr, points, decoded_info, decoded_type); + return ok; +} + +bool BarcodeImpl::detect(InputArray img, OutputArray points) const +{ + Mat inarr; + if (!checkBarInputImage(img, inarr)) + { + points.release(); + return false; + } + + Detect bardet; + bardet.init(inarr); + bardet.localization(); + if (!bardet.computeTransformationPoints()) + { return false; } + vector> pnts2f = bardet.getTransformationPoints(); + vector trans_points; + for (auto &i : pnts2f) + { + for (const auto &j : i) + { + trans_points.push_back(j); + } + } + updatePointsResult(points, trans_points); + return true; +} + +string BarcodeImpl::decode(InputArray img, InputArray points, OutputArray straight_code) const +{ + CV_UNUSED(straight_code); + vector decoded_info; + vector decoded_type; + if (!decodeWithType(img, points, decoded_info, decoded_type)) + return string(); + if (decoded_info.size() < 1) + return string(); + return decoded_info[0]; +} + +string BarcodeImpl::detectAndDecode(InputArray img, OutputArray points, OutputArray straight_code) const +{ + CV_UNUSED(straight_code); + vector decoded_info; + vector decoded_type; + vector points_; + if (!detectAndDecodeWithType(img, decoded_info, decoded_type, points_)) + return string(); + if (points_.size() < 4 || decoded_info.size() < 1) + return string(); + points_.resize(4); + points.setTo(points_); + return decoded_info[0]; +} + +bool BarcodeImpl::detectMulti(InputArray img, OutputArray points) const +{ + return detect(img, points); +} + +bool BarcodeImpl::decodeMulti(InputArray img, InputArray points, vector &decoded_info, OutputArrayOfArrays straight_code) const +{ + CV_UNUSED(straight_code); + vector decoded_type; + return decodeWithType(img, points, decoded_info, decoded_type); +} + +bool BarcodeImpl::detectAndDecodeMulti(InputArray img, vector &decoded_info, OutputArray points, OutputArrayOfArrays straight_code) const +{ + CV_UNUSED(straight_code); + vector decoded_type; + return detectAndDecodeWithType(img, decoded_info, decoded_type, points); +} + +//================================================================================================== +// Public class implementation + +BarcodeDetector::BarcodeDetector() + : BarcodeDetector(string(), string()) +{ +} + +BarcodeDetector::BarcodeDetector(const string &prototxt_path, const string &model_path) +{ + Ptr p_ = new BarcodeImpl(); + p = p_; + if (!prototxt_path.empty() && !model_path.empty()) + { + CV_Assert(utils::fs::exists(prototxt_path)); + CV_Assert(utils::fs::exists(model_path)); + p_->sr = make_shared(); + int res = p_->sr->init(prototxt_path, model_path); + CV_Assert(res == 0); + p_->use_nn_sr = true; + } +} + +BarcodeDetector::~BarcodeDetector() = default; + +bool BarcodeDetector::decodeWithType(InputArray img, InputArray points, vector &decoded_info, vector &decoded_type) const +{ + Ptr p_ = dynamic_pointer_cast(p); + CV_Assert(p_); + return p_->decodeWithType(img, points, decoded_info, decoded_type); +} + +bool BarcodeDetector::detectAndDecodeWithType(InputArray img, vector &decoded_info, vector &decoded_type, OutputArray points_) const +{ + Ptr p_ = dynamic_pointer_cast(p); + CV_Assert(p_); + return p_->detectAndDecodeWithType(img, decoded_info, decoded_type, points_); +} + +}// namespace barcode +} // namespace cv diff --git a/modules/objdetect/src/barcode_decoder/abs_decoder.cpp b/modules/objdetect/src/barcode_decoder/abs_decoder.cpp new file mode 100644 index 0000000000..9eadf4bc31 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/abs_decoder.cpp @@ -0,0 +1,118 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "../precomp.hpp" +#include "abs_decoder.hpp" + +namespace cv { +namespace barcode { + +void cropROI(const Mat &src, Mat &dst, const std::vector &rects) +{ + std::vector vertices = rects; + int height = cvRound(norm(vertices[0] - vertices[1])); + int width = cvRound(norm(vertices[1] - vertices[2])); + if (height > width) + { + std::swap(height, width); + Point2f v0 = vertices[0]; + vertices.erase(vertices.begin()); + vertices.push_back(v0); + } + std::vector dst_vertices{ + Point2f(0, (float) (height - 1)), Point2f(0, 0), Point2f((float) (width - 1), 0), + Point2f((float) (width - 1), (float) (height - 1))}; + dst.create(Size(width, height), CV_8UC1); + Mat M = getPerspectiveTransform(vertices, dst_vertices); + warpPerspective(src, dst, M, dst.size(), cv::INTER_LINEAR, BORDER_CONSTANT, Scalar(255)); +} + +void fillCounter(const std::vector &row, uint start, Counter &counter) +{ + size_t counter_length = counter.pattern.size(); + std::fill(counter.pattern.begin(), counter.pattern.end(), 0); + counter.sum = 0; + size_t end = row.size(); + uchar color = row[start]; + uint counterPosition = 0; + while (start < end) + { + if (row[start] == color) + { // that is, exactly one is true + counter.pattern[counterPosition]++; + counter.sum++; + } + else + { + counterPosition++; + if (counterPosition == counter_length) + { + break; + } + else + { + counter.pattern[counterPosition] = 1; + counter.sum++; + color = 255 - color; + } + } + ++start; + } +} + +static inline uint +patternMatchVariance(const Counter &counter, const std::vector &pattern, uint maxIndividualVariance) +{ + size_t numCounters = counter.pattern.size(); + int total = static_cast(counter.sum); + int patternLength = std::accumulate(pattern.cbegin(), pattern.cend(), 0); + if (total < patternLength) + { + // If we don't even have one pixel per unit of bar width, assume this is too small + // to reliably match, so fail: + // and use constexpr functions + return WHITE;// max + } + // We're going to fake floating-point math in integers. We just need to use more bits. + // Scale up patternLength so that intermediate values below like scaledCounter will have + // more "significant digits" + + int unitBarWidth = (total << INTEGER_MATH_SHIFT) / patternLength; + maxIndividualVariance = (maxIndividualVariance * unitBarWidth) >> INTEGER_MATH_SHIFT; + uint totalVariance = 0; + for (uint x = 0; x < numCounters; x++) + { + int cnt = counter.pattern[x] << INTEGER_MATH_SHIFT; + int scaledPattern = pattern[x] * unitBarWidth; + uint variance = std::abs(cnt - scaledPattern); + if (variance > maxIndividualVariance) + { + return WHITE; + } + totalVariance += variance; + } + return totalVariance / total; +} + +/** +* Determines how closely a set of observed counts of runs of black/white values matches a given +* target pattern. This is reported as the ratio of the total variance from the expected pattern +* proportions across all pattern elements, to the length of the pattern. +* +* @param counters observed counters +* @param pattern expected pattern +* @param maxIndividualVariance The most any counter can differ before we give up +* @return ratio of total variance between counters and pattern compared to total pattern size, +* where the ratio has been multiplied by 256. So, 0 means no variance (perfect match); 256 means +* the total variance between counters and patterns equals the pattern length, higher values mean +* even more variance +*/ +uint patternMatch(const Counter &counters, const std::vector &pattern, uint maxIndividual) +{ + CV_Assert(counters.pattern.size() == pattern.size()); + return patternMatchVariance(counters, pattern, maxIndividual); +} +} +} diff --git a/modules/objdetect/src/barcode_decoder/abs_decoder.hpp b/modules/objdetect/src/barcode_decoder/abs_decoder.hpp new file mode 100644 index 0000000000..87b33e7f1d --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/abs_decoder.hpp @@ -0,0 +1,99 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_BARCODE_ABS_DECODER_HPP +#define OPENCV_BARCODE_ABS_DECODER_HPP + +#include "opencv2/objdetect/barcode.hpp" + +namespace cv { +namespace barcode { +using std::string; +using std::vector; +constexpr static uchar BLACK = std::numeric_limits::min(); +// WHITE elemental area is 0xff +constexpr static uchar WHITE = std::numeric_limits::max(); + + +struct Result +{ + enum BarcodeType + { + BARCODE_NONE, + BARCODE_EAN_8, + BARCODE_EAN_13, + BARCODE_UPC_A, + BARCODE_UPC_E, + BARCODE_UPC_EAN_EXTENSION + }; + + std::string result; + BarcodeType format = Result::BARCODE_NONE; + + Result() = default; + + Result(const std::string &_result, BarcodeType _format) + { + result = _result; + format = _format; + } + string typeString() const + { + switch (format) + { + case Result::BARCODE_EAN_8: return "EAN_8"; + case Result::BARCODE_EAN_13: return "EAN_13"; + case Result::BARCODE_UPC_E: return "UPC_E"; + case Result::BARCODE_UPC_A: return "UPC_A"; + case Result::BARCODE_UPC_EAN_EXTENSION: return "UPC_EAN_EXTENSION"; + default: return string(); + } + } + bool isValid() const + { + return format != BARCODE_NONE; + } +}; + +struct Counter +{ + std::vector pattern; + uint sum; + + explicit Counter(const vector &_pattern) + { + pattern = _pattern; + sum = 0; + } +}; + +class AbsDecoder +{ +public: + virtual std::pair decodeROI(const Mat &bar_img) const = 0; + + virtual ~AbsDecoder() = default; + +protected: + virtual Result decode(const vector &data) const = 0; + + virtual bool isValid(const string &result) const = 0; + + size_t bits_num{}; + size_t digit_number{}; +}; + +void cropROI(const Mat &_src, Mat &_dst, const std::vector &rect); + +void fillCounter(const std::vector &row, uint start, Counter &counter); + +constexpr static uint INTEGER_MATH_SHIFT = 8; +constexpr static uint PATTERN_MATCH_RESULT_SCALE_FACTOR = 1 << INTEGER_MATH_SHIFT; + +uint patternMatch(const Counter &counters, const std::vector &pattern, uint maxIndividual); +} +} // namespace cv + +#endif // OPENCV_BARCODE_ABS_DECODER_HPP diff --git a/modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.cpp b/modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.cpp new file mode 100644 index 0000000000..76d63d6e46 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.cpp @@ -0,0 +1,195 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Modified from ZXing. Copyright ZXing authors. +// Licensed under the Apache License, Version 2.0 (the "License"). + +#include "../../precomp.hpp" +#include "hybrid_binarizer.hpp" + +namespace cv { +namespace barcode { + + +#define CLAMP(x, x1, x2) x < (x1) ? (x1) : ((x) > (x2) ? (x2) : (x)) + +// This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. +// So this is the smallest dimension in each axis we can accept. +constexpr static int BLOCK_SIZE_POWER = 3; +constexpr static int BLOCK_SIZE = 1 << BLOCK_SIZE_POWER; // ...0100...00 +constexpr static int BLOCK_SIZE_MASK = BLOCK_SIZE - 1; // ...0011...11 +constexpr static int MINIMUM_DIMENSION = BLOCK_SIZE * 5; +constexpr static int MIN_DYNAMIC_RANGE = 24; + +void +calculateThresholdForBlock(const std::vector &luminances, int sub_width, int sub_height, int width, int height, + const Mat &black_points, Mat &dst) +{ + int maxYOffset = height - BLOCK_SIZE; + int maxXOffset = width - BLOCK_SIZE; + for (int y = 0; y < sub_height; y++) + { + int yoffset = y << BLOCK_SIZE_POWER; + if (yoffset > maxYOffset) + { + yoffset = maxYOffset; + } + int top = CLAMP(y, 2, sub_height - 3); + for (int x = 0; x < sub_width; x++) + { + int xoffset = x << BLOCK_SIZE_POWER; + if (xoffset > maxXOffset) + { + xoffset = maxXOffset; + } + int left = CLAMP(x, 2, sub_width - 3); + int sum = 0; + const auto *black_row = black_points.ptr(top - 2); + for (int z = 0; z <= 4; z++) + { + sum += black_row[left - 2] + black_row[left - 1] + black_row[left] + black_row[left + 1] + + black_row[left + 2]; + black_row += black_points.cols; + } + int average = sum / 25; + int temp_y = 0; + + auto *ptr = dst.ptr(yoffset, xoffset); + for (int offset = yoffset * width + xoffset; temp_y < 8; offset += width) + { + for (int temp_x = 0; temp_x < 8; ++temp_x) + { + *(ptr + temp_x) = (luminances[offset + temp_x] & 255) <= average ? 0 : 255; + } + ++temp_y; + ptr += width; + } + } + } + +} + +Mat calculateBlackPoints(std::vector luminances, int sub_width, int sub_height, int width, int height) +{ + int maxYOffset = height - BLOCK_SIZE; + int maxXOffset = width - BLOCK_SIZE; + Mat black_points(Size(sub_width, sub_height), CV_8UC1); + for (int y = 0; y < sub_height; y++) + { + int yoffset = y << BLOCK_SIZE_POWER; + if (yoffset > maxYOffset) + { + yoffset = maxYOffset; + } + for (int x = 0; x < sub_width; x++) + { + int xoffset = x << BLOCK_SIZE_POWER; + if (xoffset > maxXOffset) + { + xoffset = maxXOffset; + } + int sum = 0; + int min = 0xFF; + int max = 0; + for (int yy = 0, offset = yoffset * width + xoffset; yy < BLOCK_SIZE; yy++, offset += width) + { + for (int xx = 0; xx < BLOCK_SIZE; xx++) + { + int pixel = luminances[offset + xx] & 0xFF; + sum += pixel; + // still looking for good contrast + if (pixel < min) + { + min = pixel; + } + if (pixel > max) + { + max = pixel; + } + } + // short-circuit min/max tests once dynamic range is met + if (max - min > MIN_DYNAMIC_RANGE) + { + // finish the rest of the rows quickly + for (yy++, offset += width; yy < BLOCK_SIZE; yy++, offset += width) + { + for (int xx = 0; xx < BLOCK_SIZE; xx++) + { + sum += luminances[offset + xx] & 0xFF; + } + } + } + } + + // The default estimate is the average of the values in the block. + int average = sum >> (BLOCK_SIZE_POWER * 2); + if (max - min <= MIN_DYNAMIC_RANGE) + { + // If variation within the block is low, assume this is a block with only light or only + // dark pixels. In that case we do not want to use the average, as it would divide this + // low contrast area into black and white pixels, essentially creating data out of noise. + // + // The default assumption is that the block is light/background. Since no estimate for + // the level of dark pixels exists locally, use half the min for the block. + average = min / 2; + + if (y > 0 && x > 0) + { + // Correct the "white background" assumption for blocks that have neighbors by comparing + // the pixels in this block to the previously calculated black points. This is based on + // the fact that dark barcode symbology is always surrounded by some amount of light + // background for which reasonable black point estimates were made. The bp estimated at + // the boundaries is used for the interior. + + // The (min < bp) is arbitrary but works better than other heuristics that were tried. + int averageNeighborBlackPoint = + (black_points.at(y - 1, x) + (2 * black_points.at(y, x - 1)) + + black_points.at(y - 1, x - 1)) / 4; + if (min < averageNeighborBlackPoint) + { + average = averageNeighborBlackPoint; + } + } + } + black_points.at(y, x) = (uchar) average; + } + } + return black_points; + +} + + +void hybridBinarization(const Mat &src, Mat &dst) +{ + int width = src.cols; + int height = src.rows; + + if (width >= MINIMUM_DIMENSION && height >= MINIMUM_DIMENSION) + { + std::vector luminances(src.begin(), src.end()); + + int sub_width = width >> BLOCK_SIZE_POWER; + if ((width & BLOCK_SIZE_MASK) != 0) + { + sub_width++; + } + + int sub_height = height >> BLOCK_SIZE_POWER; + if ((height & BLOCK_SIZE_MASK) != 0) + { + sub_height++; + } + + Mat black_points = calculateBlackPoints(luminances, sub_width, sub_height, width, height); + + dst.create(src.size(), src.type()); + calculateThresholdForBlock(luminances, sub_width, sub_height, width, height, black_points, dst); + } + else + { + threshold(src, dst, 155, 255, THRESH_OTSU + THRESH_BINARY); + } + +} +} +} diff --git a/modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.hpp b/modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.hpp new file mode 100644 index 0000000000..88f93d03c6 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/common/hybrid_binarizer.hpp @@ -0,0 +1,22 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Modified from ZXing. Copyright ZXing authors. +// Licensed under the Apache License, Version 2.0 (the "License"). + +#ifndef OPENCV_BARCODE_HYBRID_BINARIZER_HPP +#define OPENCV_BARCODE_HYBRID_BINARIZER_HPP + +namespace cv { +namespace barcode { + +void hybridBinarization(const Mat &src, Mat &dst); + +void +calculateThresholdForBlock(const std::vector &luminances, int sub_width, int sub_height, int width, int height, + const Mat &black_points, Mat &dst); + +Mat calculateBlackPoints(std::vector luminances, int sub_width, int sub_height, int width, int height); +} +} +#endif // OPENCV_BARCODE_HYBRID_BINARIZER_HPP diff --git a/modules/objdetect/src/barcode_decoder/common/super_scale.cpp b/modules/objdetect/src/barcode_decoder/common/super_scale.cpp new file mode 100644 index 0000000000..0c9f75f156 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/common/super_scale.cpp @@ -0,0 +1,77 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// Tencent is pleased to support the open source community by making WeChat QRCode available. +// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved. +// Modified by darkliang wangberlinT + +#include "../../precomp.hpp" +#include "super_scale.hpp" + +#ifdef HAVE_OPENCV_DNN + +namespace cv { +namespace barcode { +constexpr static float MAX_SCALE = 4.0f; + +int SuperScale::init(const std::string &proto_path, const std::string &model_path) +{ + srnet_ = dnn::readNetFromCaffe(proto_path, model_path); + net_loaded_ = true; + return 0; +} + +void SuperScale::processImageScale(const Mat &src, Mat &dst, float scale, const bool &use_sr, int sr_max_size) +{ + scale = min(scale, MAX_SCALE); + if (scale > .0 && scale < 1.0) + { // down sample + resize(src, dst, Size(), scale, scale, INTER_AREA); + } + else if (scale > 1.5 && scale < 2.0) + { + resize(src, dst, Size(), scale, scale, INTER_CUBIC); + } + else if (scale >= 2.0) + { + int width = src.cols; + int height = src.rows; + if (use_sr && (int) sqrt(width * height * 1.0) < sr_max_size && net_loaded_) + { + superResolutionScale(src, dst); + if (scale > 2.0) + { + processImageScale(dst, dst, scale / 2.0f, use_sr); + } + } + else + { resize(src, dst, Size(), scale, scale, INTER_CUBIC); } + } +} + +int SuperScale::superResolutionScale(const Mat &src, Mat &dst) +{ + Mat blob; + dnn::blobFromImage(src, blob, 1.0 / 255, Size(src.cols, src.rows), {0.0f}, false, false); + + srnet_.setInput(blob); + auto prob = srnet_.forward(); + + dst = Mat(prob.size[2], prob.size[3], CV_8UC1); + + for (int row = 0; row < prob.size[2]; row++) + { + const float *prob_score = prob.ptr(0, 0, row); + auto *dst_row = dst.ptr(row); + for (int col = 0; col < prob.size[3]; col++) + { + dst_row[col] = saturate_cast(prob_score[col] * 255.0f); + } + } + return 0; +} +} // namespace barcode +} // namespace cv + +#endif // HAVE_OPENCV_DNN diff --git a/modules/objdetect/src/barcode_decoder/common/super_scale.hpp b/modules/objdetect/src/barcode_decoder/common/super_scale.hpp new file mode 100644 index 0000000000..70e47424e4 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/common/super_scale.hpp @@ -0,0 +1,69 @@ +/// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// Tencent is pleased to support the open source community by making WeChat QRCode available. +// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved. + +#ifndef OPENCV_BARCODE_SUPER_SCALE_HPP +#define OPENCV_BARCODE_SUPER_SCALE_HPP + +#ifdef HAVE_OPENCV_DNN + +#include "opencv2/dnn.hpp" + +namespace cv { +namespace barcode { + +class SuperScale +{ +public: + SuperScale() = default; + + ~SuperScale() = default; + + int init(const std::string &proto_path, const std::string &model_path); + + void processImageScale(const Mat &src, Mat &dst, float scale, const bool &use_sr, int sr_max_size = 160); + +private: + dnn::Net srnet_; + bool net_loaded_ = false; + + int superResolutionScale(const cv::Mat &src, cv::Mat &dst); +}; + +} // namespace barcode +} // namespace cv + +#else // HAVE_OPENCV_DNN + +#include "opencv2/core.hpp" +#include "opencv2/core/utils/logger.hpp" + +namespace cv { +namespace barcode { + +class SuperScale +{ +public: + int init(const std::string &, const std::string &) + { + return 0; + } + void processImageScale(const Mat &src, Mat &dst, float scale, const bool & isEnabled, int) + { + if (isEnabled) + { + CV_LOG_WARNING(NULL, "objdetect/barcode: SuperScaling disabled - OpenCV has been built without DNN support"); + } + resize(src, dst, Size(), scale, scale, INTER_CUBIC); + } +}; + +} // namespace barcode +} // namespace cv + +#endif // !HAVE_OPENCV_DNN + +#endif // OPENCV_BARCODE_SUPER_SCALE_HPP diff --git a/modules/objdetect/src/barcode_decoder/common/utils.cpp b/modules/objdetect/src/barcode_decoder/common/utils.cpp new file mode 100644 index 0000000000..123955c665 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/common/utils.cpp @@ -0,0 +1,36 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "../../precomp.hpp" +#include "utils.hpp" +#include "hybrid_binarizer.hpp" + +namespace cv { +namespace barcode { + + +void sharpen(const Mat &src, const Mat &dst) +{ + Mat blur; + GaussianBlur(src, blur, Size(0, 0), 25); + addWeighted(src, 2, blur, -1, -20, dst); +} + +void binarize(const Mat &src, Mat &dst, BinaryType mode) +{ + switch (mode) + { + case OTSU: + threshold(src, dst, 155, 255, THRESH_OTSU + THRESH_BINARY); + break; + case HYBRID: + hybridBinarization(src, dst); + break; + default: + CV_Error(Error::StsNotImplemented, "This binary type is not yet implemented"); + } +} +} +} diff --git a/modules/objdetect/src/barcode_decoder/common/utils.hpp b/modules/objdetect/src/barcode_decoder/common/utils.hpp new file mode 100644 index 0000000000..85597c017b --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/common/utils.hpp @@ -0,0 +1,26 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_BARCODE_UTILS_HPP +#define OPENCV_BARCODE_UTILS_HPP + + +namespace cv { +namespace barcode { + +enum BinaryType +{ + OTSU = 0, HYBRID = 1 +}; +static constexpr BinaryType binary_types[] = {OTSU, HYBRID}; + +void sharpen(const Mat &src, const Mat &dst); + +void binarize(const Mat &src, Mat &dst, BinaryType mode); + +} +} + +#endif // OPENCV_BARCODE_UTILS_HPP diff --git a/modules/objdetect/src/barcode_decoder/ean13_decoder.cpp b/modules/objdetect/src/barcode_decoder/ean13_decoder.cpp new file mode 100644 index 0000000000..8be6122a7c --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/ean13_decoder.cpp @@ -0,0 +1,92 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "../precomp.hpp" +#include "ean13_decoder.hpp" + +// three digit decode method from https://baike.baidu.com/item/EAN-13 + +namespace cv { +namespace barcode { + +static constexpr size_t EAN13BITS_NUM = 95; +static constexpr size_t EAN13DIGIT_NUM = 13; +// default thought that mat is a matrix after binary-transfer. +/** +* decode EAN-13 +* @prama: data: the input array, +* @prama: start: the index of start order, begin at 0, max-value is data.size()-1 +* it scan begin at the data[start] +*/ +Result Ean13Decoder::decode(const vector &data) const +{ + string result; + char decode_result[EAN13DIGIT_NUM + 1]{'\0'}; + if (data.size() < EAN13BITS_NUM) + { + return Result("Wrong Size", Result::BARCODE_NONE); + } + pair pattern; + if (!findStartGuardPatterns(data, pattern)) + { + return Result("Begin Pattern Not Found", Result::BARCODE_NONE); + } + uint start = pattern.second; + Counter counter(vector{0, 0, 0, 0}); + size_t end = data.size(); + int first_char_bit = 0; + // [1,6] are left part of EAN, [7,12] are right part, index 0 is calculated by left part + for (int i = 1; i < 7 && start < end; ++i) + { + int bestMatch = decodeDigit(data, counter, start, get_AB_Patterns()); + if (bestMatch == -1) + { + return Result("Decode Error", Result::BARCODE_NONE); + } + decode_result[i] = static_cast('0' + bestMatch % 10); + start = counter.sum + start; + first_char_bit += (bestMatch >= 10) << i; + } + decode_result[0] = static_cast(FIRST_CHAR_ARRAY()[first_char_bit >> 2] + '0'); + // why there need >> 2? + // first, the i in for-cycle is begin in 1 + // second, the first i = 1 is always + Counter middle_counter(vector(MIDDLE_PATTERN().size())); + if (!findGuardPatterns(data, start, true, MIDDLE_PATTERN(), middle_counter, pattern)) + { + return Result("Middle Pattern Not Found", Result::BARCODE_NONE); + + } + start = pattern.second; + for (int i = 0; i < 6 && start < end; ++i) + { + int bestMatch = decodeDigit(data, counter, start, get_A_or_C_Patterns()); + if (bestMatch == -1) + { + return Result("Decode Error", Result::BARCODE_NONE); + } + decode_result[i + 7] = static_cast('0' + bestMatch); + start = counter.sum + start; + } + Counter end_counter(vector(BEGIN_PATTERN().size())); + if (!findGuardPatterns(data, start, false, BEGIN_PATTERN(), end_counter, pattern)) + { + return Result("End Pattern Not Found", Result::BARCODE_NONE); + } + result = string(decode_result); + if (!isValid(result)) + { + return Result("Wrong: " + result.append(string(EAN13DIGIT_NUM - result.size(), ' ')), Result::BARCODE_NONE); + } + return Result(result, Result::BARCODE_EAN_13); +} + +Ean13Decoder::Ean13Decoder() +{ + this->bits_num = EAN13BITS_NUM; + this->digit_number = EAN13DIGIT_NUM; +} +} +} diff --git a/modules/objdetect/src/barcode_decoder/ean13_decoder.hpp b/modules/objdetect/src/barcode_decoder/ean13_decoder.hpp new file mode 100644 index 0000000000..1fcedd7c67 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/ean13_decoder.hpp @@ -0,0 +1,31 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_BARCODE_EAN13_DECODER_HPP +#define OPENCV_BARCODE_EAN13_DECODER_HPP + +#include "upcean_decoder.hpp" + +namespace cv { +namespace barcode { +//extern struct EncodePair; +using std::string; +using std::vector; +using std::pair; + + +class Ean13Decoder : public UPCEANDecoder +{ +public: + Ean13Decoder(); + + ~Ean13Decoder() override = default; + +protected: + Result decode(const vector &data) const override; +}; +} +} // namespace cv +#endif // OPENCV_BARCODE_EAN13_DECODER_HPP diff --git a/modules/objdetect/src/barcode_decoder/ean8_decoder.cpp b/modules/objdetect/src/barcode_decoder/ean8_decoder.cpp new file mode 100644 index 0000000000..23be9dcd6c --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/ean8_decoder.cpp @@ -0,0 +1,79 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "../precomp.hpp" +#include "ean8_decoder.hpp" + +namespace cv { +namespace barcode { +static constexpr size_t EAN8BITS_NUM = 70; +static constexpr size_t EAN8DIGIT_NUM = 8; + +Result Ean8Decoder::decode(const vector &data) const +{ + std::string result; + char decode_result[EAN8DIGIT_NUM + 1]{'\0'}; + if (data.size() < EAN8BITS_NUM) + { + return Result("Wrong Size", Result::BARCODE_NONE); + } + pair pattern; + if (!findStartGuardPatterns(data, pattern)) + { + return Result("Begin Pattern Not Found", Result::BARCODE_NONE); + } + uint start = pattern.second; + Counter counter(vector{0, 0, 0, 0}); + size_t end = data.size(); + for (int i = 0; i < 4 && start < end; ++i) + { + int bestMatch = decodeDigit(data, counter, start, get_A_or_C_Patterns()); + if (bestMatch == -1) + { + return Result("Decode Error", Result::BARCODE_NONE); + } + decode_result[i] = static_cast('0' + bestMatch % 10); + start = counter.sum + start; + } + + Counter middle_counter(vector(MIDDLE_PATTERN().size())); + + if (!findGuardPatterns(data, start, true, MIDDLE_PATTERN(), middle_counter, pattern)) + { + return Result("Middle Pattern Not Found", Result::BARCODE_NONE); + } + + start = pattern.second; + for (int i = 0; i < 4 && start < end; ++i) + { + int bestMatch = decodeDigit(data, counter, start, get_A_or_C_Patterns()); + if (bestMatch == -1) + { + return Result("Decode Error", Result::BARCODE_NONE); + } + decode_result[i + 4] = static_cast('0' + bestMatch); + start = counter.sum + start; + } + Counter end_counter(vector(BEGIN_PATTERN().size())); + if (!findGuardPatterns(data, start, false, BEGIN_PATTERN(), end_counter, pattern)) + { + return Result("End Pattern Not Found", Result::BARCODE_NONE); + } + result = string(decode_result); + if (!isValid(result)) + { + return Result("Wrong: " + result.append(string(EAN8DIGIT_NUM - result.size(), ' ')), Result::BARCODE_NONE); + } + return Result(result, Result::BARCODE_EAN_8); +} + +Ean8Decoder::Ean8Decoder() +{ + this->digit_number = EAN8DIGIT_NUM; + this->bits_num = EAN8BITS_NUM; +} + +} +} diff --git a/modules/objdetect/src/barcode_decoder/ean8_decoder.hpp b/modules/objdetect/src/barcode_decoder/ean8_decoder.hpp new file mode 100644 index 0000000000..4f5a0624ef --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/ean8_decoder.hpp @@ -0,0 +1,32 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_BARCODE_EAN8_DECODER_HPP +#define OPENCV_BARCODE_EAN8_DECODER_HPP + +#include "upcean_decoder.hpp" + +namespace cv { +namespace barcode { + +using std::string; +using std::vector; +using std::pair; + +class Ean8Decoder : public UPCEANDecoder +{ + +public: + Ean8Decoder(); + + ~Ean8Decoder() override = default; + +protected: + Result decode(const vector &data) const override; +}; +} +} + +#endif // OPENCV_BARCODE_EAN8_DECODER_HPP diff --git a/modules/objdetect/src/barcode_decoder/upcean_decoder.cpp b/modules/objdetect/src/barcode_decoder/upcean_decoder.cpp new file mode 100644 index 0000000000..2288f5b81f --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/upcean_decoder.cpp @@ -0,0 +1,290 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "../precomp.hpp" +#include "upcean_decoder.hpp" +#include + +namespace cv { +namespace barcode { + +static constexpr int DIVIDE_PART = 15; +static constexpr int BIAS_PART = 2; + +#if 0 +void UPCEANDecoder::drawDebugLine(Mat &debug_img, const Point2i &begin, const Point2i &end) const +{ + Result result; + std::vector middle; + LineIterator line = LineIterator(debug_img, begin, end); + middle.reserve(line.count); + for (int cnt = 0; cnt < line.count; cnt++, line++) + { + middle.push_back(debug_img.at(line.pos())); + } + std::pair start_range; + if (findStartGuardPatterns(middle, start_range)) + { + circle(debug_img, Point2i(begin.x + start_range.second, begin.y), 2, Scalar(0), 2); + } + result = this->decode(middle); + if (result.format == Result::BARCODE_NONE) + { + result = this->decode(std::vector(middle.crbegin(), middle.crend())); + } + if (result.format == Result::BARCODE_NONE) + { + cv::line(debug_img, begin, end, Scalar(0), 2); + cv::putText(debug_img, result.result, begin, cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255), 1); + } +} +#endif + +bool UPCEANDecoder::findGuardPatterns(const std::vector &row, uint rowOffset, uchar whiteFirst, + const std::vector &pattern, Counter &counter, std::pair &result) +{ + size_t patternLength = pattern.size(); + size_t width = row.size(); + uchar color = whiteFirst ? WHITE : BLACK; + rowOffset = (int) (std::find(row.cbegin() + rowOffset, row.cend(), color) - row.cbegin()); + uint counterPosition = 0; + uint patternStart = rowOffset; + for (uint x = rowOffset; x < width; x++) + { + if (row[x] == color) + { + counter.pattern[counterPosition]++; + counter.sum++; + } + else + { + if (counterPosition == patternLength - 1) + { + if (patternMatch(counter, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) + { + result.first = patternStart; + result.second = x; + return true; + } + patternStart += counter.pattern[0] + counter.pattern[1]; + counter.sum -= counter.pattern[0] + counter.pattern[1]; + + std::copy(counter.pattern.begin() + 2, counter.pattern.end(), counter.pattern.begin()); + + counter.pattern[patternLength - 2] = 0; + counter.pattern[patternLength - 1] = 0; + counterPosition--; + } + else + { + counterPosition++; + } + counter.pattern[counterPosition] = 1; + counter.sum++; + color = (std::numeric_limits::max() - color); + } + } + return false; +} + +bool UPCEANDecoder::findStartGuardPatterns(const std::vector &row, std::pair &start_range) +{ + bool is_find = false; + int next_start = 0; + while (!is_find) + { + Counter guard_counters(std::vector{0, 0, 0}); + if (!findGuardPatterns(row, next_start, BLACK, BEGIN_PATTERN(), guard_counters, start_range)) + { + return false; + } + int start = static_cast(start_range.first); + next_start = static_cast(start_range.second); + int quiet_start = max(start - (next_start - start), 0); + is_find = (quiet_start != start) && + (std::find(std::begin(row) + quiet_start, std::begin(row) + start, BLACK) == std::begin(row) + start); + } + return true; +} + +int UPCEANDecoder::decodeDigit(const std::vector &row, Counter &counters, uint rowOffset, + const std::vector> &patterns) +{ + fillCounter(row, rowOffset, counters); + int bestMatch = -1; + uint bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept + int i = 0; + for (const auto &pattern : patterns) + { + uint variance = patternMatch(counters, pattern, MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) + { + bestVariance = variance; + bestMatch = i; + } + i++; + } + return std::max(-1, bestMatch); + // -1 is Mismatch or means error. +} + +/*Input a ROI mat return result */ +std::pair UPCEANDecoder::decodeROI(const Mat &bar_img) const +{ + if ((size_t) bar_img.cols < this->bits_num) + { + return std::make_pair(Result{string(), Result::BARCODE_NONE}, 0.0F); + } + + std::map result_vote; + std::map format_vote; + int vote_cnt = 0; + int total_vote = 0; + std::string max_result; + Result::BarcodeType max_type = Result::BARCODE_NONE; + + const int step = bar_img.rows / (DIVIDE_PART + BIAS_PART); + Result result; + int row_num; + for (int i = 0; i < DIVIDE_PART; ++i) + { + row_num = (i + BIAS_PART / 2) * step; + if (row_num < 0 || row_num > bar_img.rows) + { + continue; + } + const auto *ptr = bar_img.ptr(row_num); + vector line(ptr, ptr + bar_img.cols); + result = decodeLine(line); + if (result.format != Result::BARCODE_NONE) + { + total_vote++; + result_vote[result.result] += 1; + if (result_vote[result.result] > vote_cnt) + { + vote_cnt = result_vote[result.result]; + max_result = result.result; + max_type = result.format; + } + } + } + if (total_vote == 0 || (vote_cnt << 2) < total_vote) + { + return std::make_pair(Result(string(), Result::BARCODE_NONE), 0.0f); + } + + float confidence = (float) vote_cnt / (float) DIVIDE_PART; + //Check if it is UPC-A format + if (max_type == Result::BARCODE_EAN_13 && max_result[0] == '0') + { + max_result = max_result.substr(1, 12); //UPC-A length 12 + max_type = Result::BARCODE_UPC_A; + } + return std::make_pair(Result(max_result, max_type), confidence); +} + + +Result UPCEANDecoder::decodeLine(const vector &line) const +{ + Result result = this->decode(line); + if (result.format == Result::BARCODE_NONE) + { + result = this->decode(std::vector(line.crbegin(), line.crend())); + } + return result; +} + +bool UPCEANDecoder::isValid(const string &result) const +{ + if (result.size() != digit_number) + { + return false; + } + int sum = 0; + for (int index = (int) result.size() - 2, i = 1; index >= 0; index--, i++) + { + int temp = result[index] - '0'; + sum += (temp + ((i & 1) != 0 ? temp << 1 : 0)); + } + return (result.back() - '0') == ((10 - (sum % 10)) % 10); +} + +// right for A +const std::vector> &get_A_or_C_Patterns() +{ + static const std::vector> A_or_C_Patterns{{3, 2, 1, 1}, // 0 + {2, 2, 2, 1}, // 1 + {2, 1, 2, 2}, // 2 + {1, 4, 1, 1}, // 3 + {1, 1, 3, 2}, // 4 + {1, 2, 3, 1}, // 5 + {1, 1, 1, 4}, // 6 + {1, 3, 1, 2}, // 7 + {1, 2, 1, 3}, // 8 + {3, 1, 1, 2} // 9 + }; + return A_or_C_Patterns; +} + +const std::vector> &get_AB_Patterns() +{ + static const std::vector> AB_Patterns = [] { + constexpr uint offset = 10; + auto AB_Patterns_inited = std::vector>(offset << 1, std::vector(PATTERN_LENGTH, 0)); + std::copy(get_A_or_C_Patterns().cbegin(), get_A_or_C_Patterns().cend(), AB_Patterns_inited.begin()); + //AB pattern is + for (uint i = 0; i < offset; ++i) + { + for (uint j = 0; j < PATTERN_LENGTH; ++j) + { + AB_Patterns_inited[i + offset][j] = AB_Patterns_inited[i][PATTERN_LENGTH - j - 1]; + } + } + return AB_Patterns_inited; + }(); + return AB_Patterns; +} + +const std::vector &BEGIN_PATTERN() +{ + // it just need it's 1:1:1(black:white:black) + static const std::vector BEGIN_PATTERN_(3, 1); + return BEGIN_PATTERN_; +} + +const std::vector &MIDDLE_PATTERN() +{ + // it just need it's 1:1:1:1:1(white:black:white:black:white) + static const std::vector MIDDLE_PATTERN_(5, 1); + return MIDDLE_PATTERN_; +} + +const std::array &FIRST_CHAR_ARRAY() +{ + // use array to simulation a Hashmap, + // because the data's size is small, + // use a hashmap or brute-force search 10 times both can not accept + static const std::array pattern{ + '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x06', '\x00', '\x00', '\x00', '\x09', '\x00', + '\x08', '\x03', '\x00', '\x00', '\x00', '\x00', '\x05', '\x00', '\x07', '\x02', '\x00', '\x00', '\x04', + '\x01', '\x00', '\x00', '\x00', '\x00', '\x00'}; + // length is 32 to ensure the security + // 0x00000 -> 0 -> 0 + // 0x11010 -> 26 -> 1 + // 0x10110 -> 22 -> 2 + // 0x01110 -> 14 -> 3 + // 0x11001 -> 25 -> 4 + // 0x10011 -> 19 -> 5 + // 0x00111 -> 7 -> 6 + // 0x10101 -> 21 -> 7 + // 0x01101 -> 13 -> 8 + // 0x01011 -> 11 -> 9 + // delete the 1-13's 2 number's bit, + // it always be A which do not need to count. + return pattern; +} +} + +} // namespace cv diff --git a/modules/objdetect/src/barcode_decoder/upcean_decoder.hpp b/modules/objdetect/src/barcode_decoder/upcean_decoder.hpp new file mode 100644 index 0000000000..6efc1094a5 --- /dev/null +++ b/modules/objdetect/src/barcode_decoder/upcean_decoder.hpp @@ -0,0 +1,67 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_BARCODE_UPCEAN_DECODER_HPP +#define OPENCV_BARCODE_UPCEAN_DECODER_HPP + +#include "abs_decoder.hpp" + +/** + * upcean_decoder the abstract basic class for decode formats, + * it will have ean13/8,upc_a,upc_e , etc.. class extend this class +*/ +namespace cv { +namespace barcode { +using std::string; +using std::vector; + +class UPCEANDecoder : public AbsDecoder +{ + +public: + ~UPCEANDecoder() override = default; + + std::pair decodeROI(const Mat &bar_img) const override; + +protected: + static int decodeDigit(const std::vector &row, Counter &counters, uint rowOffset, + const std::vector> &patterns); + + static bool + findGuardPatterns(const std::vector &row, uint rowOffset, uchar whiteFirst, const std::vector &pattern, + Counter &counter, std::pair &result); + + static bool findStartGuardPatterns(const std::vector &row, std::pair &start_range); + + Result decodeLine(const vector &line) const; + + Result decode(const vector &bar) const override = 0; + + bool isValid(const string &result) const override; + +private: + #if 0 + void drawDebugLine(Mat &debug_img, const Point2i &begin, const Point2i &end) const; + #endif +}; + +const std::vector> &get_A_or_C_Patterns(); + +const std::vector> &get_AB_Patterns(); + +const std::vector &BEGIN_PATTERN(); + +const std::vector &MIDDLE_PATTERN(); + +const std::array &FIRST_CHAR_ARRAY(); + +constexpr static uint PATTERN_LENGTH = 4; +constexpr static uint MAX_AVG_VARIANCE = static_cast(PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.48f); +constexpr static uint MAX_INDIVIDUAL_VARIANCE = static_cast(PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.7f); + +} +} // namespace cv + +#endif // OPENCV_BARCODE_UPCEAN_DECODER_HPP diff --git a/modules/objdetect/src/barcode_detector/bardetect.cpp b/modules/objdetect/src/barcode_detector/bardetect.cpp new file mode 100644 index 0000000000..b156d1b25d --- /dev/null +++ b/modules/objdetect/src/barcode_detector/bardetect.cpp @@ -0,0 +1,510 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#include "../precomp.hpp" +#include "bardetect.hpp" + + +namespace cv { +namespace barcode { +static constexpr float PI = static_cast(CV_PI); +static constexpr float HALF_PI = static_cast(CV_PI / 2); + +#define CALCULATE_SUM(ptr, result) \ + top_left = static_cast(*((ptr) + left_col + integral_cols * top_row));\ + top_right = static_cast(*((ptr) + integral_cols * top_row + right_col));\ + bottom_right = static_cast(*((ptr) + right_col + bottom_row * integral_cols));\ + bottom_left = static_cast(*((ptr) + bottom_row * integral_cols + left_col));\ + (result) = (bottom_right - bottom_left - top_right + top_left); + + +inline bool Detect::isValidCoord(const Point &coord, const Size &limit) +{ + if ((coord.x < 0) || (coord.y < 0)) + { + return false; + } + + if ((unsigned) coord.x > (unsigned) (limit.width - 1) || ((unsigned) coord.y > (unsigned) (limit.height - 1))) + { + return false; + } + + return true; +} + +//============================================================================== +// NMSBoxes copied from modules/dnn/src/nms.inl.hpp +// TODO: move NMSBoxes outside the dnn module to allow other modules use it + +namespace +{ + +template +static inline bool SortScorePairDescend(const std::pair& pair1, + const std::pair& pair2) +{ + return pair1.first > pair2.first; +} + +inline void GetMaxScoreIndex(const std::vector& scores, const float threshold, const int top_k, + std::vector >& score_index_vec) +{ + CV_DbgAssert(score_index_vec.empty()); + // Generate index score pairs. + for (size_t i = 0; i < scores.size(); ++i) + { + if (scores[i] > threshold) + { + score_index_vec.push_back(std::make_pair(scores[i], (int)i)); + } + } + + // Sort the score pair according to the scores in descending order + std::stable_sort(score_index_vec.begin(), score_index_vec.end(), + SortScorePairDescend); + + // Keep top_k scores if needed. + if (top_k > 0 && top_k < (int)score_index_vec.size()) + { + score_index_vec.resize(top_k); + } +} + +template +inline void NMSFast_(const std::vector& bboxes, + const std::vector& scores, const float score_threshold, + const float nms_threshold, const float eta, const int top_k, + std::vector& indices, + float (*computeOverlap)(const BoxType&, const BoxType&), + size_t limit = std::numeric_limits::max()) +{ + CV_Assert(bboxes.size() == scores.size()); + + // Get top_k scores (with corresponding indices). + std::vector > score_index_vec; + GetMaxScoreIndex(scores, score_threshold, top_k, score_index_vec); + + // Do nms. + float adaptive_threshold = nms_threshold; + indices.clear(); + for (size_t i = 0; i < score_index_vec.size(); ++i) { + const int idx = score_index_vec[i].second; + bool keep = true; + for (int k = 0; k < (int)indices.size() && keep; ++k) { + const int kept_idx = indices[k]; + float overlap = computeOverlap(bboxes[idx], bboxes[kept_idx]); + keep = overlap <= adaptive_threshold; + } + if (keep) { + indices.push_back(idx); + if (indices.size() >= limit) { + break; + } + } + if (keep && eta < 1 && adaptive_threshold > 0.5) { + adaptive_threshold *= eta; + } + } +} + +static inline float rotatedRectIOU(const RotatedRect& a, const RotatedRect& b) +{ + std::vector inter; + int res = rotatedRectangleIntersection(a, b, inter); + if (inter.empty() || res == INTERSECT_NONE) + return 0.0f; + if (res == INTERSECT_FULL) + return 1.0f; + float interArea = (float)contourArea(inter); + return interArea / (a.size.area() + b.size.area() - interArea); +} + +static void NMSBoxes(const std::vector& bboxes, const std::vector& scores, + const float score_threshold, const float nms_threshold, + std::vector& indices, const float eta = 1.f, const int top_k = 0) +{ + CV_Assert_N(bboxes.size() == scores.size(), score_threshold >= 0, + nms_threshold >= 0, eta > 0); + NMSFast_(bboxes, scores, score_threshold, nms_threshold, eta, top_k, indices, rotatedRectIOU); +} + +} // namespace :: + + +//============================================================================== + +void Detect::init(const Mat &src) +{ + const double min_side = std::min(src.size().width, src.size().height); + if (min_side > 512.0) + { + purpose = SHRINKING; + coeff_expansion = min_side / 512.0; + width = cvRound(src.size().width / coeff_expansion); + height = cvRound(src.size().height / coeff_expansion); + Size new_size(width, height); + resize(src, resized_barcode, new_size, 0, 0, INTER_AREA); + } +// else if (min_side < 512.0) +// { +// purpose = ZOOMING; +// coeff_expansion = 512.0 / min_side; +// width = cvRound(src.size().width * coeff_expansion); +// height = cvRound(src.size().height * coeff_expansion); +// Size new_size(width, height); +// resize(src, resized_barcode, new_size, 0, 0, INTER_CUBIC); +// } + else + { + purpose = UNCHANGED; + coeff_expansion = 1.0; + width = src.size().width; + height = src.size().height; + resized_barcode = src.clone(); + } + // median blur: sometimes it reduces the noise, but also reduces the recall + // medianBlur(resized_barcode, resized_barcode, 3); + +} + + +void Detect::localization() +{ + + localization_bbox.clear(); + bbox_scores.clear(); + + // get integral image + preprocess(); + // empirical setting + static constexpr float SCALE_LIST[] = {0.01f, 0.03f, 0.06f, 0.08f}; + const auto min_side = static_cast(std::min(width, height)); + int window_size; + for (const float scale:SCALE_LIST) + { + window_size = cvRound(min_side * scale); + if(window_size == 0) { + window_size = 1; + } + calCoherence(window_size); + barcodeErode(); + regionGrowing(window_size); + } + +} + + +bool Detect::computeTransformationPoints() +{ + + bbox_indices.clear(); + transformation_points.clear(); + transformation_points.reserve(bbox_indices.size()); + RotatedRect rect; + Point2f temp[4]; + const float THRESHOLD_SCORE = float(width * height) / 300.f; + NMSBoxes(localization_bbox, bbox_scores, THRESHOLD_SCORE, 0.1f, bbox_indices); + + for (const auto &bbox_index : bbox_indices) + { + rect = localization_bbox[bbox_index]; + if (purpose == ZOOMING) + { + rect.center /= coeff_expansion; + rect.size.height /= static_cast(coeff_expansion); + rect.size.width /= static_cast(coeff_expansion); + } + else if (purpose == SHRINKING) + { + rect.center *= coeff_expansion; + rect.size.height *= static_cast(coeff_expansion); + rect.size.width *= static_cast(coeff_expansion); + } + rect.points(temp); + transformation_points.emplace_back(vector{temp[0], temp[1], temp[2], temp[3]}); + } + + return !transformation_points.empty(); +} + + +void Detect::preprocess() +{ + Mat scharr_x, scharr_y, temp; + static constexpr double THRESHOLD_MAGNITUDE = 64.; + Scharr(resized_barcode, scharr_x, CV_32F, 1, 0); + Scharr(resized_barcode, scharr_y, CV_32F, 0, 1); + // calculate magnitude of gradient and truncate + magnitude(scharr_x, scharr_y, temp); + threshold(temp, temp, THRESHOLD_MAGNITUDE, 1, THRESH_BINARY); + temp.convertTo(gradient_magnitude, CV_8U); + integral(gradient_magnitude, integral_edges, CV_32F); + + + for (int y = 0; y < height; y++) + { + auto *const x_row = scharr_x.ptr(y); + auto *const y_row = scharr_y.ptr(y); + auto *const magnitude_row = gradient_magnitude.ptr(y); + for (int pos = 0; pos < width; pos++) + { + if (magnitude_row[pos] == 0) + { + x_row[pos] = 0; + y_row[pos] = 0; + continue; + } + if (x_row[pos] < 0) + { + x_row[pos] *= -1; + y_row[pos] *= -1; + } + } + } + integral(scharr_x, temp, integral_x_sq, CV_32F, CV_32F); + integral(scharr_y, temp, integral_y_sq, CV_32F, CV_32F); + integral(scharr_x.mul(scharr_y), integral_xy, temp, CV_32F, CV_32F); +} + + +// Change coherence orientation edge_nums +// depend on width height integral_edges integral_x_sq integral_y_sq integral_xy +void Detect::calCoherence(int window_size) +{ + static constexpr float THRESHOLD_COHERENCE = 0.9f; + int right_col, left_col, top_row, bottom_row; + float xy, x_sq, y_sq, d, rect_area; + const float THRESHOLD_AREA = float(window_size * window_size) * 0.42f; + Size new_size(width / window_size, height / window_size); + coherence = Mat(new_size, CV_8U), orientation = Mat(new_size, CV_32F), edge_nums = Mat(new_size, CV_32F); + + float top_left, top_right, bottom_left, bottom_right; + int integral_cols = width + 1; + const auto *edges_ptr = integral_edges.ptr(), *x_sq_ptr = integral_x_sq.ptr(), *y_sq_ptr = integral_y_sq.ptr(), *xy_ptr = integral_xy.ptr(); + for (int y = 0; y < new_size.height; y++) + { + auto *coherence_row = coherence.ptr(y); + auto *orientation_row = orientation.ptr(y); + auto *edge_nums_row = edge_nums.ptr(y); + if (y * window_size >= height) + { + continue; + } + top_row = y * window_size; + bottom_row = min(height, (y + 1) * window_size); + + for (int pos = 0; pos < new_size.width; pos++) + { + + // then calculate the column locations of the rectangle and set them to -1 + // if they are outside the matrix bounds + if (pos * window_size >= width) + { + continue; + } + left_col = pos * window_size; + right_col = min(width, (pos + 1) * window_size); + + //we had an integral image to count non-zero elements + CALCULATE_SUM(edges_ptr, rect_area) + if (rect_area < THRESHOLD_AREA) + { + // smooth region + coherence_row[pos] = 0; + continue; + } + + CALCULATE_SUM(x_sq_ptr, x_sq) + CALCULATE_SUM(y_sq_ptr, y_sq) + CALCULATE_SUM(xy_ptr, xy) + + // get the values of the rectangle corners from the integral image - 0 if outside bounds + d = sqrt((x_sq - y_sq) * (x_sq - y_sq) + 4 * xy * xy) / (x_sq + y_sq); + if (d > THRESHOLD_COHERENCE) + { + coherence_row[pos] = 255; + orientation_row[pos] = atan2(x_sq - y_sq, 2 * xy) / 2.0f; + edge_nums_row[pos] = rect_area; + } + else + { + coherence_row[pos] = 0; + } + + } + + } +} + +// will change localization_bbox bbox_scores +// will change coherence, +// depend on coherence orientation edge_nums +void Detect::regionGrowing(int window_size) +{ + static constexpr float LOCAL_THRESHOLD_COHERENCE = 0.95f, THRESHOLD_RADIAN = + PI / 30, LOCAL_RATIO = 0.5f, EXPANSION_FACTOR = 1.2f; + static constexpr uint THRESHOLD_BLOCK_NUM = 35; + Point pt_to_grow, pt; //point to grow + + float src_value; + float cur_value; + float edge_num; + float rect_orientation; + float sin_sum, cos_sum; + uint counter; + //grow direction + static constexpr int DIR[8][2] = {{-1, -1}, + {0, -1}, + {1, -1}, + {1, 0}, + {1, 1}, + {0, 1}, + {-1, 1}, + {-1, 0}}; + vector growingPoints, growingImgPoints; + for (int y = 0; y < coherence.rows; y++) + { + auto *coherence_row = coherence.ptr(y); + + for (int x = 0; x < coherence.cols; x++) + { + if (coherence_row[x] == 0) + { + continue; + } + // flag + coherence_row[x] = 0; + growingPoints.clear(); + growingImgPoints.clear(); + + pt = Point(x, y); + cur_value = orientation.at(pt); + sin_sum = sin(2 * cur_value); + cos_sum = cos(2 * cur_value); + counter = 1; + edge_num = edge_nums.at(pt); + growingPoints.push_back(pt); + growingImgPoints.push_back(Point(pt)); + while (!growingPoints.empty()) + { + pt = growingPoints.back(); + growingPoints.pop_back(); + src_value = orientation.at(pt); + + //growing in eight directions + for (auto i : DIR) + { + pt_to_grow = Point(pt.x + i[0], pt.y + i[1]); + + //check if out of boundary + if (!isValidCoord(pt_to_grow, coherence.size())) + { + continue; + } + + if (coherence.at(pt_to_grow) == 0) + { + continue; + } + cur_value = orientation.at(pt_to_grow); + if (abs(cur_value - src_value) < THRESHOLD_RADIAN || + abs(cur_value - src_value) > PI - THRESHOLD_RADIAN) + { + coherence.at(pt_to_grow) = 0; + sin_sum += sin(2 * cur_value); + cos_sum += cos(2 * cur_value); + counter += 1; + edge_num += edge_nums.at(pt_to_grow); + growingPoints.push_back(pt_to_grow); //push next point to grow back to stack + growingImgPoints.push_back(pt_to_grow); + } + } + } + //minimum block num + if (counter < THRESHOLD_BLOCK_NUM) + { + continue; + } + float local_coherence = (sin_sum * sin_sum + cos_sum * cos_sum) / static_cast(counter * counter); + // minimum local gradient orientation_arg coherence_arg + if (local_coherence < LOCAL_THRESHOLD_COHERENCE) + { + continue; + } + RotatedRect minRect = minAreaRect(growingImgPoints); + if (edge_num < minRect.size.area() * float(window_size * window_size) * LOCAL_RATIO || + static_cast(counter) < minRect.size.area() * LOCAL_RATIO) + { + continue; + } + const float local_orientation = atan2(cos_sum, sin_sum) / 2.0f; + // only orientation_arg is approximately equal to the rectangle orientation_arg + rect_orientation = (minRect.angle) * PI / 180.f; + if (minRect.size.width < minRect.size.height) + { + rect_orientation += (rect_orientation <= 0.f ? HALF_PI : -HALF_PI); + std::swap(minRect.size.width, minRect.size.height); + } + if (abs(local_orientation - rect_orientation) > THRESHOLD_RADIAN && + abs(local_orientation - rect_orientation) < PI - THRESHOLD_RADIAN) + { + continue; + } + minRect.angle = local_orientation * 180.f / PI; + minRect.size.width *= static_cast(window_size) * EXPANSION_FACTOR; + minRect.size.height *= static_cast(window_size); + minRect.center.x = (minRect.center.x + 0.5f) * static_cast(window_size); + minRect.center.y = (minRect.center.y + 0.5f) * static_cast(window_size); + localization_bbox.push_back(minRect); + bbox_scores.push_back(edge_num); + + } + } +} + +inline const std::array &getStructuringElement() +{ + static const std::array structuringElement{ + Mat_{{3, 3}, + {255, 0, 0, 0, 0, 0, 0, 0, 255}}, Mat_{{3, 3}, + {0, 0, 255, 0, 0, 0, 255, 0, 0}}, + Mat_{{3, 3}, + {0, 0, 0, 255, 0, 255, 0, 0, 0}}, Mat_{{3, 3}, + {0, 255, 0, 0, 0, 0, 0, 255, 0}}}; + return structuringElement; +} + +// Change mat +void Detect::barcodeErode() +{ + static const std::array &structuringElement = getStructuringElement(); + Mat m0, m1, m2, m3; + dilate(coherence, m0, structuringElement[0]); + dilate(coherence, m1, structuringElement[1]); + dilate(coherence, m2, structuringElement[2]); + dilate(coherence, m3, structuringElement[3]); + int sum; + for (int y = 0; y < coherence.rows; y++) + { + auto coherence_row = coherence.ptr(y); + auto m0_row = m0.ptr(y); + auto m1_row = m1.ptr(y); + auto m2_row = m2.ptr(y); + auto m3_row = m3.ptr(y); + + for (int pos = 0; pos < coherence.cols; pos++) + { + if (coherence_row[pos] != 0) + { + sum = m0_row[pos] + m1_row[pos] + m2_row[pos] + m3_row[pos]; + //more than 2 group + coherence_row[pos] = sum > 600 ? 255 : 0; + } + } + } +} +} +} diff --git a/modules/objdetect/src/barcode_detector/bardetect.hpp b/modules/objdetect/src/barcode_detector/bardetect.hpp new file mode 100644 index 0000000000..9f084d20aa --- /dev/null +++ b/modules/objdetect/src/barcode_detector/bardetect.hpp @@ -0,0 +1,62 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (c) 2020-2021 darkliang wangberlinT Certseeds + +#ifndef OPENCV_BARCODE_BARDETECT_HPP +#define OPENCV_BARCODE_BARDETECT_HPP + + +#include + +namespace cv { +namespace barcode { +using std::vector; + +class Detect +{ +private: + vector localization_rects; + vector localization_bbox; + vector bbox_scores; + vector bbox_indices; + vector> transformation_points; + + +public: + void init(const Mat &src); + + void localization(); + + vector> getTransformationPoints() + { return transformation_points; } + + bool computeTransformationPoints(); + +protected: + enum resize_direction + { + ZOOMING, SHRINKING, UNCHANGED + } purpose = UNCHANGED; + + + double coeff_expansion = 1.0; + int height, width; + Mat resized_barcode, gradient_magnitude, coherence, orientation, edge_nums, integral_x_sq, integral_y_sq, integral_xy, integral_edges; + + void preprocess(); + + void calCoherence(int window_size); + + static inline bool isValidCoord(const Point &coord, const Size &limit); + + void regionGrowing(int window_size); + + void barcodeErode(); + + +}; +} +} + +#endif // OPENCV_BARCODE_BARDETECT_HPP diff --git a/modules/objdetect/src/precomp.hpp b/modules/objdetect/src/precomp.hpp index cbefc396be..790a980697 100644 --- a/modules/objdetect/src/precomp.hpp +++ b/modules/objdetect/src/precomp.hpp @@ -44,10 +44,13 @@ #define __OPENCV_PRECOMP_H__ #include "opencv2/objdetect.hpp" +#include "opencv2/objdetect/barcode.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/core/utility.hpp" #include "opencv2/core/ocl.hpp" #include "opencv2/core/private.hpp" +#include + #endif diff --git a/modules/objdetect/test/test_barcode.cpp b/modules/objdetect/test/test_barcode.cpp new file mode 100644 index 0000000000..b04401d59a --- /dev/null +++ b/modules/objdetect/test/test_barcode.cpp @@ -0,0 +1,127 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "test_precomp.hpp" +#include "opencv2/objdetect/barcode.hpp" + +namespace opencv_test{namespace{ + +typedef std::vector stringvec; +typedef std::map datasetType; + +inline stringvec explode(const std::string &s, const char &c) +{ + std::string buff; + stringvec v; + + for (auto n:s) + { + if (n != c) { buff += n; } + else if (n == c && !buff.empty()) + { + v.push_back(buff); + buff = ""; + } + } + if (!buff.empty()) { v.push_back(buff); } + + return v; +} + +inline datasetType buildDataSet(std::string result_file_path) +{ + std::ifstream result_file; + datasetType dataset; + result_file.open(result_file_path); + std::string line; + if (result_file.is_open()) + { + while (std::getline(result_file, line)) + { + stringvec result = explode(line, ','); + std::string filename = result[0]; + if (dataset.find(filename) == dataset.end()) + { + dataset[filename] = result[1]; + } + } + } + + result_file.close(); + return dataset; +} + +inline datasetType initValidation(std::string path) +{ + const std::string valid_path = findDataFile(path); + return buildDataSet(valid_path); +} + +//============================================================================== + +TEST(BARCODE_BarcodeDetector_single, regression) +{ + const std::string root = "barcode/single/"; + datasetType validation = initValidation(root + "result.csv"); + auto bardet = barcode::BarcodeDetector(); + datasetType::iterator iterator = validation.begin(); + while (iterator != validation.end()) + { + std::string img_name = iterator->first; + std::string result = iterator->second; + std::string image_path = findDataFile(root + img_name); + Mat img = imread(image_path); + EXPECT_FALSE(img.empty()) << "Can't read image: " << image_path; + std::vector points; + std::vector infos; + std::vector formats; + bardet.detectAndDecodeWithType(img, infos, formats, points); + EXPECT_FALSE(points.empty()) << "Nothing detected: " << image_path; + bool is_correct = false; + for (const auto &ans : infos) + { + if (ans == result) + { + is_correct = true; + break; + } + } + EXPECT_TRUE(is_correct) << "No results for " << img_name; + iterator++; + } +} + +TEST(BARCODE_BarcodeDetector_detect_multi, detect_regression) +{ + const std::string root = "barcode/multiple/"; + datasetType validation = initValidation(root + "result.csv"); + auto bardet = barcode::BarcodeDetector(); + datasetType::iterator iterator = validation.begin(); + while (iterator != validation.end()) + { + std::string img = iterator->first; + size_t expect_corners_size = std::stoi(iterator->second); + std::string image_path = findDataFile(root + img); + Mat src = imread(image_path); + EXPECT_FALSE(src.empty()) << "Can't read image: " << image_path; + + std::vector corners; + bardet.detectMulti(src, corners); + EXPECT_EQ(corners.size(), expect_corners_size) << "Can't detect all barcodes: " << img; + iterator++; + } +} + +TEST(BARCODE_BarcodeDetector_basic, not_found_barcode) +{ + auto bardet = barcode::BarcodeDetector(); + std::vector corners; + vector decoded_info; + Mat zero_image = Mat::zeros(256, 256, CV_8UC1); + EXPECT_FALSE(bardet.detectMulti(zero_image, corners)); + corners = std::vector(4); + EXPECT_ANY_THROW(bardet.decodeMulti(zero_image, corners, decoded_info)); +} + +}} // opencv_test:::: diff --git a/samples/cpp/barcode.cpp b/samples/cpp/barcode.cpp new file mode 100644 index 0000000000..5955d4a74d --- /dev/null +++ b/samples/cpp/barcode.cpp @@ -0,0 +1,223 @@ +#include +#include "opencv2/objdetect.hpp" +#include "opencv2/imgproc.hpp" +#include "opencv2/highgui.hpp" + +using namespace cv; +using namespace std; + +static const Scalar greenColor(0, 255, 0); +static const Scalar redColor(0, 0, 255); +static const Scalar yellowColor(0, 255, 255); +static Scalar randColor() +{ + RNG &rng = theRNG(); + return Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); +} + +//============================================================================== + +struct TheApp +{ + Ptr bardet; + //! [output] + vector corners; + vector decode_info; + vector decode_type; + //! [output] + bool detectOnly; + + void cleanup() + { + corners.clear(); + decode_info.clear(); + decode_type.clear(); + } + + inline string modeString() const + { + return detectOnly ? "" : ""; + } + + void drawResults(Mat &frame) const + { + //! [visualize] + for (size_t i = 0; i < corners.size(); i += 4) + { + const size_t idx = i / 4; + const bool isDecodable = idx < decode_info.size() + && idx < decode_type.size() + && !decode_type[idx].empty(); + const Scalar lineColor = isDecodable ? greenColor : redColor; + // draw barcode rectangle + vector contour(corners.begin() + i, corners.begin() + i + 4); + const vector< vector > contours {contour}; + drawContours(frame, contours, 0, lineColor, 1); + // draw vertices + for (size_t j = 0; j < 4; j++) + circle(frame, contour[j], 2, randColor(), -1); + // write decoded text + if (isDecodable) + { + ostringstream buf; + buf << "[" << decode_type[idx] << "] " << decode_info[idx]; + putText(frame, buf.str(), contour[1], FONT_HERSHEY_COMPLEX, 0.8, yellowColor, 1); + } + } + //! [visualize] + } + + void drawFPS(Mat &frame, double fps) const + { + ostringstream buf; + buf << modeString() + << " (" << corners.size() / 4 << "/" << decode_type.size() << "/" << decode_info.size() << ") " + << cv::format("%.2f", fps) << " FPS "; + putText(frame, buf.str(), Point(25, 25), FONT_HERSHEY_COMPLEX, 0.8, redColor, 2); + } + + inline void call_decode(Mat &frame) + { + cleanup(); + if (detectOnly) + { + //! [detect] + bardet->detectMulti(frame, corners); + //! [detect] + } + else + { + //! [detectAndDecode] + bardet->detectAndDecodeWithType(frame, decode_info, decode_type, corners); + //! [detectAndDecode] + } + } + + int liveBarCodeDetect() + { + VideoCapture cap(0); + if (!cap.isOpened()) + { + cout << "Cannot open a camera" << endl; + return 2; + } + Mat frame; + Mat result; + cap >> frame; + cout << "Image size: " << frame.size() << endl; + cout << "Press 'd' to switch between and modes" << endl; + cout << "Press 'ESC' to exit" << endl; + for (;;) + { + cap >> frame; + if (frame.empty()) + { + cout << "End of video stream" << endl; + break; + } + if (frame.channels() == 1) + cvtColor(frame, frame, COLOR_GRAY2BGR); + TickMeter timer; + timer.start(); + call_decode(frame); + timer.stop(); + drawResults(frame); + drawFPS(frame, timer.getFPS()); + imshow("barcode", frame); + const char c = (char)waitKey(1); + if (c == 'd') + { + detectOnly = !detectOnly; + cout << "Mode switched to " << modeString() << endl; + } + else if (c == 27) + { + cout << "'ESC' is pressed. Exiting..." << endl; + break; + } + } + return 0; + } + + int imageBarCodeDetect(const string &in_file, const string &out_file) + { + Mat frame = imread(in_file, IMREAD_COLOR); + cout << "Image size: " << frame.size() << endl; + cout << "Mode is " << modeString() << endl; + const int count_experiments = 100; + TickMeter timer; + for (size_t i = 0; i < count_experiments; i++) + { + timer.start(); + call_decode(frame); + timer.stop(); + } + cout << "FPS: " << timer.getFPS() << endl; + drawResults(frame); + if (!out_file.empty()) + { + cout << "Saving result: " << out_file << endl; + imwrite(out_file, frame); + } + imshow("barcode", frame); + cout << "Press any key to exit ..." << endl; + waitKey(0); + return 0; + } +}; + + +//============================================================================== + +int main(int argc, char **argv) +{ + const string keys = "{h help ? | | print help messages }" + "{i in | | input image path (also switches to image detection mode) }" + "{detect | false | detect 1D barcode only (skip decoding) }" + "{o out | | path to result file (only for single image decode) }" + "{sr_prototxt| | super resolution prototxt path }" + "{sr_model | | super resolution model path }"; + CommandLineParser cmd_parser(argc, argv, keys); + cmd_parser.about("This program detects the 1D barcodes from camera or images using the OpenCV library."); + if (cmd_parser.has("help")) + { + cmd_parser.printMessage(); + return 0; + } + const string in_file = cmd_parser.get("in"); + const string out_file = cmd_parser.get("out"); + const string sr_prototxt = cmd_parser.get("sr_prototxt"); + const string sr_model = cmd_parser.get("sr_model"); + if (!cmd_parser.check()) + { + cmd_parser.printErrors(); + return -1; + } + + TheApp app; + app.detectOnly = cmd_parser.has("detect") && cmd_parser.get("detect"); + //! [initialize] + try + { + app.bardet = makePtr(sr_prototxt, sr_model); + } + catch (const std::exception& e) + { + cout << + "\n---------------------------------------------------------------\n" + "Failed to initialize super resolution.\n" + "Please, download 'sr.*' from\n" + "https://github.com/WeChatCV/opencv_3rdparty/tree/wechat_qrcode\n" + "and put them into the current directory.\n" + "Or you can leave sr_prototxt and sr_model unspecified.\n" + "---------------------------------------------------------------\n"; + cout << e.what() << endl; + return -1; + } + //! [initialize] + + if (in_file.empty()) + return app.liveBarCodeDetect(); + else + return app.imageBarCodeDetect(in_file, out_file); +}