diff --git a/samples/android/CMakeLists.txt b/samples/android/CMakeLists.txt
index 360f1aa2cb..360a2189b8 100644
--- a/samples/android/CMakeLists.txt
+++ b/samples/android/CMakeLists.txt
@@ -8,6 +8,7 @@ ocv_warnings_disable(CMAKE_CXX_FLAGS -Wmissing-declarations)
add_subdirectory(15-puzzle)
add_subdirectory(face-detection)
+add_subdirectory(qr-detection)
add_subdirectory(image-manipulations)
add_subdirectory(camera-calibration)
add_subdirectory(color-blob-detection)
diff --git a/samples/android/qr-detection/AndroidManifest.xml b/samples/android/qr-detection/AndroidManifest.xml
new file mode 100644
index 0000000000..deb4c6ff12
--- /dev/null
+++ b/samples/android/qr-detection/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/android/qr-detection/CMakeLists.txt b/samples/android/qr-detection/CMakeLists.txt
new file mode 100644
index 0000000000..00c76dc1d6
--- /dev/null
+++ b/samples/android/qr-detection/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(sample example-qr-detection)
+
+add_android_project(${sample} "${CMAKE_CURRENT_SOURCE_DIR}" LIBRARY_DEPS "${OPENCV_ANDROID_LIB_DIR}" SDK_TARGET 11 "${ANDROID_SDK_TARGET}")
+if(TARGET ${sample})
+ add_dependencies(opencv_android_examples ${sample})
+endif()
diff --git a/samples/android/qr-detection/build.gradle.in b/samples/android/qr-detection/build.gradle.in
new file mode 100644
index 0000000000..274f0b4129
--- /dev/null
+++ b/samples/android/qr-detection/build.gradle.in
@@ -0,0 +1,36 @@
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'org.opencv.samples.qrdetection'
+ compileSdkVersion @ANDROID_COMPILE_SDK_VERSION@
+ defaultConfig {
+ applicationId "org.opencv.samples.qrdetection"
+ minSdkVersion @ANDROID_MIN_SDK_VERSION@
+ targetSdkVersion @ANDROID_TARGET_SDK_VERSION@
+ versionCode 301
+ versionName "3.01"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ sourceSets {
+ main {
+ java.srcDirs = @ANDROID_SAMPLE_JAVA_PATH@
+ aidl.srcDirs = @ANDROID_SAMPLE_JAVA_PATH@
+ res.srcDirs = @ANDROID_SAMPLE_RES_PATH@
+ manifest.srcFile '@ANDROID_SAMPLE_MANIFEST_PATH@'
+ }
+ }
+}
+
+dependencies {
+ //implementation fileTree(dir: 'libs', include: ['*.jar'])
+ if (gradle.opencv_source == "sdk_path") {
+ implementation project(':opencv')
+ } else if (gradle.opencv_source == "maven_local" || gradle.opencv_source == "maven_cenral") {
+ implementation 'org.opencv:opencv:@OPENCV_VERSION_PLAIN@'
+ }
+}
diff --git a/samples/android/qr-detection/gradle/AndroidManifest.xml b/samples/android/qr-detection/gradle/AndroidManifest.xml
new file mode 100644
index 0000000000..83d4aafc8c
--- /dev/null
+++ b/samples/android/qr-detection/gradle/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/android/qr-detection/res/drawable/icon.png b/samples/android/qr-detection/res/drawable/icon.png
new file mode 100644
index 0000000000..630454927b
Binary files /dev/null and b/samples/android/qr-detection/res/drawable/icon.png differ
diff --git a/samples/android/qr-detection/res/values/strings.xml b/samples/android/qr-detection/res/values/strings.xml
new file mode 100644
index 0000000000..824161eee5
--- /dev/null
+++ b/samples/android/qr-detection/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ OpenCV QR Detector
+
diff --git a/samples/android/qr-detection/src/org/opencv/samples/qrdetection/QRProcessor.java b/samples/android/qr-detection/src/org/opencv/samples/qrdetection/QRProcessor.java
new file mode 100644
index 0000000000..f3217d7ffe
--- /dev/null
+++ b/samples/android/qr-detection/src/org/opencv/samples/qrdetection/QRProcessor.java
@@ -0,0 +1,87 @@
+package org.opencv.samples.qrdetection;
+
+import org.opencv.core.Core;
+import org.opencv.core.Mat;
+import org.opencv.core.MatOfPoint;
+import org.opencv.core.Scalar;
+import org.opencv.core.Size;
+import org.opencv.core.Point;
+import org.opencv.imgproc.Imgproc;
+import org.opencv.objdetect.GraphicalCodeDetector;
+import org.opencv.objdetect.QRCodeDetector;
+import org.opencv.objdetect.QRCodeDetectorAruco;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class QRProcessor {
+ private GraphicalCodeDetector detector;
+ private static final String TAG = "QRProcessor";
+ private Scalar LineColor = new Scalar(255, 0, 0);
+ private Scalar FontColor = new Scalar(0, 0, 255);
+
+ public QRProcessor(boolean useArucoDetector) {
+ if (useArucoDetector)
+ detector = new QRCodeDetectorAruco();
+ else
+ detector = new QRCodeDetector();
+ }
+
+ private boolean findQRs(Mat inputFrame, List decodedInfo, MatOfPoint points,
+ boolean tryDecode, boolean multiDetect) {
+ boolean result = false;
+ if (multiDetect) {
+ if (tryDecode)
+ result = detector.detectAndDecodeMulti(inputFrame, decodedInfo, points);
+ else
+ result = detector.detectMulti(inputFrame, points);
+ }
+ else {
+ if(tryDecode) {
+ String s = detector.detectAndDecode(inputFrame, points);
+ result = !points.empty();
+ if (result)
+ decodedInfo.add(s);
+ }
+ else {
+ result = detector.detect(inputFrame, points);
+ }
+ }
+ return result;
+ }
+
+ private void renderQRs(Mat inputFrame, List decodedInfo, MatOfPoint points) {
+ for (int i = 0; i < points.rows(); i++) {
+ for (int j = 0; j < points.cols(); j++) {
+ Point pt1 = new Point(points.get(i, j));
+ Point pt2 = new Point(points.get(i, (j + 1) % 4));
+ Imgproc.line(inputFrame, pt1, pt2, LineColor, 3);
+ }
+ if (!decodedInfo.isEmpty()) {
+ String decode = decodedInfo.get(i);
+ if (decode.length() > 15) {
+ decode = decode.substring(0, 12) + "...";
+ }
+ int baseline[] = {0};
+ Size textSize = Imgproc.getTextSize(decode, Imgproc.FONT_HERSHEY_COMPLEX, .95, 3, baseline);
+ Scalar sum = Core.sumElems(points.row(i));
+ Point start = new Point(sum.val[0] / 4. - textSize.width / 2., sum.val[1] / 4. - textSize.height / 2.);
+ Imgproc.putText(inputFrame, decode, start, Imgproc.FONT_HERSHEY_COMPLEX, .95, FontColor, 3);
+ }
+ }
+ }
+
+ /* this method to be called from the outside. It processes the frame to find QR codes. */
+ public synchronized Mat handleFrame(Mat inputFrame, boolean tryDecode, boolean multiDetect) {
+ List decodedInfo = new ArrayList();
+ MatOfPoint points = new MatOfPoint();
+ boolean result = findQRs(inputFrame, decodedInfo, points, tryDecode, multiDetect);
+ if (result) {
+ renderQRs(inputFrame, decodedInfo, points);
+ }
+ points.release();
+ return inputFrame;
+ }
+}
diff --git a/samples/android/qr-detection/src/org/opencv/samples/qrdetection/QRdetectionActivity.java b/samples/android/qr-detection/src/org/opencv/samples/qrdetection/QRdetectionActivity.java
new file mode 100644
index 0000000000..39361e2d98
--- /dev/null
+++ b/samples/android/qr-detection/src/org/opencv/samples/qrdetection/QRdetectionActivity.java
@@ -0,0 +1,130 @@
+package org.opencv.samples.qrdetection;
+
+import org.opencv.android.CameraActivity;
+import org.opencv.android.OpenCVLoader;
+import org.opencv.core.Mat;
+import org.opencv.android.CameraBridgeViewBase;
+import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener;
+import org.opencv.android.JavaCameraView;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import java.util.Collections;
+import java.util.List;
+
+public class QRdetectionActivity extends CameraActivity implements CvCameraViewListener {
+
+ private static final String TAG = "QRdetection::Activity";
+
+ private CameraBridgeViewBase mOpenCvCameraView;
+ private QRProcessor mQRDetector;
+ private MenuItem mItemQRCodeDetectorAruco;
+ private MenuItem mItemQRCodeDetector;
+ private MenuItem mItemTryDecode;
+ private MenuItem mItemMulti;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ if (OpenCVLoader.initLocal()) {
+ Log.i(TAG, "OpenCV loaded successfully");
+ } else {
+ Log.e(TAG, "OpenCV initialization failed!");
+ (Toast.makeText(this, "OpenCV initialization failed!", Toast.LENGTH_LONG)).show();
+ return;
+ }
+
+ Log.d(TAG, "Creating and setting view");
+ mOpenCvCameraView = new JavaCameraView(this, -1);
+ setContentView(mOpenCvCameraView);
+ mOpenCvCameraView.setVisibility(CameraBridgeViewBase.VISIBLE);
+ mOpenCvCameraView.setCvCameraViewListener(this);
+ mQRDetector = new QRProcessor(true);
+ }
+
+ @Override
+ public void onPause()
+ {
+ super.onPause();
+ if (mOpenCvCameraView != null)
+ mOpenCvCameraView.disableView();
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+ if (mOpenCvCameraView != null) {
+ mOpenCvCameraView.enableView();
+ }
+ }
+
+ @Override
+ protected List extends CameraBridgeViewBase> getCameraViewList() {
+ return Collections.singletonList(mOpenCvCameraView);
+ }
+
+ public void onDestroy() {
+ super.onDestroy();
+ if (mOpenCvCameraView != null)
+ mOpenCvCameraView.disableView();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ Log.i(TAG, "called onCreateOptionsMenu");
+ mItemQRCodeDetectorAruco = menu.add("Aruco-based QR code detector");
+ mItemQRCodeDetectorAruco.setCheckable(true);
+ mItemQRCodeDetectorAruco.setChecked(true);
+
+ mItemQRCodeDetector = menu.add("Legacy QR code detector");
+ mItemQRCodeDetector.setCheckable(true);
+ mItemQRCodeDetector.setChecked(false);
+
+ mItemTryDecode = menu.add("Try to decode QR codes");
+ mItemTryDecode.setCheckable(true);
+ mItemTryDecode.setChecked(true);
+
+ mItemMulti = menu.add("Use multi detect/decode");
+ mItemMulti.setCheckable(true);
+ mItemMulti.setChecked(true);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ Log.i(TAG, "Menu Item selected " + item);
+ if (item == mItemQRCodeDetector && !mItemQRCodeDetector.isChecked()) {
+ mQRDetector = new QRProcessor(false);
+ mItemQRCodeDetector.setChecked(true);
+ mItemQRCodeDetectorAruco.setChecked(false);
+ } else if (item == mItemQRCodeDetectorAruco && !mItemQRCodeDetectorAruco.isChecked()) {
+ mQRDetector = new QRProcessor(true);
+ mItemQRCodeDetector.setChecked(false);
+ mItemQRCodeDetectorAruco.setChecked(true);
+ } else if (item == mItemTryDecode) {
+ mItemTryDecode.setChecked(!mItemTryDecode.isChecked());
+ } else if (item == mItemMulti) {
+ mItemMulti.setChecked(!mItemMulti.isChecked());
+ }
+ return true;
+ }
+
+ public void onCameraViewStarted(int width, int height) {
+ }
+
+ public void onCameraViewStopped() {
+ }
+
+ public Mat onCameraFrame(Mat inputFrame) {
+ return mQRDetector.handleFrame(inputFrame, mItemTryDecode.isChecked(), mItemMulti.isChecked());
+ }
+}