mirror of
https://github.com/zxing/zxing.git
synced 2025-03-05 20:48:51 -08:00
I moved a chunk of the histogram/black point code out of BaseMonochromeBitmapSource and into BlackPointEstimator which makes a lot more sense. Unfortunately I had to expose three new methods and touch a bunch of files. But I did manage to reuse an array on subsequent calls, which was being allocated on every row scanned, so that might be a bit faster. It will also be easier to convert this code to JNI in the future if we want to, and/or do more advanced thresholding.
git-svn-id: https://zxing.googlecode.com/svn/trunk@945 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
parent
038f113274
commit
e19e9a83f6
|
@ -97,12 +97,12 @@ public final class YUVMonochromeBitmapSource extends BaseMonochromeBitmapSource
|
|||
* @return The luminance as an int, from 0-255
|
||||
*/
|
||||
@Override
|
||||
protected int getLuminance(int x, int y) {
|
||||
public int getLuminance(int x, int y) {
|
||||
return mYUVData[(y + mCropTop) * mDataWidth + x + mCropLeft] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] getLuminanceRow(int y, int[] row) {
|
||||
public int[] getLuminanceRow(int y, int[] row) {
|
||||
int width = getWidth();
|
||||
if (row == null || row.length < width) {
|
||||
row = new int[width];
|
||||
|
@ -116,7 +116,7 @@ public final class YUVMonochromeBitmapSource extends BaseMonochromeBitmapSource
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int[] getLuminanceColumn(int x, int[] column) {
|
||||
public int[] getLuminanceColumn(int x, int[] column) {
|
||||
int height = getHeight();
|
||||
if (column == null || column.length < height) {
|
||||
column = new int[height];
|
||||
|
|
|
@ -67,12 +67,12 @@ public final class RGBMonochromeBitmapSource extends BaseMonochromeBitmapSource
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int getLuminance(int x, int y) {
|
||||
public int getLuminance(int x, int y) {
|
||||
return mLuminances[y * getWidth() + x] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] getLuminanceRow(int y, int[] row) {
|
||||
public int[] getLuminanceRow(int y, int[] row) {
|
||||
int width = getWidth();
|
||||
if (row == null || row.length < width) {
|
||||
row = new int[width];
|
||||
|
@ -85,7 +85,7 @@ public final class RGBMonochromeBitmapSource extends BaseMonochromeBitmapSource
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int[] getLuminanceColumn(int x, int[] column) {
|
||||
public int[] getLuminanceColumn(int x, int[] column) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
if (column == null || column.length < height) {
|
||||
|
|
|
@ -101,4 +101,37 @@ public interface MonochromeBitmapSource {
|
|||
*/
|
||||
boolean isRotateSupported();
|
||||
|
||||
/**
|
||||
* Retrieves the luminance at the pixel x,y in the bitmap. This method is only used for estimating
|
||||
* the black point and implementing getBlackRow() - it is not meant for decoding, hence it is not
|
||||
* part of MonochromeBitmapSource itself, and is protected.
|
||||
*
|
||||
* @param x The x coordinate in the image.
|
||||
* @param y The y coordinate in the image.
|
||||
* @return The luminance value between 0 and 255.
|
||||
*/
|
||||
public abstract int getLuminance(int x, int y);
|
||||
|
||||
/**
|
||||
* This is the main mechanism for retrieving luminance data. It is dramatically more efficient
|
||||
* than repeatedly calling getLuminance(). As above, this is not meant for decoders.
|
||||
*
|
||||
* @param y The row to fetch
|
||||
* @param row The array to write luminance values into. It is <b>strongly</b> suggested that you
|
||||
* allocate this yourself, making sure row.length >= getWidth(), and reuse the same
|
||||
* array on subsequent calls for performance. If you pass null, you will be flogged,
|
||||
* but then I will take pity on you and allocate a sufficient array internally.
|
||||
* @return The array containing the luminance data. This is the same as row if it was usable.
|
||||
*/
|
||||
public abstract int[] getLuminanceRow(int y, int[] row);
|
||||
|
||||
/**
|
||||
* The same as getLuminanceRow(), but for columns.
|
||||
*
|
||||
* @param x The column to fetch
|
||||
* @param column The array to write luminance values into. See above.
|
||||
* @return The array containing the luminance data.
|
||||
*/
|
||||
public abstract int[] getLuminanceColumn(int x, int[] column);
|
||||
|
||||
}
|
||||
|
|
|
@ -24,16 +24,12 @@ import com.google.zxing.ReaderException;
|
|||
*/
|
||||
public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSource {
|
||||
|
||||
private static final int LUMINANCE_BITS = 5;
|
||||
private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS;
|
||||
private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS;
|
||||
|
||||
private final int height;
|
||||
private final int width;
|
||||
private int blackPoint;
|
||||
private BlackPointEstimationMethod lastMethod;
|
||||
private int lastArgument;
|
||||
private int[] luminances;
|
||||
private int[] luminances = null;
|
||||
|
||||
protected BaseMonochromeBitmapSource(int width, int height) {
|
||||
this.height = height;
|
||||
|
@ -113,30 +109,7 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour
|
|||
public void estimateBlackPoint(BlackPointEstimationMethod method, int argument)
|
||||
throws ReaderException {
|
||||
if (!method.equals(lastMethod) || argument != lastArgument) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
int[] histogram = new int[LUMINANCE_BUCKETS];
|
||||
if (method.equals(BlackPointEstimationMethod.TWO_D_SAMPLING)) {
|
||||
int minDimension = width < height ? width : height;
|
||||
int startX = (width - minDimension) >> 1;
|
||||
int startY = (height - minDimension) >> 1;
|
||||
for (int n = 0; n < minDimension; n++) {
|
||||
int luminance = getLuminance(startX + n, startY + n);
|
||||
histogram[luminance >> LUMINANCE_SHIFT]++;
|
||||
}
|
||||
} else if (method.equals(BlackPointEstimationMethod.ROW_SAMPLING)) {
|
||||
if (argument < 0 || argument >= height) {
|
||||
throw new IllegalArgumentException("Row is not within the image: " + argument);
|
||||
}
|
||||
initLuminances();
|
||||
luminances = getLuminanceRow(argument, luminances);
|
||||
for (int x = 0; x < width; x++) {
|
||||
histogram[luminances[x] >> LUMINANCE_SHIFT]++;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown method");
|
||||
}
|
||||
blackPoint = BlackPointEstimator.estimate(histogram) << LUMINANCE_SHIFT;
|
||||
blackPoint = BlackPointEstimator.estimate(this, method, argument);
|
||||
lastMethod = method;
|
||||
lastArgument = argument;
|
||||
}
|
||||
|
@ -189,7 +162,7 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour
|
|||
* @param y The y coordinate in the image.
|
||||
* @return The luminance value between 0 and 255.
|
||||
*/
|
||||
protected abstract int getLuminance(int x, int y);
|
||||
public abstract int getLuminance(int x, int y);
|
||||
|
||||
/**
|
||||
* This is the main mechanism for retrieving luminance data. It is dramatically more efficient
|
||||
|
@ -202,7 +175,7 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour
|
|||
* but then I will take pity on you and allocate a sufficient array internally.
|
||||
* @return The array containing the luminance data. This is the same as row if it was usable.
|
||||
*/
|
||||
protected abstract int[] getLuminanceRow(int y, int[] row);
|
||||
public abstract int[] getLuminanceRow(int y, int[] row);
|
||||
|
||||
/**
|
||||
* The same as getLuminanceRow(), but for columns.
|
||||
|
@ -211,6 +184,6 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour
|
|||
* @param column The array to write luminance values into. See above.
|
||||
* @return The array containing the luminance data.
|
||||
*/
|
||||
protected abstract int[] getLuminanceColumn(int x, int[] column);
|
||||
public abstract int[] getLuminanceColumn(int x, int[] column);
|
||||
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package com.google.zxing.common;
|
||||
|
||||
import com.google.zxing.BlackPointEstimationMethod;
|
||||
import com.google.zxing.MonochromeBitmapSource;
|
||||
import com.google.zxing.ReaderException;
|
||||
|
||||
/**
|
||||
|
@ -26,37 +28,97 @@ import com.google.zxing.ReaderException;
|
|||
* <a href="http://webdiis.unizar.es/~neira/12082/thresholding.pdf">http://webdiis.unizar.es/~neira/12082/thresholding.pdf</a>.
|
||||
* </p>
|
||||
*
|
||||
* NOTE: This class is not threadsafe.
|
||||
*
|
||||
* @author Sean Owen
|
||||
* @author dswitkin@google.com (Daniel Switkin)
|
||||
*/
|
||||
public final class BlackPointEstimator {
|
||||
|
||||
private static final int LUMINANCE_BITS = 5;
|
||||
private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS;
|
||||
private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS;
|
||||
|
||||
private static int[] luminances = null;
|
||||
private static int[] histogram = null;
|
||||
|
||||
private BlackPointEstimator() {
|
||||
}
|
||||
|
||||
private static void initArrays(int luminanceSize) {
|
||||
if (luminances == null || luminances.length < luminanceSize) {
|
||||
luminances = new int[luminanceSize];
|
||||
}
|
||||
if (histogram == null) {
|
||||
histogram = new int[LUMINANCE_BUCKETS];
|
||||
} else {
|
||||
for (int x = 0; x < LUMINANCE_BUCKETS; x++) {
|
||||
histogram[x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the black point for the supplied bitmap.
|
||||
*
|
||||
* @param source The bitmap to analyze.
|
||||
* @param method The pixel sampling technique to use.
|
||||
* @param argument The row index in the case of ROW_SAMPLING, otherwise ignored.
|
||||
* @return The black point as an integer 0-255.
|
||||
* @throws ReaderException An exception thrown if the blackpoint cannot be determined.
|
||||
*/
|
||||
public static int estimate(MonochromeBitmapSource source, BlackPointEstimationMethod method,
|
||||
int argument) throws ReaderException {
|
||||
int width = source.getWidth();
|
||||
int height = source.getHeight();
|
||||
initArrays(width);
|
||||
|
||||
if (method.equals(BlackPointEstimationMethod.TWO_D_SAMPLING)) {
|
||||
int minDimension = width < height ? width : height;
|
||||
int startX = (width - minDimension) >> 1;
|
||||
int startY = (height - minDimension) >> 1;
|
||||
for (int n = 0; n < minDimension; n++) {
|
||||
int luminance = source.getLuminance(startX + n, startY + n);
|
||||
histogram[luminance >> LUMINANCE_SHIFT]++;
|
||||
}
|
||||
} else if (method.equals(BlackPointEstimationMethod.ROW_SAMPLING)) {
|
||||
if (argument < 0 || argument >= height) {
|
||||
throw new IllegalArgumentException("Row is not within the image: " + argument);
|
||||
}
|
||||
|
||||
luminances = source.getLuminanceRow(argument, luminances);
|
||||
for (int x = 0; x < width; x++) {
|
||||
histogram[luminances[x] >> LUMINANCE_SHIFT]++;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown method");
|
||||
}
|
||||
return findBestValley(histogram) << LUMINANCE_SHIFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Given an array of <em>counts</em> of luminance values (i.e. a histogram), this method
|
||||
* decides which bucket of values corresponds to the black point -- which bucket contains the
|
||||
* count of the brightest luminance values that should be considered "black".</p>
|
||||
*
|
||||
* @param histogram an array of <em>counts</em> of luminance values
|
||||
* @param buckets an array of <em>counts</em> of luminance values
|
||||
* @return index within argument of bucket corresponding to brightest values which should be
|
||||
* considered "black"
|
||||
* @throws ReaderException if "black" and "white" appear to be very close in luminance
|
||||
*/
|
||||
public static int estimate(int[] histogram) throws ReaderException{
|
||||
int numBuckets = histogram.length;
|
||||
public static int findBestValley(int[] buckets) throws ReaderException {
|
||||
int numBuckets = buckets.length;
|
||||
int maxBucketCount = 0;
|
||||
// Find tallest peak in histogram
|
||||
int firstPeak = 0;
|
||||
int firstPeakSize = 0;
|
||||
for (int i = 0; i < numBuckets; i++) {
|
||||
if (histogram[i] > firstPeakSize) {
|
||||
if (buckets[i] > firstPeakSize) {
|
||||
firstPeak = i;
|
||||
firstPeakSize = histogram[i];
|
||||
firstPeakSize = buckets[i];
|
||||
}
|
||||
if (histogram[i] > maxBucketCount) {
|
||||
maxBucketCount = histogram[i];
|
||||
if (buckets[i] > maxBucketCount) {
|
||||
maxBucketCount = buckets[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +129,7 @@ public final class BlackPointEstimator {
|
|||
for (int i = 0; i < numBuckets; i++) {
|
||||
int distanceToBiggest = i - firstPeak;
|
||||
// Encourage more distant second peaks by multiplying by square of distance
|
||||
int score = histogram[i] * distanceToBiggest * distanceToBiggest;
|
||||
int score = buckets[i] * distanceToBiggest * distanceToBiggest;
|
||||
if (score > secondPeakScore) {
|
||||
secondPeak = i;
|
||||
secondPeakScore = score;
|
||||
|
@ -81,7 +143,7 @@ public final class BlackPointEstimator {
|
|||
secondPeak = temp;
|
||||
}
|
||||
|
||||
// Kind of aribtrary; if the two peaks are very close, then we figure there is so little
|
||||
// Kind of arbitrary; if the two peaks are very close, then we figure there is so little
|
||||
// dynamic range in the image, that discriminating black and white is too error-prone.
|
||||
// Decoding the image/line is either pointless, or may in some cases lead to a false positive
|
||||
// for 1D formats, which are relatively lenient.
|
||||
|
@ -97,7 +159,7 @@ public final class BlackPointEstimator {
|
|||
int fromFirst = i - firstPeak;
|
||||
// Favor a "valley" that is not too close to either peak -- especially not the black peak --
|
||||
// and that has a low value of course
|
||||
int score = fromFirst * fromFirst * (secondPeak - i) * (maxBucketCount - histogram[i]);
|
||||
int score = fromFirst * fromFirst * (secondPeak - i) * (maxBucketCount - buckets[i]);
|
||||
if (score > bestValleyScore) {
|
||||
bestValley = i;
|
||||
bestValleyScore = score;
|
||||
|
|
|
@ -95,4 +95,16 @@ public final class CroppedMonochromeBitmapSource implements MonochromeBitmapSour
|
|||
return delegate.isRotateSupported();
|
||||
}
|
||||
|
||||
public int getLuminance(int x, int y) {
|
||||
return delegate.getLuminance(x, y);
|
||||
}
|
||||
|
||||
public int[] getLuminanceRow(int y, int[] row) {
|
||||
return delegate.getLuminanceRow(y, row);
|
||||
}
|
||||
|
||||
public int[] getLuminanceColumn(int x, int[] column) {
|
||||
return delegate.getLuminanceColumn(x, column);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,18 +22,18 @@ import junit.framework.TestCase;
|
|||
/**
|
||||
* @author Sean Owen
|
||||
*/
|
||||
public final class BlackPointEstimationMethodTestCase extends TestCase {
|
||||
public final class BlackPointEstimatorTestCase extends TestCase {
|
||||
|
||||
public void testBasic() throws ReaderException {
|
||||
int[] histogram = { 0, 0, 11, 43, 37, 18, 3, 1, 0, 0, 13, 36, 24, 0, 11, 2 };
|
||||
int point = BlackPointEstimator.estimate(histogram);
|
||||
int point = BlackPointEstimator.findBestValley(histogram);
|
||||
assertEquals(8, point);
|
||||
}
|
||||
|
||||
public void testTooLittleRange() {
|
||||
try {
|
||||
int[] histogram = { 0, 0, 0, 0, 0, 0, 1, 43, 48, 18, 3, 1, 0, 0, 0, 0 };
|
||||
BlackPointEstimator.estimate(histogram);
|
||||
BlackPointEstimator.findBestValley(histogram);
|
||||
fail("Should have thrown an exception");
|
||||
} catch (ReaderException re) {
|
||||
// good
|
|
@ -38,7 +38,7 @@ public final class LCDUIImageMonochromeBitmapSource extends BaseMonochromeBitmap
|
|||
}
|
||||
|
||||
// This is expensive and should be used very sparingly.
|
||||
protected int getLuminance(int x, int y) {
|
||||
public int getLuminance(int x, int y) {
|
||||
image.getRGB(pixelHolder, 0, getWidth(), x, y, 1, 1);
|
||||
int pixel = pixelHolder[0];
|
||||
|
||||
|
@ -60,7 +60,7 @@ public final class LCDUIImageMonochromeBitmapSource extends BaseMonochromeBitmap
|
|||
}
|
||||
|
||||
// For efficiency, the RGB data and the luminance data share the same array.
|
||||
protected int[] getLuminanceRow(int y, int[] row) {
|
||||
public int[] getLuminanceRow(int y, int[] row) {
|
||||
int width = getWidth();
|
||||
if (row == null || row.length < width) {
|
||||
row = new int[width];
|
||||
|
@ -75,7 +75,7 @@ public final class LCDUIImageMonochromeBitmapSource extends BaseMonochromeBitmap
|
|||
return row;
|
||||
}
|
||||
|
||||
protected int[] getLuminanceColumn(int x, int[] column) {
|
||||
public int[] getLuminanceColumn(int x, int[] column) {
|
||||
int height = getHeight();
|
||||
if (column == null || column.length < height) {
|
||||
column = new int[height];
|
||||
|
|
|
@ -123,7 +123,7 @@ public final class BufferedImageMonochromeBitmapSource extends BaseMonochromeBit
|
|||
* where R, G, and B are values in [0,1].
|
||||
*/
|
||||
@Override
|
||||
protected int getLuminance(int x, int y) {
|
||||
public int getLuminance(int x, int y) {
|
||||
int pixel = image.getRGB(left + x, top + y);
|
||||
// Coefficients add up to 1024 to make the divide into a fast shift
|
||||
return (306 * ((pixel >> 16) & 0xFF) +
|
||||
|
@ -132,7 +132,7 @@ public final class BufferedImageMonochromeBitmapSource extends BaseMonochromeBit
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int[] getLuminanceRow(int y, int[] row) {
|
||||
public int[] getLuminanceRow(int y, int[] row) {
|
||||
int width = getWidth();
|
||||
if (row == null || row.length < width) {
|
||||
row = new int[width];
|
||||
|
@ -148,7 +148,7 @@ public final class BufferedImageMonochromeBitmapSource extends BaseMonochromeBit
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int[] getLuminanceColumn(int x, int[] column) {
|
||||
public int[] getLuminanceColumn(int x, int[] column) {
|
||||
int height = getHeight();
|
||||
if (column == null || column.length < height) {
|
||||
column = new int[height];
|
||||
|
|
Loading…
Reference in a new issue