diff --git a/src/main/java/com/example/lxcameraapi/controller/CategoryController.java b/src/main/java/com/example/lxcameraapi/controller/CategoryController.java index dab8b49..ab1a89c 100644 --- a/src/main/java/com/example/lxcameraapi/controller/CategoryController.java +++ b/src/main/java/com/example/lxcameraapi/controller/CategoryController.java @@ -55,7 +55,7 @@ public class CategoryController { String path = appConfig.getPicPath() + formattedDate + "/" + request.getCameraId() + "/" + formattedTime + ".png"; - String url = appConfig.getPicUrl() + formattedDate + "/" + request.getCameraId() + "/" + formattedTime + ".png"; + String url = appConfig.getPicUrl() + formattedDate + "/" + request.getCameraId() + "/" + formattedTime + ".png"+"detect.jpg"; log.info("开始拍照,IP: {}, 路径: {}", request.getCameraId(), path); String ip = null; for (AppConfig.Camera camera : appConfig.getHikCamera()) { diff --git a/src/main/java/com/example/lxcameraapi/controller/QtCodeController.java b/src/main/java/com/example/lxcameraapi/controller/QtCodeController.java index 58cb11c..6dda3a6 100644 --- a/src/main/java/com/example/lxcameraapi/controller/QtCodeController.java +++ b/src/main/java/com/example/lxcameraapi/controller/QtCodeController.java @@ -283,7 +283,7 @@ public class QtCodeController { boxCountResponse.setSuccess(true); boxCountResponse.setResult("Processing"); - boxCountResponse.setImagePath(imageUrls.get(0)); + boxCountResponse.setImagePath(imageUrls.get(0)+ ".jpg"); boxCountResponse.setTaskId(request.getTaskId()); boxCountResponse.setScanType(request.getScanType()); boxCountResponse.setQrCodeResults(imageUrls); diff --git a/src/main/java/com/example/lxcameraapi/service/AsyncProcessingService.java b/src/main/java/com/example/lxcameraapi/service/AsyncProcessingService.java index a6fd13f..4d8bb02 100644 --- a/src/main/java/com/example/lxcameraapi/service/AsyncProcessingService.java +++ b/src/main/java/com/example/lxcameraapi/service/AsyncProcessingService.java @@ -213,7 +213,10 @@ public class AsyncProcessingService { /** * 异步处理多图片二维码识别 - * 所有图片识别完成后统一发送结果 + * 优化流程: + * 1. 所有图片先做 ONNX 检测,收集全部候选框 + * 2. 合并后 NMS 过滤近距框 + * 3. 每个框依次尝试不同图片的裁剪解码,一张成功即跳过 */ @Async public void processMultiQrCodeAsync(Integer cameraId, String taskId, List imagePaths, @@ -222,71 +225,107 @@ public class AsyncProcessingService { try { log.info("异步多图片识别任务开始,图片数量: {}", imagePaths.size()); - // 收集所有图片的二维码结果 - List allQrCodeResults = new ArrayList<>(); - List allImagePaths = new ArrayList<>(); + // ====== Phase 1: 批量检测,收集所有候选框 ====== + List allDetections = new ArrayList<>(); + // 缓存所有图片的 BufferedImage,避免重复读取 + List cachedImages = new ArrayList<>(); for (int i = 0; i < imagePaths.size(); i++) { String path = imagePaths.get(i); - String url = imageUrls.get(i); - try { - log.info("开始识别第{}/{}张图片: {}", i + 1, imagePaths.size(), path); - + log.info("Phase1 - 检测第{}/{}张图片: {}", i + 1, imagePaths.size(), path); List detectResults = onnxServiceNew.detect26(path, "qrCode"); - log.info("第{}张图片检测结果: {}", i + 1, detectResults); - - // 读取原始图片 - BufferedImage originalImage = ImageIO.read(new File(path)); - if (originalImage == null) { - log.warn("无法读取图片: {}", path); - continue; + log.info("检测结果: {} 个框", detectResults.size()); + allDetections.addAll(detectResults); + + // 预加载图片到内存 + BufferedImage image = ImageIO.read(new File(path)); + if (image != null) { + cachedImages.add(image); + } else { + log.warn("无法读取图片: {}, 用null占位", path); + cachedImages.add(null); } + } catch (Exception e) { + log.error("第{}张图片检测或加载异常: {}", i + 1, path, e); + cachedImages.add(null); + } + } + + log.info("Phase1 完成,共检测到 {} 个候选框", allDetections.size()); + + if (allDetections.isEmpty()) { + log.warn("所有图片均未检测到二维码候选框,发送 Unknown 结果"); + sendQrCodeResult(targetIp, targetPort, targetPath, taskId, "Unknown", + imageUrls.get(0) + ".jpg", new ArrayList<>(), scanType); + return; + } - List qrCodeResults = new ArrayList<>(); - List validBoxes = new ArrayList<>(); - - for (int j = 0; j < detectResults.size(); j++) { - BoundingBox box = detectResults.get(j); - try { - BufferedImage croppedImage = cropBoundingBox(originalImage, box, 30); - - // 保存裁剪的原始图片用于调试 - String debugPath = path + "_crop_" + i + "_" + j + ".jpg"; - ImageIO.write(croppedImage, "jpg", new File(debugPath)); - log.info("DEBUG: 裁剪图片保存到: {}, 尺寸: {}x{}, 置信度: {}", - debugPath, croppedImage.getWidth(), croppedImage.getHeight(), box.getConfidence()); - - // 最小尺寸放大 - int minSize = 320; - if (croppedImage.getWidth() < minSize || croppedImage.getHeight() < minSize) { - int newWidth = Math.max(croppedImage.getWidth(), minSize); - int newHeight = Math.max(croppedImage.getHeight(), minSize); - croppedImage = resizeImage(croppedImage, newWidth, newHeight); - log.info("DEBUG: 图片已放大到: {}x{}", newWidth, newHeight); - } - - // 解码 - String qrCodeContent = decodeQrCodeWithRetry(croppedImage, box.getConfidence(), 2); - - if (qrCodeContent != null) { - qrCodeResults.add(qrCodeContent); - validBoxes.add(box); - } - } catch (Exception e) { - log.error("第{}张图片解码异常,box: {}", i + 1, box, e); + // ====== Phase 2: NMS 过滤重叠框 ====== + List filteredBoxes = ONNXServiceNew.nonMaxSuppression(allDetections, 0.5f); + log.info("Phase2 NMS完成: {} -> {} 个框", allDetections.size(), filteredBoxes.size()); + + // ====== Phase 3: 逐框跨图解码 ====== + List allQrCodeResults = new ArrayList<>(); + List validBoxes = new ArrayList<>(); + // 用第一张图片的 url 作为标注图 + String annotatedImageUrl = imageUrls.get(0) + ".jpg"; + + for (int boxIdx = 0; boxIdx < filteredBoxes.size(); boxIdx++) { + BoundingBox box = filteredBoxes.get(boxIdx); + boolean decoded = false; + + // 遍历每张图片尝试解码当前框 + for (int imgIdx = 0; imgIdx < cachedImages.size(); imgIdx++) { + BufferedImage image = cachedImages.get(imgIdx); + if (image == null) continue; + + try { + BufferedImage cropped = cropBoundingBox(image, box, 30); + + // 保存裁剪图用于调试(仅首张成功前) + if (!decoded) { + String debugPath = imagePaths.get(imgIdx) + "_crop_box" + boxIdx + ".jpg"; + ImageIO.write(cropped, "jpg", new File(debugPath)); + log.debug("裁剪图保存: {} (来源: 图{})", debugPath, imgIdx + 1); } - } - // 绘制检测框 - opencvService.drawBoundingBoxesOnImage(validBoxes, path, path + ".jpg"); + // 最小尺寸放大 + int minSize = 320; + if (cropped.getWidth() < minSize || cropped.getHeight() < minSize) { + int newWidth = Math.max(cropped.getWidth(), minSize); + int newHeight = Math.max(cropped.getHeight(), minSize); + cropped = resizeImage(cropped, newWidth, newHeight); + } + + String qrCodeContent = decodeQrCodeWithRetry(cropped, box.getConfidence(), 2); - // 收集结果 - allQrCodeResults.addAll(qrCodeResults); - allImagePaths.add(url + ".jpg"); + if (qrCodeContent != null) { + log.info("框{} 解码成功 (来源图{}): {}", boxIdx, imgIdx + 1, qrCodeContent); + allQrCodeResults.add(qrCodeContent); + validBoxes.add(box); + box.setName(qrCodeContent); + decoded = true; + break; // 本框成功,试下一个框 + } + } catch (Exception e) { + log.warn("框{} 图{} 解码异常: {}", boxIdx, imgIdx + 1, e.getMessage()); + } + } + + if (!decoded) { + log.warn("框{} 所有图片均解码失败, 坐标: ({}, {}), 尺寸: {}x{}, 置信度: {}", + boxIdx, (int) box.getX(), (int) box.getY(), (int) box.getW(), (int) box.getH(), box.getConfidence()); + } + } + // 在第一张图片上绘制所有检测框 + if (!cachedImages.isEmpty() && cachedImages.get(0) != null) { + try { + String annotatePath = imagePaths.get(0); + opencvService.drawBoundingBoxesOnImage(validBoxes, annotatePath, annotatePath + ".jpg"); } catch (Exception e) { - log.error("第{}张图片处理异常: {}", i + 1, path, e); + log.warn("绘制检测框失败", e); } } @@ -301,18 +340,25 @@ public class AsyncProcessingService { } // 发送HTTP通知 - if (targetIp != null && targetPort != null && targetPath != null) { - httpNotifyService.sendQrCodeResult(targetIp, targetPort, targetPath, - taskId, result, String.join(",", allImagePaths), allQrCodeResults, scanType); - } else { - log.warn("多图片异步任务完成,但未配置目标服务器信息"); - } + sendQrCodeResult(targetIp, targetPort, targetPath, + taskId, result, annotatedImageUrl, allQrCodeResults, scanType); } catch (Exception e) { log.error("多图片异步任务执行异常", e); } } + private void sendQrCodeResult(String targetIp, Integer targetPort, String targetPath, + String taskId, String result, String imagePath, + List qrCodeResults, String scanType) { + if (targetIp != null && targetPort != null && targetPath != null) { + httpNotifyService.sendQrCodeResult(targetIp, targetPort, targetPath, + taskId, result, imagePath, qrCodeResults, scanType); + } else { + log.warn("多图片异步任务完成,但未配置目标服务器信息"); + } + } + public static void main(String[] args) { String path = "D:\\data\\media\\2026-05-15\\1\\2026-05-15-00-07-20-909.png_3.jpg"; BufferedImage croppedImage = null; @@ -405,67 +451,67 @@ public class AsyncProcessingService { if (result != null) return result; // 2. 尝试灰度化 - try { - log.info("DEBUG: 灰度化开始, image类型={}, 尺寸={}x{}", image.getType(), image.getWidth(), image.getHeight()); - Mat mat = opencvService.bufferedImageToMat(image); - log.info("DEBUG: bufferedImageToMat完成, mat类型={}, channels={}, depth={}", - mat.type(), mat.channels(), mat.depth()); - Mat gray = opencvService.toGrayscale(mat); - log.info("DEBUG: toGrayscale完成, gray channels={}, depth={}", gray.channels(), gray.depth()); - BufferedImage grayImage = opencvService.matToBufferedImage(gray); - log.info("DEBUG: matToBufferedImage完成, grayImage类型={}", grayImage.getType()); - mat.release(); - gray.release(); - result = tryDecode(grayImage, confidence, retryIndex, "灰度"); - if (result != null) return result; - } catch (Exception e) { - log.error("灰度预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); - } - - // 3. 尝试CLAHE增强 - try { - log.info("DEBUG: CLAHE开始"); - Mat mat = opencvService.bufferedImageToMat(image); - Mat clahe = opencvService.clahe(mat); - log.info("DEBUG: clahe完成, clahe channels={}, depth={}", clahe.channels(), clahe.depth()); - BufferedImage claheImage = opencvService.matToBufferedImage(clahe); - mat.release(); - clahe.release(); - result = tryDecode(claheImage, confidence, retryIndex, "CLAHE"); - if (result != null) return result; - } catch (Exception e) { - log.error("CLAHE预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); - } - - // 4. 尝试二值化 - try { - log.info("DEBUG: 二值化开始"); - Mat mat = opencvService.bufferedImageToMat(image); - Mat binary = opencvService.adaptiveThreshold(mat); - log.info("DEBUG: adaptiveThreshold完成, binary channels={}, depth={}", binary.channels(), binary.depth()); - BufferedImage binaryImage = opencvService.matToBufferedImage(binary); - mat.release(); - binary.release(); - result = tryDecode(binaryImage, confidence, retryIndex, "二值化"); - if (result != null) return result; - } catch (Exception e) { - log.error("二值化预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); - } - - // 5. 尝试组合预处理(去噪+灰度+CLAHE+二值化) - try { - log.info("DEBUG: 组合预处理开始"); - Mat mat = opencvService.bufferedImageToMat(image); - Mat processed = opencvService.preprocessForQrCode(mat); - log.info("DEBUG: preprocessForQrCode完成, processed channels={}, depth={}", processed.channels(), processed.depth()); - BufferedImage processedImage = opencvService.matToBufferedImage(processed); - mat.release(); - processed.release(); - result = tryDecode(processedImage, confidence, retryIndex, "组合预处理"); - if (result != null) return result; - } catch (Exception e) { - log.error("组合预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); - } +// try { +// log.info("DEBUG: 灰度化开始, image类型={}, 尺寸={}x{}", image.getType(), image.getWidth(), image.getHeight()); +// Mat mat = opencvService.bufferedImageToMat(image); +// log.info("DEBUG: bufferedImageToMat完成, mat类型={}, channels={}, depth={}", +// mat.type(), mat.channels(), mat.depth()); +// Mat gray = opencvService.toGrayscale(mat); +// log.info("DEBUG: toGrayscale完成, gray channels={}, depth={}", gray.channels(), gray.depth()); +// BufferedImage grayImage = opencvService.matToBufferedImage(gray); +// log.info("DEBUG: matToBufferedImage完成, grayImage类型={}", grayImage.getType()); +// mat.release(); +// gray.release(); +// result = tryDecode(grayImage, confidence, retryIndex, "灰度"); +// if (result != null) return result; +// } catch (Exception e) { +// log.error("灰度预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); +// } +// +// // 3. 尝试CLAHE增强 +// try { +// log.info("DEBUG: CLAHE开始"); +// Mat mat = opencvService.bufferedImageToMat(image); +// Mat clahe = opencvService.clahe(mat); +// log.info("DEBUG: clahe完成, clahe channels={}, depth={}", clahe.channels(), clahe.depth()); +// BufferedImage claheImage = opencvService.matToBufferedImage(clahe); +// mat.release(); +// clahe.release(); +// result = tryDecode(claheImage, confidence, retryIndex, "CLAHE"); +// if (result != null) return result; +// } catch (Exception e) { +// log.error("CLAHE预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); +// } +// +// // 4. 尝试二值化 +// try { +// log.info("DEBUG: 二值化开始"); +// Mat mat = opencvService.bufferedImageToMat(image); +// Mat binary = opencvService.adaptiveThreshold(mat); +// log.info("DEBUG: adaptiveThreshold完成, binary channels={}, depth={}", binary.channels(), binary.depth()); +// BufferedImage binaryImage = opencvService.matToBufferedImage(binary); +// mat.release(); +// binary.release(); +// result = tryDecode(binaryImage, confidence, retryIndex, "二值化"); +// if (result != null) return result; +// } catch (Exception e) { +// log.error("二值化预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); +// } +// +// // 5. 尝试组合预处理(去噪+灰度+CLAHE+二值化) +// try { +// log.info("DEBUG: 组合预处理开始"); +// Mat mat = opencvService.bufferedImageToMat(image); +// Mat processed = opencvService.preprocessForQrCode(mat); +// log.info("DEBUG: preprocessForQrCode完成, processed channels={}, depth={}", processed.channels(), processed.depth()); +// BufferedImage processedImage = opencvService.matToBufferedImage(processed); +// mat.release(); +// processed.release(); +// result = tryDecode(processedImage, confidence, retryIndex, "组合预处理"); +// if (result != null) return result; +// } catch (Exception e) { +// log.error("组合预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e); +// } return null; } diff --git a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/QrCode/WeChatDeCode.java b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/QrCode/WeChatDeCode.java index 77994a7..0fa27cd 100644 --- a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/QrCode/WeChatDeCode.java +++ b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/QrCode/WeChatDeCode.java @@ -21,49 +21,75 @@ import javax.imageio.ImageIO; import static org.bytedeco.opencv.global.opencv_imgcodecs.imread; import static org.opencv.core.CvType.CV_8UC3; -import org.bytedeco.javacpp.Loader; /** - * + * 微信二维码解码器(单例模式,模型只加载一次) */ public class WeChatDeCode { + // ============ 单例模型缓存 ============ + private static volatile WeChatQRCode instance; + + private static synchronized WeChatQRCode getWeChatQRCode() { + if (instance == null) { + String modelBasePath = "D:/ccd/wechatqrcode/"; + + // 优先使用 _2021nov 版本 + String detectPrototxt = findModel(modelBasePath, "detect_2021nov.prototxt", "detect.prototxt"); + String detectCaffemodel = findModel(modelBasePath, "detect_2021nov.caffemodel", "detect.caffemodel"); + String srPrototxt = findModel(modelBasePath, "sr_2021nov.prototxt", "sr.prototxt"); + String srCaffemodel = findModel(modelBasePath, "sr_2021nov.caffemodel", "sr.caffemodel"); + + validateModel(detectPrototxt, "detect prototxt"); + validateModel(detectCaffemodel, "detect caffemodel"); + validateModel(srPrototxt, "sr prototxt"); + validateModel(srCaffemodel, "sr caffemodel"); + + instance = new WeChatQRCode(detectPrototxt, detectCaffemodel, srPrototxt, srCaffemodel); + System.out.println("WeChatQRCode 模型加载成功(单例)"); + } + return instance; + } + + private static String findModel(String basePath, String preferred, String fallback) { + String path = basePath + preferred; + if (new File(path).exists()) return path; + return basePath + fallback; + } + + private static void validateModel(String path, String name) { + if (!new File(path).exists()) { + throw new RuntimeException("WeChatQRCode 模型文件不存在: " + name + " (" + path + ")"); + } + } + // ============ 单例模型缓存 end ============ + public static void main(String... args) { String imagePath = "D:\\data\\media\\2026-05-20\\1\\2026-05-20-12-20-05-578_1.png_crop_1_6.jpg"; Mat img = imread(imagePath); System.out.println(deCode(img)); - - //下载二维码到本地识别 -// String url=""; -// System.out.println(openPic(url)); } public static String openPic(String url) { - String savePath = "d:\\img"; String filename = UUID.randomUUID().toString() + ".jpg"; - String re="0"; + String re = "0"; try { dxz(url, savePath, filename); Mat img = imread(savePath + "\\" + filename); - re=deCode(img); - //如果识别失败那么尝试放大缩小图片尝试识别,提高准确率 - if(re.equals("0")) { + re = deCode(img); + if (re.equals("0")) { ys(savePath, filename); Mat img1 = imread(savePath + "\\1_" + filename); - re=deCode(img1); + re = deCode(img1); } - if(re.equals("0")) { + if (re.equals("0")) { fd(savePath, filename); Mat img2 = imread(savePath + "\\2_" + filename); - re=deCode(img2); + re = deCode(img2); } return re; - - - } catch (Exception e) { - // TODO Auto-generated catch block e.printStackTrace(); return "0"; } @@ -71,13 +97,10 @@ public class WeChatDeCode { private static void ys(String savePath, String filename) throws Exception { try { - // 读取原始图片 BufferedImage image = ImageIO.read(new FileInputStream(savePath + "\\" + filename)); System.out.println("Width: " + image.getWidth()); System.out.println("Height: " + image.getHeight()); - // 调整图片大小 - BufferedImage newImage = resizeImage(image, image.getWidth()*2, image.getHeight()*2); - // 图像缓冲区图片保存为图片文件(文件不存在会自动创建文件保存,文件存在会覆盖原文件保存) + BufferedImage newImage = resizeImage(image, image.getWidth() * 2, image.getHeight() * 2); ImageIO.write(newImage, "jpg", new File(savePath + "\\1_" + filename)); } catch (Exception e) { e.printStackTrace(); @@ -86,13 +109,10 @@ public class WeChatDeCode { private static void fd(String savePath, String filename) throws Exception { try { - // 读取原始图片 BufferedImage image = ImageIO.read(new FileInputStream(savePath + "\\" + filename)); System.out.println("Width: " + image.getWidth()); System.out.println("Height: " + image.getHeight()); - // 调整图片大小 BufferedImage newImage = resizeImage(image, 200, 200); - // 图像缓冲区图片保存为图片文件(文件不存在会自动创建文件保存,文件存在会覆盖原文件保存) ImageIO.write(newImage, "jpg", new File(savePath + "\\2_" + filename)); } catch (Exception e) { e.printStackTrace(); @@ -100,24 +120,17 @@ public class WeChatDeCode { } private static void dxz(String urlString, String savePath, String filename) throws Exception { - // 构造URL URL url = new URL(urlString); - // 打开连接 URLConnection con = url.openConnection(); - // 设置请求超时为20s con.setConnectTimeout(20 * 1000); - // 文件路径不存在 则创建 File sf = new File(savePath); if (!sf.exists()) { sf.mkdirs(); } - // jdk 1.7 新特性自动关闭 try (InputStream in = con.getInputStream(); OutputStream out = new FileOutputStream(sf.getPath() + "\\" + filename)) { - // 创建缓冲区 byte[] buff = new byte[1024]; int n; - // 开始读取 while ((n = in.read(buff)) >= 0) { out.write(buff, 0, n); } @@ -125,6 +138,7 @@ public class WeChatDeCode { e.printStackTrace(); } } + public static String deCode(BufferedImage croppedImage) { Mat img = bufferedImageToMat(croppedImage); try { @@ -140,146 +154,52 @@ public class WeChatDeCode { if (image == null) { return null; } - int width = image.getWidth(); int height = image.getHeight(); - - // 创建 Mat 对象 Mat mat = new Mat(height, width, CV_8UC3); - - // 获取像素数据 int[] pixels = new int[width * height]; image.getRGB(0, 0, width, height, pixels, 0, width); - - // 将像素数据复制到 Mat byte[] data = new byte[width * height * 3]; for (int i = 0; i < width * height; i++) { int pixel = pixels[i]; int r = (pixel >> 16) & 0xFF; int g = (pixel >> 8) & 0xFF; int b = pixel & 0xFF; - - // OpenCV 使用 BGR 格式 data[i * 3] = (byte) b; data[i * 3 + 1] = (byte) g; data[i * 3 + 2] = (byte) r; } - mat.data().put(data); return mat; } - private static String getModelPath(String filename) { - // 对于 detect 和 sr 文件,优先尝试 _2021nov 版本 - String altFilename = filename - .replace("detect.", "detect_2021nov.") - .replace("sr.", "sr_2021nov."); - - // 模型文件路径(不含中文) - String modelBasePath = "D:/ccd/wechatqrcode/"; - File altFile = new File(modelBasePath + altFilename); - File origFile = new File(modelBasePath + filename); - - if (altFile.exists()) { - System.out.println("Loading model: " + altFile.getAbsolutePath()); - return altFile.getAbsolutePath(); - } else if (origFile.exists()) { - System.out.println("Loading model: " + origFile.getAbsolutePath()); - return origFile.getAbsolutePath(); - } - - System.out.println("Model file not found: " + modelBasePath + altFilename); - return modelBasePath + altFilename; - } public static String deCode(Mat img) { - // 获取模型文件路径 - String detectPrototxt = getModelPath("detect_2021nov.prototxt"); - String detectCaffemodel = getModelPath("detect_2021nov.caffemodel"); - String srPrototxt = getModelPath("sr_2021nov.prototxt"); - String srCaffemodel = getModelPath("sr_2021nov.caffemodel"); - - // 验证所有文件都存在 - if (!new File(detectPrototxt).exists()) { - throw new RuntimeException("文件不存在: " + detectPrototxt); - } - if (!new File(detectCaffemodel).exists()) { - throw new RuntimeException("文件不存在: " + detectCaffemodel); - } - if (!new File(srPrototxt).exists()) { - throw new RuntimeException("文件不存在: " + srPrototxt); - } - if (!new File(srCaffemodel).exists()) { - throw new RuntimeException("文件不存在: " + srCaffemodel); - } - - System.out.println("=== 模型文件路径 ==="); - System.out.println("detectPrototxt: " + detectPrototxt + " (" + new File(detectPrototxt).length() + " bytes)"); - System.out.println("detectCaffemodel: " + detectCaffemodel + " (" + new File(detectCaffemodel).length() + " bytes)"); - System.out.println("srPrototxt: " + srPrototxt + " (" + new File(srPrototxt).length() + " bytes)"); - System.out.println("srCaffemodel: " + srCaffemodel + " (" + new File(srCaffemodel).length() + " bytes)"); - System.out.println("===================="); - - // 微信二维码对象,需要传入模型文件路径 - WeChatQRCode we = new WeChatQRCode(detectPrototxt, detectCaffemodel, srPrototxt, srCaffemodel); - - // 使用 MatVector 存储坐标点 + WeChatQRCode we = getWeChatQRCode(); org.bytedeco.opencv.opencv_core.MatVector points = new org.bytedeco.opencv.opencv_core.MatVector(); - - // 微信二维码引擎解码,返回的valList中存放的是解码后的数据,points中存放的是二维码4个角的坐标 StringVector stringVector = we.detectAndDecode(img, points); if (stringVector.empty()) { - System.out.println("未识别到二维码"); return "0"; } - // 获取第一个二维码内容,并去掉末尾的 null 字符 String qrContent = stringVector.get(0).getString(StandardCharsets.UTF_8); - // 去掉末尾的 null 字符 if (qrContent != null) { qrContent = qrContent.replaceAll("\\x00+$", ""); } - - // 输出所有识别到的二维码信息 - System.out.println("========== 识别结果汇总 =========="); - System.out.println("共识别到 " + stringVector.size() + " 个二维码"); - System.out.println("二维码 1: " + qrContent); - - // 输出第一个二维码四个角的坐标 - if (points.size() > 0) { - Mat pointMat = points.get(0); - System.out.println(" 坐标位置:"); - for (int j = 0; j < pointMat.rows(); j++) { - float x = pointMat.ptr(j).getFloat(0); - float y = pointMat.ptr(j).getFloat(4); - System.out.println(" 角" + (j + 1) + ": (" + x + ", " + y + ")"); - } - } - System.out.println("=================================="); - return qrContent; } /** * 调整图片大小 - * @param originalImage 原始图片 - * @param targetWidth 目标宽度 - * @param targetHeight 目标高度 - * @return 调整大小后的图片 */ private static BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) { BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); java.awt.Graphics2D g2d = resizedImage.createGraphics(); - - // 设置渲染质量 g2d.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, java.awt.RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON); - - // 绘制缩放后的图片 g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null); g2d.dispose(); - return resizedImage; } diff --git a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java index 312446c..af4c087 100644 --- a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java +++ b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java @@ -31,6 +31,12 @@ public class ImageCaptureService { // SDK初始化状态 private boolean isSdkInitialized = false; + // 相机重连最大重试次数 + private static final int RECONNECT_MAX_RETRIES = 3; + + // 相机重连间隔(ms) + private static final int RECONNECT_INTERVAL = 3000; + @Resource private AppConfig appConfig; /** @@ -242,6 +248,17 @@ public class ImageCaptureService { * @return 是否成功 */ public boolean captureImage(String ip, String savePath) { + return captureImage(ip, savePath, false); + } + + /** + * 根据IP拍照并保存 + * @param ip 相机IP + * @param savePath 保存路径 + * @param isRetry 是否为重试(避免无限递归) + * @return 是否成功 + */ + private boolean captureImage(String ip, String savePath, boolean isRetry) { if (!isSdkInitialized) { log.error("SDK未初始化"); return false; @@ -251,6 +268,13 @@ public class ImageCaptureService { Handle hCamera = handleMap.get(ip); if (hCamera == null) { log.error("相机 {} 未初始化,请先调用init方法", ip); + // 未初始化时也尝试重连 + if (!isRetry) { + Handle newHandle = reconnectCamera(ip); + if (newHandle != null) { + return captureImage(ip, savePath, true); + } + } return false; } @@ -262,6 +286,12 @@ public class ImageCaptureService { int nRet = MvCameraControl.MV_CC_GetIntValue(hCamera, "PayloadSize", stParam); if (MV_OK != nRet) { log.error("获取PayloadSize失败,errcode: [0x{}]", Integer.toHexString(nRet)); + if (!isRetry) { + Handle newHandle = reconnectCamera(ip); + if (newHandle != null) { + return captureImage(ip, savePath, true); + } + } return false; } @@ -272,6 +302,12 @@ public class ImageCaptureService { if (MV_OK != nRet) { log.error("获取图像失败,errcode: [0x{}]", Integer.toHexString(nRet)); + if (!isRetry) { + Handle newHandle = reconnectCamera(ip); + if (newHandle != null) { + return captureImage(ip, savePath, true); + } + } return false; } @@ -283,6 +319,12 @@ public class ImageCaptureService { } catch (Exception e) { log.error("拍照异常", e); + if (!isRetry) { + Handle newHandle = reconnectCamera(ip); + if (newHandle != null) { + return captureImage(ip, savePath, true); + } + } return false; } } @@ -405,6 +447,69 @@ public class ImageCaptureService { return handleMap.containsKey(ip); } + /** + * 销毁单个相机的句柄并从map中移除 + */ + private void destroyCameraHandle(String ip) { + Handle hCamera = handleMap.remove(ip); + if (hCamera == null) return; + try { + MvCameraControl.MV_CC_StopGrabbing(hCamera); + } catch (Exception e) { + log.warn("相机 {} 停止抓图异常", ip, e); + } + try { + MvCameraControl.MV_CC_CloseDevice(hCamera); + } catch (Exception e) { + log.warn("相机 {} 关闭设备异常", ip, e); + } + try { + MvCameraControl.MV_CC_DestroyHandle(hCamera); + } catch (Exception e) { + log.warn("相机 {} 销毁句柄异常", ip, e); + } + log.info("相机 {} 句柄已销毁", ip); + } + + /** + * 重连相机,带重试 + * @return 新句柄,失败返回null + */ + private Handle reconnectCamera(String ip) { + log.warn("相机 {} 开始重连...", ip); + destroyCameraHandle(ip); + + for (int i = 1; i <= RECONNECT_MAX_RETRIES; i++) { + try { + // 重新枚举设备 + ArrayList stDeviceList = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE); + if (stDeviceList == null || stDeviceList.isEmpty()) { + log.warn("相机 {} 重连失败(第{}/{}次),未枚举到设备", ip, i, RECONNECT_MAX_RETRIES); + } else { + Handle newHandle = initSingleCamera(ip, stDeviceList); + if (newHandle != null) { + log.info("相机 {} 重连成功(第{}次尝试)", ip, i); + return newHandle; + } + log.warn("相机 {} 重连失败(第{}/{}次),初始化句柄失败", ip, i, RECONNECT_MAX_RETRIES); + } + } catch (Exception e) { + log.warn("相机 {} 重连异常(第{}/{}次)", ip, i, RECONNECT_MAX_RETRIES, e); + } + + if (i < RECONNECT_MAX_RETRIES) { + try { + Thread.sleep(RECONNECT_INTERVAL); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + log.error("相机 {} 重连失败,已达最大重试次数 {}", ip, RECONNECT_MAX_RETRIES); + return null; + } + /** * 连续拍照 * @param ip 相机IP diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3067d1d..976fac1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,7 +52,7 @@ deleteFile: - "D://go/deleteFile" day: 7 picPath: "D:/data/media/" -picUrl: "http://127.0.0.1:9012/pic/" +picUrl: "http://192.168.100.110:9012/pic/" serve: ip: 127.0.0.1 @@ -76,6 +76,6 @@ yoloModelConfig: # 图片大小 imgSize: 2048 # 置信度 - confThreshold: 0.90 + confThreshold: 0.9 names: ['code']