liu
10 小时以前 724be707ec15ac6558f1561366dd367d75f28bf7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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;
    }
}