|
|
|
@ -213,7 +213,10 @@ public class AsyncProcessingService {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* 异步处理多图片二维码识别
|
|
|
|
* 异步处理多图片二维码识别
|
|
|
|
* 所有图片识别完成后统一发送结果
|
|
|
|
* 优化流程:
|
|
|
|
|
|
|
|
* 1. 所有图片先做 ONNX 检测,收集全部候选框
|
|
|
|
|
|
|
|
* 2. 合并后 NMS 过滤近距框
|
|
|
|
|
|
|
|
* 3. 每个框依次尝试不同图片的裁剪解码,一张成功即跳过
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@Async
|
|
|
|
@Async
|
|
|
|
public void processMultiQrCodeAsync(Integer cameraId, String taskId, List<String> imagePaths,
|
|
|
|
public void processMultiQrCodeAsync(Integer cameraId, String taskId, List<String> imagePaths,
|
|
|
|
@ -222,71 +225,107 @@ public class AsyncProcessingService {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
log.info("异步多图片识别任务开始,图片数量: {}", imagePaths.size());
|
|
|
|
log.info("异步多图片识别任务开始,图片数量: {}", imagePaths.size());
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有图片的二维码结果
|
|
|
|
// ====== Phase 1: 批量检测,收集所有候选框 ======
|
|
|
|
List<String> allQrCodeResults = new ArrayList<>();
|
|
|
|
List<BoundingBox> allDetections = new ArrayList<>();
|
|
|
|
List<String> allImagePaths = new ArrayList<>();
|
|
|
|
// 缓存所有图片的 BufferedImage,避免重复读取
|
|
|
|
|
|
|
|
List<BufferedImage> cachedImages = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < imagePaths.size(); i++) {
|
|
|
|
for (int i = 0; i < imagePaths.size(); i++) {
|
|
|
|
String path = imagePaths.get(i);
|
|
|
|
String path = imagePaths.get(i);
|
|
|
|
String url = imageUrls.get(i);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
log.info("开始识别第{}/{}张图片: {}", i + 1, imagePaths.size(), path);
|
|
|
|
log.info("Phase1 - 检测第{}/{}张图片: {}", i + 1, imagePaths.size(), path);
|
|
|
|
|
|
|
|
|
|
|
|
List<BoundingBox> detectResults = onnxServiceNew.detect26(path, "qrCode");
|
|
|
|
List<BoundingBox> detectResults = onnxServiceNew.detect26(path, "qrCode");
|
|
|
|
log.info("第{}张图片检测结果: {}", i + 1, detectResults);
|
|
|
|
log.info("检测结果: {} 个框", detectResults.size());
|
|
|
|
|
|
|
|
allDetections.addAll(detectResults);
|
|
|
|
// 读取原始图片
|
|
|
|
|
|
|
|
BufferedImage originalImage = ImageIO.read(new File(path));
|
|
|
|
// 预加载图片到内存
|
|
|
|
if (originalImage == null) {
|
|
|
|
BufferedImage image = ImageIO.read(new File(path));
|
|
|
|
log.warn("无法读取图片: {}", path);
|
|
|
|
if (image != null) {
|
|
|
|
continue;
|
|
|
|
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<String> qrCodeResults = new ArrayList<>();
|
|
|
|
// ====== Phase 2: NMS 过滤重叠框 ======
|
|
|
|
List<BoundingBox> validBoxes = new ArrayList<>();
|
|
|
|
List<BoundingBox> filteredBoxes = ONNXServiceNew.nonMaxSuppression(allDetections, 0.5f);
|
|
|
|
|
|
|
|
log.info("Phase2 NMS完成: {} -> {} 个框", allDetections.size(), filteredBoxes.size());
|
|
|
|
for (int j = 0; j < detectResults.size(); j++) {
|
|
|
|
|
|
|
|
BoundingBox box = detectResults.get(j);
|
|
|
|
// ====== Phase 3: 逐框跨图解码 ======
|
|
|
|
try {
|
|
|
|
List<String> allQrCodeResults = new ArrayList<>();
|
|
|
|
BufferedImage croppedImage = cropBoundingBox(originalImage, box, 30);
|
|
|
|
List<BoundingBox> validBoxes = new ArrayList<>();
|
|
|
|
|
|
|
|
// 用第一张图片的 url 作为标注图
|
|
|
|
// 保存裁剪的原始图片用于调试
|
|
|
|
String annotatedImageUrl = imageUrls.get(0) + ".jpg";
|
|
|
|
String debugPath = path + "_crop_" + i + "_" + j + ".jpg";
|
|
|
|
|
|
|
|
ImageIO.write(croppedImage, "jpg", new File(debugPath));
|
|
|
|
for (int boxIdx = 0; boxIdx < filteredBoxes.size(); boxIdx++) {
|
|
|
|
log.info("DEBUG: 裁剪图片保存到: {}, 尺寸: {}x{}, 置信度: {}",
|
|
|
|
BoundingBox box = filteredBoxes.get(boxIdx);
|
|
|
|
debugPath, croppedImage.getWidth(), croppedImage.getHeight(), box.getConfidence());
|
|
|
|
boolean decoded = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 最小尺寸放大
|
|
|
|
// 遍历每张图片尝试解码当前框
|
|
|
|
int minSize = 320;
|
|
|
|
for (int imgIdx = 0; imgIdx < cachedImages.size(); imgIdx++) {
|
|
|
|
if (croppedImage.getWidth() < minSize || croppedImage.getHeight() < minSize) {
|
|
|
|
BufferedImage image = cachedImages.get(imgIdx);
|
|
|
|
int newWidth = Math.max(croppedImage.getWidth(), minSize);
|
|
|
|
if (image == null) continue;
|
|
|
|
int newHeight = Math.max(croppedImage.getHeight(), minSize);
|
|
|
|
|
|
|
|
croppedImage = resizeImage(croppedImage, newWidth, newHeight);
|
|
|
|
try {
|
|
|
|
log.info("DEBUG: 图片已放大到: {}x{}", newWidth, newHeight);
|
|
|
|
BufferedImage cropped = cropBoundingBox(image, box, 30);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存裁剪图用于调试(仅首张成功前)
|
|
|
|
// 解码
|
|
|
|
if (!decoded) {
|
|
|
|
String qrCodeContent = decodeQrCodeWithRetry(croppedImage, box.getConfidence(), 2);
|
|
|
|
String debugPath = imagePaths.get(imgIdx) + "_crop_box" + boxIdx + ".jpg";
|
|
|
|
|
|
|
|
ImageIO.write(cropped, "jpg", new File(debugPath));
|
|
|
|
if (qrCodeContent != null) {
|
|
|
|
log.debug("裁剪图保存: {} (来源: 图{})", debugPath, imgIdx + 1);
|
|
|
|
qrCodeResults.add(qrCodeContent);
|
|
|
|
|
|
|
|
validBoxes.add(box);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
|
|
log.error("第{}张图片解码异常,box: {}", i + 1, box, e);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制检测框
|
|
|
|
// 最小尺寸放大
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 收集结果
|
|
|
|
if (qrCodeContent != null) {
|
|
|
|
allQrCodeResults.addAll(qrCodeResults);
|
|
|
|
log.info("框{} 解码成功 (来源图{}): {}", boxIdx, imgIdx + 1, qrCodeContent);
|
|
|
|
allImagePaths.add(url + ".jpg");
|
|
|
|
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) {
|
|
|
|
} catch (Exception e) {
|
|
|
|
log.error("第{}张图片处理异常: {}", i + 1, path, e);
|
|
|
|
log.warn("绘制检测框失败", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -301,18 +340,25 @@ public class AsyncProcessingService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送HTTP通知
|
|
|
|
// 发送HTTP通知
|
|
|
|
if (targetIp != null && targetPort != null && targetPath != null) {
|
|
|
|
sendQrCodeResult(targetIp, targetPort, targetPath,
|
|
|
|
httpNotifyService.sendQrCodeResult(targetIp, targetPort, targetPath,
|
|
|
|
taskId, result, annotatedImageUrl, allQrCodeResults, scanType);
|
|
|
|
taskId, result, String.join(",", allImagePaths), allQrCodeResults, scanType);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log.warn("多图片异步任务完成,但未配置目标服务器信息");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
} catch (Exception e) {
|
|
|
|
log.error("多图片异步任务执行异常", e);
|
|
|
|
log.error("多图片异步任务执行异常", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void sendQrCodeResult(String targetIp, Integer targetPort, String targetPath,
|
|
|
|
|
|
|
|
String taskId, String result, String imagePath,
|
|
|
|
|
|
|
|
List<String> 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) {
|
|
|
|
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";
|
|
|
|
String path = "D:\\data\\media\\2026-05-15\\1\\2026-05-15-00-07-20-909.png_3.jpg";
|
|
|
|
BufferedImage croppedImage = null;
|
|
|
|
BufferedImage croppedImage = null;
|
|
|
|
@ -405,67 +451,67 @@ public class AsyncProcessingService {
|
|
|
|
if (result != null) return result;
|
|
|
|
if (result != null) return result;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 尝试灰度化
|
|
|
|
// 2. 尝试灰度化
|
|
|
|
try {
|
|
|
|
// try {
|
|
|
|
log.info("DEBUG: 灰度化开始, image类型={}, 尺寸={}x{}", image.getType(), image.getWidth(), image.getHeight());
|
|
|
|
// log.info("DEBUG: 灰度化开始, image类型={}, 尺寸={}x{}", image.getType(), image.getWidth(), image.getHeight());
|
|
|
|
Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
// Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
log.info("DEBUG: bufferedImageToMat完成, mat类型={}, channels={}, depth={}",
|
|
|
|
// log.info("DEBUG: bufferedImageToMat完成, mat类型={}, channels={}, depth={}",
|
|
|
|
mat.type(), mat.channels(), mat.depth());
|
|
|
|
// mat.type(), mat.channels(), mat.depth());
|
|
|
|
Mat gray = opencvService.toGrayscale(mat);
|
|
|
|
// Mat gray = opencvService.toGrayscale(mat);
|
|
|
|
log.info("DEBUG: toGrayscale完成, gray channels={}, depth={}", gray.channels(), gray.depth());
|
|
|
|
// log.info("DEBUG: toGrayscale完成, gray channels={}, depth={}", gray.channels(), gray.depth());
|
|
|
|
BufferedImage grayImage = opencvService.matToBufferedImage(gray);
|
|
|
|
// BufferedImage grayImage = opencvService.matToBufferedImage(gray);
|
|
|
|
log.info("DEBUG: matToBufferedImage完成, grayImage类型={}", grayImage.getType());
|
|
|
|
// log.info("DEBUG: matToBufferedImage完成, grayImage类型={}", grayImage.getType());
|
|
|
|
mat.release();
|
|
|
|
// mat.release();
|
|
|
|
gray.release();
|
|
|
|
// gray.release();
|
|
|
|
result = tryDecode(grayImage, confidence, retryIndex, "灰度");
|
|
|
|
// result = tryDecode(grayImage, confidence, retryIndex, "灰度");
|
|
|
|
if (result != null) return result;
|
|
|
|
// if (result != null) return result;
|
|
|
|
} catch (Exception e) {
|
|
|
|
// } catch (Exception e) {
|
|
|
|
log.error("灰度预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
// log.error("灰度预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
|
|
|
|
//
|
|
|
|
// 3. 尝试CLAHE增强
|
|
|
|
// // 3. 尝试CLAHE增强
|
|
|
|
try {
|
|
|
|
// try {
|
|
|
|
log.info("DEBUG: CLAHE开始");
|
|
|
|
// log.info("DEBUG: CLAHE开始");
|
|
|
|
Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
// Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
Mat clahe = opencvService.clahe(mat);
|
|
|
|
// Mat clahe = opencvService.clahe(mat);
|
|
|
|
log.info("DEBUG: clahe完成, clahe channels={}, depth={}", clahe.channels(), clahe.depth());
|
|
|
|
// log.info("DEBUG: clahe完成, clahe channels={}, depth={}", clahe.channels(), clahe.depth());
|
|
|
|
BufferedImage claheImage = opencvService.matToBufferedImage(clahe);
|
|
|
|
// BufferedImage claheImage = opencvService.matToBufferedImage(clahe);
|
|
|
|
mat.release();
|
|
|
|
// mat.release();
|
|
|
|
clahe.release();
|
|
|
|
// clahe.release();
|
|
|
|
result = tryDecode(claheImage, confidence, retryIndex, "CLAHE");
|
|
|
|
// result = tryDecode(claheImage, confidence, retryIndex, "CLAHE");
|
|
|
|
if (result != null) return result;
|
|
|
|
// if (result != null) return result;
|
|
|
|
} catch (Exception e) {
|
|
|
|
// } catch (Exception e) {
|
|
|
|
log.error("CLAHE预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
// log.error("CLAHE预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
|
|
|
|
//
|
|
|
|
// 4. 尝试二值化
|
|
|
|
// // 4. 尝试二值化
|
|
|
|
try {
|
|
|
|
// try {
|
|
|
|
log.info("DEBUG: 二值化开始");
|
|
|
|
// log.info("DEBUG: 二值化开始");
|
|
|
|
Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
// Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
Mat binary = opencvService.adaptiveThreshold(mat);
|
|
|
|
// Mat binary = opencvService.adaptiveThreshold(mat);
|
|
|
|
log.info("DEBUG: adaptiveThreshold完成, binary channels={}, depth={}", binary.channels(), binary.depth());
|
|
|
|
// log.info("DEBUG: adaptiveThreshold完成, binary channels={}, depth={}", binary.channels(), binary.depth());
|
|
|
|
BufferedImage binaryImage = opencvService.matToBufferedImage(binary);
|
|
|
|
// BufferedImage binaryImage = opencvService.matToBufferedImage(binary);
|
|
|
|
mat.release();
|
|
|
|
// mat.release();
|
|
|
|
binary.release();
|
|
|
|
// binary.release();
|
|
|
|
result = tryDecode(binaryImage, confidence, retryIndex, "二值化");
|
|
|
|
// result = tryDecode(binaryImage, confidence, retryIndex, "二值化");
|
|
|
|
if (result != null) return result;
|
|
|
|
// if (result != null) return result;
|
|
|
|
} catch (Exception e) {
|
|
|
|
// } catch (Exception e) {
|
|
|
|
log.error("二值化预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
// log.error("二值化预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
|
|
|
|
//
|
|
|
|
// 5. 尝试组合预处理(去噪+灰度+CLAHE+二值化)
|
|
|
|
// // 5. 尝试组合预处理(去噪+灰度+CLAHE+二值化)
|
|
|
|
try {
|
|
|
|
// try {
|
|
|
|
log.info("DEBUG: 组合预处理开始");
|
|
|
|
// log.info("DEBUG: 组合预处理开始");
|
|
|
|
Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
// Mat mat = opencvService.bufferedImageToMat(image);
|
|
|
|
Mat processed = opencvService.preprocessForQrCode(mat);
|
|
|
|
// Mat processed = opencvService.preprocessForQrCode(mat);
|
|
|
|
log.info("DEBUG: preprocessForQrCode完成, processed channels={}, depth={}", processed.channels(), processed.depth());
|
|
|
|
// log.info("DEBUG: preprocessForQrCode完成, processed channels={}, depth={}", processed.channels(), processed.depth());
|
|
|
|
BufferedImage processedImage = opencvService.matToBufferedImage(processed);
|
|
|
|
// BufferedImage processedImage = opencvService.matToBufferedImage(processed);
|
|
|
|
mat.release();
|
|
|
|
// mat.release();
|
|
|
|
processed.release();
|
|
|
|
// processed.release();
|
|
|
|
result = tryDecode(processedImage, confidence, retryIndex, "组合预处理");
|
|
|
|
// result = tryDecode(processedImage, confidence, retryIndex, "组合预处理");
|
|
|
|
if (result != null) return result;
|
|
|
|
// if (result != null) return result;
|
|
|
|
} catch (Exception e) {
|
|
|
|
// } catch (Exception e) {
|
|
|
|
log.error("组合预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
// log.error("组合预处理失败: type={}, msg={}", e.getClass().getName(), e.getMessage(), e);
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|