Finished work on the local binarizer and renamed it to HybridBinarizer. It uses the old histogram for 1D and the new approach for 2D. The unit tests don't fully demonstrate how much better this is in real-world lighting, but it pretty much solves the situation of pointing at a QR Code for 30 seconds without scanning, due to a shadow or gradient.

HybridBinarizer is now the default across all the fast clients and the unit tests.

git-svn-id: https://zxing.googlecode.com/svn/trunk@1157 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
dswitkin 2009-12-13 21:08:57 +00:00
parent fde805cc02
commit 5cc1df3c92
19 changed files with 81 additions and 94 deletions

View file

@ -23,7 +23,7 @@ import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.ResultPointCallback;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -165,7 +165,7 @@ final class DecodeThread extends Thread {
long start = System.currentTimeMillis();
Result rawResult = null;
PlanarYUVLuminanceSource source = CameraManager.get().buildLuminanceSource(data, width, height);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {

View file

@ -20,7 +20,7 @@ import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import android.os.Debug;
import android.os.Message;
@ -94,7 +94,7 @@ final class BenchmarkThread extends Thread {
// scheduling and what else is happening in the system.
long now = Debug.threadCpuTimeNanos();
try {
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
result = mMultiFormatReader.decodeWithState(bitmap);
success = true;
} catch (ReaderException e) {

View file

@ -26,10 +26,12 @@ import com.google.zxing.ReaderException;
* algorithm. However, because it picks a global black point, it cannot handle difficult shadows
* and gradients.
*
* Faster mobile devices and all desktop applications should probably use HybridBinarizer instead.
*
* @author dswitkin@google.com (Daniel Switkin)
* @author Sean Owen
*/
public final class GlobalHistogramBinarizer extends Binarizer {
public class GlobalHistogramBinarizer extends Binarizer {
private static final int LUMINANCE_BITS = 5;
private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS;
@ -105,7 +107,7 @@ public final class GlobalHistogramBinarizer extends Binarizer {
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x< width; x++) {
int pixel = localLuminances[offset + x] & 0xff;
int pixel = localLuminances[offset + x] & 0xff;
if (pixel < blackPoint) {
matrix.set(x, y);
}

View file

@ -18,6 +18,7 @@ package com.google.zxing.common;
import com.google.zxing.Binarizer;
import com.google.zxing.LuminanceSource;
import com.google.zxing.ReaderException;
/**
* This class implements a local thresholding algorithm, which while slower than the
@ -27,51 +28,56 @@ import com.google.zxing.LuminanceSource;
* However it tends to produce artifacts on lower frequency images and is therefore not
* a good general purpose binarizer for uses outside ZXing.
*
* NOTE: This class is still experimental and may not be ready for prime time yet.
* This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,
* and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already
* inherently local, and only fails for horizontal gradients. We can revisit that problem later,
* but for now it was not a win to use local blocks for 1D.
*
* This Binarizer is the default for the unit tests and the recommended class for library users.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
public final class LocalBlockBinarizer extends Binarizer {
public final class HybridBinarizer extends GlobalHistogramBinarizer {
// This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.
// So this is the smallest dimension in each axis we can accept.
private static final int MINIMUM_DIMENSION = 40;
private BitMatrix matrix = null;
public LocalBlockBinarizer(LuminanceSource source) {
public HybridBinarizer(LuminanceSource source) {
super(source);
}
// TODO: Consider a different strategy for 1D Readers.
public BitArray getBlackRow(int y, BitArray row) {
binarizeEntireImage();
return matrix.getRow(y, row);
}
// TODO: If getBlackRow() calculates its own values, removing sharpening here.
public BitMatrix getBlackMatrix() {
public BitMatrix getBlackMatrix() throws ReaderException {
binarizeEntireImage();
return matrix;
}
public Binarizer createBinarizer(LuminanceSource source) {
return new LocalBlockBinarizer(source);
return new HybridBinarizer(source);
}
// Calculates the final BitMatrix once for all requests. This could be called once from the
// constructor instead, but there are some advantages to doing it lazily, such as making
// profiling easier, and not doing heavy lifting when callers don't expect it.
private void binarizeEntireImage() {
private void binarizeEntireImage() throws ReaderException {
if (matrix == null) {
LuminanceSource source = getLuminanceSource();
byte[] luminances = source.getMatrix();
int width = source.getWidth();
int height = source.getHeight();
sharpenRow(luminances, width, height);
if (source.getWidth() >= MINIMUM_DIMENSION && source.getHeight() >= MINIMUM_DIMENSION) {
byte[] luminances = source.getMatrix();
int width = source.getWidth();
int height = source.getHeight();
int subWidth = width >> 3;
int subHeight = height >> 3;
int[][] blackPoints = calculateBlackPoints(luminances, subWidth, subHeight, width);
int subWidth = width >> 3;
int subHeight = height >> 3;
int[][] blackPoints = calculateBlackPoints(luminances, subWidth, subHeight, width);
matrix = new BitMatrix(width, height);
calculateThresholdForBlock(luminances, subWidth, subHeight, width, blackPoints, matrix);
matrix = new BitMatrix(width, height);
calculateThresholdForBlock(luminances, subWidth, subHeight, width, blackPoints, matrix);
} else {
// If the image is too small, fall back to the global histogram approach.
matrix = super.getBlackMatrix();
}
}
}
@ -148,26 +154,4 @@ public final class LocalBlockBinarizer extends Binarizer {
return blackPoints;
}
// Applies a simple -1 4 -1 box filter with a weight of 2 to each row.
private static void sharpenRow(byte[] luminances, int width, int height) {
for (int y = 0; y < height; y++) {
int offset = y * width;
int left = luminances[offset] & 0xff;
int center = luminances[offset + 1] & 0xff;
for (int x = 1; x < width - 1; x++) {
int right = luminances[offset + x + 1] & 0xff;
int pixel = ((center << 2) - left - right) >> 1;
// Must clamp values to 0..255 so they will fit in a byte.
if (pixel > 255) {
pixel = 255;
} else if (pixel < 0) {
pixel = 0;
}
luminances[offset + x] = (byte)pixel;
left = center;
center = right;
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 856 B

View file

@ -184,7 +184,7 @@ public abstract class AbstractBlackBoxTestCase extends TestCase {
float rotation = testResults.get(x).getRotation();
BufferedImage rotatedImage = rotateImage(image, rotation);
LuminanceSource source = new BufferedImageLuminanceSource(rotatedImage);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
if (decode(bitmap, rotation, expectedText, false)) {
passedCounts[x]++;
}
@ -294,4 +294,4 @@ public abstract class AbstractBlackBoxTestCase extends TestCase {
}
}
}
}

View file

@ -109,7 +109,7 @@ public abstract class AbstractNegativeBlackBoxTestCase extends AbstractBlackBoxT
private boolean checkForFalsePositives(BufferedImage image, float rotationInDegrees) {
BufferedImage rotatedImage = rotateImage(image, rotationInDegrees);
LuminanceSource source = new BufferedImageLuminanceSource(rotatedImage);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Result result;
try {
result = getReader().decode(bitmap);

View file

@ -27,10 +27,10 @@ public final class DataMatrixBlackBox2TestCase extends AbstractBlackBoxTestCase
public DataMatrixBlackBox2TestCase() {
// TODO use MultiFormatReader here once Data Matrix decoder is done
super("test/data/blackbox/datamatrix-2", new DataMatrixReader(), BarcodeFormat.DATAMATRIX);
addTest(2, 2, 0.0f);
addTest(4, 4, 0.0f);
addTest(1, 1, 90.0f);
addTest(4, 4, 180.0f);
addTest(3, 3, 270.0f);
addTest(3, 3, 180.0f);
addTest(1, 1, 270.0f);
}
}

View file

@ -34,7 +34,7 @@ public final class PDF417BlackBox2TestCase extends AbstractBlackBoxTestCase {
public PDF417BlackBox2TestCase() {
super("test/data/blackbox/pdf417-2", new MultiFormatReader(), BarcodeFormat.PDF417);
addTest(11, 11, 0.0f);
addTest(12, 12, 180.0f);
addTest(11, 11, 180.0f);
}
@Override
@ -46,4 +46,4 @@ public final class PDF417BlackBox2TestCase extends AbstractBlackBoxTestCase {
return table;
}
}
}

View file

@ -27,10 +27,10 @@ public final class QRCodeBlackBox1TestCase extends AbstractBlackBoxTestCase {
public QRCodeBlackBox1TestCase() {
super("test/data/blackbox/qrcode-1", new MultiFormatReader(), BarcodeFormat.QR_CODE);
addTest(19, 19, 0.0f);
addTest(15, 15, 90.0f);
addTest(17, 17, 180.0f);
addTest(15, 15, 270.0f);
addTest(17, 17, 0.0f);
addTest(13, 13, 90.0f);
addTest(16, 16, 180.0f);
addTest(14, 14, 270.0f);
}
}
}

View file

@ -27,10 +27,10 @@ public final class QRCodeBlackBox2TestCase extends AbstractBlackBoxTestCase {
public QRCodeBlackBox2TestCase() {
super("test/data/blackbox/qrcode-2", new MultiFormatReader(), BarcodeFormat.QR_CODE);
addTest(23, 23, 0.0f);
addTest(21, 21, 90.0f);
addTest(21, 21, 0.0f);
addTest(18, 18, 90.0f);
addTest(20, 20, 180.0f);
addTest(18, 18, 270.0f);
}
}
}

View file

@ -27,10 +27,10 @@ public final class QRCodeBlackBox3TestCase extends AbstractBlackBoxTestCase {
public QRCodeBlackBox3TestCase() {
super("test/data/blackbox/qrcode-3", new MultiFormatReader(), BarcodeFormat.QR_CODE);
addTest(33, 33, 0.0f);
addTest(36, 36, 90.0f);
addTest(32, 32, 180.0f);
addTest(38, 38, 270.0f);
addTest(36, 36, 0.0f);
addTest(37, 37, 90.0f);
addTest(35, 35, 180.0f);
addTest(37, 37, 270.0f);
}
}
}

View file

@ -29,10 +29,10 @@ public final class QRCodeBlackBox4TestCase extends AbstractBlackBoxTestCase {
public QRCodeBlackBox4TestCase() {
super("test/data/blackbox/qrcode-4", new MultiFormatReader(), BarcodeFormat.QR_CODE);
addTest(32, 32, 0.0f);
addTest(37, 37, 90.0f);
addTest(36, 36, 180.0f);
addTest(35, 35, 0.0f);
addTest(36, 36, 90.0f);
addTest(34, 34, 180.0f);
addTest(34, 34, 270.0f);
}
}
}

View file

@ -23,6 +23,7 @@ import com.google.zxing.common.AbstractBlackBoxTestCase;
/**
* Some very difficult exposure conditions including self-shadowing, which happens a lot when
* pointing down at a barcode (i.e. the phone's shadow falls across part of the image).
* The global histogram gets about 5/15, where the local one gets 15/15.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
@ -30,10 +31,10 @@ public final class QRCodeBlackBox5TestCase extends AbstractBlackBoxTestCase {
public QRCodeBlackBox5TestCase() {
super("test/data/blackbox/qrcode-5", new MultiFormatReader(), BarcodeFormat.QR_CODE);
addTest(5, 5, 0.0f);
addTest(4, 4, 90.0f);
addTest(5, 5, 180.0f);
addTest(5, 5, 270.0f);
addTest(15, 15, 0.0f);
addTest(15, 15, 90.0f);
addTest(15, 15, 180.0f);
addTest(15, 15, 270.0f);
}
}

View file

@ -16,18 +16,18 @@
package com.google.zxing.client.j2se;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.result.ParsedResult;
import com.google.zxing.client.result.ResultParser;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import java.awt.image.BufferedImage;
import java.io.File;
@ -200,7 +200,7 @@ public final class CommandLineRunner {
}
try {
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
if (dumpBlackPoint) {
dumpBlackPoint(uri, image, bitmap);
}

View file

@ -21,7 +21,7 @@ import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import java.awt.Container;
import java.awt.Dimension;
@ -95,7 +95,7 @@ public final class GUIRunner extends JFrame {
return "Could not decode image";
}
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Result result;
try {
result = new MultiFormatReader().decode(bitmap);

View file

@ -21,7 +21,7 @@ import com.google.zxing.LuminanceSource;
import com.google.zxing.ReaderException;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import java.awt.image.BufferedImage;
import java.io.File;
@ -96,7 +96,7 @@ public final class ImageConverter {
private static void convertImage(URI uri) throws IOException {
BufferedImage image = ImageIO.read(uri.toURL());
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
int width = bitmap.getWidth();
int height = bitmap.getHeight();

View file

@ -23,7 +23,7 @@ import com.google.zxing.Reader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import java.awt.image.BufferedImage;
import java.io.IOException;
@ -147,7 +147,7 @@ final class DecodeEmailTask extends TimerTask {
Result result = null;
try {
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
result = reader.decode(bitmap, DecodeServlet.HINTS);
} catch (ReaderException re) {
log.info("Decoding FAILED");
@ -191,4 +191,4 @@ final class DecodeEmailTask extends TimerTask {
new DecodeEmailTask(emailAddress, emailAuthenticator).run();
}
}
}

View file

@ -27,7 +27,7 @@ import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.client.result.ParsedResult;
import com.google.zxing.client.result.ResultParser;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
@ -78,7 +78,7 @@ import javax.servlet.http.HttpServletResponse;
/**
* {@link HttpServlet} which decodes images containing barcodes. Given a URL, it will
* retrieve the image and decode it. It can also process image files uploaded via POST.
*
*
* @author Sean Owen
*/
public final class DecodeServlet extends HttpServlet {
@ -102,7 +102,7 @@ public final class DecodeServlet extends HttpServlet {
possibleFormats.add(BarcodeFormat.ITF);
possibleFormats.add(BarcodeFormat.QR_CODE);
possibleFormats.add(BarcodeFormat.DATAMATRIX);
possibleFormats.add(BarcodeFormat.PDF417);
possibleFormats.add(BarcodeFormat.PDF417);
HINTS.put(DecodeHintType.POSSIBLE_FORMATS, possibleFormats);
}
@ -249,7 +249,7 @@ public final class DecodeServlet extends HttpServlet {
Result result;
try {
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
result = reader.decode(bitmap, HINTS);
} catch (ReaderException re) {
log.info("DECODE FAILED: " + re.toString());