Tesseract 是一款开源的 OCR 引擎,由 Google 维护。它支持多种语言的文字识别,具有较高的识别准确率和良好的扩展性。Tesseract 的核心作用是对经过预处理的图像进行分析,提取其中的文字信息并转换为文本。它可以处理不同字体、大小和格式的文字,并且能够通过训练来提高对特定场景文字的识别能力。
官方网站:https://github.com/Tesseract-ocr/Tesseract
官方文档:https://github.com/Tesseract-ocr/tessdoc
语言包地址:https://github.com/Tesseract-ocr/tessdata
下载地址:https://digi.bib.uni-mannheim.de/Tesseract/
OCR是什么?
文字识别,即 Optical Character Recognition,简称 OCR,是指通过电子设备(如扫描仪、相机等)将图像中的文字转换为可编辑的文本格式的技术。其应用场景极为广泛:如电子文档转换,车牌识别系统的自动识别和管理;金融的票据识别提取支票、发票上的关键信息;翻译类 APP 通过摄像头识别外文并实时翻译,极大地便利了人们的跨语言交流。
然而,OCR 技术的实现并非易事,面临着诸多技术难点。首先是图像质量的影响,如模糊、倾斜、光照不均、存在噪声等,都会导致文字识别准确率下降。其次,文字的字体、大小、颜色各异,以及可能存在的复杂背景,也会给识别带来挑战。此外,对于手写体文字,由于其个性化强、规范性差,识别难度更大,所以一般需要使用 OpenCV 来搭配处理图像。
Tesseract 的特点
Tesseract 最初是由惠普公司开发的 OCR 引擎,后来被 Google 收购并开源。经过多年的发展和优化,Tesseract 已经成为目前最受欢迎的开源 OCR 引擎之一。
Tesseract 具有以下特点:
Tesseract 的核心功能与工作原理
Tesseract 的核心功能是对图像中的文字进行识别并转换为文本。其工作原理主要包括以下几个步骤:
|
1 |
brew install tesseract |
验证安装:
|
1 2 |
tesseract --version tesseract --list-langs |
Apple Silicon Mac 的配置通常是:
|
1 2 3 |
tesseract: datapath: /opt/homebrew/share/tessdata language: eng |
Intel Mac 通常是:
|
1 2 3 |
tesseract: datapath: /usr/local/share/tessdata language: eng |
可以用下面命令获取准确目录:
|
1 2 |
brew --prefix tesseract find "$(brew --prefix tesseract)" -name eng.traineddata |
如果输出为:
|
1 |
/opt/homebrew/share/tessdata/eng.traineddata |
那么 datapath 就配置为:
|
1 |
/opt/homebrew/share/tessdata |
CentOS Stream 8/9、Rocky Linux、AlmaLinux
|
1 2 |
sudo dnf install -y epel-release sudo dnf install -y tesseract tesseract-langpack-eng |
验证:
|
1 2 3 |
tesseract --version tesseract --list-langs ldconfig -p | grep tesseract |
查找实际路径:
|
1 2 |
find /usr -name 'libtesseract.so*' 2>/dev/null find /usr -name 'eng.traineddata' 2>/dev/null |
常见配置为:
|
1 2 3 4 |
tesseract: datapath: /usr/share/tesseract/tessdata library-path: /usr/lib64 language: eng |
如果 eng.traineddata 位于 /usr/share/tessdata/eng.traineddata,则配置:
|
1 2 3 4 |
tesseract: datapath: /usr/share/tessdata library-path: /usr/lib64 language: eng |
安装后刷新动态库缓存:
|
1 |
sudo ldconfig |
|
1 2 3 4 5 6 |
<!-- Tesseract OCR Java 封装 --> <dependency> <groupId>net.sourceforge.tess4j</groupId> <artifactId>tess4j</artifactId> <version>5.13.0</version> </dependency> |
2.服务类
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * 身份证图片识别服务。 */ public interface IdCardOcrService { /** * 从身份证人像面图片中识别身份证号。 * * @param imageUrl 可公开访问或带有效签名的图片地址 * @return 身份证号 */ String recognizeIdCardNumber(String imageUrl); } |
|
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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
import com.sun.jna.NativeLibrary; import net.sourceforge.tess4j.ITessAPI; import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.ConvolveOp; import java.awt.image.BufferedImage; import java.awt.image.Kernel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.ConnectException; import java.net.URI; import java.net.URISyntaxException; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; /** * Tesseract 身份证 OCR 实现。 */ @Service public class IdCardOcrServiceImpl implements IdCardOcrService { private static final int MAX_URL_LENGTH = 4096; private static final int MAX_IMAGE_SIZE = 7 * 1024 * 1024; private static final long MAX_SOURCE_IMAGE_PIXELS = 100_000_000L; private static final long MAX_DECODED_IMAGE_PIXELS = 16_000_000L; private static final int FULL_IMAGE_TARGET_WIDTH = 2200; private static final int NUMBER_REGION_TARGET_WIDTH = 2400; private static final int RAW_NUMBER_LINE_TARGET_WIDTH = 1600; private static final int CONNECT_TIMEOUT_MILLIS = 5000; private static final int READ_TIMEOUT_MILLIS = 10000; private static final int MAX_REDIRECTS = 3; private static final Logger LOGGER = Logger.getLogger(IdCardOcrServiceImpl.class.getName()); private final String tessDataPath; private final String language; public IdCardOcrServiceImpl( @Value("${tesseract.datapath:}") String tessDataPath, @Value("${tesseract.language:eng}") String language, @Value("${tesseract.library-path:}") String libraryPath) { this.tessDataPath = tessDataPath; this.language = language; if (StringUtils.isNotBlank(libraryPath)) { NativeLibrary.addSearchPath("tesseract", libraryPath); } } @Override public String recognizeIdCardNumber(String imageUrl) { try { BufferedImage sourceImage = downloadImage(validateImageUrl(imageUrl), 0); for (RecognitionImage recognitionImage : buildRecognitionImages(sourceImage)) { String idCardNumber = extractValidIdCardNumber( recognize(recognitionImage.image, recognitionImage.pageSegMode)); if (idCardNumber != null) { return idCardNumber; } } throw new ApiResultException("未识别到有效身份证号,请上传清晰的身份证人像面图片"); } catch (TesseractException e) { LOGGER.log(Level.WARNING, "Tesseract身份证OCR识别失败: " + e.getMessage()); throw new ApiResultException("身份证识别失败,请稍后重试"); } catch (IOException e) { LOGGER.log(Level.WARNING, "身份证图片处理失败: " + e.getMessage()); throw new ApiResultException("身份证图片处理失败,请稍后重试"); } } private URI validateImageUrl(String imageUrl) { if (StringUtils.isBlank(imageUrl)) { throw new ApiResultException("身份证图片地址不能为空"); } if (imageUrl.length() > MAX_URL_LENGTH) { throw new ApiResultException("身份证图片地址过长"); } try { URI uri = new URI(imageUrl); String scheme = uri.getScheme(); if (StringUtils.isBlank(uri.getHost()) || !("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) { throw new ApiResultException("身份证图片地址格式错误"); } return uri; } catch (URISyntaxException e) { throw new ApiResultException("身份证图片地址格式错误"); } } private BufferedImage downloadImage(URI uri, int redirectCount) throws IOException { if (redirectCount > MAX_REDIRECTS) { throw new ApiResultException("身份证图片地址重定向次数过多"); } HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); connection.setConnectTimeout(CONNECT_TIMEOUT_MILLIS); connection.setReadTimeout(READ_TIMEOUT_MILLIS); connection.setInstanceFollowRedirects(false); connection.setRequestProperty("Accept", "image/*"); connection.setRequestProperty("User-Agent", "health-user-id-card-ocr/1.0"); try { int status; try { status = connection.getResponseCode(); } catch (ConnectException e) { LOGGER.log(Level.WARNING, "身份证图片服务器拒绝连接: " + formatAuthority(uri)); throw new ApiResultException("身份证图片服务器拒绝连接,请检查图片地址和端口"); } catch (SocketTimeoutException e) { LOGGER.log(Level.WARNING, "身份证图片服务器连接超时: " + formatAuthority(uri)); throw new ApiResultException("身份证图片下载超时,请稍后重试"); } if (status >= 300 && status < 400) { String location = connection.getHeaderField("Location"); if (StringUtils.isBlank(location)) { throw new ApiResultException("身份证图片地址重定向无效"); } return downloadImage(validateImageUrl(uri.resolve(location).toString()), redirectCount + 1); } if (status != HttpURLConnection.HTTP_OK) { throw new ApiResultException("身份证图片下载失败"); } int contentLength = connection.getContentLength(); if (contentLength > MAX_IMAGE_SIZE) { throw new ApiResultException("身份证图片不能超过7MB"); } byte[] imageBytes; try (InputStream inputStream = connection.getInputStream()) { imageBytes = readLimited(inputStream); } catch (SocketTimeoutException e) { LOGGER.log(Level.WARNING, "身份证图片读取超时: " + formatAuthority(uri)); throw new ApiResultException("身份证图片下载超时,请稍后重试"); } return decodeImage(imageBytes); } finally { connection.disconnect(); } } private String formatAuthority(URI uri) { int port = uri.getPort(); if (port < 0) { port = "https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80; } return uri.getScheme() + "://" + uri.getHost() + ":" + port; } private byte[] readLimited(InputStream inputStream) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; int total = 0; int length; while ((length = inputStream.read(buffer)) != -1) { total += length; if (total > MAX_IMAGE_SIZE) { throw new ApiResultException("身份证图片不能超过7MB"); } outputStream.write(buffer, 0, length); } return outputStream.toByteArray(); } private BufferedImage decodeImage(byte[] imageBytes) throws IOException { try (ImageInputStream imageInputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(imageBytes))) { if (imageInputStream == null) { throw new ApiResultException("身份证图片格式不支持"); } Iterator<ImageReader> readers = ImageIO.getImageReaders(imageInputStream); if (!readers.hasNext()) { throw new ApiResultException("身份证图片格式不支持"); } ImageReader reader = readers.next(); try { reader.setInput(imageInputStream, true, true); int width = reader.getWidth(0); int height = reader.getHeight(0); long sourcePixels = (long) width * height; if (sourcePixels > MAX_SOURCE_IMAGE_PIXELS) { throw new ApiResultException("身份证图片分辨率过大"); } ImageReadParam readParam = reader.getDefaultReadParam(); int subsampling = calculateSubsampling(sourcePixels); if (subsampling > 1) { readParam.setSourceSubsampling(subsampling, subsampling, 0, 0); } return reader.read(0, readParam); } finally { reader.dispose(); } } } private int calculateSubsampling(long sourcePixels) { int subsampling = 1; while (sourcePixels / ((long) subsampling * subsampling) > MAX_DECODED_IMAGE_PIXELS) { subsampling++; } return subsampling; } private List<RecognitionImage> buildRecognitionImages(BufferedImage source) { List<RecognitionImage> images = new ArrayList<>(); // A compact raw-line pass is the most reliable option for screen photos and // defocused large images, where sparse-text mode tends to split every digit. BufferedImage rawNumberLine = cropByRatio(source, 0.25, 0.60, 0.55, 0.18); BufferedImage enhancedRawNumberLine = sharpen(stretchContrast( resizeAndGrayscale(rawNumberLine, RAW_NUMBER_LINE_TARGET_WIDTH))); images.add(new RecognitionImage( enhancedRawNumberLine, ITessAPI.TessPageSegMode.PSM_RAW_LINE)); // Try a tight number strip first, then a wider region for photos whose card is // not perfectly framed. Sharpened and adaptive-threshold variants help motion blur. BufferedImage numberStrip = cropByRatio(source, 0.16, 0.70, 0.82, 0.24); BufferedImage normalizedStrip = resizeAndGrayscale(numberStrip, NUMBER_REGION_TARGET_WIDTH); BufferedImage enhancedStrip = sharpen(stretchContrast(normalizedStrip)); images.add(new RecognitionImage(normalizedStrip, ITessAPI.TessPageSegMode.PSM_SINGLE_LINE)); images.add(new RecognitionImage(enhancedStrip, ITessAPI.TessPageSegMode.PSM_SINGLE_LINE)); images.add(new RecognitionImage( adaptiveBinarize(enhancedStrip), ITessAPI.TessPageSegMode.PSM_SINGLE_LINE)); BufferedImage numberRegion = cropByRatio(source, 0.10, 0.58, 0.88, 0.38); BufferedImage enhancedNumberRegion = sharpen(stretchContrast( resizeAndGrayscale(numberRegion, NUMBER_REGION_TARGET_WIDTH))); images.add(new RecognitionImage( enhancedNumberRegion, ITessAPI.TessPageSegMode.PSM_SPARSE_TEXT)); images.add(new RecognitionImage( binarize(enhancedNumberRegion), ITessAPI.TessPageSegMode.PSM_SPARSE_TEXT)); BufferedImage lowerHalf = cropByRatio(source, 0, 0.48, 1, 0.52); images.add(new RecognitionImage( resizeAndGrayscale(lowerHalf, FULL_IMAGE_TARGET_WIDTH), ITessAPI.TessPageSegMode.PSM_SPARSE_TEXT)); images.add(new RecognitionImage( resizeAndGrayscale(source, FULL_IMAGE_TARGET_WIDTH), ITessAPI.TessPageSegMode.PSM_SPARSE_TEXT)); return images; } private BufferedImage cropByRatio(BufferedImage source, double xRatio, double yRatio, double widthRatio, double heightRatio) { int x = Math.max(0, (int) Math.round(source.getWidth() * xRatio)); int y = Math.max(0, (int) Math.round(source.getHeight() * yRatio)); int width = Math.min(source.getWidth() - x, (int) Math.round(source.getWidth() * widthRatio)); int height = Math.min(source.getHeight() - y, (int) Math.round(source.getHeight() * heightRatio)); return source.getSubimage(x, y, Math.max(1, width), Math.max(1, height)); } private BufferedImage resizeAndGrayscale(BufferedImage source, int targetWidth) { int width = normalizedWidth(source.getWidth(), targetWidth); int height = Math.max(1, (int) Math.round(source.getHeight() * (double) width / source.getWidth())); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); Graphics2D graphics = image.createGraphics(); graphics.setColor(Color.WHITE); graphics.fillRect(0, 0, width, height); graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); graphics.drawImage(source, 0, 0, width, height, null); graphics.dispose(); return image; } private BufferedImage stretchContrast(BufferedImage grayscale) { int[] histogram = histogram(grayscale); int pixelCount = grayscale.getWidth() * grayscale.getHeight(); int low = percentile(histogram, pixelCount, 0.02); int high = percentile(histogram, pixelCount, 0.98); if (high - low < 32) { return grayscale; } BufferedImage stretched = new BufferedImage( grayscale.getWidth(), grayscale.getHeight(), BufferedImage.TYPE_BYTE_GRAY); for (int y = 0; y < grayscale.getHeight(); y++) { for (int x = 0; x < grayscale.getWidth(); x++) { int value = grayscale.getRaster().getSample(x, y, 0); int adjusted = Math.max(0, Math.min(255, (value - low) * 255 / (high - low))); stretched.getRaster().setSample(x, y, 0, adjusted); } } return stretched; } private int percentile(int[] histogram, int pixelCount, double percentile) { int target = (int) Math.round(pixelCount * percentile); int count = 0; for (int i = 0; i < histogram.length; i++) { count += histogram[i]; if (count >= target) { return i; } } return 255; } private BufferedImage sharpen(BufferedImage grayscale) { float[] kernel = { 0, -0.2f, 0, -0.2f, 1.8f, -0.2f, 0, -0.2f, 0 }; ConvolveOp operation = new ConvolveOp( new Kernel(3, 3, kernel), ConvolveOp.EDGE_NO_OP, null); return operation.filter(grayscale, null); } private int normalizedWidth(int sourceWidth, int targetWidth) { if (sourceWidth < 1200) { return Math.min(targetWidth, sourceWidth * 2); } return Math.min(sourceWidth, targetWidth); } private BufferedImage binarize(BufferedImage grayscale) { int[] histogram = histogram(grayscale); int threshold = otsuThreshold(histogram, grayscale.getWidth() * grayscale.getHeight()); BufferedImage binary = new BufferedImage( grayscale.getWidth(), grayscale.getHeight(), BufferedImage.TYPE_BYTE_BINARY); for (int y = 0; y < grayscale.getHeight(); y++) { for (int x = 0; x < grayscale.getWidth(); x++) { int value = grayscale.getRaster().getSample(x, y, 0) <= threshold ? 0 : 1; binary.getRaster().setSample(x, y, 0, value); } } return binary; } private int[] histogram(BufferedImage grayscale) { int[] histogram = new int[256]; for (int y = 0; y < grayscale.getHeight(); y++) { for (int x = 0; x < grayscale.getWidth(); x++) { histogram[grayscale.getRaster().getSample(x, y, 0)]++; } } return histogram; } private BufferedImage adaptiveBinarize(BufferedImage grayscale) { int width = grayscale.getWidth(); int height = grayscale.getHeight(); long[] integral = new long[(width + 1) * (height + 1)]; for (int y = 1; y <= height; y++) { long rowSum = 0; for (int x = 1; x <= width; x++) { rowSum += grayscale.getRaster().getSample(x - 1, y - 1, 0); integral[y * (width + 1) + x] = integral[(y - 1) * (width + 1) + x] + rowSum; } } BufferedImage binary = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY); int radius = Math.max(12, Math.min(width, height) / 30); for (int y = 0; y < height; y++) { int top = Math.max(0, y - radius); int bottom = Math.min(height - 1, y + radius); for (int x = 0; x < width; x++) { int left = Math.max(0, x - radius); int right = Math.min(width - 1, x + radius); long sum = rectangleSum(integral, width + 1, left, top, right, bottom); int area = (right - left + 1) * (bottom - top + 1); int value = grayscale.getRaster().getSample(x, y, 0); binary.getRaster().setSample(x, y, 0, value < sum / area - 8 ? 0 : 1); } } return binary; } private long rectangleSum(long[] integral, int stride, int x1, int y1, int right, int bottom) { int x2 = right + 1; int y2 = bottom + 1; return integral[y2 * stride + x2] - integral[y1 * stride + x2] - integral[y2 * stride + x1] + integral[y1 * stride + x1]; } private int otsuThreshold(int[] histogram, int pixelCount) { long totalIntensity = 0; for (int i = 0; i < histogram.length; i++) { totalIntensity += (long) i * histogram[i]; } long backgroundIntensity = 0; int backgroundCount = 0; double maxVariance = -1; int threshold = 127; for (int i = 0; i < histogram.length; i++) { backgroundCount += histogram[i]; if (backgroundCount == 0) { continue; } int foregroundCount = pixelCount - backgroundCount; if (foregroundCount == 0) { break; } backgroundIntensity += (long) i * histogram[i]; double backgroundMean = (double) backgroundIntensity / backgroundCount; double foregroundMean = (double) (totalIntensity - backgroundIntensity) / foregroundCount; double meanDifference = backgroundMean - foregroundMean; double variance = (double) backgroundCount * foregroundCount * meanDifference * meanDifference; if (variance > maxVariance) { maxVariance = variance; threshold = i; } } return threshold; } private String recognize(BufferedImage image, int pageSegMode) throws TesseractException { Tesseract tesseract = new Tesseract(); if (StringUtils.isNotBlank(tessDataPath)) { tesseract.setDatapath(tessDataPath); } tesseract.setLanguage(language); tesseract.setPageSegMode(pageSegMode); tesseract.setVariable("user_defined_dpi", "300"); tesseract.setVariable("tessedit_char_whitelist", "0123456789Xx"); return tesseract.doOCR(image); } private record RecognitionImage(BufferedImage image, int pageSegMode) { } private String extractValidIdCardNumber(String text) { if (StringUtils.isBlank(text)) { return null; } // Process each OCR line independently so unrelated numeric fields cannot be // concatenated; punctuation and spaces inside a number are removed below. for (String line : text.split("\\R")) { String candidate = findValidIdCardNumber(line); if (candidate != null) { return candidate; } } return null; } private String findValidIdCardNumber(String text) { String digits = text.replaceAll("[^0-9Xx]", "").toUpperCase(Locale.ROOT); int length = 18; if (digits.length() < length) { return null; } for (int start = 0; start <= digits.length() - length; start++) { String candidate = digits.substring(start, start + length); if (IdentificationCodeUtil.isIdentityCode(candidate)) { return candidate; } } return null; } } |
IdentificationCodeUtil.java
|
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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; public class IdentificationCodeUtil { static final int IDENTITYCODE_OLD = 15; // 老身份证15位 static final int IDENTITYCODE_NEW = 18; // 新身份证18位 static final int[] Wi = new int[17]; /** * 判断身份证号码是否正确。 * * @param code * 身份证号码。 * @return 如果身份证号码正确,则返回true,否则返回false。 */ public static boolean isIdentityCode(String code) { if (code == null || "".equals(code.trim())) { return false; } code = code.trim(); // 长度只有15和18两种情况 if ((code.length() != IDENTITYCODE_OLD) && (code.length() != IDENTITYCODE_NEW)) { return false; } // 身份证号码必须为数字(18位的新身份证最后一位可以是x) Pattern pt = Pattern.compile("\\d{15,17}([\\dxX]{1})?"); Matcher mt = pt.matcher(code); if (!mt.find()) { return false; } String birthDay = ""; // 验证生日 if (code.length() == IDENTITYCODE_OLD) { birthDay = "19" + code.substring(6, 12); } else { birthDay = code.substring(6, 14); } try { new SimpleDateFormat("yyyyMMdd").parse(birthDay); } catch (ParseException e) { return false; } // if (!TimeUtil.isRightDate(birthDay, "yyyyMMdd")) { // return false; // } // 最后一位校验码验证 if (code.length() == IDENTITYCODE_NEW) { String lastNum = getCheckFlag(code.substring(0, IDENTITYCODE_NEW - 1)); // check last digit if (!("" + code.charAt(IDENTITYCODE_NEW - 1)).toUpperCase(Locale.getDefault()).equals( lastNum)) { return false; } } return true; } /** * 获取新身份证的最后一位:检验位 * * @param code * 18位身份证的前17位 * @return 新身份证的最后一位 */ private static String getCheckFlag(String code) { int[] varArray = new int[code.length()]; String lastNum = ""; int numSum = 0; // 初始化位权值 setWiBuffer(); for (int i = 0; i < code.length(); i++) { varArray[i] = Integer.parseInt("" + code.charAt(i)); varArray[i] = varArray[i] * Wi[i]; numSum = numSum + varArray[i]; } int checkDigit = 12 - numSum % 11; switch (checkDigit) { case 10: lastNum = "X"; break; case 11: lastNum = "0"; break; case 12: lastNum = "1"; break; default: lastNum = String.valueOf(checkDigit); } return lastNum; } /** * 初始化位权值 */ private static void setWiBuffer() { for (int i = 0; i < Wi.length; i++) { int k = (int) Math.pow(2, (Wi.length - i)); Wi[i] = k % 11; } } /** * 判别是否字符串为null或者没有内容,或者全部为空格。 */ public static boolean empty(String o) { return ((null == o) || (o.length() <= 0) || ("".equals(o.trim()))); } /** * 将15位身份证号码升级为18位身份证号码 * * @param code * 15位身份证号码 * @return 18位身份证号码 */ public static String update2eighteen(String code) { if (code == null || "".equals(code.trim())) { return ""; } code = code.trim(); if (code.length() != IDENTITYCODE_OLD || !isIdentityCode(code)) { return ""; } code = code.substring(0, 6) + "19" + code.substring(6); // code = code + getCheckFlag(code); return code; } /** * 还原15位身份证号码 * @param code * @return */ public static String resume2fifteen(String code){ if (code == null || "".equals(code.trim())) { return ""; } code = code.trim(); // if (code.length() != IDENTITYCODE_NEW || !isIdentityCode(code)) { // return ""; // } if (code.length() != IDENTITYCODE_NEW ) { return ""; } StringBuilder codebuffer = new StringBuilder(code); codebuffer.delete(6, 8); codebuffer.deleteCharAt(codebuffer.length() -1 ); return codebuffer.toString(); } } |
ApiResultException.java
|
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 |
public class ApiResultException extends RuntimeException { /** * 异常信息 */ private String errorMsg; /** * 错误码 */ private Integer code; public String getErrorMsg() { return errorMsg; } public Integer getCode() { return code; } public ApiResultException(Integer code, String errorMsg) { super(errorMsg); this.code = code; this.errorMsg = errorMsg; } public ApiResultException(String errorMsg) { super(errorMsg); this.code = 1001; this.errorMsg = errorMsg; } public ApiResultException() { } } |
|
1 2 3 4 5 6 7 8 |
@Operation(summary = "从身份证图片中识别身份证号", parameters = { @Parameter(name = "imageUrl", description = "身份证人像面图片地址", in = ParameterIn.QUERY, required = true) }) @PostMapping("/recognizeIdCard") @RequiresAuthentication public ResponseDto<String> recognizeIdCard(@RequestParam("imageUrl") String imageUrl) { return ResponseDto.success(idCardOcrService.recognizeIdCardNumber(imageUrl)); } |
|
1 2 3 4 5 6 7 |
tesseract --version tesseract 5.5.3 leptonica-1.87.0 libgif 5.2.2 : libjpeg 8d (libjpeg-turbo 3.1.3) : libpng 1.6.58 : libtiff 4.7.2 : zlib 1.2.12 : libwebp 1.6.0 : libopenjp2 2.5.4 Found NEON Found libarchive 3.8.9 zlib/1.2.12 liblzma/5.8.3 bz2lib/1.0.8 liblz4/1.10.0 libzstd/1.5.7 expat/expat_2.7.4 CommonCrypto/system libb2/system Found libcurl/8.7.1 SecureTransport (LibreSSL/3.3.6) zlib/1.2.12 nghttp2/1.68.1 |
参考资料:
https://www.cnblogs.com/linuxAndMcu/p/19049953