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 fullFailCropOk = new ArrayList<>(); List hardFail = new ArrayList<>(); List 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 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; } }