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.NotFoundException;
|
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 javax.imageio.ImageIO;
|
import java.awt.image.BufferedImage;
|
import java.io.File;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.EnumMap;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* 二维码标签解码校验
|
*
|
* 用法:java DecodeQr <标签目录>
|
*
|
* 对每张标签分别用「整图」和「裁到二维码区域」两种方式解码:
|
* 整图失败而裁图成功,说明标签本身没坏,是下方文字干扰了检测器,属解码端问题;
|
* 两种方式都失败,才是标签真的不可识读。
|
*/
|
public class DecodeQr {
|
|
/** 标签排版参数,须与 QrCodeImageUtils 保持一致 */
|
private static final int PADDING = 16;
|
private static final int QR_SIZE = 260;
|
/** 裁图时向外多留的余量,保证二维码静区不被切掉 */
|
private static final int CROP_MARGIN = 10;
|
|
public static void main(String[] args) throws Exception {
|
if (args.length == 0) {
|
System.out.println("用法: java DecodeQr <标签目录>");
|
return;
|
}
|
File dir = new File(args[0]);
|
File[] files = dir.listFiles((d, n) -> n.toLowerCase().endsWith(".png"));
|
if (files == null || files.length == 0) {
|
System.out.println("目录内没有 PNG 文件:" + dir.getAbsolutePath());
|
return;
|
}
|
Arrays.sort(files);
|
|
int fullOk = 0;
|
int cropOk = 0;
|
int bothFail = 0;
|
List<String> fullFailCropOk = new ArrayList<>();
|
List<String> hardFail = new ArrayList<>();
|
List<String> mismatched = new ArrayList<>();
|
String sampleContent = null;
|
|
for (File f : files) {
|
String name = f.getName();
|
String code = name.endsWith(".png") ? name.substring(0, name.length() - 4) : name;
|
BufferedImage image = ImageIO.read(f);
|
if (image == null) {
|
hardFail.add(name + " -> 图片无法读取");
|
bothFail++;
|
continue;
|
}
|
String full = decode(image);
|
String crop = decode(cropQrArea(image));
|
if (full != null) {
|
fullOk++;
|
}
|
if (crop != null) {
|
cropOk++;
|
}
|
if (full == null && crop == null) {
|
bothFail++;
|
hardFail.add(name + " -> 整图与裁图均解码失败");
|
continue;
|
}
|
if (full == null) {
|
fullFailCropOk.add(name);
|
}
|
String text = full != null ? full : crop;
|
if (!text.contains(code)) {
|
mismatched.add(name + " -> 内容与码值不符: " + text);
|
}
|
if (sampleContent == null) {
|
sampleContent = "码值 " + code + "\n 二维码内容 " + text;
|
}
|
}
|
|
int total = files.length;
|
System.out.println("标签总数 : " + total);
|
System.out.printf ("整图解码成功 : %d (%.2f%%)%n", fullOk, fullOk * 100.0 / total);
|
System.out.printf ("裁图解码成功 : %d (%.2f%%)%n", cropOk, cropOk * 100.0 / total);
|
System.out.printf ("两种方式都失败 : %d (%.2f%%)%n", bothFail, bothFail * 100.0 / total);
|
System.out.println("整图失败但裁图成功(检测受下方文字干扰,标签本身正常): " + fullFailCropOk.size());
|
System.out.println("\n--- 样本 ---\n " + sampleContent);
|
|
if (!fullFailCropOk.isEmpty()) {
|
System.out.println("\n--- 仅整图失败的码(前 10) ---");
|
fullFailCropOk.stream().limit(10).forEach(s -> System.out.println(" " + s));
|
}
|
if (!mismatched.isEmpty()) {
|
System.out.println("\n!!! 内容与码值不符(前 10) !!!");
|
mismatched.stream().limit(10).forEach(s -> System.out.println(" " + s));
|
}
|
if (!hardFail.isEmpty()) {
|
System.out.println("\n--- 真正不可识读的标签(前 20) ---");
|
hardFail.stream().limit(20).forEach(s -> System.out.println(" " + s));
|
}
|
}
|
|
/** 裁出二维码所在区域,去掉下方叠加的文字 */
|
private static BufferedImage cropQrArea(BufferedImage src) {
|
int x = Math.max(0, PADDING - CROP_MARGIN);
|
int y = Math.max(0, PADDING - CROP_MARGIN);
|
int w = Math.min(src.getWidth() - x, QR_SIZE + CROP_MARGIN * 2);
|
int h = Math.min(src.getHeight() - y, QR_SIZE + CROP_MARGIN * 2);
|
return src.getSubimage(x, y, w, h);
|
}
|
|
/** 尝试多种二值化策略解码,全部失败返回 null */
|
private static String decode(BufferedImage image) {
|
LuminanceSource source = new BufferedImageLuminanceSource(image);
|
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
|
hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
|
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
|
hints.put(DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE));
|
// HybridBinarizer 适合光照不均,GlobalHistogramBinarizer 对高对比度图更稳,两者互补
|
for (var binarizer : List.of(new HybridBinarizer(source), new GlobalHistogramBinarizer(source))) {
|
try {
|
Result r = new MultiFormatReader().decode(new BinaryBitmap(binarizer), hints);
|
if (r != null && r.getText() != null) {
|
return r.getText();
|
}
|
} catch (NotFoundException ignored) {
|
// 换下一种策略
|
} catch (Exception ignored) {
|
// 同上
|
}
|
}
|
return null;
|
}
|
}
|