Zxing解析含有税字logo的发票二维码

48 阅读3分钟

解析发票二维码图片,前提是二维码图片拿到,其他文章都有,自己找。

步骤:

1、导入依赖

<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.5.2</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.2</version>
</dependency>
<!-- OpenCV (bytedeco platform 包含本地库,方便跨平台) -->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv-platform</artifactId>
    <version>1.5.7</version>
</dependency>

2、解析

package xxx.xxx.xxx;

import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.global.opencv_photo;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Rect;
import org.bytedeco.opencv.opencv_core.Size;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Map;

/**
 * 发票二维码解析工具
 */
public class DecodeInvoiceQRCodeUtil {

    public static void main(String[] args) throws Exception {
        // 替换为你的图片路径
        String path = "xxx.jpg";
        BufferedImage img = ImageIO.read(new File(path));

        String result = decodeInvoiceQRCode(img);
        System.out.println("解析结果: " + result);
    }

    /**
     * 尝试多策略解析
     * @param img
     * @return
     * @throws IOException
     */
    public static String decodeInvoiceQRCode(BufferedImage img) {
        // 1. 直接尝试(原图)
        String r = tryDecodeZXing(img);
        if (r != null) return r;

        // 2. 放大后尝试
        BufferedImage big = resize(img, img.getWidth()*2, img.getHeight()*2);
        r = tryDecodeZXing(big);
        if (r != null) return r;

        // 3. 旋转若干角度尝试
        for (int deg : new int[]{0, 90, 180, 270}) {
            BufferedImage rot = rotateImage(big, deg);
            r = tryDecodeZXing(rot);
            if (r != null) return r;
        }

        // 4. OpenCV 预处理:去噪,二值化,多形态学,尝试
        BufferedImage pre = preprocessWithOpenCV(img);
        r = tryDecodeZXing(pre);
        if (r != null) return r;

        // 5. OpenCV 去 logo(inpaint)后尝试
        BufferedImage inpainted = removeLogoByInpainting(img);
        r = tryDecodeZXing(inpainted);
        if (r != null) return r;

        // 6. 组合尝试:放大 + inpaint + 锐化
        BufferedImage inpaintedBig = resize(inpainted, inpainted.getWidth()*2, inpainted.getHeight()*2);
        r = tryDecodeZXing(inpaintedBig);
        if (r != null) return r;

        return null; // 解析失败
    }

    /**
     * ZXing 解码(尝试一些常用 hint)
     * @param image
     * @return
     */
    public static String tryDecodeZXing(BufferedImage image) {
        try {
            LuminanceSource source = new BufferedImageLuminanceSource(image);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

            Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
            hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
            // 若二维码不是纯黑白
            hints.put(DecodeHintType.PURE_BARCODE, Boolean.FALSE);
            hints.put(DecodeHintType.POSSIBLE_FORMATS, java.util.Arrays.asList(BarcodeFormat.QR_CODE));
            hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");

            com.google.zxing.Result result = new QRCodeReader().decode(bitmap, hints);
            return result.getText();
        } catch (NotFoundException nf) {
            // 未找到
            return null;
        } catch (FormatException | ChecksumException e) {
            return null;
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * 缩放
     * @param img
     * @param newW
     * @param newH
     * @return
     */
    public static BufferedImage resize(BufferedImage img, int newW, int newH) {
        Image tmp = img.getScaledInstance(newW, newH, Image.SCALE_SMOOTH);
        BufferedImage dimg = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = dimg.createGraphics();
        g2d.setComposite(AlphaComposite.Src);
        g2d.drawImage(tmp, 0, 0, null);
        g2d.dispose();
        return dimg;
    }

    /**
     * 旋转
     * @param src
     * @param degrees
     * @return
     */
    public static BufferedImage rotateImage(BufferedImage src, double degrees) {
        int w = src.getWidth();
        int h = src.getHeight();
        BufferedImage dest = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = dest.createGraphics();
        AffineTransform at = AffineTransform.getRotateInstance(Math.toRadians(degrees), w/2.0, h/2.0);
        g2.setTransform(at);
        g2.drawImage(src, 0, 0, null);
        g2.dispose();
        return dest;
    }

    /**
     * OpenCV 预处理(灰度、双边滤波、阈值、形态学)
     * @param input
     * @return
     */
    public static BufferedImage preprocessWithOpenCV(BufferedImage input) {
        Mat src = bufferedImageToMat(input);
        Mat gray = new Mat();
        opencv_imgproc.cvtColor(src, gray, opencv_imgproc.COLOR_BGR2GRAY);

        Mat denoise = new Mat();
        opencv_imgproc.GaussianBlur(gray, denoise, new Size(3,3), 0);

        // 自适应阈值
        Mat thresh = new Mat();
        opencv_imgproc.adaptiveThreshold(denoise, thresh, 255,
                opencv_imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
                opencv_imgproc.THRESH_BINARY, 25, 10);

        // 形态学闭运算填补小洞
        Mat kernel = opencv_imgproc.getStructuringElement(opencv_imgproc.MORPH_RECT, new Size(3,3));
        opencv_imgproc.morphologyEx(thresh, thresh, opencv_imgproc.MORPH_CLOSE, kernel);

        BufferedImage out = matToBufferedImage(thresh);
        return out;
    }

    /**
     * 尝试检测并 inpaint(去掉中间 logo)
     * @param input
     * @return
     */
    public static BufferedImage removeLogoByInpainting(BufferedImage input) {
        Mat src = bufferedImageToMat(input);
        Mat gray = new Mat();
        opencv_imgproc.cvtColor(src, gray, opencv_imgproc.COLOR_BGR2GRAY);

        // 1. 找显著中心区域(简单方法:检测中心附近亮/非黑区域)
        int w = gray.cols(), h = gray.rows();
        // 选择中心区域一个方框 (约占 30% 大小),假设 logo 在中心
        int cx = w/2, cy = h/2;
        int rw = Math.max(20, w/5), rh = Math.max(20, h/5);
        Rect centerRect = new Rect(Math.max(0, cx - rw/2), Math.max(0, cy - rh/2),
                Math.min(rw, w), Math.min(rh, h));
        Mat roi = new Mat(gray, centerRect);

        // 2. 在中心区域做二值化,找出非二维码黑白模式(logo 通常有连续的亮色或复杂纹理)
        Mat bin = new Mat();
        opencv_imgproc.threshold(roi, bin, 0, 255, opencv_imgproc.THRESH_BINARY | opencv_imgproc.THRESH_OTSU);

        // 3. 如果中心区域亮/暗变化明显,构造 mask(扩大并做膨胀)
        Mat mask = Mat.zeros(gray.size(), opencv_core.CV_8UC1).asMat();
        // 把 bin 写回 mask 的对应位置
        Mat maskRoi = new Mat(mask, centerRect);
        bin.copyTo(maskRoi);

        // 膨胀使 mask 覆盖更大范围
        Mat k = opencv_imgproc.getStructuringElement(opencv_imgproc.MORPH_ELLIPSE, new Size(25,25));
        opencv_imgproc.dilate(mask, mask, k);

        // 4. Inpaint 填充(TELEA)
        Mat inpainted = new Mat();
        opencv_photo.inpaint(src, mask, inpainted, 3, opencv_photo.INPAINT_TELEA);

        return matToBufferedImage(inpainted);
    }

    /**
     * 辅助:BufferedImage <-> Mat
     * @param bi
     * @return
     */
    public static Mat bufferedImageToMat(BufferedImage bi) {
        // Convert BufferedImage to Mat (BGR)
        if (bi.getType() != BufferedImage.TYPE_3BYTE_BGR) {
            BufferedImage converted = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
            Graphics2D g = converted.createGraphics();
            g.drawImage(bi, 0, 0, null);
            g.dispose();
            bi = converted;
        }
        byte[] pixels = ((DataBufferByte) bi.getRaster().getDataBuffer()).getData();
        Mat mat = new Mat(bi.getHeight(), bi.getWidth(), opencv_core.CV_8UC3);
        mat.data().put(pixels);
        // Convert RGB->BGR if needed (BufferedImage TYPE_3BYTE_BGR stores in BGR order already), so ok.
        return mat;
    }

    /**
     * Mat 转 BufferedImage
     * @param mat
     * @return
     */
    public static BufferedImage matToBufferedImage(Mat mat) {
        // Convert Mat (BGR or GRAY) to BufferedImage TYPE_3BYTE_BGR or TYPE_BYTE_GRAY
        int type = BufferedImage.TYPE_BYTE_GRAY;
        Mat mat2 = new Mat();
        if (mat.channels() == 3) {
            type = BufferedImage.TYPE_3BYTE_BGR;
            mat2 = mat;
        } else if (mat.channels() == 1) {
            mat2 = mat;
        } else if (mat.channels() == 4) {
            opencv_imgproc.cvtColor(mat, mat2, opencv_imgproc.COLOR_BGRA2BGR);
            type = BufferedImage.TYPE_3BYTE_BGR;
        } else {
            mat.convertTo(mat2, opencv_core.CV_8U);
        }

        int width = mat2.cols();
        int height = mat2.rows();
        int channels = mat2.channels();
        byte[] data = new byte[width * height * channels];
        mat2.data().get(data);

        BufferedImage image = new BufferedImage(width, height, type);
        final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
        System.arraycopy(data, 0, targetPixels, 0, data.length);
        return image;
    }

}