简单修改

物流展会算法
LAPTOP-S9HJSOEB\昊天 5 months ago
parent d371fb5d1a
commit 3435f85495

@ -115,11 +115,11 @@
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<!-- SpringDoc OpenAPI 依赖(替代SpringFox) -->
<!-- SpringDoc OpenAPI 依赖(Spring Boot 3.x) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.15</version>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>

@ -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通知
*/

@ -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<Mat> 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<Mat> 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<Mat> pyr1 = buildLaplacianPyramid(i1f, levels);
List<Mat> pyr2 = buildLaplacianPyramid(i2f, levels);
List<Mat> pyrMask = buildGaussianPyramid(mask3, levels);
// 每层融合
List<Mat> 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<Mat> gauss1 = new ArrayList<>();
List<Mat> 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<Mat> buildGaussianPyramid(Mat src, int levels) {
List<Mat> 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<Mat> buildLaplacianPyramid(Mat src, int levels) {
List<Mat> gauss = buildGaussianPyramid(src, levels);
List<Mat> 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<Mat> 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();
}
}
}

@ -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"

Loading…
Cancel
Save