All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.alg.fiducial.microqr.MicroQrCodeDecoderImage Maven / Gradle / Ivy

/*
 * Copyright (c) 2023, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.alg.fiducial.microqr;

import boofcv.alg.distort.LensDistortionNarrowFOV;
import boofcv.alg.fiducial.qrcode.*;
import boofcv.misc.BoofMiscOps;
import boofcv.struct.image.ImageGray;
import georegression.geometry.UtilPolygons2D_F64;
import georegression.struct.point.Point2D_I32;
import georegression.transform.homography.HomographyPointOps_F64;
import lombok.Getter;
import org.ddogleg.struct.DogArray;
import org.ddogleg.struct.DogArray_F32;
import org.ddogleg.struct.VerbosePrint;
import org.jetbrains.annotations.Nullable;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Given an image and a known location of a Micro QR Code, decode the marker.
 *
 * @author Peter Abeles
 */
public class MicroQrCodeDecoderImage> implements VerbosePrint {
	/** Converts read bits into a message */
	@Getter MicroQrCodeDecoderBits decoder;

	public boolean considerTransposed = true;

	DogArray storageQR = new DogArray<>(MicroQrCode::new, MicroQrCode::reset);
	@Getter List found = new ArrayList<>();
	@Getter List failures = new ArrayList<>();

	// storage for read in bits from the grid
	PackedBits8 bits = new PackedBits8();

	// internal workspace
	QrCodeAlignmentPatternLocator alignmentLocator;
	QrCodeBinaryGridReader gridReader;

	// Storage for pixel intensity. There are N samples for each bit
	DogArray_F32 intensityBits = new DogArray_F32();

	@Nullable PrintStream verbose = null;

	/**
	 * @param forceEncoding Force the default encoding to be this. Null for default
	 */
	public MicroQrCodeDecoderImage( @Nullable String forceEncoding, String defaultEncoding, Class imageType ) {
		decoder = new MicroQrCodeDecoderBits(forceEncoding, defaultEncoding);
		gridReader = new QrCodeBinaryGridReader<>(imageType);
		alignmentLocator = new QrCodeAlignmentPatternLocator<>(imageType);
	}

	/**
	 * Attempts to decode a marker at every found position pattern inside the image
	 *
	 * @param pps List of potential markers
	 * @param gray Original gray scale image
	 */
	public void process( List pps, T gray ) {
		gridReader.setImage(gray);
		storageQR.reset();
		found.clear();
		failures.clear();

		for (int i = 0; i < pps.size(); i++) {
			PositionPatternNode ppn = pps.get(i);

			MicroQrCode qr = storageQR.grow();
			qr.thresholdPP = ppn.grayThreshold;
			qr.pp.setTo(ppn.square);

			boolean success = false;

			// consider that the encoder is non-standard and rendered a transposed marker
			escape:for (int transposed = 0; transposed < (considerTransposed ? 2 : 1); transposed++) {
				if (transposed == 1)
					QrCodeDecoderImage.transposeCorners(qr.pp);

				// try different orientations. We don't know which one is valid
				for (int orientation = 0; orientation < 4; orientation++) {
					if (verbose != null) verbose.printf("idx=%d trans=%d orientation=%d pp=%s\n", i, transposed, orientation, qr.pp);
					// Decode the entire marker now
					if (decode(qr)) {
						found.add(qr);
						qr.bitsTransposed = transposed == 1;
						success = true;
						break escape;
					} else if (qr.failureCause.ordinal() > QrCode.Failure.ERROR_CORRECTION.ordinal()) {
						// If ECC was successful and it still failed, that means there's a bug in this decoder
						// or the decoder is non-compliant / buggy
						success = false;
						break escape;
					}

					// Try another orientation
					UtilPolygons2D_F64.shiftDown(qr.pp);
				}
			}


			if (!success) {
				failures.add(qr);
			}
		}
	}

	/**
	 * Decodes the message
	 *
	 * @return true if successful
	 */
	private boolean decode( MicroQrCode qr ) {
		qr.failureCause = QrCode.Failure.NONE;

		// Convert pixel values into format bit values
		if (!readFormatBitValues(qr)) {
			return false;
		}

		// Save the mapping from marker to pixel space as well as the location inside the image
		// Doing it here so if you look at a failed marker you know where it was
		qr.Hinv.setTo(gridReader.getTransformGrid().Hinv);

		if (!decodeFormatBitValues(qr)) {
			// Set the bounds to the position pattern since we don't know the version and can't infer the bounds
			qr.bounds.setTo(qr.pp);
			if (verbose != null) verbose.print("_ failed: reading format\n");
			qr.failureCause = QrCode.Failure.FORMAT;
			return false;
		}

		// Save where the marker is now that the version is known
		setBoundsOfMarker(qr);

		if (verbose != null) verbose.printf("valid: version=%d error=%s mask=%s\n", qr.version, qr.error, qr.mask);

		if (!readRawData(qr)) {
			if (verbose != null) verbose.print("_ failed: reading bits\n");
			qr.failureCause = QrCode.Failure.READING_BITS;
			return false;
		}

		if (!decoder.applyErrorCorrection(qr)) {
			if (verbose != null) verbose.print("_ failed: error correction\n");
			qr.failureCause = QrCode.Failure.ERROR_CORRECTION;
			return false;
		}

		if (!decoder.decodeMessage(qr)) {
			if (verbose != null) verbose.print("_ failed: decoding message\n");
			qr.failureCause = QrCode.Failure.DECODING_MESSAGE;
			return false;
		}

		if (verbose != null) verbose.printf("_ success: message='%s'\n", qr.message);

		return true;
	}

	/**
	 * Specify the bounds using the position pattern and corner estimates using the homography
	 */
	private void setBoundsOfMarker( MicroQrCode qr ) {
		int N = qr.getNumberOfModules();
		qr.bounds.get(0).setTo(qr.pp.get(0));
		HomographyPointOps_F64.transform(qr.Hinv, N, 0, qr.bounds.get(1));
		HomographyPointOps_F64.transform(qr.Hinv, N, N, qr.bounds.get(2));
		HomographyPointOps_F64.transform(qr.Hinv, 0, N, qr.bounds.get(3));
	}

	private boolean decodeFormatBitValues( MicroQrCode qr ) {
		// Decode the read in bits
		int bitField = this.bits.read(0, 15, false);
		// All zeros and All ones are typical failure modes for reading all white or black regions
		if (bitField == 0 || (bitField & 0b01111111_11111111) == 0b01111111_11111111)
			return false;
		bitField ^= MicroQrCode.FORMAT_MASK;

		return qr.decodeFormatBits(bitField);
	}

	boolean readFormatBitValues( MicroQrCode qr ) {
		gridReader.setSquare(qr.pp, (float)qr.thresholdPP);

		bits.resize(15);
		bits.zero();

		for (int i = 0; i < 8; i++) {
			read(i, i + 1, 8);
		}
		for (int i = 0; i < 7; i++) {
			read(i + 8, 8, 7 - i);
		}

		return true;
	}

	boolean readRawData( MicroQrCode qr ) {
		MicroQrCode.VersionInfo info = MicroQrCode.VERSION_INFO[qr.version];

		// Get the location of each bit
		List locationBits = MicroQrCode.LOCATION_BITS[qr.version];

		qr.rawbits = new byte[info.codewords];

		readBitIntensityValues(locationBits);
		bitIntensityToBitValue(qr, locationBits);

		// copy over the results
		System.arraycopy(bits.data, 0, qr.rawbits, 0, qr.rawbits.length);

		return true;
	}

	/**
	 * Sample the pixel intensity values around each data bit
	 */
	void readBitIntensityValues( List locationBits ) {
		intensityBits.reserve(locationBits.size()*5);
		intensityBits.reset();

		for (int bitIndex = 0; bitIndex < locationBits.size(); bitIndex++) {
			Point2D_I32 b = locationBits.get(bitIndex);
			gridReader.readBitIntensity(b.y, b.x, intensityBits);
		}
	}

	/**
	 * Use the previously sampled pixel intensity values to determine the bit values for the message
	 */
	void bitIntensityToBitValue( MicroQrCode qr, List locationBits ) {
		// Handle the situation where the last data word is 4-bits only. We will force the upper 4 bits to be zero
		int numDataBits = qr.getMaxDataBits();
		if (numDataBits%8 == 0) {
			numDataBits = Integer.MAX_VALUE; // don't do anything when full word
		}

		bits.resize(locationBits.size());
		bits.zero();

		float threshold = (float)qr.thresholdPP;
		for (int intensityIndex = 0; intensityIndex < intensityBits.size; ) {
			int bitIndex = intensityIndex/5;

			Point2D_I32 b = locationBits.get(bitIndex);

			// Black bits are encoded as 1
			int votes = 0;
			votes += intensityBits.data[intensityIndex++] < threshold ? 1 : 0;
			votes += intensityBits.data[intensityIndex++] < threshold ? 1 : 0;
			votes += intensityBits.data[intensityIndex++] < threshold ? 1 : 0;
			votes += intensityBits.data[intensityIndex++] < threshold ? 1 : 0;
			votes += intensityBits.data[intensityIndex++] < threshold ? 1 : 0;

			int bit = votes >= 3 ? 1 : 0;

			// Skip over those bits as the word is only 4-bits not 8-bits
			if (bitIndex >= numDataBits)
				bitIndex += 4;
			bits.set(bitIndex, qr.mask.apply(b.y, b.x, bit));
		}
	}

	/**
	 * Reads a bit from the image.
	 *
	 * @param bit Index the bit will be written to
	 * @param row row in qr code grid
	 * @param col column in qr code grid
	 */
	private void read( int bit, int row, int col ) {
		int value = gridReader.readBit(row, col);
		if (value == -1) {
			// The requested region is outside the image. A partial QR code can be read so let's just
			// assign it a value of zero and let error correction handle this
			value = 0;
		}
		bits.set(bit, value);
	}

	/**
	 * 

Specifies transforms which can be used to change coordinates from distorted to undistorted and the opposite * coordinates. The undistorted image is never explicitly created.

* * @param width Input image width. Used in sanity check only. * @param height Input image height. Used in sanity check only. * @param model distortion model. Null to remove a distortion model. */ public void setLensDistortion( int width, int height, @Nullable LensDistortionNarrowFOV model ) { alignmentLocator.setLensDistortion(width, height, model); gridReader.setLensDistortion(width, height, model); } @Override public void setVerbose( @Nullable PrintStream out, @Nullable Set configuration ) { this.verbose = BoofMiscOps.addPrefix(this, out); BoofMiscOps.verboseChildren(out, configuration, decoder); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy