Replaced the finder-like pattern detection algorithm with a more sophisticated and accurate one, including documentation comments, only for the Java version of the library. This fixes nearly all the false negatives/positives in the previous implementation.

pull/62/head
Project Nayuki 6 years ago
parent 40d24f38aa
commit b2ff7ce765

@ -596,10 +596,11 @@ public final class QrCode {
int result = 0;
// Adjacent modules in row having same color, and finder-like patterns
FinderPatternDetector det = new FinderPatternDetector();
for (int y = 0; y < size; y++) {
int[] runHistory = new int[7];
boolean color = false;
int runX = 0;
det.reset();
for (int x = 0; x < size; x++) {
if (modules[y][x] == color) {
runX++;
@ -608,22 +609,16 @@ public final class QrCode {
else if (runX > 5)
result++;
} else {
addRunToHistory(runX, runHistory);
if (!color && hasFinderLikePattern(runHistory))
result += PENALTY_N3;
color = modules[y][x];
runX = 1;
}
result += det.addModuleAndMatch(color) * PENALTY_N3;
}
addRunToHistory(runX, runHistory);
if (color)
addRunToHistory(0, runHistory); // Dummy run of white
if (hasFinderLikePattern(runHistory))
result += PENALTY_N3;
result += det.terminateAndMatch() * PENALTY_N3;
}
// Adjacent modules in column having same color, and finder-like patterns
for (int x = 0; x < size; x++) {
int[] runHistory = new int[7];
det.reset();
boolean color = false;
int runY = 0;
for (int y = 0; y < size; y++) {
@ -634,18 +629,12 @@ public final class QrCode {
else if (runY > 5)
result++;
} else {
addRunToHistory(runY, runHistory);
if (!color && hasFinderLikePattern(runHistory))
result += PENALTY_N3;
color = modules[y][x];
runY = 1;
}
result += det.addModuleAndMatch(color) * PENALTY_N3;
}
addRunToHistory(runY, runHistory);
if (color)
addRunToHistory(0, runHistory); // Dummy run of white
if (hasFinderLikePattern(runHistory))
result += PENALTY_N3;
result += det.terminateAndMatch() * PENALTY_N3;
}
// 2*2 blocks of modules having same color
@ -735,24 +724,6 @@ public final class QrCode {
}
// Inserts the given value to the front of the given array, which shifts over the
// existing values and deletes the last value. A helper function for getPenaltyScore().
private static void addRunToHistory(int run, int[] history) {
System.arraycopy(history, 0, history, 1, history.length - 1);
history[0] = run;
}
// Tests whether the given run history has the pattern of ratio 1:1:3:1:1 in the middle, and
// surrounded by at least 4 on either or both ends. A helper function for getPenaltyScore().
// Must only be called immediately after a run of white modules has ended.
private static boolean hasFinderLikePattern(int[] runHistory) {
int n = runHistory[1];
return n > 0 && runHistory[2] == n && runHistory[4] == n && runHistory[5] == n
&& runHistory[3] == n * 3 && Math.max(runHistory[0], runHistory[6]) >= n * 4;
}
// Returns true iff the i'th bit of x is set to 1.
static boolean getBit(int x, int i) {
return ((x >>> i) & 1) != 0;
@ -911,4 +882,118 @@ public final class QrCode {
}
/*---- Private helper class ----*/
/**
* Detects finder-like patterns in a line of modules, for the purpose of penalty score calculation.
* A finder-like pattern has alternating black and white modules with run length ratios of 1:1:3:1:1,
* such that the center run is black and this pattern is surrounded by at least a ratio
* of 4:1 white modules on one side and at least 1:1 white modules on the other side.
* The finite line of modules is conceptually padded with an infinite number of white modules on both sides.
*
* Here are graphic examples of the designed behavior, where '[' means start of line,
* ']' means end of line, '.' means white module, and '#' means black module:
* - [....#.###.#....] Two matches
* - [#.###.#] Two matches, because of infinite white border
* - [##..######..##] Two matches, with a scale of 2
* - [#.###.#.#] One match, using the infinite white left border
* - [#.#.###.#.#] Zero matches, due to insufficient white modules surrounding the 1:1:3:1:1 pattern
* - [#.###.##] Zero matches, because the rightmost black bar is too long
* - [#.###.#.###.#] Two matches, with the matches overlapping and sharing modules
*/
private static final class FinderPatternDetector {
/*-- Fields --*/
// Mutable running state
private boolean currentRunColor; // false = white, true = black
private int currentRunLength; // In modules, always positive
// runHistory[0] = length of most recently ended run,
// runHistory[1] = length of next older run of opposite color, etc.
// This array begins as all zeros. Zero is not a valid run length.
private int[] runHistory = new int[7];
/*-- Methods --*/
/**
* Re-initializes this detector to the start of a row or column.
* This allows reuse of this object and its array, reducing memory allocation.
*/
public void reset() {
currentRunColor = false;
currentRunLength = QR_CODE_SIZE_MAX; // Add white border to initial run
Arrays.fill(runHistory, 0);
}
/**
* Tells this detector that the next module has the specified color, and returns
* the number of finder-like patterns detected due to processing the current module.
* The result is usually 0, but can be 1 or 2 only when transitioning from
* white to black (i.e. {@code currentRunColor == false && color == true}).
* @param color the color of the next module, where {@code true} denotes black and {@code false} is white
* @return either 0, 1, or 2
*/
public int addModuleAndMatch(boolean color) {
if (color == currentRunColor)
currentRunLength++;
else {
addToHistory(currentRunLength);
currentRunColor = color;
currentRunLength = 1;
if (color) // Transitioning from white to black
return countCurrentMatches();
}
return 0;
}
/**
* Tells this detector that the line of modules has ended, and
* returns the number of finder-like patterns detected at the end.
* After this, {@link #reset()} must be called before any other methods.
* @return either 0, 1, or 2
*/
public int terminateAndMatch() {
if (currentRunColor) { // Terminate black run
addToHistory(currentRunLength);
currentRunLength = 0;
}
currentRunLength += QR_CODE_SIZE_MAX; // Add white border to final run
addToHistory(currentRunLength);
return countCurrentMatches();
}
// Shifts the array back and puts the given value at the front.
private void addToHistory(int run) {
System.arraycopy(runHistory, 0, runHistory, 1, runHistory.length - 1);
runHistory[0] = run;
}
// Can only be called immediately after a white run is added.
private int countCurrentMatches() {
int n = runHistory[1];
assert n <= QR_CODE_SIZE_MAX * 3;
boolean core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n;
if (core) {
return (runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0)
+ (runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
} else
return 0;
}
/*-- Constant --*/
// This amount of padding is enough to guarantee at least 4 scaled
// white modules at any pattern scale that fits inside any QR Code.
private static final int QR_CODE_SIZE_MAX = QrCode.MAX_VERSION * 4 + 17;
}
}

Loading…
Cancel
Save