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

boofcv.alg.fiducial.aztec.AztecEncoder 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.aztec;

import boofcv.alg.fiducial.aztec.AztecCode.Mode;
import boofcv.alg.fiducial.qrcode.PackedBits8;
import boofcv.misc.BoofMiscOps;
import org.ddogleg.struct.DogArray_I8;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Encodes the data message into binary data.
 *
 * @author Peter Abeles
 */
public class AztecEncoder extends AztecMessageErrorCorrection {
	// maker which is going to have the encoding stored on it
	AztecCode workMarker = new AztecCode();

	// Workspace for encoding each segment
	PackedBits8 bits = new PackedBits8();

	// Used for automatically selecting encoding mechanism
	AztecEncoderAutomatic automatic = new AztecEncoderAutomatic();

	/**
	 * Amount of error correction is determined by this number. ecc_words = errorCorrectionLength*code_words - 3.
	 * Larger values will require more data and larger markers, but do offer more fault tolerance.
	 *
	 * See G.2 in ISO document.
	 */
	public double errorCorrectionLength = 0.77;

	private final List segments = new ArrayList<>();

	public AztecEncoder() {reset();}

	public void reset() {
		workMarker.reset();
		// default to full since that's what is needed 99.99% of the time
		workMarker.structure = AztecCode.Structure.FULL;
		workMarker.dataLayers = -1;
		segments.clear();
	}

	public AztecEncoder setEcc( double fraction ) {
		errorCorrectionLength = fraction;
		return this;
	}

	public AztecEncoder setStructure( AztecCode.Structure structure ) {
		this.workMarker.structure = structure;
		return this;
	}

	public AztecEncoder setLayers( int numLayers ) {
		workMarker.dataLayers = numLayers;
		return this;
	}

	public AztecEncoder addUpper( String message ) {
		message = message.toUpperCase(Locale.ROOT);
		var values = new DogArray_I8(message.length());
		for (int i = 0; i < message.length(); i++) {
			char c = message.charAt(i);
			if (c == ' ') {
				values.add(1);
				continue;
			}
			int value = c - 'A';
			if (value >= 26) {
				throw new IllegalArgumentException("Only space and letters in the alphabet are allowed, not '" + c + "'");
			}
			values.add((char)(value + 2));
		}
		segments.add(new MessageSegment(Mode.UPPER, values, message));
		return this;
	}

	public AztecEncoder addLower( String message ) {
		message = message.toLowerCase(Locale.ROOT);
		var values = new DogArray_I8(message.length());
		for (int i = 0; i < message.length(); i++) {
			char c = message.charAt(i);
			if (c == ' ') {
				values.add(1);
				continue;
			}
			int value = c - 'a';
			if (value >= 26) {
				throw new IllegalArgumentException("Only space and letters in the alphabet are allowed, not '" + c + "'");
			}
			values.add((char)(value + 2));
		}
		segments.add(new MessageSegment(Mode.LOWER, values, message));
		return this;
	}

	public AztecEncoder addMixed( String message ) {
		byte[] ascii = message.getBytes(StandardCharsets.US_ASCII);
		var values = new DogArray_I8(ascii.length);
		for (int i = 0; i < ascii.length; i++) {
			int v = ascii[i]%0xFF;
			if (v == 32) {
				values.add(1);
			} else if (v >= 1 && v <= 13) {
				values.add((byte)(v + 1));
			} else if (v >= 27 && v <= 31) {
				values.add((byte)(v - 27 + 15));
			} else if (v == 64) {
				values.add(20);
			} else if (v == 92) {
				values.add(21);
			} else if (v >= 94 && v <= 96) {
				values.add((byte)(v - 94 + 22));
			} else if (v == 124) {
				values.add(25);
			} else if (v == 126) {
				values.add(26);
			} else if (v == 127) {
				values.add(27);
			} else {
				throw new IllegalArgumentException("Invalid ascii " + v);
			}
		}

		segments.add(new MessageSegment(Mode.MIXED, values, message));
		return this;
	}

	public AztecEncoder addPunctuation( String message ) {
		var values = new DogArray_I8(message.length());
		for (int i = 0; i < message.length(); i++) {
			char a = message.charAt(i);
			// some punctuations are two characters
			if (i + 1 < message.length()) {
				char b = message.charAt(i + 1);
				boolean matched = true;
				if (a == '\r' && b == '\n') {
					values.add(2);
				} else if (b == ' ') {
					if (a == '.') {
						values.add(3);
					} else if (a == ',') {
						values.add(4);
					} else if (a == ':') {
						values.add(5);
					} else {
						matched = false;
					}
				} else {
					matched = false;
				}

				// jump to next character and iterate
				if (matched) {
					i++;
					continue;
				}
			}
			if (a == 13) {
				values.add(1);
			} else if (a >= 33 && a <= 47) {
				values.add(a - 33 + 6);
			} else if (a >= 58 && a <= 59) {
				values.add(a - 58 + 21);
			} else if (a >= 60 && a <= 63) {
				values.add(a - 60 + 23);
			} else if (a == 91) {
				values.add(27);
			} else if (a == 93) {
				values.add(28);
			} else if (a == 123) {
				values.add(29);
			} else if (a == 125) {
				values.add(30);
			} else {
				throw new IllegalArgumentException("Invalid ascii " + (int)a);
			}
		}
		segments.add(new MessageSegment(Mode.PUNCT, values, message));
		return this;
	}

	public AztecEncoder addDigit( String message ) {
		var values = new DogArray_I8(message.length());
		for (int i = 0; i < message.length(); i++) {
			char c = message.charAt(i);
			if (c == ' ') {
				values.add(1);
			} else if (c >= 48 && c <= 57) {
				values.add(c - 48 + 2);
			} else if (c == ',') {
				values.add(12);
			} else if (c == '.') {
				values.add(13);
			}
		}
		segments.add(new MessageSegment(Mode.DIGIT, values, message));
		return this;
	}

	public AztecEncoder addBytes( byte[] data, int offset, int length ) {
		var values = new DogArray_I8(length);
		values.size = length;
		System.arraycopy(data, offset, values.data, 0, length);
		segments.add(new MessageSegment(Mode.BYTE, values,
				new String(data, offset, length, StandardCharsets.ISO_8859_1)));
		return this;
	}

	/**
	 * Automatically select which encoding mode it should encode this message. This will select the most efficient
	 * encoding possible.
	 */
	public AztecEncoder addAutomatic( String message ) {
		automatic.process(message, this);
		return this;
	}

	/**
	 * Encodes into binary data all the segments, computes ECC, and constructs the final marker. Anything
	 * which is not explicitly specified will have a reasonable value auto selected.
	 *
	 * @return Returns a new marker.
	 */
	public AztecCode fixate() {
		segmentsToEncodedBits();
		selectNumberOfLayers();
		bitsToWords();
		computeEccWords(workMarker.getWordBitCount(), workMarker.getCapacityWords());
		return copyIntoResults();
	}

	/**
	 * Encodes all the segments into the {@link #bits},
	 */
	void segmentsToEncodedBits() {
		Mode currentMode = Mode.UPPER;
		for (int segIdx = 0; segIdx < segments.size(); segIdx++) {
			MessageSegment m = segments.get(segIdx);
			// Switch into the new encoding
			boolean latched = transitionIntoMode(currentMode, m);

			// Write the data
			switch (m.encodingMode) {
				case UPPER -> append(m.data, 5);
				case LOWER -> append(m.data, 5);
				case MIXED -> append(m.data, 5);
				case PUNCT -> append(m.data, 5);
				case DIGIT -> append(m.data, 4);
				case BYTE -> appendByteArray(m.data);
				default -> throw new IllegalArgumentException("Encoding not yet supported: " + m.encodingMode);
			}

			workMarker.message += m.message;

			// Update the current mode
			if (latched) {
				currentMode = m.encodingMode;
			}
		}
	}

	/**
	 * Converts the raw data stores in 'bits' into words which are written to the marker.
	 * Words which are all zeros or ones are handled as a special case for image processing
	 * reasons.
	 */
	void bitsToWords() {
		// Convert the binary data into a format ECC computation can understand
		int wordBitCount = workMarker.getWordBitCount();

		// A word which is filled with all ones
		int ones = (1 << wordBitCount) - 1;
		int onesMinusOne = ones - 1;

		// Write all the data that fills an entire word first
		storageDataWords.reset();
		int bitLocation = 0;
		while (bitLocation + wordBitCount <= bits.size) {
			// Get the word's value
			short word = (short)bits.read(bitLocation, wordBitCount, true);

			// If a word is all zeros or all ones that's a special case. The
			// least-significant bit is zero to the opposition value and that bit
			// is punted to the next word
			if (word == 0) {
				storageDataWords.add(1);
				bitLocation += wordBitCount - 1;
			} else if (word == (short)ones) {
				storageDataWords.add(onesMinusOne);
				bitLocation += wordBitCount - 1;
			} else {
				storageDataWords.add(word);
				bitLocation += wordBitCount;
			}

			// Add the least significant bit again since it will be stripped away when decoded
			if (word == 1 || word == onesMinusOne) {
				bitLocation -= 1;
			}
		}

		// Last word is a special case
		if (bits.size > bitLocation) {
			// Read in the remaining bits
			int readBits = bits.size - bitLocation;
			int word = bits.read(bitLocation, readBits, true);

			// shift the data into the upper bits and fill the lower bits with all ones
			int remainder = wordBitCount - readBits;

			// pad it with 1's
			word = (word << remainder) | (0xFFFF >> (16 - remainder));
			// make sure it's not all 1's
			if (word == ones) {
				word = onesMinusOne;
			}
			storageDataWords.add(word);
		}

		// Record how many words are in the encoded message
		workMarker.messageWordCount = storageDataWords.size;

		if (workMarker.messageWordCount > workMarker.getCapacityWords())
			throw new RuntimeException("Encoded message is larger than the capacity. " +
					"Increase number of layers if manual or report a bug if automatic");
	}

	/**
	 * Selects the minim number of layers needed to store the data and error correction information
	 */
	void selectNumberOfLayers() {
		// See if the number of layers in the marker has been manually configured
		if (workMarker.dataLayers > 0) {
			return;
		}

		// Estimate the number of bits needed to encode the entire message based on the data and desired level
		// of error correction
		int targetBits = (int)(bits.size*(1.0 + errorCorrectionLength));

		// Determine the smallest marker size that can contain this data
		int maxLayers = workMarker.structure.maxDataLayers;

		// Sanity check to make sure this can be encoded
		int maxPossibleBits = workMarker.structure.getCapacityBits(maxLayers);
		if (targetBits > maxPossibleBits) {
			// See if the problem can be fixed by reducing error correction
			if (bits.size <= maxPossibleBits) {
				throw new IllegalArgumentException("Too large with ECC level. Try reducing amount of ECC");
			} else {
				throw new IllegalArgumentException("Too large to be encoded inside a single marker");
			}
		}

		// Select the smallest number of layers to store the data
		for (int layers = 1; layers <= maxLayers; layers++) {
			int capacityBits = workMarker.structure.getCapacityBits(layers);
			if (capacityBits >= targetBits) {
				workMarker.dataLayers = layers;
				break;
			}
		}
	}

	/** Copies data and other parameters into a new marker */
	AztecCode copyIntoResults() {
		AztecCode results = new AztecCode().setTo(workMarker);

		// To make it easier to process we will write the data words into bits, which internally
		// uses a byte array
		int wordBitCount = workMarker.getWordBitCount();
		bits.resize(0);
		for (int wordIdx = 0; wordIdx < storageDataWords.size; wordIdx++) {
			bits.append(storageDataWords.get(wordIdx) & 0xFFFF, wordBitCount, false);
		}

		// copy the message into "corrected"
		bits.size = storageDataWords.size*wordBitCount;
		// line above is done to make sure the last byte is a full word
		results.corrected = new byte[BoofMiscOps.bitToByteCount(bits.size)];
		System.arraycopy(bits.data, 0, results.corrected, 0, results.corrected.length);

		// Add ecc data to bits
		for (int word = 0; word < storageEccWords.size; word++) {
			bits.append(storageEccWords.get(word) & 0xFFFF, wordBitCount, false);
		}

		// Copy the entire encoded with ECC message into raw bits
		results.rawbits = new byte[BoofMiscOps.bitToByteCount(bits.size)];
		System.arraycopy(bits.data, 0, results.rawbits, 0, results.rawbits.length);

		return results;
	}

	/**
	 * Transitions from one mode into another.
	 *
	 * @return If it's latched to the new mode or false if not
	 */
	private boolean transitionIntoMode( Mode currentMode, MessageSegment m ) {
		boolean latched = true;
		// @formatter:off
		switch (currentMode) {
			case UPPER -> {
				switch (m.encodingMode) {
					case UPPER -> {}
					case LOWER -> append(28, 5);
					case MIXED -> append(29, 5);
					case DIGIT -> append(30, 5);
					case BYTE -> {append(31, 5); latched = false;}
					case PUNCT -> {
						if (m.data.size == 1) {
							append(0, 5); latched = false;
						} else {
							append(29, 5); // mixed
							append(30, 5); // punctuation-latched
						}
					}
					default -> throwUnsupported(currentMode, m.encodingMode);
				}
			}
			case LOWER -> {
				switch (m.encodingMode) {
					case LOWER -> {}
					case UPPER -> { append(28, 5); latched = false; }
					case MIXED -> append(29, 5);
					case DIGIT -> append(30, 5);
					case BYTE -> {append(31, 5); latched = false;}
					case PUNCT -> {
						if (m.data.size == 1) {
							append(0, 5); latched = false;
						} else {
							append(29, 5); // mixed
							append(30, 5); // punctuation-latched
						}
					}
					default -> throwUnsupported(currentMode, m.encodingMode);
				}
			}
			case MIXED -> {
				switch (m.encodingMode) {
					case MIXED -> {}
					case LOWER -> append(28, 5);
					case UPPER -> append(29, 5);
					case PUNCT -> {
						if (m.data.size == 1) {
							append(0, 5); // shift
							latched = false;
						} else {
							append(30, 5); // latch
						}
					}
					case DIGIT -> { append(29, 5); append(30, 5); }
					case BYTE -> {append(31, 5); latched = false;}
					default -> throwUnsupported(currentMode, m.encodingMode);
				}
			}
			// page 37
			case PUNCT -> {
				switch (m.encodingMode) {
					case PUNCT -> {}
					case LOWER -> { append(31, 5); append(28, 5); }
					case UPPER -> append(31, 5);
					case MIXED -> { append(31, 5); append(29, 5); }
					case DIGIT -> { append(31, 5); append(30, 5); }
					case BYTE -> { append(31, 5); append(31, 5); latched = false;}
					default -> throwUnsupported(currentMode, m.encodingMode);
				}
			}
			case DIGIT -> {
				switch (m.encodingMode) {
					case DIGIT -> {}
					case LOWER -> { append(14, 4); append(28, 5); }
					case UPPER -> {
						// if it's a single digit don't latch it
						if (m.data.size == 1) {
							latched = false;
							append(15, 4);
						} else {
							append(14, 4);
						}
					}
					case PUNCT -> {
						if (m.data.size == 1) {
							append(0, 4); latched = false;
						} else {
							append(14, 4); // upper
							append(29, 5); // mixed
							append(30, 5); // punctuation-latched
						}
					}
					case MIXED -> { append(14, 4); append(29, 5); }
					case BYTE -> { append(14, 4); append(31, 5); latched = false;}
					default -> throwUnsupported(currentMode, m.encodingMode);
				}
			}
			default -> throw new IllegalArgumentException("Unsupported. current="+currentMode+" encoding=" + m.encodingMode);
		}
		// @formatter:on
		return latched;
	}

	/**
	 * Adds the specified data to bits.
	 *
	 * @param value Value being written
	 * @param wordBits Number of bits to write
	 */
	private void append( int value, int wordBits ) {
		bits.append(value, wordBits, false);
	}

	/**
	 * Copies the array into bits.
	 *
	 * @param message message to be copied
	 * @param wordBits How many bits in each byte it should write
	 */
	private void append( DogArray_I8 message, int wordBits ) {
		for (int i = 0; i < message.size; i++) {
			bits.append(message.data[i] & 0xFF, wordBits, false);
		}
	}

	/**
	 * Adds a byte array. This is a special case as it first specifies the length.
	 */
	private void appendByteArray( DogArray_I8 message ) {
		// If a smaller array encoding its length in the next 5-bits
		if (message.size < 32)
			bits.append(message.size, 5, false);
		else if (message.size < 0b0111_1111_1111 + 31) {
			// it's a longer array, so signal that with a 0 here
			bits.append(0, 5, false);
			// Then specify the actual length, minus 31
			bits.append(message.size - 31, 11, false);
		} else {
			throw new IllegalArgumentException("Message is too long to be encoded: " + message.size);
		}
		bits.append(message.data, message.size*8, false);
	}

	/** Convenience function for throwing an exception for incompatible transitions */
	private static int throwUnsupported( Mode src, Mode dst ) throws UnsupportedOperationException {
		throw new UnsupportedOperationException("Can't transition from " + src + " to " + dst);
	}

	/**
	 * Contains the data for a sequence of characters that are all encoded in the same mode
	 */
	public static class MessageSegment {
		public Mode encodingMode;
		public DogArray_I8 data;
		public String message;

		public MessageSegment( Mode encodingMode, DogArray_I8 data, String message ) {
			this.encodingMode = encodingMode;
			this.data = data;
			this.message = message;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy