Java实现离线身份证号码OCR识别

最近公司要求做离线身份证OCR功能,找了一圈总算是找到了,在这里对文档做个整理,方便后来者,感谢码龄23年博主的分享

系统:Windows11,红旗Linux Asianux8.1

  • 文档中Linux全root用户操作;
  • 需先安装JDK环境,OpenCV 4.5系列JDK8应该都没问题。OpenCV 4.10需要JDK11
  • Windows系统需安装OpenCV,Linux系统需安装Tesseract OCR。
  • 文档中使用到的文件,提取码为hcyz。

一、Linux安装Tesseract OCR

1、安装编译工具和Tesseract和Leptonica所需的其他依赖库:

perl 复制代码
#确保Linux系统已经安装了必要的编译工具

yum install gcc gcc-c++ make

#安装Tesseract和Leptonica所需的其他依赖库

yum install autoconf automake libtool

yum install libjpeg-devel libpng-devel libtiff-devel zlib-devel

2、安装Leptonica库(Leptonica是Tesseract的一个依赖库)

perl 复制代码
#下载编译leptonica

cd /usr/local

wget http://www.leptonica.org/source/leptonica-1.82.0.tar.gz

tar -xvf leptonica-1.82.0.tar.gz

cd leptonica-1.82.0

./configure make && make install

3、安装Tesseract-OCR

perl 复制代码
#下载编译Tesseract OCR

cd /usr/local

wget https://codeload.github.com/tesseract-ocr/tesseract/tar.gz/4.1.0

tar -xzvf tesseract-4.1.0.tar.gz cd tesseract-4.1.0

./autogen.sh

./configure

make && make install sudo ldconfig

4、常见问题解决

perl 复制代码
#在安装Tesseract时,错误提示
configure: error: Leptonica 1.74 or higher is required. Try to install libleptonica-dev package.

#确保已正确安装Leptonica,并将相关路径添加到环境变量中
vim /etc/profile
 
# 添加以下内容
export LD_LIBRARY_PATH=$LD_LIBRARY_PAYT:/usr/local/lib
export LIBLEPT_HEADERSDIR=/usr/local/include
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig

#刷新环境变量并重新执行安装步骤
source /etc/profile 
./autogen.sh
./configure
make && make install
sudo ldconfig

#为了确保Tesseract能够正常运行,将.so文件复制到系统库路径中
cp /usr/local/lib/*.so.* /usr/lib64/
#下载Tesseract使预训练文件
地址:https://github.com/tesseract-ocr/tessdata
chi_sim.traineddata用于中文,放置在/usr/local/share/tessdata目录中。

5、测试安装

perl 复制代码
#检查Tesseract版本,确保安装成功
tesseract

#测试文字识别功能,将图像中的文字识别并保存到本地文本文件中
#img.png需要识别的图片,test.txt 识别后存储文件
tesseract img.png test.txt -l eng

二、安装OpenCV 4.5.5及生成.jar和.so文件教程

1、安装编译工具:

perl 复制代码
#opencv压缩包下载地址:https://opencv.org/releases/,Linux系统用Sources
#安装所有需要的依赖包
yum install cmake gcc gcc-c++ gtk+-devel gimp-devel gimp-devel-tools gimp-help-browser z

2、Windows安装CMake

执行.exe安装文件,安装到指定路径下。 java文件夹里是需要的jar包和系统文件,根据电脑系统选择64还是32位。

3、Linux安装CMake

perl 复制代码
#查询Linux系统上的CMake版本,低于3.5需要重装
cmake --version
//下载安装CMake
cd /usr/local
sudo yum install cmake
#或者链接下载压缩包,这里用到的是3.5.1,其他版本可以在官网下载(https://cmake.org/download/)
wget https://cmake.org/files/v3.5/cmake-3.5.1.tar.gz
tar -zxvf cmake-3.5.1.tar.gz
cd cmake-3.5.1
./bootstrap
gmake
sudo make install

#如重命名旧的CMake文件,没有则不用管
cd /usr/bin
mv cmake cmake3

#将新版本的CMake链接到/usr/bin/目录
ln -s /usr/local/cmake-3.5.1/bin/cmake /usr/bin/

4、Linux安装Ant

perl 复制代码
#查询Linux系统上的ant安装信息
ant --version
//未安装则执行命令安装
yum install ant

5、Linux安装OpenCV

perl 复制代码
#下载解压opencv压缩包
cd /usr/local
wget https://github.com/opencv/opencv/archive/4.5.5.zip
unzip 4.5.5.zip
lib-devel libtiff-devel libjpeg-devel libpng-devel gstreamer-devel libavc1394-devel libraw1394-devel libdc1394-devel jasper-devel jasper-utils swig python libtool nasm build-essential ant
#Linux需要创建build文件夹,Windows不需要
cd opencv-4.5.5/
mkdir build
cd build

#配置OpenCV构建环境并编译
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local ..
make
sudo make install

#生成jar包和so文件
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -DBUILD_TESTS=OFF ..
make -j8
sudo make install
#提示:在Linux环境中,OpenCV需要.so文件,而在Windows中则需要.dll文件。
#查看编译结果
cd  /usr/local/share/java/opencv4/

5、常见问题解决

perl 复制代码
#生成jar包和so文件成功后如图,如果红框里面都是NO则为编译失败,需要检查jdk和ant的环境变量
#如果遇到 Error: Error executing cmake::LoadCache(). Aborting错误,可以清除缓存并检查CMake版本:
hash -r
cmake --version

三、Java代码

1、pom.xml中添加Maven依赖

XML 复制代码
<!--进入jar文件夹,命令行执行-->
<!--mvn install:install-file -Dfile=opencv-455.jar -DgroupId=org.opencv -DartifactId=opencv -Dversion=4.5.5 -Dpackaging=jar
-->
<!--Linux编译后的jar或者Windows里面的jar-->
<dependency>
    <groupId>org.opencv</groupId>
    <artifactId>opencv</artifactId>
    <version>4.5.5</version>
</dependency>

<dependency>
    <groupId>net.sourceforge.tess4j</groupId>
    <artifactId>tess4j</artifactId>
    <version>4.4.1</version>
</dependency>

2、设置Tesseract语言数据路径

3、代码类

  • DLL加载器和配置类,加载dll文件

    java 复制代码
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    
    public class DllLoaderUtil {
    
        public static void loadDllFromResource(String resourcePath, String dllName, String dllSuffix) {
            try {
                // 检查库文件是否已经加载
                String tempDir = System.getProperty("java.io.tmpdir");
                File tempDll = new File(tempDir, dllName + dllSuffix);
                if (!tempDll.exists()) {
                    // 提取DLL到临时文件
                    extractDllFromResource(resourcePath, tempDll);
                }
                // 加载 DLL
                System.load(tempDll.getAbsolutePath());
            } catch (Exception e) {
                throw new RuntimeException("Failed to load DLL from resource", e);
            }
        }
    
        private static void extractDllFromResource(String resourcePath, File tempDll) throws IOException {
            URL dllUrl = DllLoaderUtil.class.getClassLoader().getResource(resourcePath);
            if (dllUrl == null) {
                throw new IllegalStateException("DLL resource not found: " + resourcePath);
            }
    
            try (InputStream in = dllUrl.openStream();
                 FileOutputStream out = new FileOutputStream(tempDll)) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                }
            }
            tempDll.deleteOnExit();
        }
    }
  • 图像转换类,用于在 OpenCV 的 Mat 和 Java 的 BufferedImage 之间进行转换。

java 复制代码
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.imgcodecs.Imgcodecs;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class ImageConvert {

    /**
     * 8-bit RGBA convert 8-bit RGB
     *
     * @param original 原来的BufferedImage图片
     * @param type     BufferedImage图片类型
     * @return
     */
    private static BufferedImage toBufferedImageOfType(BufferedImage original, int type) {
        if (original == null) {
            throw new IllegalArgumentException("original == null");
        }
        // Don't convert if it already has correct type
        if (original.getType() == type) {
            return original;
        }
        // Create a buffered image
        BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), type);
        // Draw the image onto the new buffer
        Graphics2D g = image.createGraphics();
        try {
            g.setComposite(AlphaComposite.Src);
            g.drawImage(original, 0, 0, null);
        } finally {
            g.dispose();
        }
        return image;
    }

    /**
     * BufferedImage转换成Mat
     *
     * @param originalImage 要转换的BufferedImage
     */
    public static Mat BufImg2Mat(BufferedImage originalImage) throws IOException {
        // Convert INT to BYTE
        originalImage = toBufferedImageOfType(originalImage, BufferedImage.TYPE_3BYTE_BGR);
        // Convert bufferedimage to byte array
        byte[] pixels = ((DataBufferByte) originalImage.getRaster().getDataBuffer()).getData();
        // Create a Matrix the same size of image
        Mat image = new Mat(originalImage.getHeight(), originalImage.getWidth(), 16);
        // Fill Matrix with image values
        image.put(0, 0, pixels);
        return image;
    }

    /**
     * Mat转换成BufferedImage
     *
     * @param src           要转换的Mat
     * @param fileExtension 格式为 ".jpg", ".png", etc
     * @return
     */
    public static BufferedImage Mat2BufImg(Mat src, String fileExtension) {
        //编码图像
        MatOfByte matOfByte = new MatOfByte();
        Imgcodecs.imencode(fileExtension, src, matOfByte);
        //将编码的Mat存储在字节数组中
        byte[] byteArray = matOfByte.toArray();

        BufferedImage bufImage = null;
        //准备缓冲图像
        try {
            InputStream in = new ByteArrayInputStream(byteArray);
            bufImage = ImageIO.read(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bufImage;
    }
}
  • 图像过滤类,用于执行图像亮度调整、灰度转换和其他图像过滤操作。
java 复制代码
import net.sourceforge.tess4j.util.ImageHelper;

import java.awt.*;
import java.awt.image.BufferedImage;

public class ImageFilterUtil {

    //比较三个数的大小并取最大数
    public static int getBiggest(int x, int y, int z) {
        if (x >= y && x >= z) {
            return x;
        } else if (y >= x && y >= z) {
            return y;
        } else if (z >= x && z >= y) {
            return z;
        } else {
            return 0;
        }
    }

    //比较三个数的大小并取最小数
    public static int getSmallest(int x, int y, int z) {
        if (x <= y && x <= z) {
            return x;
        } else if (y <= x && y <= z) {
            return y;
        } else if (z <= x && z <= y) {
            return z;
        } else {
            return 0;
        }
    }

    //计算并返回三个数的平均值
    public static int getAvg(int x, int y, int z) {
        int avg = (x + y + z) / 3;
        return avg;
    }

    /**
     * 图片对比度设置
     *
     * @param image 原始图片
     * @param rate  对比率
     * @return 调整后的图片(引用原始图片)
     */
    public static BufferedImage imageContrast(BufferedImage image, float rate) {
        for (int x = image.getMinX(); x < image.getWidth(); x++) {
            for (int y = image.getMinY(); y < image.getHeight(); y++) {
                Object data = image.getRaster().getDataElements(x, y, null);
                int dataRed = image.getColorModel().getRed(data);
                int dataBlue = image.getColorModel().getBlue(data);
                int dataGreen = image.getColorModel().getGreen(data);

                float newRed = dataRed * rate > 255 ? 255 : dataRed * rate;
                newRed = newRed < 0 ? 0 : newRed;
                float newGreen = dataGreen * rate > 255 ? 255 : dataGreen * rate;
                newGreen = newGreen < 0 ? 0 : newGreen;
                float newBlue = dataBlue * rate > 255 ? 255 : dataBlue * rate;
                newBlue = newBlue < 0 ? 0 : newBlue;
                Color dataColor = new Color(newRed, newGreen, newBlue);
                image.setRGB(x, y, dataColor.getRGB());
            }
        }

        return image;
    }

    /**
     * 图片亮度调整
     *
     * @param image
     * @param brightness
     * @return
     */
    public static BufferedImage imageBrightness(BufferedImage image, int brightness) {
        for (int x = image.getMinX(); x < image.getWidth(); x++) {
            for (int y = image.getMinY(); y < image.getHeight(); y++) {
                Object data = image.getRaster().getDataElements(x, y, null);
                int dataRed = image.getColorModel().getRed(data);
                int dataBlue = image.getColorModel().getBlue(data);
                int dataGreen = image.getColorModel().getGreen(data);
                int dataAlpha = image.getColorModel().getAlpha(data);
                int newRed = dataRed + brightness > 255 ? 255 : dataRed + brightness;
                newRed = newRed < 0 ? 0 : newRed;
                int newBlue = dataBlue + brightness > 255 ? 255 : dataBlue + brightness;
                newBlue = newBlue < 0 ? 0 : newBlue;
                int newGreen = dataGreen + brightness > 255 ? 255 : dataGreen + brightness;
                newGreen = newGreen < 0 ? 0 : newGreen;
                Color dataColor = new Color(newRed, newGreen, newBlue, dataAlpha);
                image.setRGB(x, y, dataColor.getRGB());
            }
        }
        return image;
    }

    /**
     * 获取图片亮度
     *
     * @param image
     * @return
     */
    public static int imageBrightness(BufferedImage image) {
        long totalRed = 0;
        long totalGreen = 0;
        long totalBlue = 0;
        for (int x = image.getMinX(); x < image.getWidth(); x++) {
            for (int y = image.getMinY(); y < image.getHeight(); y++) {
                Object data = image.getRaster().getDataElements(x, y, null);
                int dataRed = image.getColorModel().getRed(data);
                int dataBlue = image.getColorModel().getBlue(data);
                int dataGreen = image.getColorModel().getGreen(data);
                int dataAlpha = image.getColorModel().getAlpha(data);
                totalRed += dataRed;
                totalGreen += dataGreen;
                totalBlue += dataBlue;
//        totalBrightness += dataColor.getRGB();
            }
        }

        float avgRed = totalRed / (image.getHeight() * image.getWidth());
        float avgGreen = totalGreen / (image.getWidth() * image.getHeight());
        float avgBlue = totalBlue / (image.getWidth() * image.getHeight());

        int avgBrightNess = (int) ((avgRed + avgGreen + avgBlue) / 3);

        return avgBrightNess;
    }


    /**
     * 灰度化
     *
     * @param image 灰度化处理的图片
     * @return
     */
    public static BufferedImage gray(BufferedImage image) {
        int[] rgb = new int[3];
        int width = image.getWidth();
        int height = image.getHeight();
        int minx = image.getMinX();
        int miny = image.getMinY();
        BufferedImage grayImage = new BufferedImage(width, height, image.getType());
        for (int i = minx; i < width - 1; i++) {
            for (int j = miny; j < height; j++) {
                int pixel = image.getRGB(i, j);
                rgb[0] = (pixel & 0xff0000) >> 16;
                rgb[1] = (pixel & 0xff00) >> 8;
                rgb[2] = (pixel & 0xff);

                int gray = getBiggest(rgb[0], rgb[1], rgb[2]);//最大值法灰度化
//                int gray = getSmallest(rgb[0], rgb[1], rgb[2]);//最小值法灰度化
//                int gray = getAvg(rgb[0], rgb[1], rgb[2]);//均值法灰度化
//                int gray = (int) (0.3 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2]);//加权法灰度化

                Color newColor = new Color(gray, gray, gray);
                int newRgb = newColor.getRGB();
                grayImage.setRGB(i, j, newRgb);
            }
        }
        return grayImage;
    }

    /**
     * 把图像处理成黑白照片
     *
     * @param image 黑白处理的图片
     * @return
     */
    public static BufferedImage replaceWithWhiteColor(BufferedImage image) {
        int[] rgb = new int[3];

        int width = image.getWidth();
        int height = image.getHeight();
        int minx = image.getMinX();
        int miny = image.getMinY();
        //遍历图片的像素,为处理图片上的杂色,所以要把指定像素上的颜色换成目标白色 用二层循环遍历长和宽上的每个像素
        int hitCount = 0;
        for (int i = minx; i < width; i++) {
            for (int j = miny; j < height; j++) {
                //得到指定像素(i,j)上的RGB值,
                int pixel = image.getRGB(i, j);
                //分别进行位操作得到 r g b上的值
                rgb[0] = (pixel & 0xff0000) >> 16;
                rgb[1] = (pixel & 0xff00) >> 8;
                rgb[2] = (pixel & 0xff);

                /**
                 *
                 * 进行换色操作,我这里是要换成白底,那么就判断图片中rgb值是否在范围内的像素
                 *
                 */
                // 经过不断尝试,RGB数值相互间相差15以内的都基本上是灰色,
                // 对以身份证来说特别是介于73到78之间,还有大于100的部分RGB值都是干扰色,将它们一次性转变成白色
                if (
                        (Math.abs(rgb[0] - rgb[1]) < 15)
                                && (Math.abs(rgb[0] - rgb[2]) < 15)
                                && (Math.abs(rgb[1] - rgb[2]) < 15)
                                && (((rgb[0] > 73) && (rgb[0] < 78)) || (rgb[0] > 100))
                                || (rgb[0] + rgb[1] + rgb[2] > 300 && Math.abs(rgb[0] - rgb[1]) < 20 && Math.abs(rgb[0] - rgb[2]) < 20 && Math.abs(rgb[1] - rgb[2]) < 20)
                                || (rgb[0] + rgb[1] + rgb[2] > 300)
                ) {
                    // 进行换色操作,0xffffff是白色
                    image.setRGB(i, j, 0xffffff);
                }
            }
        }
        return image;
    }

    /**
     * 图片像素RGB差值滤镜--将彩色的地方涂白
     *
     * @param image
     * @param differenceValue 最大允许差值
     * @return
     */
    public static BufferedImage imageRGBDifferenceFilter(BufferedImage image, int differenceValue) {
        int[] rgb = new int[3];
        int width = image.getWidth();
        int height = image.getHeight();
        int minx = image.getMinX();
        int miny = image.getMinY();
        for (int i = minx; i < width; i++) {
            for (int j = miny; j < height; j++) {
                Object data = image.getRaster().getDataElements(i, j, null);
                int r = image.getColorModel().getRed(data);
                int g = image.getColorModel().getGreen(data);
                int b = image.getColorModel().getBlue(data);

                if (differenceValue <= Math.abs(r - b)
                        && differenceValue <= Math.abs(r - g)
                        && differenceValue <= Math.abs(b - g)
                        && b - g > 0) {
                    //把超过最大差值的像素涂白
                    image.setRGB(i, j, Color.white.getRGB());
                }
            }
        }
        return image;
    }

    /**
     * 作用:图片缩放
     *
     * @param image  需要缩放的图片
     * @param width  需要缩放宽度
     * @param height 需要缩放高度
     * @return
     */
    public static BufferedImage testZoom(BufferedImage image, int width, int height) {
        return ImageHelper.getScaledInstance(image, width, height);
    }

    /**
     * 作用:图片缩放
     *
     * @param image 需要缩放的图片
     * @return
     */
    public static BufferedImage zoom(BufferedImage image) {
        return ImageHelper.getScaledInstance(image, 673, 425);
    }

}
  • 图像OpenCV处理类,类用于使用 OpenCV 执行各种图像处理任务,如校正、裁剪、缩放和灰度化。
java 复制代码
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import static org.opencv.imgproc.Imgproc.CHAIN_APPROX_SIMPLE;
import static org.opencv.imgproc.Imgproc.RETR_EXTERNAL;

public class ImageOpencvUtil {
    private static final int BLACK = 0;
    private static final int WHITE = 255;
    private static final Size STANDARDSIZE = new Size(673, 425);

    // 私有化构造函数
    private ImageOpencvUtil() {
    }

    /**
     * 作用:均值滤波
     *
     * @param src Mat矩阵图像
     * @return
     */
    public static Mat blur(Mat src) {
        Mat dst = src.clone();
        Imgproc.blur(src, dst, new Size(9, 9), new Point(-1, -1), Core.BORDER_DEFAULT);
        return dst;
    }

    /**
     * 作用:高斯滤波
     *
     * @param src Mat矩阵图像
     * @return
     */
    public static Mat gaussianBlur(Mat src) {
        Mat dst = src.clone();
        Imgproc.GaussianBlur(src, dst, new Size(9, 9), 0, 0, Core.BORDER_DEFAULT);
        return dst;
    }

    /**
     * 作用:中值滤波
     *
     * @param src Mat矩阵图像
     * @return
     */
    public static Mat medianBlur(Mat src) {
        Mat dst = src.clone();
        Imgproc.medianBlur(src, dst, 7);
        return dst;
    }

    /**
     * 作用:非局部均值去噪
     *
     * @param src Mat矩阵图像
     * @return
     */
    public static Mat pyrMeanShiftFiltering(Mat src) {
        Mat dst = src.clone();
        Imgproc.pyrMeanShiftFiltering(src, dst, 10, 50);
        return dst;
    }

    /**
     * 伽马校正
     * 伽马校正对图像的修正作用就是通过增强低灰度或高灰度的细节实现的
     * 值越小,对图像低灰度部分的扩展作用就越强,值越大,对图像高灰度部分的扩展作用就越强,
     * 通过不同的值,就可以达到增强低灰度或高灰度部分细节的作用。
     * 在对图像进行伽马变换时,如果输入的图像矩阵是CV_8U,在进行幂运算时,大于255的值会自动截断为255;
     * 所以,先将图像的灰度值归一化到【0,1】范围,然后再进行幂运算
     *
     * @param src
     */
    public static Mat imageBrightness(Mat src) {

        //定义2个与输入图像大小类型一致的空对象
        Mat dst = new Mat(src.size(), src.type());
        Mat dst_1 = new Mat(src.size(), src.type());
        /*
         * 缩放并转换到另外一种数据类型:
         * dst:目的矩阵;
         * type:需要的输出矩阵类型,或者更明确的,是输出矩阵的深度,如果是负值(常用-1)则输出矩阵和输入矩阵类型相同;
         * scale:比例因子(输入矩阵参数*比例因子);
         * shift:将输入数组元素按比例缩放后添加的值(第三个参数处理后+第四个参数);
         * CV_64F:64 -表示双精度 32-表示单精度 F - 浮点  Cx - 通道数,例如RGB就是三通道
         */
        src.convertTo(dst, CvType.CV_64F, 1.0 / 255, 0);

        /*  将每个数组元素提升为幂:
         *  对于非整数幂指数,将使用输入数组元素的绝对值。 但是,可以使用一些额外的操作获得负值的真实值。
         *  对于某些幂值(例如整数值0.5和-0.5),使用了专用的更快算法。
         *  不处理特殊值(NaN,Inf)。
         *  @param 输入数组。
         *  @param 幂的幂指数。
         *  @param 输出数组,其大小和类型与输入数组相同。
         */
        Core.pow(dst, 0.7, dst_1);
        /* 缩放并转换到另外一种数据类型:
         * CV_8UC1---8位无符号的单通道---灰度图片
         * CV_8UC3---8位无符号的三通道---RGB彩色图像
         * CV_8UC4---8位无符号的四通道---带透明色的RGB图像
         */
        dst_1.convertTo(dst_1, CvType.CV_8U, 255, 0);

        return dst_1;
    }

    /**
     * 作用:灰度化
     *
     * @param src 需灰度化处理的Mat矩阵图像
     * @return
     */
    public static Mat gray(Mat src) {
        Mat grayImage = new Mat();
        try {
            grayImage = new Mat(src.height(), src.width(), CvType.CV_8UC1);
            Imgproc.cvtColor(src, grayImage, Imgproc.COLOR_BGR2GRAY);
        } catch (Exception e) {
            grayImage = src.clone();
            grayImage.convertTo(grayImage, CvType.CV_8UC1);
//            System.out.println("The Image File Is Not The RGB File!已处理...");
        }
        return grayImage;
    }

    /**
     * 作用:二值化
     *
     * @param grayImage 需二值化处理的灰度化后的Mat矩阵图像
     * @return
     */
    public static Mat ImgBinarization(Mat grayImage) {
        Mat threshImage = new Mat(grayImage.height(), grayImage.width(), CvType.CV_8UC1);
//        Imgproc.threshold(grayImage, threshImage, 100, 255, Imgproc.THRESH_BINARY);//效果不好
//        Imgproc.threshold(grayImage, threshImage, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
//        Imgproc.threshold(grayImage, threshImage, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_TRIANGLE);//白黑不行
        Imgproc.threshold(grayImage, threshImage, 127, 255, Imgproc.THRESH_TRUNC);//还行
//        Imgproc.threshold(grayImage, threshImage, 127, 255, Imgproc.THRESH_TOZERO);//不行
        return threshImage;
    }

    /**
     * 作用:自适应选取阀值
     *
     * @param src Mat矩阵图像
     * @return
     */
    private static int getAdapThreshold(Mat src) {
        int threshold = 0, thresholdNew = 127;
        int nWhiteCount, nBlackCount;
        int nWhiteSum, nBlackSum;
        int value, i, j;
        int width = src.cols();
        int height = src.rows();

        while (threshold != thresholdNew) {
            nWhiteSum = nBlackSum = 0;
            nWhiteCount = nBlackCount = 0;
            for (j = 0; j < height; j++) {
                for (i = 0; i < width; i++) {
                    value = (int) src.get(j, i)[0];
                    if (value > thresholdNew) {
                        nWhiteCount++;
                        nWhiteSum += value;
                    } else {
                        nBlackCount++;
                        nBlackSum += value;
                    }
                }
            }
            threshold = thresholdNew;
            thresholdNew = (nWhiteSum / nWhiteCount + nBlackSum / nBlackCount) / 2;
        }
        return threshold;
    }

    /**
     * 作用:翻转图像像素
     *
     * @param src Mat矩阵图像
     * @return
     */
    private static Mat turnPixel(Mat src) {
        if (src.channels() != 1) {
            throw new RuntimeException("不是单通道图,需要先灰度化!!!");
        }
        int j, i, value;
        int width = src.cols();
        int height = src.rows();
        for (j = 0; j < height; j++) {
            for (i = 0; i < width; i++) {
                value = (int) src.get(j, i)[0];
                if (value == 0) {
                    src.put(j, i, WHITE);
                } else {
                    src.put(j, i, BLACK);
                }
            }
        }
        return src;
    }

    /**
     * 图像二值化 阀值自适应确定
     *
     * @param src Mat矩阵图像
     * @return
     */
    public static Mat binaryzation(Mat src) {
        Mat dst = src.clone();
        if (dst.channels() != 1) {
            throw new RuntimeException("不是单通道图,需要先灰度化!!!");
        }
        int nWhiteSum = 0, nBlackSum = 0;
        int i, j;
        int width = dst.cols();
        int height = dst.rows();
        int value;

        int threshold = getAdapThreshold(dst);

        for (j = 0; j < height; j++) {
            for (i = 0; i < width; i++) {
                value = (int) dst.get(j, i)[0];
                if (value > threshold) {
                    dst.put(j, i, WHITE);
                    nWhiteSum++;
                } else {
                    dst.put(j, i, BLACK);
                    nBlackSum++;
                }
            }
        }
        if (true) {
            // 白底黑字
            if (nBlackSum > nWhiteSum) {
                dst = turnPixel(dst);
            }
        } else {
            // 黑底白字
            if (nWhiteSum > nBlackSum) {
                dst = turnPixel(dst);
            }
        }
        return dst;
    }

    /**
     * 根据二值化图片进行膨胀与腐蚀
     *
     * @param binaryImage 需膨胀腐蚀处理的二值化后的Mat矩阵图像
     * @return
     */
    public static Mat corrosion(Mat binaryImage) {
        Mat element1 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(1, 1));
        Mat element2 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(1, 1));
        //膨胀
        Mat dilate1 = new Mat();
//        Imgproc.dilate(binaryImage, dilate1, element2);
        Imgproc.dilate(binaryImage, dilate1, element2, new Point(-1, -1), 1, 1, new Scalar(1));
        //腐蚀
        Mat erode1 = new Mat();
        Imgproc.erode(dilate1, erode1, element1);
        //膨胀
        Mat dilate2 = new Mat();
        Imgproc.dilate(erode1, dilate2, element2);
        return dilate2;
    }

    /**
     * 作用:获取文字区域
     *
     * @param img 膨胀与腐蚀后的Mat矩阵图像
     * @return
     */
    public static List<RotatedRect> findTextRegion(Mat img, int scope) {
        List<RotatedRect> rects = new ArrayList<RotatedRect>();
        //1.查找轮廓
        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(img, contours, hierarchy, RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(-1, -1));//11.png不能被倾斜校正
//        Imgproc.findContours(img, contours, hierarchy, Imgproc.RETR_CCOMP, CHAIN_APPROX_SIMPLE, new Point(-1, -1));//11.png可以被倾斜校正
//        Imgproc.findContours(img, contours, hierarchy, RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));//11.png不能被倾斜校正

        int img_width = img.width();
        int img_height = img.height();
        int size = contours.size();

        //2.筛选那些面积小的
        for (int i = 0; i < size; i++) {
            double area = Imgproc.contourArea(contours.get(i));
            if (area < scope)//原来是1000
                continue;
            //轮廓近似,作用较小,approxPolyDP函数有待研究
            double epsilon = 0.001 * Imgproc.arcLength(new MatOfPoint2f(contours.get(i).toArray()), true);
            MatOfPoint2f approxCurve = new MatOfPoint2f();
            Imgproc.approxPolyDP(new MatOfPoint2f(contours.get(i).toArray()), approxCurve, epsilon, true);

            //找到最小矩形,该矩形可能有方向
            RotatedRect rect = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(i).toArray()));
            //计算高和宽
            int m_width = rect.boundingRect().width;
            int m_height = rect.boundingRect().height;

            //筛选那些太细的矩形,留下扁的
            if (m_width < m_height)
                continue;
            if (img_width == rect.boundingRect().br().x)
                continue;
            if (img_height == rect.boundingRect().br().y)
                continue;
            //符合条件的rect添加到rects集合中
            rects.add(rect);
        }
        return rects;
    }

    /**
     * 作用:摆正图片
     *
     * @param rects    文字区域
     * @param srcImage 原Mat矩阵图像
     * @return
     */
    public static Mat correction(List<RotatedRect> rects, Mat srcImage) {
        double degree = 0;
        double degreeCount = 0;
        for (int i = 0; i < rects.size(); i++) {
            if (rects.get(i).angle >= -90 && rects.get(i).angle < -45) {
                degree = rects.get(i).angle;
                if (rects.get(i).angle != 0) {
                    degree += 90;
                }
            }
            if (rects.get(i).angle > -45 && rects.get(i).angle <= 0) {
                degree = rects.get(i).angle;
            }
            if (rects.get(i).angle <= 90 && rects.get(i).angle > 45) {
                degree = rects.get(i).angle;
                if (rects.get(i).angle != 0) {
                    degree -= 90;
                }
            }
            if (rects.get(i).angle < 45 && rects.get(i).angle >= 0) {
                degree = rects.get(i).angle;
            }
            if (degree > -5 && degree < 5) {
                degreeCount += degree;
            }

        }
        if (degreeCount != 0) {
            // 获取平均水平度数
            degree = degreeCount / rects.size();
        }
        Point center = new Point(srcImage.cols() / 2, srcImage.rows() / 2);
        Mat rotm = Imgproc.getRotationMatrix2D(center, degree, 1.0);    //获取仿射变换矩阵
        Mat dst = new Mat();
        Imgproc.warpAffine(srcImage, dst, rotm, srcImage.size(), Imgproc.INTER_LINEAR, 0, new Scalar(255, 255, 255));    // 进行图像旋转操作
        return dst;
    }

    /**
     * 倾斜矫正
     *
     * @param src 需倾斜校正的Mat矩阵图像
     * @return
     */
    public static Mat imgCorrection(Mat src, int scope) {
        //灰度化
        Mat grayImg = gray(src);
        //二值化
        Mat binaryImg = binaryzation(grayImg);
        //膨胀与腐蚀
        Mat corrodedImg = corrosion(binaryImg);
        //查找和筛选文字区域
        List<RotatedRect> rects = findTextRegion(corrodedImg, scope);
        //倾斜校正
        Mat correctedImg = correction(rects, src);

        //todo 可优化添加后两行代码,并返回zoomedImg
        //倾斜校正后裁剪
//        Mat cuttedImg = cutRect(correctedImg);
        //裁剪后标准化
//        Mat zoomedImg = zoom(cuttedImg);

        return correctedImg;
    }

    /**
     * canny算法,边缘检测
     *
     * @param src Mat矩阵图像
     * @return
     */
    public static Mat canny(Mat src) {
        Mat dst = src.clone();
        src = gray(src);
//        src = binaryzation(grayImage);
//        src = ImgBinarization(src);
        //Canny边缘检测
        Imgproc.Canny(src, dst, 20, 60, 3, false);
        //膨胀,连接边缘
        Imgproc.dilate(dst, dst, new Mat(), new Point(-1, -1), 1, 1, new Scalar(0.5));
        return dst;
    }

    /**
     * 返回边缘检测之后的最大矩形轮廓,并返回
     *
     * @param cannyMat Canny之后的mat矩阵
     * @return
     */
    public static RotatedRect findMaxRect(Mat cannyMat) {
        //边缘检测
        cannyMat = canny(cannyMat);
//        Imgproc.dilate(cannyMat, cannyMat, new Mat(), new Point(-1, -1), 1, 1, new Scalar(0.5));
        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();

        //轮廓提取
        Imgproc.findContours(cannyMat, contours, hierarchy, RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));
//        Imgproc.findContours(cannyMat, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

        // 找出匹配到的最大轮廓
        double area = Imgproc.boundingRect(contours.get(0)).area();
        int index = 0;

        // 找出匹配到的最大轮廓
        for (int i = 0; i < contours.size(); i++) {
            double tempArea = Imgproc.boundingRect(contours.get(i)).area();
            if (tempArea > area) {
                area = tempArea;
                index = i;
            }
        }

        MatOfPoint2f matOfPoint2f = new MatOfPoint2f(contours.get(index).toArray());

        RotatedRect rect = Imgproc.minAreaRect(matOfPoint2f);

        return rect;
    }

    /**
     * 作用:把矫正后的图像切割出来
     *
     * @param correctMat 图像矫正后的Mat矩阵
     */
    public static Mat cutRect(Mat correctMat) {
        // 获取最大矩形
        RotatedRect rect = findMaxRect(correctMat);

        Point[] rectPoint = new Point[4];
        rect.points(rectPoint);

        int startLeft = (int) Math.abs(rectPoint[0].x);
        int startUp = (int) Math.abs(Math.abs(rectPoint[0].y) < Math.abs(rectPoint[1].y) ? rectPoint[0].y : rectPoint[1].y);
        int width = (int) Math.abs(rectPoint[2].x - rectPoint[0].x);
//        int height = (int) Math.abs(rectPoint[1].y - rectPoint[0].y);
        int height = (int) Math.abs(rectPoint[3].y - rectPoint[1].y);

//        System.out.println("startLeft = " + startLeft);
//        System.out.println("startUp = " + startUp);
//        System.out.println("width = " + width);
//        System.out.println("height = " + height);

        //检测的高度过低,则说明拍照时身份证边框没拍全,直接返回correctMat,如检测的不是身份证则不需要这个if()判断
        //怎么判断如果一个矩形最大边没有被完全检测,即检测的不是一个闭合的矩形,但是仍应该保留这个矩形
//        if (height < 0.3 * width)
//            return correctMat;

//        for (Point p : rectPoint) {
//            System.out.println(p.x + " , " + p.y);
//        }

        if (startLeft + width > correctMat.width()) {
            width = correctMat.width() - startLeft;
        }
        if (startUp + height > correctMat.height()) {
            height = correctMat.height() - startUp;
        }

        Mat temp = new Mat(correctMat, new Rect(startLeft, startUp, width, height));
//        try {
//            temp = new Mat(correctMat, new Rect(startLeft, startUp, width, height));
//        } catch (Exception e) {
//            System.out.println(e);
//        }

        return temp;
    }

    /**
     * 作用:缩放图片
     *
     * @param src 需要缩放的Mat矩阵图像
     * @return
     */
    public static Mat zoom(Mat src) {
        Mat dst = new Mat();
        //区域插值(INTER_AREA):图像放大时类似于线性插值,图像缩小时可以避免波纹出现
        Imgproc.resize(src, dst, STANDARDSIZE, 0, 0, Imgproc.INTER_AREA);
        return dst;
    }

    /**
     * 根据二值化图片进行膨胀与腐蚀
     *
     * @param src 需膨胀腐蚀处理的灰度化后的Mat矩阵图像
     * @return
     */
    public static Mat preprocess(Mat src) {
        //1.Sobel算子,x方向求梯度
        Mat sobel = new Mat();
        Imgproc.Sobel(src, sobel, 0, 1, 0, 3);

        //2.二值化
        Mat binaryImage = new Mat();
        Imgproc.threshold(sobel, binaryImage, 0, 255, Imgproc.THRESH_OTSU | Imgproc.THRESH_BINARY);

        //3.腐蚀和膨胀操作核设定
        Mat element1 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(24, 9));
        //设置高度大小可以控制上下行的膨胀程度,例如3比4的区分能力更强,但也会造成漏检
        Mat element2 = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(26, 9));

        //4.膨胀一次,让轮廓突出
        Mat dilate1 = new Mat();
        Imgproc.dilate(binaryImage, dilate1, element2);
//        Imgproc.dilate(binaryImage, dilate1, element2, new Point(-1, -1), 1, 1, new Scalar(1));

        //5.腐蚀一次,去掉细节,表格线等。这里去掉的是竖直的线
        Mat erode1 = new Mat();
        Imgproc.erode(dilate1, erode1, element1);

        //6.再次膨胀,让轮廓明显一些
        Mat dilate2 = new Mat();
        Imgproc.dilate(erode1, dilate2, element2);
//        Imgproc.dilate(erode1, dilate2, element2, new Point(-1, -1), 1, 1, new Scalar(1));

        return dilate2;
    }

    /**
     * 作用:获取文字区域矩形框
     *
     * @param img 膨胀与腐蚀后的Mat矩阵图像
     * @return
     */
    public static List<RotatedRect> findTextRegionRect(Mat img, int scope) {
        //保存姓名、名族、地址、身份证号信息的矩形框
        List<RotatedRect> rects = new ArrayList<RotatedRect>();
        //保存性别、名族、出生年月日信息的矩形框,并将名族信息矩形框添加到rects中
        List<RotatedRect> _rects = new ArrayList<RotatedRect>();

        //1.查找轮廓
        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
//        Imgproc.findContours(img, contours, hierarchy, RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(-1, -1));//11.png不能被倾斜校正
        Imgproc.findContours(img, contours, hierarchy, Imgproc.RETR_CCOMP, CHAIN_APPROX_SIMPLE, new Point(-1, -1));//11.png可以被倾斜校正
//        Imgproc.findContours(img, contours, hierarchy, RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));//11.png不能被倾斜校正

        int img_width = img.width();
        int img_height = img.height();
        int size = contours.size();
        //身份证号宽度
        int idWidth = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(0).toArray())).boundingRect().width;
        //身份证号矩形框在rects中的索引下标
        int index = 0;
        //2.筛选那些面积小的
        for (int i = 0; i < size; i++) {
            double area = Imgproc.contourArea(contours.get(i));
            //原来是1000
            if (area < scope)
                continue;

            //轮廓近似,作用较小,approxPolyDP函数有待研究
            double epsilon = 0.001 * Imgproc.arcLength(new MatOfPoint2f(contours.get(i).toArray()), true);
            MatOfPoint2f approxCurve = new MatOfPoint2f();
            Imgproc.approxPolyDP(new MatOfPoint2f(contours.get(i).toArray()), approxCurve, epsilon, true);

            //找到最小矩形,该矩形可能有方向
            RotatedRect rect = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(i).toArray()));
            //计算高和宽
            int m_width = rect.boundingRect().width;
            int m_height = rect.boundingRect().height;

//            System.out.println("width = " + m_width);

//            if (m_width < 80)
//                continue;
//            if (m_width < m_height * 1.2)
//                continue;

            //筛选那些太细的矩形,留下扁的/**/
            if (m_width * 1.2 < m_height)
                continue;
            if (img_width == rect.boundingRect().br().x)
                continue;
            if (img_height == rect.boundingRect().br().y)
                continue;

            //符合条件的rect添加到rects集合中
            rects.add(rect);
        }

        //遍历找到身份证矩形框的宽度大小及在rects中的索引下标index
        for (int i = 0; i < rects.size(); i++) {
            int tempIdWidth = rects.get(i).boundingRect().width;
            if (tempIdWidth > idWidth) {
                idWidth = tempIdWidth;
                index = i;
            }
        }
//        System.out.println("身份证号下标:" + index);
        //如果身份证号周围有矩形框(公民身份证号码文本矩形框),则将其从rects中移除
        while (idWidth == rects.get(index).boundingRect().width) {
            if (index + 1 < rects.size()) {
                if (Math.abs(rects.get(index).center.y - rects.get(index + 1).center.y) < 10) {
                    rects.remove(index + 1);
                }
            }
            break;
        }
        //将身份证矩形框存储到索引为0的位置
        if (index != 0) {
            RotatedRect rotatedRect = rects.get(index);
            rects.set(index, rects.get(0));
            rects.set(0, rotatedRect);
            index = 0;
        }
//        System.out.println("修改索引后的身份证号下标:" + index);

        /*for (int i = 0; i < rects.size(); i++) {
            if (rects.get(i).center.x > rects.get(index).center.x)
                rects.remove(i);
        }
        //将上面的for循环代码--删除身份证号矩形框右边的矩形框改为Iterator迭代器实现

        //下面的代码可能会漏掉一些符合if条件即需要被删除的元素,因为在删除某个元素后,
        //List对象rects的大小发生了变化,而元素索引也在变化,所以会导致在遍历的时候漏掉某些元素。
        for (int i = 0; i < rects.size(); i++) {
            if (rects.get(i).center.x - rects.get(index).center.x < 0 && 80 < rects.get(i).center.y && rects.get(i).center.y < 200) {
                _rects.add(rects.get(i));
                rects.remove(i);
            }
        }*/

        //使用下面的迭代器实现循环rects并删除rects的元素
        Iterator<RotatedRect> iterator = rects.iterator();
        while (iterator.hasNext()) {
            RotatedRect rect = iterator.next();
            //删除身份证号矩形框右边的矩形框
            if (rect.center.x > rects.get(index).center.x)
                iterator.remove();
                //将高度处于(80,200)位置的矩形框添加到_rects中
            else if (rect.center.x < rects.get(index).center.x && 80 < rect.center.y && rect.center.y < 200) {
                _rects.add(rect);
                iterator.remove();
            }
        }

        //_rects.get(_rects.size() - 2)为 名族信息 矩形框
        if (_rects.size() >= 2) {
            for (int i = rects.size() - 1; i >= 0; i--) {
                //rects按照rects.get(i).center.y从大到小排列,则将名族信息矩形框插入到原来姓名信息矩形框的所在位置
                if (_rects.get(_rects.size() - 2).center.y > rects.get(i).center.y && _rects.get(_rects.size() - 2).center.y < rects.get(i - 1).center.y) {
                    rects.add(i, _rects.get(_rects.size() - 2));
                    break;
                }
            }
        }


//        System.out.println("中心坐标x:");
//        for (int i = 0; i < rects.size(); i++) {
//            System.out.println(rects.get(i).center.x);
//        }
//        System.out.println("中心坐标y:");
//        for (int i = 0; i < rects.size(); i++) {
//            System.out.println(rects.get(i).center.y);
//        }
//        System.out.println("rects.size:" + rects.size());

        return rects;
    }


    /**
     * 作用:获取文字区域矩形框
     *
     * @param img 膨胀与腐蚀后的Mat矩阵图像
     * @return
     */
    public static List<RotatedRect> findBankTextRegionRect(Mat img, int scope) {
        List<RotatedRect> rects = new ArrayList<>();
        List<MatOfPoint> contours = new ArrayList<>();
        Mat hierarchy = new Mat();

        Imgproc.findContours(img, contours, hierarchy, Imgproc.RETR_CCOMP, Imgproc.CHAIN_APPROX_SIMPLE);

        int imgWidth = img.width();
        int imgHeight = img.height();
        int contourSize = contours.size();

        // 计算第一个轮廓的最小外接矩形宽度
        int idWidth = contourSize > 0 ? Imgproc.minAreaRect(new MatOfPoint2f(contours.get(0).toArray())).boundingRect().width : 0;
        int index = 0;

        for (int i = 0; i < contourSize; i++) {
            double area = Imgproc.contourArea(contours.get(i));
            if (area < scope) {
                continue;
            }

            RotatedRect rect = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(i).toArray()));
            int mWidth = rect.boundingRect().width;
            int mHeight = rect.boundingRect().height;

            // 筛选出太细的矩形
            if (mWidth * 1.2 < mHeight || imgWidth == rect.boundingRect().br().x || imgHeight == rect.boundingRect().br().y) {
                continue;
            }

            rects.add(rect);
        }

        // 找出银行卡矩形框的最大宽度并获取索引
        for (int i = 0; i < rects.size(); i++) {
            int tempIdWidth = rects.get(i).boundingRect().width;
            if (tempIdWidth > idWidth) {
                idWidth = tempIdWidth;
                index = i;
            }
        }

        // 将银行卡矩形框放在第一个位置
        if (index != 0) {
            Collections.swap(rects, 0, index);
        }

        // 使用迭代器移除身份证号矩形框右边的矩形框
        rects.removeIf(rect -> rect.center.x > rects.get(0).center.x);

        return rects;
    }

//    public static Mat cropImage(Mat src, Rect rect) throws Exception {
//        if (src.empty())
//            throw new Exception("image is empty");
//
//        Mat dst = new Mat(src, rect);
//        return dst;
//    }

    public static Mat cropImage(Mat src, Rect rect) {
        // 计算裁剪区域,确保区域在图像范围内
        int x = Math.max(rect.x, 0);
        int y = Math.max(rect.y, 0);
        int width = Math.min(rect.width, src.cols() - x);
        int height = Math.min(rect.height, src.rows() - y);

        // 确保裁剪区域有效
        if (width > 0 && height > 0) {
            return new Mat(src, new Rect(x, y, width, height));
        } else {
            throw new IllegalArgumentException("Invalid rectangle dimensions for cropping");
        }
    }

}
  • TesseractConfig的实现类,用于区分操作系统,动态加载相应的本地库文件。
java 复制代码
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@Slf4j
public class TesseractConfig {
    static {
        String osName = System.getProperty("os.name").toLowerCase();
        log.info("系统环境:"+osName);
        if (osName.contains("win")) {
            DllLoaderUtil.loadDllFromResource("opencv/opencv_java455.dll", "opencv_java455", ".dll");
        } else if (osName.contains("nix") || osName.contains("nux") || osName.contains("mac")) {
            log.info("系统环境2:"+osName);
            DllLoaderUtil.loadDllFromResource("opencv/libopencv_java455.so", "libopencv_java455", ".so");
        } else {
            throw new UnsupportedOperationException("Unsupported operating system: " + osName);
        }

    }
    public static void initialize() {
        try {
            // 从类路径中获取 tessdata 目录
            URL tessDataUrl = TesseractConfig.class.getClassLoader().getResource("tessdata");
            if (tessDataUrl == null) {
                throw new IllegalStateException("tessdata directory not found!");
            }

            // 创建临时目录
            File tempDir = new File(System.getProperty("java.io.tmpdir"), "tessdata");
            if (!tempDir.exists()) {
                tempDir.mkdirs();
            }

            // 复制 tessdata 文件到临时目录
            String[] languages = {"chi_sim.traineddata", "eng.traineddata"}; // 需要的语言文件
            for (String lang : languages) {
                try (InputStream in = TesseractConfig.class.getClassLoader().getResourceAsStream("tessdata/" + lang);
                     FileOutputStream out = new FileOutputStream(new File(tempDir, lang))) {
                    if (in == null) {
                        throw new IllegalStateException("File " + lang + " not found in tessdata directory!");
                    }
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) != -1) {
                        out.write(buffer, 0, bytesRead);
                    }
                }
            }

            // 设置 TESSDATA_PREFIX 系统属性
            System.setProperty("TESSDATA_PREFIX", tempDir.getAbsolutePath() + File.separator);

        } catch (IOException e) {
            throw new RuntimeException("Failed to initialize TesseractConfig", e);
        }
    }

}
  • Tesseract加载器,用来简化Tesseract的初始化过程。
java 复制代码
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;

public class TesseractLoaderUtil {

    public static ITesseract createTesseractInstance(String tessDataPath, String language) {
        ITesseract instance = new Tesseract();
        instance.setDatapath(tessDataPath);
        instance.setLanguage(language);
        instance.setTessVariable("user_defined_dpi", "300");
        return instance;
    }
}
  • 图像OpenCV测试类,用来测试证件号识别。
java 复制代码
import com.alibaba.fastjson.JSONObject;
import com.xydtech.api.authorization.exception.TokenErrorException;
import com.xydtech.api.config.ResultStatus;
import com.xydtech.api.dto.ResponseModel;
import com.xydtech.api.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.TesseractException;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

/**
 * @program: bigDataApi
 * @ClassName: OpenCVTest
 * @description:
 * @author: yjs
 * @create: 2024-11-20 15:04
 */
@Slf4j
public  class OpenCVTest {

    public static void main(String[] args) {
        offlineOCRRecognition("G:\\444.jpg");
    }
    public static JSONObject offlineOCRRecognition(String imgUrl) {
        // 设置TESSDATA_PREFIX 环境变量
        TesseractConfig.initialize();
        // 读取图像
        JSONObject reObject = new JSONObject();
        try {
            long startTime = System.currentTimeMillis();

            Mat image = Imgcodecs.imread(imgUrl);
            if (image.empty()) {
                throw new ApiException("文件是空!");
            }
            Mat correctedImg = ImageOpencvUtil.imgCorrection(image, 2000);
            Mat cuttedImg = ImageOpencvUtil.cutRect(correctedImg);
            Mat zoomedImg = ImageOpencvUtil.zoom(cuttedImg);
            Mat img = zoomedImg.clone();
            BufferedImage bufferedImage = ImageConvert.Mat2BufImg(img, ".png");

            int brightness = ImageFilterUtil.imageBrightness(bufferedImage);
            BufferedImage brightnessImg = brightness > 180 ? ImageFilterUtil.imageBrightness(bufferedImage, -60) : bufferedImage;
            BufferedImage grayImage = ImageFilterUtil.gray(brightnessImg);
            Mat matImg = ImageConvert.BufImg2Mat(grayImage);
            Mat denoiseImg = ImageOpencvUtil.pyrMeanShiftFiltering(matImg);
            Mat grayImg = ImageOpencvUtil.gray(denoiseImg);
            Mat dilationImg = ImageOpencvUtil.preprocess(grayImg);
            List<RotatedRect> rects = ImageOpencvUtil.findTextRegionRect(dilationImg, 2000);

            for (RotatedRect rotatedRect : rects) {
                Point[] rectPoint = new Point[4];
                rotatedRect.points(rectPoint);
                for (int j = 0; j <= 3; j++) {
                    Imgproc.line(img, rectPoint[j], rectPoint[(j + 1) % 4], new Scalar(0, 0, 255), 2);
                }
            }

            List<Mat> lstMat = new ArrayList<>();
            for (RotatedRect rect : rects) {
                Mat dst = ImageOpencvUtil.cropImage(matImg, rect.boundingRect());
                lstMat.add(dst);
            }

            List<BufferedImage> lstBufferedImg = new ArrayList<>();
            for (int i = lstMat.size() - 1; i >= 0; i--) {
                BufferedImage tempBufferedImg = ImageConvert.Mat2BufImg(lstMat.get(i), ".png");
                lstBufferedImg.add(tempBufferedImg);
            }


            ITesseract instance = TesseractLoaderUtil.createTesseractInstance(System.getProperty("TESSDATA_PREFIX"), "chi_sim");
            String idNumber = instance.doOCR(lstBufferedImg.get(lstBufferedImg.size() - 1)).replaceAll("[^0-9xX]", "");
            reObject.put("身份证号", idNumber);
            System.out.println("身份证号:" + idNumber);
            String name = instance.doOCR(lstBufferedImg.get(0)).replaceAll("\\s*", "");
            System.out.println("姓名:" + name.trim());
            reObject.put("姓名", name);
            long endTime = System.currentTimeMillis();
            System.out.println("识别用时:" + (endTime - startTime) + "ms");
            reObject.put("识别用时", (endTime - startTime) + "ms");
            return reObject;
        } catch (Exception ex) {
            throw new ApiException("异常:"+ex);
        }

    }

}

如何在Linux环境中安装Tesseract OCR:

原文链接:如何在Linux环境中安装Tesseract OCR_linux安装tesseract-CSDN博客

Linux环境下安装OpenCV 4.5.2及生成.so文件的详细教程:

原文链接:Linux环境下安装OpenCV 4.5.2及生成.so文件的详细教程_opencv 4.5.2 安装 linux-CSDN博客

使用 Java、OpenCV 和 Tesseract OCR 实现身份证号码自动识别

原文链接:使用 Java、OpenCV 和 Tesseract OCR 实现身份证号码自动识别_java opencv 识别身份证-CSDN博客

相关推荐
likuolei13 分钟前
XQuery 完整语法速查表(2025 最新版,XQuery 3.1)
xml·java·数据库
雨中飘荡的记忆18 分钟前
LangChain4j 实战指南
java·langchain
okseekw20 分钟前
Java 中的方法:从定义到重载的完整指南
java
雨中飘荡的记忆21 分钟前
深入理解设计模式之适配器模式
java·设计模式
用户849137175471622 分钟前
生产级故障排查实战:从制造 OOM 到 IDEA Profiler 深度破案
java·jvm
雨中飘荡的记忆25 分钟前
深入理解设计模式之装饰者模式
java·设计模式
雨中飘荡的记忆29 分钟前
秒杀系统设计与实现
java·redis·lua
CryptoPP34 分钟前
使用 KLineChart 这个轻量级的前端图表库
服务器·开发语言·前端·windows·后端·golang
18你磊哥40 分钟前
chromedriver.exe的使用和python基本处理
开发语言·python
小坏讲微服务1 小时前
Spring Cloud Alibaba 整合 Scala 教程完整使用
java·开发语言·分布式·spring cloud·sentinel·scala·后端开发