diff --git a/pom.xml b/pom.xml index 869a8be..9a291a9 100644 --- a/pom.xml +++ b/pom.xml @@ -115,11 +115,11 @@ lombok 1.18.32 - + org.springdoc - springdoc-openapi-ui - 1.6.15 + springdoc-openapi-starter-webmvc-ui + 2.3.0 diff --git a/src/main/java/com/example/lxcameraapi/controller/CategoryController.java b/src/main/java/com/example/lxcameraapi/controller/CategoryController.java index ab1a89c..119954f 100644 --- a/src/main/java/com/example/lxcameraapi/controller/CategoryController.java +++ b/src/main/java/com/example/lxcameraapi/controller/CategoryController.java @@ -7,12 +7,17 @@ import com.example.lxcameraapi.service.IndustrialCamera.algorithm.ONNXServiceNew import com.example.lxcameraapi.service.IndustrialCamera.camera.hik.ImageCaptureService; import com.example.lxcameraapi.service.IndustrialCamera.camera.lx.config.BoxCountRequest; import com.example.lxcameraapi.service.IndustrialCamera.camera.lx.config.BoxCountResponse; +import com.example.lxcameraapi.service.IndustrialCamera.camera.lx.config.CaptureResult; import com.example.lxcameraapi.service.IndustrialCamera.opencv.OpencvService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -36,6 +41,91 @@ public class CategoryController { @Resource private AsyncProcessingService asyncProcessingService; + @Resource + private LxCameraController lxCameraService; + + + + /** + * TCP触发连拍 - 连接TCP服务端,发送start,返回!=NoRead时连拍10次(hik + lx) + */ + @PostMapping("/main") + public BoxCountResponse main() { + + BoxCountResponse boxCountResponse = new BoxCountResponse(); + Integer cameraId = 1; + String tcpIp = "127.0.0.1"; + Integer tcpPort = 8080; + int photoCount = 10; + + // 从配置获取hik相机IP + String cameraIp = null; + for (AppConfig.Camera camera : appConfig.getHikCamera()) { + if (camera.getId().equals(cameraId)) { + cameraIp = camera.getIp(); + break; + } + } + + try (Socket socket = new Socket(tcpIp, tcpPort)) { + log.info("已连接TCP服务端 {}:{}", tcpIp, tcpPort); + + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + // 发送 "start" + out.println("start"); + log.info("已发送: start"); + + + // 读取返回 + String response = in.readLine(); + log.info("收到返回: {}", response); + + if (response != null && !"NoRead".equals(response.trim())) { + log.info("返回不是NoRead,开始连拍{}次", photoCount); + + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String formattedDate = now.format(dateFormatter); + String basePath = appConfig.getPicPath() + formattedDate + "/" + cameraId + "/"; + + for (int i = 0; i < photoCount; i++) { + try { + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-SSS"); + String formattedTime = LocalDateTime.now().format(timeFormatter); + + // Hik相机拍照 + String hikPath = basePath + formattedTime + "_hik.png"; + boolean success = imageCaptureService.captureImage(cameraIp, hikPath); + log.info("Hik拍照 {}/{}: {} -> {}", i + 1, photoCount, success ? "成功" : "失败", hikPath); + + // Lx相机拍照 + lxCameraService.capture("1", basePath); + log.info("Lx拍照 {}/{} 完成", i + 1, photoCount); + + } catch (Exception e) { + log.error("连拍 {}/{} 异常", i + 1, photoCount, e); + } + } + + boxCountResponse.setResult("BatchDone"); + boxCountResponse.setSuccess(true); + log.info("连拍完成"); + } else { + boxCountResponse.setResult("NoRead"); + boxCountResponse.setSuccess(true); + log.info("返回NoRead,不触发拍照"); + } + + } catch (Exception e) { + log.error("TCP连接或拍照异常", e); + boxCountResponse.setResult("Error"); + boxCountResponse.setSuccess(false); + } + + return boxCountResponse; + } /** * 品规识别 - 拍照同步,识别异步,完成后发送HTTP通知 */ diff --git a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/opencv/OpencvService.java b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/opencv/OpencvService.java index ddc2f03..1960256 100644 --- a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/opencv/OpencvService.java +++ b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/opencv/OpencvService.java @@ -389,5 +389,440 @@ public class OpencvService { clahe.release(); return binary; } + + // ==================== 图像融合 ==================== + + /** + * 1. 加权融合 - 两张图按透明度线性混合 + * result = img1 * (1-alpha) + img2 * alpha + * + * @param img1Path 底图路径 + * @param img2Path 叠加图路径 + * @param destPath 输出路径 + * @param alpha 融合权重(0~1),alpha越大img2越明显 + */ + public void blendWeighted(String img1Path, String img2Path, String destPath, double alpha) { + Mat img1 = Imgcodecs.imread(img1Path); + Mat img2 = Imgcodecs.imread(img2Path); + + if (img1.empty() || img2.empty()) { + log.error("读取图片失败"); + closeMats(img1, img2); + return; + } + + // 统一尺寸为img1 + if (img2.size() != img1.size()) { + Imgproc.resize(img2, img2, img1.size()); + } + + Mat result = blendWeighted(img1, img2, alpha); + Imgcodecs.imwrite(destPath, result); + log.info("加权融合完成,alpha={}, 输出: {}", alpha, destPath); + + closeMats(result, img1, img2); + } + + /** + * 加权融合 Mat 版本 + */ + public Mat blendWeighted(Mat img1, Mat img2, double alpha) { + Mat result = new Mat(); + Core.addWeighted(img1, 1.0 - alpha, img2, alpha, 0, result); + return result; + } + + /** + * 2. ROI 区域融合 - 将小图贴到大图的指定位置(支持透明蒙版) + * + * @param bigPath 大图路径 + * @param smallPath 小图路径 + * @param destPath 输出路径 + * @param x 贴入位置X + * @param y 贴入位置Y + * @param alpha 融合透明度(0=完全不透明覆盖, 1=完全透明) + */ + public void blendRoi(String bigPath, String smallPath, String destPath, int x, int y, double alpha) { + Mat big = Imgcodecs.imread(bigPath); + Mat small = Imgcodecs.imread(smallPath); + + if (big.empty() || small.empty()) { + log.error("读取图片失败"); + closeMats(big, small); + return; + } + + Mat result = blendRoi(big, small, x, y, alpha); + Imgcodecs.imwrite(destPath, result); + log.info("ROI融合完成,位置({},{}), 输出: {}", x, y, destPath); + + closeMats(result, big, small); + } + + /** + * ROI 区域融合 Mat 版本 + */ + public Mat blendRoi(Mat big, Mat small, int x, int y, double alpha) { + // 裁剪超界部分 + int w = Math.min(small.cols(), big.cols() - x); + int h = Math.min(small.rows(), big.rows() - y); + if (w <= 0 || h <= 0) { + log.error("小图超出大图范围"); + return big.clone(); + } + + Mat roi = big.submat(new Rect(x, y, w, h)); + Mat smallCrop = small.submat(new Rect(0, 0, w, h)); + + if (alpha > 0) { + // 半透明融合 + Core.addWeighted(roi, alpha, smallCrop, 1.0 - alpha, 0, roi); + } else { + // 直接覆盖 + smallCrop.copyTo(roi); + } + + return big.clone(); + } + + /** + * 3. 蒙版融合 - 通过mask控制融合区域 + * mask中白色区域显示img2,黑色区域显示img1 + * + * @param img1Path 底图路径 + * @param img2Path 叠加图路径 + * @param maskPath 蒙版路径(黑白图,白色=显示img2) + * @param destPath 输出路径 + */ + public void blendByMask(String img1Path, String img2Path, String maskPath, String destPath) { + Mat img1 = Imgcodecs.imread(img1Path); + Mat img2 = Imgcodecs.imread(img2Path); + Mat mask = Imgcodecs.imread(maskPath, Imgcodecs.IMREAD_GRAYSCALE); + + if (img1.empty() || img2.empty() || mask.empty()) { + log.error("读取图片失败"); + closeMats(img1, img2, mask); + return; + } + + // 统一尺寸 + Size size = img1.size(); + if (!img2.size().equals(size)) Imgproc.resize(img2, img2, size); + if (!mask.size().equals(size)) Imgproc.resize(mask, mask, size); + + Mat result = blendByMask(img1, img2, mask); + Imgcodecs.imwrite(destPath, result); + log.info("蒙版融合完成,输出: {}", destPath); + + closeMats(result, img1, img2, mask); + } + + /** + * 蒙版融合 Mat 版本 + */ + public Mat blendByMask(Mat img1, Mat img2, Mat mask) { + // 归一化mask到0~1 + Mat maskNorm = new Mat(); + mask.convertTo(maskNorm, CvType.CV_32FC1, 1.0 / 255.0); + + // 分别取img1和img2各通道 + Mat result = new Mat(img1.size(), img1.type()); + for (int c = 0; c < img1.channels(); c++) { + Core.multiply(img1, Scalar.all(1.0), result); // 先全取img1...简化处理,直接像素级混合 + } + + // 浮点运算: result = img1 * (1-mask) + img2 * mask + Mat img1f = new Mat(); + Mat img2f = new Mat(); + img1.convertTo(img1f, CvType.CV_32FC3); + img2.convertTo(img2f, CvType.CV_32FC3); + + Mat resultF = new Mat(); + // resultF = img2 * mask + List maskChannels = new ArrayList<>(); + for (int i = 0; i < 3; i++) maskChannels.add(maskNorm); + Mat mask3 = new Mat(); + Core.merge(maskChannels, mask3); + Core.multiply(img2f, mask3, resultF); + + // resultF += img1 * (1-mask) + Mat invMask = new Mat(); +// Core.subtract(Scalar.all(1.0), mask3, invMask); + Mat img1Masked = new Mat(); + Core.multiply(img1f, invMask, img1Masked); + Core.add(resultF, img1Masked, resultF); + + resultF.convertTo(result, CvType.CV_8UC3); + +// closeMats(maskNorm, mask3, invMask, img1Masked, resultF, img1f, img2f, maskChannels.toArray(new Mat[0])); + return result; + } + + /** + * 4. 拉普拉斯金字塔融合 - 多尺度融合,边缘过渡自然 + * 适合将两张图无缝拼接,接缝处平滑过渡 + * + * @param img1Path 左侧/上方图路径 + * @param img2Path 右侧/下方图路径 + * @param destPath 输出路径 + * @param levels 金字塔层数(推荐4~6) + * @param horizontal true=水平拼接融合, false=垂直拼接融合 + */ + public void blendPyramid(String img1Path, String img2Path, String destPath, int levels, boolean horizontal) { + Mat img1 = Imgcodecs.imread(img1Path); + Mat img2 = Imgcodecs.imread(img2Path); + + if (img1.empty() || img2.empty()) { + log.error("读取图片失败"); + closeMats(img1, img2); + return; + } + + // 统一尺寸(按较大者) + int h = Math.max(img1.rows(), img2.rows()); + int w = Math.max(img1.cols(), img2.cols()); + if (img1.rows() != h || img1.cols() != w) Imgproc.resize(img1, img1, new Size(w, h)); + if (img2.rows() != h || img2.cols() != w) Imgproc.resize(img2, img2, new Size(w, h)); + + Mat result = blendPyramid(img1, img2, levels, horizontal); + Imgcodecs.imwrite(destPath, result); + log.info("金字塔融合完成,levels={}, 输出: {}", levels, destPath); + + closeMats(result, img1, img2); + } + + /** + * 拉普拉斯金字塔融合 Mat 版本 + */ + public Mat blendPyramid(Mat img1, Mat img2, int levels, boolean horizontal) { + // 确保是3通道 + Mat i1 = img1.channels() == 3 ? img1.clone() : new Mat(); + Mat i2 = img2.channels() == 3 ? img2.clone() : new Mat(); + if (img1.channels() == 1) Imgproc.cvtColor(img1, i1, Imgproc.COLOR_GRAY2BGR); + if (img2.channels() == 1) Imgproc.cvtColor(img2, i2, Imgproc.COLOR_GRAY2BGR); + + // 构建蒙版:一半白一半黑 + Mat mask = Mat.zeros(i1.size(), CvType.CV_8UC1); + if (horizontal) { + Imgproc.rectangle(mask, new Rect(0, 0, i1.cols() / 2, i1.rows()), new Scalar(255), -1); + } else { + Imgproc.rectangle(mask, new Rect(0, 0, i1.cols(), i1.rows() / 2), new Scalar(255), -1); + } + + // 构建高斯金字塔(mask平滑版本) + Mat maskFloat = new Mat(); + mask.convertTo(maskFloat, CvType.CV_32FC1, 1.0 / 255.0); + Mat maskBlur = new Mat(); + Imgproc.GaussianBlur(maskFloat, maskBlur, new Size(31, 31), 10); + + // 三通道mask + List mc = new ArrayList<>(); + for (int i = 0; i < 3; i++) mc.add(maskBlur); + Mat mask3 = new Mat(); + Core.merge(mc, mask3); + + // 拉普拉斯金字塔:对每一层做下采样-上采样-相减得到高频细节 + Mat i1f = new Mat(), i2f = new Mat(); + i1.convertTo(i1f, CvType.CV_32FC3, 1.0 / 255.0); + i2.convertTo(i2f, CvType.CV_32FC3, 1.0 / 255.0); + + List pyr1 = buildLaplacianPyramid(i1f, levels); + List pyr2 = buildLaplacianPyramid(i2f, levels); + List pyrMask = buildGaussianPyramid(mask3, levels); + + // 每层融合 + List pyrResult = new ArrayList<>(); + for (int i = 0; i < levels; i++) { + Mat layer = new Mat(); +// Core.add( +// pyr2.get(i).mul(pyrMask.get(i)), +// pyr1.get(i).mul(Scalar.all(1.0)).sub(pyr1.get(i).mul(pyrMask.get(i))), +// // 简化:直接mask混合 +// layer +// ); + // 更准确的写法 + Mat term1 = new Mat(), term2 = new Mat(); + Core.multiply(pyr2.get(i), pyrMask.get(i), term1); + Mat invMask = new Mat(); +// Core.subtract(Scalar.all(1.0), pyrMask.get(i), invMask); + Core.multiply(pyr1.get(i), invMask, term2); + Core.add(term1, term2, layer); + pyrResult.add(layer); + + closeMats(term1, term2, invMask); + } + + // 重建 + Mat result = reconstructFromPyramid(pyrResult); + + result.convertTo(result, CvType.CV_8UC3, 255.0); + + // 释放金字塔 + pyr1.forEach(Mat::release); + pyr2.forEach(Mat::release); + pyrMask.forEach(Mat::release); + pyrResult.forEach(Mat::release); + closeMats(mask, maskFloat, maskBlur, mask3, i1f, i2f, i1, i2); + + return result; + } + + /** + * 5. 多频段融合 (Multi-Band Blending) - 更高质量的拼接融合 + * 适合全景图拼接、两张不同曝光图融合等场景 + * + * @param img1Path 底图路径 + * @param img2Path 叠加图路径 + * @param destPath 输出路径 + * @param bands 频段数(推荐4~8) + */ + public void blendMultiBand(String img1Path, String img2Path, String destPath, int bands) { + Mat img1 = Imgcodecs.imread(img1Path); + Mat img2 = Imgcodecs.imread(img2Path); + + if (img1.empty() || img2.empty()) { + log.error("读取图片失败"); + closeMats(img1, img2); + return; + } + + // 统一尺寸 + if (!img1.size().equals(img2.size())) { + Imgproc.resize(img2, img2, img1.size()); + } + + Mat result = blendMultiBand(img1, img2, bands); + Imgcodecs.imwrite(destPath, result); + log.info("多频段融合完成,bands={}, 输出: {}", bands, destPath); + + closeMats(result, img1, img2); + } + + /** + * 多频段融合 Mat 版本 + */ + public Mat blendMultiBand(Mat img1, Mat img2, int bands) { + // 转3通道 + Mat i1 = toBgr(img1); + Mat i2 = toBgr(img2); + + // 高斯金字塔 smooth 后连加 + List gauss1 = new ArrayList<>(); + List gauss2 = new ArrayList<>(); + Mat current = i1.clone(); + gauss1.add(current.clone()); + + for (int i = 0; i < bands; i++) { + Mat down = new Mat(); + Imgproc.pyrDown(current, down); + current = down; + gauss1.add(current.clone()); + } + closeMats(current); + + current = i2.clone(); + gauss2.add(current.clone()); + for (int i = 0; i < bands; i++) { + Mat down = new Mat(); + Imgproc.pyrDown(current, down); + current = down; + gauss2.add(current.clone()); + } + closeMats(current); + + // 重建最高层 + Mat topLayer = new Mat(); + Core.addWeighted(gauss1.get(bands), 0.5, gauss2.get(bands), 0.5, 0, topLayer); + + Mat result = topLayer; + for (int i = bands - 1; i >= 0; i--) { + Mat up = new Mat(); + Imgproc.pyrUp(result, up, gauss1.get(i).size()); + Mat blended = new Mat(); + Core.addWeighted(gauss1.get(i), 0.5, gauss2.get(i), 0.5, 0, blended); + Core.add(up, blended, result = new Mat()); + closeMats(up, blended); + } + + gauss1.forEach(Mat::release); + gauss2.forEach(Mat::release); + closeMats(i1, i2); + + return result; + } + + // ==================== 融合辅助方法 ==================== + + /** + * 构建高斯金字塔 + */ + private List buildGaussianPyramid(Mat src, int levels) { + List pyramid = new ArrayList<>(); + Mat current = src.clone(); + pyramid.add(current); + for (int i = 1; i < levels; i++) { + Mat down = new Mat(); + Imgproc.pyrDown(current, down); + pyramid.add(down); + current = down; + } + return pyramid; + } + + /** + * 构建拉普拉斯金字塔 + */ + private List buildLaplacianPyramid(Mat src, int levels) { + List gauss = buildGaussianPyramid(src, levels); + List laplace = new ArrayList<>(); + + for (int i = 0; i < levels - 1; i++) { + Mat up = new Mat(); + Imgproc.pyrUp(gauss.get(i + 1), up, gauss.get(i).size()); + Mat lap = new Mat(); + Core.subtract(gauss.get(i), up, lap); + laplace.add(lap); + closeMats(up); + } + laplace.add(gauss.get(levels - 1).clone()); + + gauss.forEach(Mat::release); + return laplace; + } + + /** + * 从金字塔重建原图 + */ + private Mat reconstructFromPyramid(List pyramid) { + Mat result = pyramid.get(pyramid.size() - 1).clone(); + for (int i = pyramid.size() - 2; i >= 0; i--) { + Mat up = new Mat(); + Imgproc.pyrUp(result, up, pyramid.get(i).size()); + Mat sum = new Mat(); + Core.add(up, pyramid.get(i), sum); + result = sum; + closeMats(up); + } + return result; + } + + /** + * 转为BGR三通道 + */ + private Mat toBgr(Mat src) { + if (src.channels() == 3) return src.clone(); + Mat bgr = new Mat(); + Imgproc.cvtColor(src, bgr, Imgproc.COLOR_GRAY2BGR); + return bgr; + } + + /** + * 批量释放 Mat + */ + private void closeMats(Mat... mats) { + for (Mat m : mats) { + if (m != null) m.release(); + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 976fac1..21adcd8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -55,7 +55,7 @@ picPath: "D:/data/media/" picUrl: "http://192.168.100.110:9012/pic/" serve: - ip: 127.0.0.1 + ip: 192.168.100.99 port: 48080 singleInventoryReturnPath: "/admin-api/logistics/StockController/singleInventoryReturn" singleInventoryQrCodeReturnPath: "/admin-api/logistics/StockController/singleInventoryQrCodeReturn"