diff --git a/web/pom.xml b/web/pom.xml index 3ceb44d..f4a7149 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -37,6 +37,22 @@ org.springframework.boot spring-boot-starter-web + + + MvCameraControlWrapper + MvCameraControlWrapper + 1.0 + system + ${project.basedir}/src/main/resources/libs/MvCameraControlWrapper.jar + + + + org + opencv + system + 1.0 + ${project.basedir}/src/main/resources/libs/opencv-480.jar + org.projectlombok lombok @@ -102,6 +118,14 @@ true + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + diff --git a/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java b/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java index 680baf9..facc777 100644 --- a/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java +++ b/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java @@ -1,5 +1,6 @@ package com.zhehekeji.web.config; +import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -88,6 +89,10 @@ public class ConfigProperties { private Long delayDownloadMp4; public Integer verticalAdjustmentTime = 2; + + private String industrialCamera; + + private String camera3D; } @Data diff --git a/web/src/main/java/com/zhehekeji/web/controller/IndustrialCameraController.java b/web/src/main/java/com/zhehekeji/web/controller/IndustrialCameraController.java new file mode 100644 index 0000000..68021aa --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/controller/IndustrialCameraController.java @@ -0,0 +1,165 @@ +package com.zhehekeji.web.controller; + +import com.zhehekeji.core.pojo.Result; +import com.zhehekeji.web.config.ConfigProperties; +import com.zhehekeji.web.entity.Street; +import com.zhehekeji.web.pojo.IndustrialCameraVO; +import com.zhehekeji.web.pojo.stock.CheckLogSearch; +import com.zhehekeji.web.service.IndustrialCamera.HikSaveImage; +import com.zhehekeji.web.service.IndustrialCamera.LxPointCloudSaveImage; +import com.zhehekeji.web.service.PlcService; +import com.zhehekeji.web.service.StreetService; +import com.zhehekeji.web.service.algorithm.FeatureMatchingExample; +import com.zhehekeji.web.service.algorithm.InventoryService; +import com.zhehekeji.web.service.algorithm.PcdPojo; +import com.zhehekeji.web.service.algorithm.PointCloudProcessor; +import com.zhehekeji.web.service.client.TransmissionPojo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.opencv.core.Mat; +import org.opencv.imgcodecs.Imgcodecs; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import static com.zhehekeji.web.service.algorithm.FeatureMatchingExample.base642Mat; + +@Slf4j +@Api(tags = "工业相机") +@RequestMapping("/industrialCamera") +@RestController +public class IndustrialCameraController { + @Resource + HikSaveImage hikSaveImage; + + @Resource + StreetService streetService; + @Resource + PlcService plcService; + + + @Resource + ConfigProperties configProperties; + + @ApiOperation("拍照") + @PostMapping("pic") + public Result pic(@RequestBody CheckLogSearch checkLogSearch){ + Street street = streetService.streetById(checkLogSearch.getStreetId()); + if(configProperties.getCameraConfig().getIndustrialCamera()!=null) { + String path = "industrialCamera/" + UUID.randomUUID() + ".jpeg"; + hikSaveImage.saveImage(configProperties.getCameraConfig().getIndustrialCamera(), configProperties.getSavePath().getMediaPath() + path, "sn"); + return new Result<>(path); + }else { + return new Result<>(""); + } + } + + @ApiOperation("识别") + @PostMapping("Macth") + public Result Macth(@RequestBody IndustrialCameraVO industrialCameraVo){ + String srcImg = configProperties.getSavePath().getMediaPath() + industrialCameraVo.getPicImg(); + String srcImgRest = configProperties.getSavePath().getMediaPath() + industrialCameraVo.getPicImg(); + //识别结果 + boolean re = FeatureMatchingExample.matchTemplate( + InventoryService.readImagesInFolder(configProperties.getSavePath().getMediaPath() + "template/" + industrialCameraVo.getTypeMacth()), + srcImg); + if (re){ + + return new Result<>(industrialCameraVo.getPicImg()+".jpg"); + }else { + + return new Result<>(""); + } + } + + + @ApiOperation("识别3D") + @PostMapping("Macth3D") + public Result Macth3D(@RequestBody IndustrialCameraVO industrialCameraVo){ + LocalDate currentDate = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + //路径 + String pathPcd = configProperties.getSavePath().getMediaPath() + +"camera3D/" + +currentDate.format(formatter)+"/" +UUID.randomUUID()+"-"; + Integer count = inventoryService.match3D(industrialCameraVo.getName(),pathPcd+"-"+configProperties.getCameraConfig().getCamera3D()+".pcd"); + return Result.success(count); + } + + @Resource + InventoryService inventoryService; +// +// @ApiOperation("匹配所有") +// @PostMapping("matchFeatures") +// public Boolean matchFeatures(@RequestBody TransmissionPojo transmissionPojo) { +// //return true; +// +// return null; +// } + +// +// @ApiOperation("随行") +// @PostMapping("actionType") +// public void action(@RequestBody TransmissionPojo transmissionPojo) { +// +// Street street = streetService.getStreetByPlcId(transmissionPojo.getStreetNumber()); +// +// +// //随行地址 +// String pcdPath1 =configProperties.getSavePath().getMediaPath() + street.getLeft3D()+"\\000\\" +// +transmissionPojo.getRow()+"-"+transmissionPojo.getColumn()+"-"+transmissionPojo.getDirection()+"-L"+".pcd"; +// +// String pcdPath2 =configProperties.getSavePath().getMediaPath() + street.getRight3D()+"\\000\\" +// +transmissionPojo.getRow()+"-"+transmissionPojo.getColumn()+"-"+transmissionPojo.getDirection()+"-R"+".pcd"; +// //拍照 +// System.out.println(street.getLeft3D()+" 111211 "+ pcdPath1); +// LxPointCloudSaveImage lxPointCloudSaveImage = new LxPointCloudSaveImage(); +// lxPointCloudSaveImage.saveImage(street.getLeft3D(), pcdPath1,1); +// +// LxPointCloudSaveImage lxPointCloudSaveImage1 = new LxPointCloudSaveImage(); +// lxPointCloudSaveImage1.saveImage(street.getRight3D(), pcdPath2,2); +// } +// @ApiOperation("模板建立") +// @PostMapping("MacthCreate") +// public Result MacthCreate(@RequestBody IndustrialCameraVO industrialCameraVo){ +// Path folder = Paths.get(configProperties.getSavePath().getMediaPath()+"template/"+industrialCameraVo.getTypeMacth()); +// try { +// Files.walkFileTree(folder, new SimpleFileVisitor() { +// @Override +// public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { +// Files.delete(file); +// return FileVisitResult.CONTINUE; +// } +// +// @Override +// public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { +// Files.delete(dir); +// return FileVisitResult.CONTINUE; +// } +// }); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// for (int i = 0; i < industrialCameraVo.getFileList().size(); i++){ +// try { +// Mat template = base642Mat(industrialCameraVo.getFileList().get(i).getThumbUrl() +// .replace("data:image/jpeg;base64,","").replace("data:image/png;base64,","")); +// +// Imgcodecs.imwrite(configProperties.getSavePath().getMediaPath()+"template/"+industrialCameraVo.getTypeMacth()+"/"+i+".jpg", template); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } +// } +// return new Result<>(true); +// } +} diff --git a/web/src/main/java/com/zhehekeji/web/controller/KuKouController.java b/web/src/main/java/com/zhehekeji/web/controller/KuKouController.java index 5845e42..55a658c 100644 --- a/web/src/main/java/com/zhehekeji/web/controller/KuKouController.java +++ b/web/src/main/java/com/zhehekeji/web/controller/KuKouController.java @@ -1,11 +1,45 @@ package com.zhehekeji.web.controller; +import com.github.pagehelper.PageInfo; +import com.zhehekeji.core.pojo.Result; +import com.zhehekeji.web.entity.KuKou; +import com.zhehekeji.web.pojo.OrderSearch; +import com.zhehekeji.web.pojo.OrderVO; +import com.zhehekeji.web.service.KuKouService; +import com.zhehekeji.web.service.OrderService; import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.annotation.Resource; + @Api(tags = "盘点统计") @RequestMapping("/KuKouController") @RestController public class KuKouController { + @Resource + private KuKouService kuKouService; + @Resource + private OrderService orderService; + + + @PostMapping("/list") + @ApiOperation(value = "查询") + //@SessionHandler + public Result> list(@RequestBody OrderSearch orderSearch) { + //validatorUtil.validate(orderSearch); + return Result.success(kuKouService.pageInfo(orderSearch)); + } + + @PostMapping("/latest") + @ApiOperation(value = "最新数据") + //@SessionHandler + public Result latest() { + //validatorUtil.validate(orderSearch); + return Result.success(kuKouService.latest()); + } + } diff --git a/web/src/main/java/com/zhehekeji/web/entity/KuKou.java b/web/src/main/java/com/zhehekeji/web/entity/KuKou.java index e7a33c7..2ac3d8f 100644 --- a/web/src/main/java/com/zhehekeji/web/entity/KuKou.java +++ b/web/src/main/java/com/zhehekeji/web/entity/KuKou.java @@ -1,5 +1,7 @@ package com.zhehekeji.web.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; @@ -9,10 +11,13 @@ import java.time.LocalDateTime; @TableName("kukou") @Data public class KuKou { + + @TableId(type = IdType.UUID) private int id; private int count; private String code; private String path; + private int flag; @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; diff --git a/web/src/main/java/com/zhehekeji/web/entity/Street.java b/web/src/main/java/com/zhehekeji/web/entity/Street.java index b843850..4af1c77 100644 --- a/web/src/main/java/com/zhehekeji/web/entity/Street.java +++ b/web/src/main/java/com/zhehekeji/web/entity/Street.java @@ -57,4 +57,6 @@ public class Street { private Integer videoStyleRow; private Integer videoStyleColumn; + + } diff --git a/web/src/main/java/com/zhehekeji/web/mapper/KukouMapper.java b/web/src/main/java/com/zhehekeji/web/mapper/KukouMapper.java new file mode 100644 index 0000000..f419309 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/mapper/KukouMapper.java @@ -0,0 +1,8 @@ +package com.zhehekeji.web.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.zhehekeji.web.entity.EmptyCheckPic; +import com.zhehekeji.web.entity.KuKou; + +public interface KukouMapper extends BaseMapper { +} diff --git a/web/src/main/java/com/zhehekeji/web/pojo/IndustrialCameraVO.java b/web/src/main/java/com/zhehekeji/web/pojo/IndustrialCameraVO.java new file mode 100644 index 0000000..0528cc0 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/pojo/IndustrialCameraVO.java @@ -0,0 +1,17 @@ +package com.zhehekeji.web.pojo; + +import lombok.Data; + +import java.util.List; + +@Data +public class IndustrialCameraVO { + private String name; + private String picImg; + private String typeMacth; + //private List fileList; + private String pcdPath; + + + private String streetId; +} diff --git a/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/CameraSaveUtil.java b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/CameraSaveUtil.java new file mode 100644 index 0000000..ef67264 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/CameraSaveUtil.java @@ -0,0 +1,12 @@ +package com.zhehekeji.web.service.IndustrialCamera; + +public interface CameraSaveUtil { + /** + * + * @param sn sn + * @param path 图片路径 + * @param type 暂时默认sn 后续可以添加ip等 + * @return + */ + boolean saveImage(String sn,String path,String type); +} diff --git a/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/DcLibrary.java b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/DcLibrary.java new file mode 100644 index 0000000..1d0af86 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/DcLibrary.java @@ -0,0 +1,50 @@ +package com.zhehekeji.web.service.IndustrialCamera; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.ptr.PointerByReference; + +import java.util.Arrays; +import java.util.List; +public interface DcLibrary extends Library { + DcLibrary INSTANCE = (DcLibrary) Native.load((System.getProperty("user.dir"))+"\\libs\\plc\\LxCameraApiR.dll", DcLibrary.class); + // 创建一个新的实例 + + // 设备详细信息结构体 + class LxDeviceInfo extends Structure { + public long handle; // 使用 long 类型表示 DcHandle + public short dev_type; + public byte[] id = new byte[32]; + public byte[] ip = new byte[32]; + public byte[] sn = new byte[32]; + public byte[] mac = new byte[32]; + public byte[] firmware_ver = new byte[32]; + public byte[] algor_ver = new byte[32]; + public byte[] name = new byte[32]; + public byte[] reserve = new byte[32]; + public byte[] reserve2 = new byte[32]; + public byte[] reserve3 = new byte[64]; + public byte[] reserve4 = new byte[128]; + + @Override + protected List getFieldOrder() { + return Arrays.asList("handle", "dev_type", "id", "ip", "sn", "mac", "firmware_ver", "algor_ver", "name", "reserve", "reserve2", "reserve3", "reserve4"); + } + } + + // 设备打开函数 + int DcOpenDevice(int open_mode, String param, PointerByReference handle, LxDeviceInfo info); + + String DcGetDeviceList(PointerByReference devlist, int devnum); + int DcCloseDevice(Pointer handle); + + int DcStartStream(Pointer handle); + + int DcStopStream(Pointer handle); + + int DcSaveXYZ(Pointer handle, String filename); + int DcSetCmd(Pointer handle, int cmd); +} +// Define LX_OPEN_MODE as an enum or int based on your C definition diff --git a/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/HikSaveImage.java b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/HikSaveImage.java new file mode 100644 index 0000000..b599723 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/HikSaveImage.java @@ -0,0 +1,321 @@ +package com.zhehekeji.web.service.IndustrialCamera; /*************************************************************************************************** + * @file SaveImage.java + * @breif Use functions provided in MvCameraControlWrapper.jar to save image as JPEG。 + * @author zhanglei72 + * @date 2020/02/10 + * + * @warning + * @version V1.0.0 2020/02/10 Create this file + * @since 2020/02/10 + **************************************************************************************************/ + +import MvCameraControlWrapper.*; +import org.springframework.stereotype.Service; + +import javax.annotation.PreDestroy; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static MvCameraControlWrapper.MvCameraControl.*; +import static MvCameraControlWrapper.MvCameraControlDefines.*; + +@Service +public class HikSaveImage implements CameraSaveUtil{ + + // 设备句柄保存 + static Map handleMap = new ConcurrentHashMap<>(); + private static void printDeviceInfo(MV_CC_DEVICE_INFO stDeviceInfo) + { + if (null == stDeviceInfo) { + System.out.println("stDeviceInfo is null"); + return; + } + + if (stDeviceInfo.transportLayerType == MV_GIGE_DEVICE) { + System.out.println("\tCurrentIp: " + stDeviceInfo.gigEInfo.currentIp); + System.out.println("\tserialNumber: " + stDeviceInfo.gigEInfo.serialNumber); + System.out.println("\tModel: " + stDeviceInfo.gigEInfo.modelName); + System.out.println("\tUserDefinedName: " + stDeviceInfo.gigEInfo.userDefinedName); + } else if (stDeviceInfo.transportLayerType == MV_USB_DEVICE) { + System.out.println("\tUserDefinedName: " + stDeviceInfo.usb3VInfo.userDefinedName); + System.out.println("\tSerial Number: " + stDeviceInfo.usb3VInfo.serialNumber); + System.out.println("\tDevice Number: " + stDeviceInfo.usb3VInfo.deviceNumber); + } else { + System.err.print("Device is not supported! \n"); + } + + System.out.println("\tAccessible: " + + MvCameraControl.MV_CC_IsDeviceAccessible(stDeviceInfo, MV_ACCESS_Exclusive)); + System.out.println(""); + } + + private static void printFrameInfo(MV_FRAME_OUT_INFO stFrameInfo) + { + if (null == stFrameInfo) + { + System.err.println("stFrameInfo is null"); + return; + } + + StringBuilder frameInfo = new StringBuilder(""); + frameInfo.append(("\tFrameNum[" + stFrameInfo.frameNum + "]")); + frameInfo.append("\tWidth[" + stFrameInfo.width + "]"); + frameInfo.append("\tHeight[" + stFrameInfo.height + "]"); + frameInfo.append(String.format("\tPixelType[%#x]", stFrameInfo.pixelType.getnValue())); + + System.out.println(frameInfo.toString()); + } + + public static void saveDataToFile(byte[] dataToSave, int dataSize, String fileName) + { + OutputStream os = null; + + try + { + Path path = Paths.get(fileName); + + // 检查文件是否存在 + if (!Files.exists(path)) { + try { + // 创建所有父目录 + Files.createDirectories(path.getParent()); + + + } catch (IOException e) { + System.err.println("Failed to create file: " + e.getMessage()); + } + } else { + System.out.println("File already exists: " + path); + } + // 创建文件 + + os = new FileOutputStream( fileName); + os.write(dataToSave, 0, dataSize); + System.out.println("SaveImage succeed."); + // Create directory + + } + catch (IOException e) + { + e.printStackTrace(); + } + finally + { + // Close file stream + try + { + os.close(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + } + + public static int chooseCamera(ArrayList stDeviceList,String sn ,String type) + { + if (null == stDeviceList) + { + return -1; + } + for(int i = 0; i < stDeviceList.size(); i++){ + if(type.equals("sn") && sn.equals(stDeviceList.get(i).gigEInfo.serialNumber)){ + + return i; + }else if(type.equals("ip") && sn.equals(stDeviceList.get(i).gigEInfo.currentIp)){ + + return i; + } + } + return -1; + + + } + + public static void main(String[] args) + { + HikSaveImage hikSaveImage = new HikSaveImage(); + hikSaveImage.saveImage("00J51775660", "E:\\m\\55.bpm","sn"); + } + @PreDestroy + void clean(){ + for (Handle h :handleMap.values()){ + + // Stop grabbing + int nRet = MvCameraControl.MV_CC_StopGrabbing(h); + if (MV_OK != nRet) + { + System.err.printf("StopGrabbing fail, errcode: [%#x]\n", nRet); + break; + } + nRet = MvCameraControl.MV_CC_DestroyHandle(h); + if (MV_OK != nRet) { + System.err.printf("DestroyHandle failed, errcode: [%#x]\n", nRet); + } + } + + } + @Override + public boolean saveImage(String sn, String path,String type) { + int nRet = MV_OK; + ArrayList stDeviceList; + + do + { + System.out.println("SDK Version " + MvCameraControl.MV_CC_GetSDKVersion()); + + // Enuerate GigE and USB devices + try + { + stDeviceList = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE); + if (0 >= stDeviceList.size()) + { + System.out.println("No devices found!"); + break; + } + } + catch (CameraControlException e) + { + System.err.println("Enumrate devices failed!" + e.toString()); + e.printStackTrace(); + break; + } + + // choose camera + + + Handle hCamera = init(sn,stDeviceList,type); + // Create handle + + + + // Get payload size + MVCC_INTVALUE stParam = new MVCC_INTVALUE(); + nRet = MvCameraControl.MV_CC_GetIntValue(hCamera, "PayloadSize", stParam); + if (MV_OK != nRet) + { + System.err.printf("Get PayloadSize fail, errcode: [%#x]\n", nRet); + break; + } + + + + // Get one frame + MV_FRAME_OUT_INFO stImageInfo = new MV_FRAME_OUT_INFO(); + byte[] pData = new byte[(int)stParam.curValue]; + nRet = MvCameraControl.MV_CC_GetOneFrameTimeout(hCamera, pData, stImageInfo, 1000); + while(true){ + if(stImageInfo.height >0 && stImageInfo.width >0){ + break; + } + } + if (MV_OK != nRet) + { + System.err.printf("GetOneFrameTimeout fail, errcode:[%#x]\n", nRet); + break; + } + + System.out.println("GetOneFrame: "); + printFrameInfo(stImageInfo); + int imageLen = stImageInfo.width * stImageInfo.height * 3; // Every RGB pixel takes 3 bytes + byte[] imageBuffer = new byte[imageLen]; + + // Call MV_CC_SaveImage to save image as JPEG + MV_SAVE_IMAGE_PARAM stSaveParam = new MV_SAVE_IMAGE_PARAM(); + stSaveParam.width = stImageInfo.width; // image width + stSaveParam.height = stImageInfo.height; // image height + stSaveParam.data = pData; // image data + stSaveParam.dataLen = stImageInfo.frameLen; // image data length + stSaveParam.pixelType = stImageInfo.pixelType; // image pixel format + stSaveParam.imageBuffer = imageBuffer; // output image buffer + stSaveParam.imageType = MV_SAVE_IAMGE_TYPE.MV_Image_Jpeg; // output image pixel format + stSaveParam.methodValue = 0; // Interpolation method that converts Bayer format to RGB24. 0-Neareast 1-double linear 2-Hamilton + stSaveParam.jpgQuality = 99; // JPG endoding quality(50-99] + + nRet = MvCameraControl.MV_CC_SaveImage(hCamera, stSaveParam); + if (MV_OK != nRet) + { + System.err.printf("SaveImage fail, errcode: [%#x]\n", nRet); + break; + } + + // Save buffer content to file + saveDataToFile(imageBuffer, imageLen, path); + + } while (false); + + + return false; + } + Handle init(String sn, ArrayList stDeviceList,String type){ + int nRet = MV_OK; + int camIndex = -1; + + Handle hCamera = null; + try + { + if(handleMap.containsKey(sn) && handleMap.get(sn)!=null){ + //已经获得句柄 + hCamera = handleMap.get(sn); + }else { + System.out.println("创建句柄"); + camIndex = chooseCamera(stDeviceList,sn,type); + if (camIndex == -1) + { + System.out.println("未找到摄像头"); + return null; + } + //未获得句柄并初始化 + hCamera = MvCameraControl.MV_CC_CreateHandle(stDeviceList.get(camIndex)); + handleMap.put(sn,hCamera); + + // Open device + nRet = MvCameraControl.MV_CC_OpenDevice(hCamera); + if (MV_OK != nRet) + { + System.err.printf("Connect to camera failed, errcode: [%#x]\n", nRet); + + return null; + } + + // Make sure that trigger mode is off + nRet = MvCameraControl.MV_CC_SetEnumValueByString(hCamera, "TriggerMode", "Off"); + if (MV_OK != nRet) + { + System.err.printf("SetTriggerMode failed, errcode: [%#x]\n", nRet); + + return null; + } + // Get payload size + + + // Start grabbing + nRet = MvCameraControl.MV_CC_StartGrabbing(hCamera); + if (MV_OK != nRet) + { + System.err.printf("Start Grabbing fail, errcode: [%#x]\n", nRet); + + return null; + } + } + } + catch (CameraControlException e) + { + System.err.println("Create handle failed!" + e.toString()); + e.printStackTrace(); + + + return null; + } + System.out.println(hCamera); + return hCamera; + } +} diff --git a/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/LxPointCloudSaveImage.java b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/LxPointCloudSaveImage.java new file mode 100644 index 0000000..8c16150 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/IndustrialCamera/LxPointCloudSaveImage.java @@ -0,0 +1,106 @@ +package com.zhehekeji.web.service.IndustrialCamera; + +import com.sun.jna.Native; +import com.sun.jna.ptr.PointerByReference; +import lombok.extern.slf4j.Slf4j; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class LxPointCloudSaveImage { + + static DcLibrary INSTANCER = (DcLibrary) Native.loadLibrary((System.getProperty("user.dir"))+"\\libs\\plc\\LxCameraApi.dll", DcLibrary.class); + + static Map handleMap = new ConcurrentHashMap<>(); + public static PointerByReference getHandle(String sn,int type ,DcLibrary INSTANCE){ + if(handleMap.get(sn)!=null){ + return handleMap.get(sn); + }else { + // 创建 Pointer 的引用 + PointerByReference handleRef = new PointerByReference(); + + + // 创建 PointerByReference 的实例用于接收设备列表 + // PointerByReference devlistRef = new PointerByReference(); + // 创建 LxDeviceInfo 实例 + DcLibrary.LxDeviceInfo info = new DcLibrary.LxDeviceInfo(); + + //library.DcGetDeviceList( devlistRef,0); + + // 调用 DcOpenDevice 函数 + System.out.println(sn+" "+handleRef+" "+info); + int result = INSTANCE.DcOpenDevice(1, sn, handleRef, info); + int i = 0; + if(result ==0) { + handleMap.put(sn, handleRef); + }else { + for (int ii=0;i<50;i++ ) { + result = INSTANCE.DcOpenDevice(1, sn, handleRef, info); + System.out.println(i + "次尝试"); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + // 输出结果 + System.out.println("Result: " + result); + System.out.println("Handle: " + handleRef.getValue()); // 获取 DcHandle 的值 + System.out.println("Device Type: " + info.dev_type); + System.out.println("ID: " + new String(info.id).trim()); + System.out.println("IP: " + new String(info.ip).trim()); + System.out.println("SN: " + new String(info.sn).trim()); + System.out.println("MAC: " + new String(info.mac).trim()); + System.out.println("Firmware Version: " + new String(info.firmware_ver).trim()); + System.out.println("Algorithm Version: " + new String(info.algor_ver).trim()); + System.out.println("Name: " + new String(info.name).trim()); + System.out.println("Reserve: " + new String(info.reserve).trim()); + System.out.println("Reserve2: " + new String(info.reserve2).trim()); + System.out.println("Reserve3: " + new String(info.reserve3).trim()); + System.out.println("Reserve4: " + new String(info.reserve4).trim());//lxPointCloudApi.DcCloseDevice(handle); + + + return handleRef; + } + + } + public static boolean saveImage(String sn, String path,int type) { + DcLibrary INSTANCE; + INSTANCE = INSTANCER; + int result = 0; + PointerByReference handleRef = getHandle(sn,type,INSTANCE); + result = INSTANCE.DcStartStream(handleRef.getValue()); + System.out.println("Result: " + result); + result = INSTANCE.DcSetCmd(handleRef.getValue(),5001); + + + System.out.println("Result: " + result); + Path path1 = Paths.get(path); + + try { + // 如果路径不存在,则创建目录 + Files.createDirectories(path1.getParent()); + }catch (Exception e){ + log.info("路径失败"); + } + int i = INSTANCE.DcSaveXYZ(handleRef.getValue(), path); + log.info(sn+" 3dCamera get pcd :"+path +";rest:"+i); + System.out.println(sn+" 3dCamera get pcd :"+path +";rest:"+i); + result = INSTANCE.DcStopStream(handleRef.getValue()); + + System.out.println("stop Stream Result: " + result); + + return true; + } + + public static void main(String[] args) { + saveImage("1","E:\\1-8-2-R.pcd",1); + + saveImage("2","E:\\12-R.pcd",2); + } +} diff --git a/web/src/main/java/com/zhehekeji/web/service/KuKouService.java b/web/src/main/java/com/zhehekeji/web/service/KuKouService.java new file mode 100644 index 0000000..85f2bd5 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/KuKouService.java @@ -0,0 +1,32 @@ +package com.zhehekeji.web.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.zhehekeji.web.entity.KuKou; +import com.zhehekeji.web.mapper.KukouMapper; +import com.zhehekeji.web.pojo.OrderSearch; +import com.zhehekeji.web.pojo.OrderVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class KuKouService extends ServiceImpl implements IService { + public PageInfo pageInfo(OrderSearch orderSearch){ + PageHelper.startPage(orderSearch.getPageNum(), orderSearch.getPageSize()); + List kuKous = list(new QueryWrapper<>()); + + return new PageInfo<>(kuKous); + } + + public KuKou latest(){ + KuKou kuKou = getOne(new QueryWrapper().orderByDesc("createTime").last("limit 1")); + + return kuKou; + } +} diff --git a/web/src/main/java/com/zhehekeji/web/service/PlcCmdInfo.java b/web/src/main/java/com/zhehekeji/web/service/PlcCmdInfo.java index 0305d73..182b24e 100644 --- a/web/src/main/java/com/zhehekeji/web/service/PlcCmdInfo.java +++ b/web/src/main/java/com/zhehekeji/web/service/PlcCmdInfo.java @@ -15,7 +15,7 @@ public class PlcCmdInfo { private String taskId; /** - * 前两个命令 库内?苦口? 1:库内 2:库口 + * 前两个命令 库内?苦口? 1:库内 2:库口plcCmdInfo */ private Integer side1; diff --git a/web/src/main/java/com/zhehekeji/web/service/PlcService.java b/web/src/main/java/com/zhehekeji/web/service/PlcService.java index 8ccbf9c..1ad36c1 100644 --- a/web/src/main/java/com/zhehekeji/web/service/PlcService.java +++ b/web/src/main/java/com/zhehekeji/web/service/PlcService.java @@ -12,6 +12,7 @@ import com.zhehekeji.web.pojo.stock.StockStatus; import com.zhehekeji.web.service.RFID.RFIDMap; import com.zhehekeji.web.service.RFID.RFIDSocket; import com.zhehekeji.web.entity.EmptyCheckPic; +import com.zhehekeji.web.service.algorithm.InventoryService; import com.zhehekeji.web.service.client.ClientChanel; import com.zhehekeji.web.service.client.TransmissionPojo; import com.zhehekeji.web.service.ksec.KsecDataInfo; @@ -26,7 +27,9 @@ import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.time.Duration; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -42,6 +45,8 @@ import java.util.stream.Collectors; @Service @Slf4j public class PlcService { + @Resource + KuKouService kuKouService; @Resource private OrderMapper orderMapper; @@ -80,6 +85,9 @@ public class PlcService { @Resource private CheckStreetSummaryMapper checkStreetSummaryMapper; + @Resource + private InventoryService inventoryService; + private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,8,200,TimeUnit.MINUTES,new ArrayBlockingQueue<>(100000)); public void setCameraControlModule(CameraControlModule cameraControlModule){ @@ -332,6 +340,7 @@ public class PlcService { } //update order info after capture if (path != null && needCapture) { + captureUpdateOrderAndStock(orderInfo, path); } //转向原点位 @@ -347,7 +356,7 @@ public class PlcService { @Resource ReentrantLockExample reentrantLockExample; - String scan(PlcCmdInfo plcCmdInfo,Street street,String path){ + String scan(PlcCmdInfo plcCmdInfo,Street street){ Boolean trayCheck = Boolean.FALSE; String trayCode = ""; log.info("扫码类型:" + configProperties.getScanCodeMode().getTray()); @@ -388,8 +397,29 @@ public class PlcService { } } }else { + Integer cameraId; + String code = "E1-DEEP-"; + String path = ""; + + //使用同侧的camera + if(plcCmdInfo.getLeftRight1() ==1){ + cameraId = street.getCamera1Id(); + code = code+"L"; + }else { + cameraId = street.getCamera2Id(); + code = code+"R"; + } + path = PathUtil.createFileNameByRowColumn("jpg",cameraId,plcCmdInfo.getRow1(),plcCmdInfo.getColumn1()); + gyrateCameraByCode(cameraId,code); + try { + Thread.sleep(1500L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + path = cameraCapture(cameraId,false,0L,path); - trayCode = reentrantLockExample.identify(path,configProperties.getIntelliBink().getIp(),configProperties.getIntelliBink().getPort()); + + trayCode = reentrantLockExample.identify(path,configProperties.getIntelliBink().getIp(),configProperties.getIntelliBink().getPort()); if(trayCode != null && !"".equals(trayCode)){ if ("".equals(plcCmdInfo.getWmsCode())) { //托盘码为空,无货物 @@ -419,6 +449,31 @@ public class PlcService { plcCmdInfo.setTrayCode(trayCode); return trayCode; } + public boolean checkVision(KsecDataInfo dataInfo){ + KuKou kuKou = new KuKou(); + LocalDate currentDate = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + //路径 + String path = configProperties.getSavePath().getMediaPath() + +"industrialCamera/" + +currentDate.format(formatter)+"/" +UUID.randomUUID()+"-"; + //路径 + String pathPcd = configProperties.getSavePath().getMediaPath() + +"camera3D/" + +currentDate.format(formatter)+"/" +UUID.randomUUID()+"-"; + boolean flag2D =inventoryService.match2D(dataInfo.getCategoryName(),path+configProperties.getCameraConfig().getIndustrialCamera()+".jpeg"); + int count = inventoryService.match3D(dataInfo.getCategoryName(),pathPcd+"-"+configProperties.getCameraConfig().getCamera3D()+".pcd"); + kuKou.setCreateTime(LocalDateTime.now()); + if (flag2D) { + kuKou.setFlag(1); + } + kuKou.setCode(dataInfo.getCategoryName()); + kuKou.setCount(count); + kuKouService.save(kuKou); + return true; + } + public Boolean check(PlcCmdInfo plcCmdInfo,String cmdCode,String wmsCode,String wmsTrayCode,String wmsCatagary){ @@ -526,69 +581,6 @@ public class PlcService { } Boolean trayGoodCheck = Boolean.TRUE; - //扫货物 - if(configProperties.getScanCodeMode().getGoods() == 2) { - log.info("扫码类型:" + configProperties.getScanCodeMode().getTray()); - //托盘使用sick扫码枪 - SensorGun sensorGun = sensorService.getSensorByPlc(street.getId(), plcCmdInfo.getLeftRight1()); - if (sensorGun == null) { - trayCode = "扫码枪识别异常"; - trayGoodCheck = Boolean.FALSE; - log.error("no sensor gun config in database ,street id:{},direction:{}", street.getId(), plcCmdInfo.getLeftRight1()); - } else { - trayCode = SickSocket.readOCR(sensorGun.getIp(), sensorGun.getPort()); - log.info("sensor tray code:{}", trayCode); - if ("".equals(wmsTrayCode)) { - //托盘码为空,无货物 - //只要扫码枪未识别出条码,即认为盘点正确 - if (StringUtils.isEmpty(trayCode) || trayCode.equals("NoRead")) { - trayCode = ""; - trayGoodCheck = Boolean.TRUE; - } else { - trayCode = "扫码枪识别异常"; - trayGoodCheck = Boolean.FALSE; - log.warn("sick ocr error:{}", trayCode); - } - } else { - if ((StringUtils.isEmpty(trayCode) || trayCode.equals("NoRead")) ){ - trayCode = "扫码枪识别异常"; - trayGoodCheck = Boolean.FALSE; - log.warn("sick ocr error:{}", trayCode); - } else if(trayCode.equals(wmsTrayCode)){ - //扫到码 - trayGoodCheck = Boolean.TRUE; - }else { - - trayGoodCheck = Boolean.FALSE; - } - } - } - }else - if(configProperties.getScanCodeMode().getGoods() == 3){ - Set tags = new HashSet<>(); - try { - RFIDCheck(plcCmdInfo); - Thread.sleep(1000 * configProperties.getRfid().getScanTime()); - } catch (Exception e) { - e.printStackTrace(); - } finally { - tags = RFIDStop(plcCmdInfo); - log.info("盘点rfid扫描结果:" + tags); - } - if (tags !=null && tags.contains(wmsTrayCode)) { - //托盘码为空,无货物 - //只要扫码枪未识别出条码,即认为盘点正确 - trayCode = wmsTrayCode; - trayGoodCheck = Boolean.TRUE; - - } else { - trayCode = trayCode+ "扫码枪识别异常"; - trayGoodCheck = Boolean.FALSE; - log.warn("rfid error:{}", trayCode); - - } - } - OrderInfo orderInfo = new OrderInfo(street, plcCmdInfo, 1, cmdCode); Stock stock = stockMapper.getByStreetAndDirectionAndSideAndRowColumn(orderInfo.getStreetId(), plcCmdInfo.getLeftRight1(), orderInfo.getSeparation(), orderInfo.getRow(), orderInfo.getColumn()); @@ -932,11 +924,7 @@ public class PlcService { return true; } - /** - * 保存盘点记录 - * @param scTransmission - * @return - */ + // public Stock checkStart(SCTransmission scTransmission){ // log.info("【开始盘点货位】save stock info ,taskId:{},SRMNumber:{},goodsLocation:{}",scTransmission.getTaskNo(),scTransmission.getSRMNumber(),scTransmission.getGoodsLocation()); // Street street = streetService.getStreetByPlcId(scTransmission.getSRMNumber()); diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/FeatureMatchingExample.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/FeatureMatchingExample.java new file mode 100644 index 0000000..de5b8c4 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/FeatureMatchingExample.java @@ -0,0 +1,305 @@ +package com.zhehekeji.web.service.algorithm; + + +import org.opencv.core.*; +import org.opencv.core.Point; +import org.opencv.features2d.DescriptorMatcher; +import org.opencv.features2d.Features2d; +import org.opencv.features2d.ORB; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; + +/** + * 通过opencv进行图片识别,包括图片识别,模板建立,模板识别等 + */ + +public class FeatureMatchingExample { + static { + // 加载动态库 + + try { + String path = System.getProperty("user.dir")+"\\libs"; + System.setProperty("java.library.path", path); + Field fieldSysPath = null; + fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); + + fieldSysPath.setAccessible(true); + fieldSysPath.set(null, null); + //URL url = ClassLoader.getSystemResource(System.getProperty("user.dir")+"/libs/opencv_java480.dll"); + + System.loadLibrary("opencv_java480"); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + + public static Mat base642Mat(String base64Image) throws IOException { + // 解码 Base64 字符串 + byte[] imageBytes = Base64.getDecoder().decode(base64Image); + + // 将字节数组转换为 BufferedImage + BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytes)); + + // 转换 BufferedImage 为 Mat + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + Mat mat = new Mat(height, width, CvType.CV_8UC3); + + // 读取 BufferedImage 数据到 Mat + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgb = bufferedImage.getRGB(x, y); + byte blue = (byte) (rgb & 0xFF); + byte green = (byte) ((rgb >> 8) & 0xFF); + byte red = (byte) ((rgb >> 16) & 0xFF); + mat.put(y, x, new byte[]{blue, green, red}); + } + } + + return mat; + } + + + public static void main(String[] args) { + // 加载图像 + // 创建 URL 对象 + //特征图 + + Mat img2 = Imgcodecs.imread("D:\\WeChet\\WeChat Files\\wxid_ttkf0xgmyihv22\\FileStorage\\File\\2024-09\\aa20c309-7412-4392-88cd-aef08153f7c8 (2).jpeg");;//原图 + Mat img1 = Imgcodecs.imread("D:\\WeChet\\WeChat Files\\wxid_ttkf0xgmyihv22\\FileStorage\\File\\2024-09\\aa20c309-7412-4392-88cd-aef08153f7c8.jpeg"); + + /* try { + img1 = base642Mat("data:image/png;base64,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".replace("data:image/jpeg;base64,","").replace("data:image/png;base64,","")); + } catch (IOException e) { + throw new RuntimeException(e); + }*/ + + // 生成特征图 + Mat featureMap1 = generateFeatureMap(img1); + Imgcodecs.imwrite("D:\\WeChet\\WeChat Files\\wxid_ttkf0xgmyihv22\\FileStorage\\File\\2024-09\\feature_map1.jpg", featureMap1); + + // 检查两张图片是否匹配 + boolean isMatched = matchFeatures(img1, img2); + System.out.println("Images matched: " + isMatched); + //FeatureOrbLannbased(img1, img2); + matchTemplate(img1, img2,"D:\\WeChet\\WeChat Files\\wxid_ttkf0xgmyihv22\\FileStorage\\File\\2024-09\\2oooo.jpg"); + // 裁剪图片 +// Mat croppedImage = cropImage(img1, 100, 100, 200, 200); +// Imgcodecs.imwrite("E:\\m\\3.png", croppedImage); + } + + // 生成特征图的方法 + // 生成特征图的方法 + public static Mat generateFeatureMap(Mat image) { + // 转换为灰度图 + Mat gray = new Mat(); + Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY); + + // 创建 ORB 检测器 + ORB orb = ORB.create(); + + // 找到关键点 + MatOfKeyPoint keypoints = new MatOfKeyPoint(); + orb.detect(gray, keypoints); + + // 在图像上画出关键点 + Mat featureMap = drawKeypoints(image, keypoints); + + return featureMap; + } + + // 绘制关键点的方法 + private static Mat drawKeypoints(Mat image, MatOfKeyPoint keypoints) { + Mat outputImage = image.clone(); // 创建一个输出图像的副本 + for (KeyPoint kp : keypoints.toArray()) { + // 获取关键点的位置 + double x = kp.pt.x; + double y = kp.pt.y; + + // 绘制关键点 + Imgproc.circle(outputImage, new org.opencv.core.Point(x, y), 3, new Scalar(0, 255, 0), -1); + } + + return outputImage; + } + + public static Mat FeatureOrbLannbased(Mat src, Mat dst) { + DescriptorMatcher Matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_L1); + // 转换为灰度图 + Mat gray1 = new Mat(); + Mat gray2 = new Mat(); + Imgproc.cvtColor(src, gray1, Imgproc.COLOR_BGR2GRAY); + Imgproc.cvtColor(dst, gray2, Imgproc.COLOR_BGR2GRAY); + + // 创建 ORB 检测器 + ORB orb = ORB.create(); + + // 找到关键点 + MatOfKeyPoint keypoints1 = new MatOfKeyPoint(); + MatOfKeyPoint keypoints2 = new MatOfKeyPoint(); + orb.detect(gray1, keypoints1); + orb.detect(gray2, keypoints2); + + // 计算描述符 + Mat descriptors1 = new Mat(); + Mat descriptors2 = new Mat(); + orb.compute(gray1, keypoints1, descriptors1); + orb.compute(gray2, keypoints2, descriptors2); + + Features2d.drawKeypoints(src, keypoints1, src); + + Features2d.drawKeypoints(dst, keypoints2, dst); + + + // Matching features + + MatOfDMatch Matches = new MatOfDMatch(); + Matcher.match(descriptors1, descriptors2, Matches); + + double maxDist = Double.MIN_VALUE; + double minDist = Double.MAX_VALUE; + + DMatch[] mats = Matches.toArray(); + for (int i = 0; i < mats.length; i++) { + double dist = mats[i].distance; + if (dist < minDist) { + minDist = dist; + } + if (dist > maxDist) { + maxDist = dist; + } + } + System.out.println("Min Distance:" + minDist); + System.out.println("Max Distance:" + maxDist); + List goodMatch = new LinkedList<>(); + + for (int i = 0; i < mats.length; i++) { + double dist = mats[i].distance; + if (dist < 3 * minDist && dist < 0.2f) { + goodMatch.add(mats[i]); + } + } + + Matches.fromList(goodMatch); + // Show result + Mat OutImage = new Mat(); + Features2d.drawMatches(src, keypoints1, dst, keypoints2, Matches, OutImage); + + Imgcodecs.imwrite("E:\\m\\qqqY4.jpg", OutImage); + return OutImage; + } + + public static boolean matchTemplate(List templates, String srcImage) { + String srcImgRest = srcImage; + boolean macthBoolean = false; + for(String template: templates){ + if(FeatureMatchingExample.matchTemplate( Imgcodecs.imread(template), Imgcodecs.imread(srcImgRest), srcImage+".jpg")){ + //识别成功后之后识别根据结果图识别 + + srcImgRest = srcImage+".jpg"; + + macthBoolean = true; + break; + } + } + return macthBoolean; + } + /** + * 模板匹配 + * @param template 模板 + * @param image 图 + * @return + */ + public static boolean matchTemplate(Mat template, Mat image,String path) { + // Convert the images to grayscale + Mat grayImage = new Mat(); + Mat grayTemplate = new Mat(); + Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY); + Imgproc.cvtColor(template, grayTemplate, Imgproc.COLOR_BGR2GRAY); + + // 执行模板匹配 + Mat result = new Mat(); + Imgproc.matchTemplate(grayImage, grayTemplate, result, Imgproc.TM_CCOEFF_NORMED); + + // 检测匹配的阈值 + double threshold = 0.6; // 根据需要调整此阈值 + Mat mask = new Mat(); + Core.compare(result, new Scalar(threshold), mask, Core.CMP_GT); + + // 查找匹配值高于阈值的位置 + MatOfPoint loc = new MatOfPoint(); + Core.findNonZero(mask, loc); + + // Draw rectangles around the detected features + for (Point p : loc.toArray()) { + Rect rect = new Rect(p, new Size(template.cols(), template.rows())); + Imgproc.rectangle(image, rect.tl(), rect.br(), new Scalar(0, 255, 0), 1); + } + + // 保存图片 + Imgcodecs.imwrite(path, image); + if (loc.toArray().length > 0) { + System.out.println("Template found in the image."); + return true; + } else { + System.out.println("Template not found in the image."); + return false; + } + } + // 识别两个图片是否一致的方法 + public static boolean matchFeatures(Mat img1, Mat img2) { + // 转换为灰度图 + Mat gray1 = new Mat(); + Mat gray2 = new Mat(); + Imgproc.cvtColor(img1, gray1, Imgproc.COLOR_BGR2GRAY); + Imgproc.cvtColor(img2, gray2, Imgproc.COLOR_BGR2GRAY); + + // 创建 ORB 检测器 + ORB orb = ORB.create(); + + // 找到关键点 + MatOfKeyPoint keypoints1 = new MatOfKeyPoint(); + MatOfKeyPoint keypoints2 = new MatOfKeyPoint(); + orb.detect(gray1, keypoints1); + orb.detect(gray2, keypoints2); + + // 计算描述符 + Mat descriptors1 = new Mat(); + Mat descriptors2 = new Mat(); + orb.compute(gray1, keypoints1, descriptors1); + orb.compute(gray2, keypoints2, descriptors2); + + // 创建匹配器 + DescriptorMatcher matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_HAMMING); + + // 匹配描述符 + MatOfDMatch matches = new MatOfDMatch(); + matcher.match(descriptors1, descriptors2, matches); + + // 设置阈值以判断匹配是否成功 + int threshold = 100; // 可以根据实际情况调整 + boolean l =true; + return l; + } + + // 图片裁剪的方法 + public static Mat cropImage(Mat image, int x, int y, int width, int height) { + // 裁剪图像 + Mat croppedImage = image.submat(y, y + height, x, x + width); + + return croppedImage; + } +} diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/InventoryService.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/InventoryService.java new file mode 100644 index 0000000..f8ec069 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/InventoryService.java @@ -0,0 +1,215 @@ +package com.zhehekeji.web.service.algorithm; + +import com.zhehekeji.web.config.ConfigProperties; +import com.zhehekeji.web.entity.CheckLog; +import com.zhehekeji.web.entity.Stock; +import com.zhehekeji.web.entity.Street; +import com.zhehekeji.web.mapper.CheckLogMapper; +import com.zhehekeji.web.mapper.StockMapper; +import com.zhehekeji.web.service.IndustrialCamera.CameraSaveUtil; +import com.zhehekeji.web.service.IndustrialCamera.LxPointCloudSaveImage; +import com.zhehekeji.web.service.PlcService; +import com.zhehekeji.web.service.StreetService; +import com.zhehekeji.web.service.client.TransmissionPojo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.File; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +public class InventoryService { + @Resource + StreetService streetService; + + @Resource + CameraSaveUtil cameraSaveUtil; + + @Resource + ConfigProperties configProperties; + + @Resource + CheckLogMapper checkLogMapper; + + + @Resource + StockMapper stockMapper; + @Resource + PlcService plcService; + + + public boolean match2D(String category,String path){ + boolean flag = false; + + //2d拍照并识别 + cameraSaveUtil.saveImage(configProperties.getCameraConfig().getIndustrialCamera(),path,"sn"); + log.info(configProperties.getCameraConfig().getIndustrialCamera()+" 2d拍照路径:"+path); + if(category!=null && !category.equals("")) { + flag = FeatureMatchingExample.matchTemplate( + readImagesInFolder(configProperties.getSavePath().getMediaPath() + "template/" + category), + path ); + log.info("2d识别结果:"+flag); + + }else { + + log.info("未识别到任何模板"); + } + return flag; + } + + public int match3D(String category,String path){ + LocalDate currentDate = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + Boolean flag = false; + + //拍照 + log.info("3D拍照pcd: "+path); + LxPointCloudSaveImage.saveImage(configProperties.getCameraConfig().getCamera3D(), path,1); + return 30; + } +// +// +// public boolean matchFeatures(TransmissionPojo transmissionPojo){ +// Street street = streetService.getStreetByPlcId(transmissionPojo.getStreetNumber()); +// +// LocalDate currentDate = LocalDate.now(); +// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); +// //路径 +// String path = configProperties.getSavePath().getMediaPath() +// +"industrialCamera/" +// +transmissionPojo.getStreetNumber()+"/" +// +currentDate.format(formatter)+"/" +// +transmissionPojo.getRow()+"-" +// +transmissionPojo.getColumn()+"-" +// +transmissionPojo.getDirection()+"-"; +// //结果 +// transmissionPojo.setResult(1); +// //2d拍照并识别 +// if(street != null&& street.getIndustrialCamera()!=null){ +// cameraSaveUtil.saveImage(street.getIndustrialCamera(),path+street.getIndustrialCamera()+".jpeg","sn"); +// log.info(street.getIndustrialCamera()+" 2d拍照路径:"+path+street.getIndustrialCamera()+".jpeg"); +// if(transmissionPojo.getCategory()!=null && !transmissionPojo.getCategory().equals("")) { +// boolean re = FeatureMatchingExample.matchTemplate( +// readImagesInFolder(configProperties.getSavePath().getMediaPath() + "template/" + transmissionPojo.getCategory()), +// path + street.getIndustrialCamera() + ".jpeg"); +// log.info("2d识别结果:"+re); +// if (!re) { +// transmissionPojo.setResult(0); +// } +// }else { +// transmissionPojo.setResult(0); +// log.info("未识别到任何模板"); +// } +// } +// //3d拍照盘点 +// //盘点地址 +// String pcdPath1 =configProperties.getSavePath().getMediaPath() + street.getLeft3D()+ "/"+currentDate.format(formatter)+"/"+transmissionPojo.getCheckId()+".pcd"; +// String pcdPath2 =configProperties.getSavePath().getMediaPath() + street.getRight3D()+ "/"+ currentDate.format(formatter)+"/"+transmissionPojo.getCheckId()+".pcd"; +// //随行地址 +// //随行地址 +// String pcdPath3 =configProperties.getSavePath().getMediaPath() + street.getLeft3D()+"\\000\\" +// +transmissionPojo.getRow()+"-"+transmissionPojo.getColumn()+"-"+transmissionPojo.getDirection()+"-L"+".pcd"; +// +// String pcdPath4 =configProperties.getSavePath().getMediaPath() + street.getRight3D()+"\\000\\" +// +transmissionPojo.getRow()+"-"+transmissionPojo.getColumn()+"-"+transmissionPojo.getDirection()+"-R"+".pcd"; +// +// //拍照 +// log.info("3D拍照 左侧pcd: "+pcdPath1+" 右侧pcd:: "+pcdPath2); +// +// LxPointCloudSaveImage.saveImage(street.getLeft3D(), pcdPath1,1); +// +// LxPointCloudSaveImage.saveImage(street.getRight3D(), pcdPath2,2); +// //左侧比较 +// PcdPojo pojo = new PcdPojo(); +// pojo.setPcd1(pcdPath1); +// pojo.setPcd2(pcdPath3); +// pojo.setConfigPath("D://config//3DConfig/"+street.getLeft3D()+".json"); +// log.info("对比左侧:"+pojo.toString()); +// +// double leftScore = PointCloudProcessor.similarity(pojo,10); +// log.info("3D leftScore:"+leftScore); +// +// //右侧比较 +// pojo.setPcd1(pcdPath2); +// pojo.setPcd2(pcdPath4); +// pojo.setConfigPath("D://config//3DConfig/"+street.getRight3D()+".json"); +// log.info("对比右侧:"+pojo.toString()); +// +// double rightScore = PointCloudProcessor.similarity( pojo,10); +// +// log.info("3D rightScore:"+rightScore); +// if(leftScore<0.87||rightScore<0.87){ +// transmissionPojo.setResult(0); +// } +// log.info("最终结果:"+transmissionPojo.getResult()); +// CheckLog checkLog = checkLogMapper.selectById(transmissionPojo.getCheckId()); +// plcService.visualCalculationResults(transmissionPojo,checkLog); +// +// Stock stock = stockMapper.getByStreetAndDirectionAndSideAndRowColumn(street.getId(), transmissionPojo.getDirection(),1, transmissionPojo.getRow(), transmissionPojo.getColumn()); +// +// checkLogMapper.updateById(checkLog); +// if (stock == null) { +// stock = Stock.builder() +// .streetId(street.getId()) +// .row(transmissionPojo.getRow()) +// .column(transmissionPojo.getColumn()) +// .direction(transmissionPojo.getDirection()) +// .side(1) +// .statusVision(checkLog.getStatusVision()) +// .count(checkLog.getCount()) +// .category(checkLog.getCategory()) +// .exportTime(LocalDateTime.now()).build(); +// stockMapper.insert(stock); +// } else { +// stock.setStatusVision(checkLog.getStatusVision()); +// stock.setCount(checkLog.getCount()); +// stock.setCategory(checkLog.getCategory()); +// stockMapper.updateById(stock); +// } +// return !(transmissionPojo.getResult()==0); +// +// }; +// +// +// + + + /** + * 获取文件类型 + * @param folderPath + * @return + */ + public static List readImagesInFolder(String folderPath) { + File folder = new File(folderPath); + List list = new ArrayList<>(); + // Check if the folder exists and is a directory + if (!folder.exists() || !folder.isDirectory()) { + log.info(folderPath); + log.info("The specified path is not a valid directory."); + return list; + } + // List all files in the folder that match the image filter + File[] imageFiles = folder.listFiles(); + for(File imageFile : imageFiles){ + if(!imageFile.isDirectory()) { + list.add(imageFile.getAbsolutePath()); + log.info("Found image: " + imageFile.getAbsolutePath()); + } + log.info("Found file: " + imageFile.getAbsolutePath()); + } + + + return list; + } + + public static void main(String[] args) { + + + } +} diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/PCDReader.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/PCDReader.java new file mode 100644 index 0000000..9fd455c --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/PCDReader.java @@ -0,0 +1,77 @@ +package com.zhehekeji.web.service.algorithm; + + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class PCDReader { + + public static void main(String[] args) { + String filePath = "path/to/your/file.pcd"; + try { + if (isBinaryFormat(filePath)) { + readBinaryPCD(filePath); + } else { + readAsciiPCD(filePath); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static boolean isBinaryFormat(String filePath) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("DATA")) { + return line.contains("binary"); + } + } + } + return false; + } + + private static void readAsciiPCD(String filePath) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("#") && !line.startsWith("FIELDS") && !line.startsWith("SIZE") && + !line.startsWith("TYPE") && !line.startsWith("COUNT") && !line.startsWith("WIDTH") && + !line.startsWith("HEIGHT") && !line.startsWith("VIEWPOINT") && !line.startsWith("POINTS") && + !line.startsWith("DATA")) { + String[] values = line.split(" "); + for (String value : values) { + System.out.println(value); + } + } + } + } + } + + private static void readBinaryPCD(String filePath) throws IOException { + try (FileInputStream fis = new FileInputStream(filePath)) { + ByteBuffer buffer = ByteBuffer.wrap(fis.readAllBytes()); + buffer.order(ByteOrder.LITTLE_ENDIAN); + // Skip header (assuming 11 lines of header; adjust if needed) + for (int i = 0; i < 11; i++) { + while (buffer.get() != '\n') { + // Skip header lines + } + } + // Read binary data (assuming float x, y, z and int rgb) + while (buffer.hasRemaining()) { + float x = buffer.getFloat(); + float y = buffer.getFloat(); + float z = buffer.getFloat(); + int rgb = buffer.getInt(); // Read 4 bytes for RGB + System.out.printf("x: %f, y: %f, z: %f, rgb: %d%n", x, y, z, rgb); + } + } + } + + +} diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/PcdPojo.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/PcdPojo.java new file mode 100644 index 0000000..06c211a --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/PcdPojo.java @@ -0,0 +1,42 @@ +package com.zhehekeji.web.service.algorithm; + +import lombok.Data; + +@Data +public class PcdPojo { + + double[][] rotationMatrix = { + {0.9982215762138367, 0.057795289903879166, 0.014606773853302002}, + {-0.0510917492210865, 0.7032182216644287, 0.7091360092163086}, + {0.030712969601154327, -0.7086211442947388, 0.7049204111099243} + }; // 这里使用给定的旋转矩阵 + double[] minBounds = {-905.5771484375, 174.71572875976562, 69.14165496826172}; // 裁剪最小值 + double[] maxBounds = {321.80609130859375, 1170.4776611328125, 1199.3507080078125}; // 裁剪最大值 + + + private double floorHeight; + + private double[] max_pt; + private double[] min_pt; + private double[] rotation; + //路径D://config//3DConfig/"+相机sn+".json" + private String configPath; + //盘点路径 configProperties.getSavePath().getMediaPath() + street.getLeft3D()+ currentDate.format(formatter)+"/"+transmissionPojo.getCheckId()+".pcd"; + //随行路径 configProperties.getSavePath().getMediaPath() + street.getLeft3D()+"000/"+ currentDate.format(formatter)+"/"+transmissionPojo.getRow()+".pcd"; + private String pcd1; + private String pcd2; + + private String cameraSn1; + private String cameraSn2; + private Integer count; + PcdPojo setPCDInfo(PcdPojo pojo) { + this.setMinBounds(pojo.getMin_pt()); + this.setMaxBounds(pojo.getMax_pt()); + double[] x = {pojo.getRotation()[0], pojo.getRotation()[1], pojo.getRotation()[2]}; + double[] y = {pojo.getRotation()[3], pojo.getRotation()[4], pojo.getRotation()[5]}; + double[] z = {pojo.getRotation()[6], pojo.getRotation()[7], pojo.getRotation()[8]}; + this.setRotationMatrix(new double[][]{x, y, z}); + return this; + } + +} diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java new file mode 100644 index 0000000..1520bb4 --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java @@ -0,0 +1,388 @@ +package com.zhehekeji.web.service.algorithm; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class PointCloudProcessor { + + + public static void main(String[] args) { + PcdPojo pojo = new PcdPojo(); + pojo.setPcd1("D:\\WeChet\\WeChat Files\\wxid_ttkf0xgmyihv22\\FileStorage\\File\\2024-09\\8-6-2-L.pcd"); + pojo.setPcd2("D:\\WeChet\\WeChat Files\\wxid_ttkf0xgmyihv22\\FileStorage\\File\\2024-09\\369.pcd"); + pojo.setConfigPath("E:\\D91FCF1FAB3568DB.json"); + double i = similarity(pojo,10); + System.out.println("111 "+i); + } + + /** + * 计算两个pcd的相似性 + * + * @param pojo 必须有两个pcd文件路径,和一个配置文件 + * @return + */ + //计算两个pcd的相似性 + public static double similarity(PcdPojo pojo) { + + ObjectMapper mapper = new ObjectMapper(); + + try { + // 读取 JSON 文件并转换为 配置信息 + PcdPojo pojoIn = mapper.readValue(new File(pojo.getConfigPath()), mapper.getTypeFactory().constructType(PcdPojo.class)); + + + // 打印转换后的实体类列表 + pojo = pojo.setPCDInfo(pojoIn); + + //报错则认为没有配置文件,返回盘点失败 + } catch (JsonMappingException e) { + e.printStackTrace(); + return 0; + } catch (JsonProcessingException e) { + e.printStackTrace(); + return 0; + } catch (IOException e) { + e.printStackTrace(); + return 0; + } + System.out.println(pojo.toString()); + // 计算pcd1 + List points = readPCD(pojo.getPcd1()); + if (points == null || points.isEmpty()) { + return 0; + } + + // 创建一个DecimalFormat对象,指定格式 + DecimalFormat df = new DecimalFormat("#.#"); + //map xyz + PcdPojo finalPojo = pojo; + Map> clippedPoint = points.stream() + .map(point -> { + //旋转 + return rotatePoints(point, finalPojo.getRotationMatrix()); + + }) + .filter(point -> + //截取 + clipPoints(point, finalPojo.getMinBounds(), finalPojo.getMaxBounds()) + )//z轴截取 + .collect(Collectors.groupingBy(point -> point[0], Collectors.toMap(point -> point[1], point -> point[2], (a, b) -> { + return a > b ? a : b; + }))); + //计算x,y轴平均值 + Double xAge = clippedPoint.keySet().stream().collect(Collectors.averagingDouble(v -> v)); + Double yAge = clippedPoint.values().stream().flatMap(map -> map.values().stream()).collect(Collectors.averagingDouble(v -> v)); + + //计算pcd2 + List points2 = readPCD(pojo.getPcd2()); + if (points2 == null || points2.isEmpty()) { + return 0; + } + + //map xyz + Map> clippedPoint2 = points2.stream() + .map(point -> { + //旋转 + return rotatePoints(point, finalPojo.getRotationMatrix()); + + }) + .filter(point -> + //截取 + clipPoints(point, finalPojo.getMinBounds(), finalPojo.getMaxBounds()) + )//z轴截取 + .collect(Collectors.groupingBy(point -> point[0], Collectors.toMap(point -> point[1], point -> point[2], (a, b) -> { + return a > b ? a : b; + }))); + + //计算x,y轴平均值 + Double xAge2 = clippedPoint2.keySet().stream().collect(Collectors.averagingDouble(v -> v)); + Double yAge2 = clippedPoint2.values().stream().flatMap(map -> map.values().stream()).collect(Collectors.averagingDouble(v -> v)); + //计算相似度 + return (Math.abs(yAge) < Math.abs(yAge2) ? Math.abs(yAge) / Math.abs(yAge2) : Math.abs(yAge2) / Math.abs(yAge)); + } + + public static double similarity(PcdPojo pojo,int i) { + ObjectMapper mapper = new ObjectMapper(); + try { + // 读取 JSON 文件并转换为 配置信息 + PcdPojo pojoIn = mapper.readValue(new File(pojo.getConfigPath()), mapper.getTypeFactory().constructType(PcdPojo.class)); + // 打印转换后的实体类列表 + pojo = pojo.setPCDInfo(pojoIn); + //报错则认为没有配置文件,返回盘点失败 + } catch (JsonMappingException e) { + e.printStackTrace(); + return 0; + } catch (JsonProcessingException e) { + e.printStackTrace(); + return 0; + } catch (IOException e) { + e.printStackTrace(); + return 0; + } + System.out.println(pojo.toString()); + // 计算pcd1 + List points = readPCD(pojo.getPcd1()); + if (points == null || points.isEmpty()) { + return 0; + } + + //计算pcd2 + List points2 = readPCD(pojo.getPcd2()); + if (points2 == null || points2.isEmpty()) { + return 0; + } + + //计算相似度 + return calculateTruncatedMeans(points,points2,pojo,i,new ArrayList<>()); + } + + + /** + * 计算每个子区域内的 y 坐标的截断均值 + * + * @param pointCloud 点云列表 + * @param divisions 每边分割的数量 + * @return 一个列表,其中每个元素是一个长度为3的一维数组,表示每个子区域的左下角坐标、右上角坐标和该区域内的 y 坐标的截断均值 + */ + public static double calculateTruncatedMeans(List pointCloud, List pointCloud2,PcdPojo pojo, int divisions,List results) { + + double width = (pojo.getMaxBounds()[0] - pojo.getMinBounds()[0]) / divisions; // 每个小方块的宽度 + double depth = (pojo.getMaxBounds()[2] - pojo.getMinBounds()[2]) / divisions; // 每个小方块的深度 + int count = 0; + int countSum = 0; + for (int i = 0; i < divisions; i++) { + for (int j = 0; j < divisions; j++) { + double leftBottomX = pojo.getMinBounds()[0] + i * width; // 左下角 x 坐标 + double leftBottomZ = pojo.getMinBounds()[2] + j * depth; // 左下角 z 坐标 + double rightTopX = pojo.getMinBounds()[0] + (i + 1) * width; // 右上角 x 坐标 + double rightTopZ = pojo.getMinBounds()[2] + (j + 1) * depth; // 右上角 z 坐标 + + List yValues = new ArrayList<>(); + for (double[] point : pointCloud) { + point = rotatePoints(point, pojo.getRotationMatrix()); + if(clipPoints(point, pojo.getMinBounds(), pojo.getMaxBounds())) { + if (point[0] >= leftBottomX && point[0] < rightTopX && + point[2] >= leftBottomZ && point[2] < rightTopZ) { + yValues.add(point[1]); + } + } + } + + List yValues2 = new ArrayList<>(); + for (double[] point : pointCloud2) { + point = rotatePoints(point, pojo.getRotationMatrix()); + if(clipPoints(point, pojo.getMinBounds(), pojo.getMaxBounds())) { + if (point[0] >= leftBottomX && point[0] < rightTopX && + point[2] >= leftBottomZ && point[2] < rightTopZ) { + yValues2.add(point[1]); + } + } + } + double truncatedMean = calculateTruncatedMean(yValues, 0.1); // 截断比例为10% + double truncatedMean2 = calculateTruncatedMean(yValues2, 0.1); // 截断比例为10% + double similarity = (truncatedMean - pojo.getMinBounds()[1])/(pojo.getMaxBounds()[1] - pojo.getMinBounds()[1]); + double similarity2 = (truncatedMean2 - pojo.getMinBounds()[1])/(pojo.getMaxBounds()[1] - pojo.getMinBounds()[1]); + countSum++; + if(Double.isNaN(similarity) && Double.isNaN(similarity2)){ + break; + } + System.out.println("similarity:"+similarity+"similarity2:"+similarity2); + double max = Math.max(similarity, similarity2); // 获取较大数 + double min = Math.min(similarity, similarity2); // 获取较小数 + if(max-min<0.01){ + count++; + } + results.add(new double[]{leftBottomX, leftBottomZ, truncatedMean, truncatedMean2}); + } + } + System.out.println(count); + pojo.setCount(count); + return (double) count /countSum; + } + + + /** + * 计算截断均值 + * + * @param values 值列表 + * @param trimRatio 截断比例 + * @return 截断均值 + */ + private static double calculateTruncatedMean(List values, double trimRatio) { + int trimCount = (int) Math.ceil(values.size() * trimRatio); + values.sort(Comparator.naturalOrder()); + double sum = 0; + for (int i = trimCount; i < values.size() - trimCount; i++) { + sum += values.get(i); + } + return sum / (values.size() - 2 * trimCount); + } + + public static List readPCD(String filePath) { + try { + if (isBinaryFormat(filePath)) { + return readBinaryPCD(filePath); + } else { + return readAsciiPCD(filePath); + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + + } + + private static boolean isBinaryFormat(String filePath) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("DATA")) { + return line.contains("binary"); + } + } + } + return false; + } + + private static List readAsciiPCD(String filePath) throws IOException { + List points = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + + while ((line = reader.readLine()) != null) { + if (line.startsWith("DATA")) { + break; + } + } + while ((line = reader.readLine()) != null) { + double[] point = new double[3]; + String[] values = line.split(" "); + point[0] = Double.parseDouble(values[0]); + point[1] = Double.parseDouble(values[1]); + point[2] = Double.parseDouble(values[2]); + points.add(point); + + } + } + return points; + } + + private static List readBinaryPCD(String filePath) throws IOException { + List points = new ArrayList<>(); + try (FileInputStream fis = new FileInputStream(filePath)) { + ByteBuffer buffer = ByteBuffer.wrap(fis.readAllBytes()); + buffer.order(ByteOrder.LITTLE_ENDIAN); + // Skip header (assuming header size is known, or read until DATA section) + // This example assumes 11 lines of header, which may vary + for (int i = 0; i < 11; i++) { + while (buffer.get() != '\n') { + // Skip header lines + } + } + // Read binary data + while (buffer.hasRemaining()) { + double x = buffer.getFloat(); + double y = buffer.getFloat(); + double z = buffer.getFloat(); + int rgb = buffer.getInt(); // Read 4 bytes for RGB + double[] point = {x, y, z}; + points.add(point); + } + } catch (Exception e) { + return null; + } + return points; + } + + static void getPCDArray(String filePath) { + + } + + static void getPCDArray(Map> clippedPoint, String filePath) { + + // 创建一个DecimalFormat对象,指定格式 + DecimalFormat df = new DecimalFormat("#.####"); + File file = new File(filePath + ".txt"); + file.delete(); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath + ".txt"))) { + bw.write("["); + for (Double x : clippedPoint.keySet()) { + for (Double y : clippedPoint.get(x).keySet()) { + double z = clippedPoint.get(x).get(y); + bw.write("[" + df.format(x) + ", " + df.format(y) + ", " + df.format(z) + "],\n"); + } + } + bw.write("]"); + } catch (IOException e) { + e.printStackTrace(); + } + // 读取点云数据 + + + } + + public static double[] rotatePoints(double[] point, double[][] rotationMatrix) { + + double x = point[0]; + double y = point[1]; + double z = point[2]; + + double[] rotatedPoint = new double[3]; + rotatedPoint[0] = rotationMatrix[0][0] * x + rotationMatrix[0][1] * y + rotationMatrix[0][2] * z; + rotatedPoint[1] = rotationMatrix[1][0] * x + rotationMatrix[1][1] * y + rotationMatrix[1][2] * z; + rotatedPoint[2] = rotationMatrix[2][0] * x + rotationMatrix[2][1] * y + rotationMatrix[2][2] * z; + + return rotatedPoint; + } + + public static List clipPoints(List points, double[] minBounds, double[] maxBounds) { + List clippedPoints = new ArrayList<>(); + for (double[] point : points) { + if (point[0] >= minBounds[0] && point[0] <= maxBounds[0] && + point[1] >= minBounds[1] && point[1] <= maxBounds[1] && + point[2] >= minBounds[2] && point[2] <= maxBounds[2]) { + clippedPoints.add(point); + } + } + return clippedPoints; + } + + public static boolean clipPoints(double[] point, double[] minBounds, double[] maxBounds) { + + return point[0] >= minBounds[0] && point[0] <= maxBounds[0] && //x轴截取 + point[1] >= minBounds[1] && point[1] <= maxBounds[1] && //y轴截取 + point[2] >= minBounds[2] && point[2] <= maxBounds[2]; + } + + public static void writePCD(List points, String filePath) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) { + bw.write("# .PCD v0.7 - Point Cloud Data file format\n"); + bw.write("VERSION 0.7\n"); + bw.write("FIELDS x y z\n"); + bw.write("SIZE 4 4 4\n"); + bw.write("TYPE F F F\n"); + bw.write("COUNT 1 1 1\n"); + bw.write("WIDTH " + points.size() + "\n"); + bw.write("HEIGHT 1\n"); + bw.write("VIEWPOINT 0 0 0 1 0 0 0\n"); + bw.write("PIXEL_FORMAT ascii\n"); + bw.write("DATA ascii\n"); + for (double[] point : points) { + bw.write(point[0] + " " + point[1] + " " + point[2] + "\n"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/web/src/main/java/com/zhehekeji/web/service/client/TransmissionPojo.java b/web/src/main/java/com/zhehekeji/web/service/client/TransmissionPojo.java index 77d7148..3279966 100644 --- a/web/src/main/java/com/zhehekeji/web/service/client/TransmissionPojo.java +++ b/web/src/main/java/com/zhehekeji/web/service/client/TransmissionPojo.java @@ -72,6 +72,7 @@ public class TransmissionPojo { this.checkId = convertNumberToFixedLengthChars(row, 3) + convertNumberToFixedLengthChars(column, 3); } + //输出 public String toString(TransmissionType type) { this.header = type.toString(); if (type == TransmissionType.ST) { diff --git a/web/src/main/java/com/zhehekeji/web/service/ksec/KsecDecoder.java b/web/src/main/java/com/zhehekeji/web/service/ksec/KsecDecoder.java index f593257..96b79c4 100644 --- a/web/src/main/java/com/zhehekeji/web/service/ksec/KsecDecoder.java +++ b/web/src/main/java/com/zhehekeji/web/service/ksec/KsecDecoder.java @@ -214,7 +214,7 @@ public class KsecDecoder extends DelimiterBasedFrameDecoder { log.info("盘点结束:"+ksecInfo.getData().toString()); }else if (Cmd.SC.name().equals(ksecInfo.getType())) { - //plcService.checkVision(); + plcService.checkVision(dataInfo); } //找到该货位的最后一张照片与现在的照片比照 //plcService.recordStock(plcCmdInfo, dataInfo.getCode(), 0, 0); diff --git a/web/src/main/resources/application-prod.yml b/web/src/main/resources/application-prod.yml index f509222..acdf228 100644 --- a/web/src/main/resources/application-prod.yml +++ b/web/src/main/resources/application-prod.yml @@ -51,6 +51,10 @@ cameraConfig: # 利珀延迟10s就可 # 单位毫秒 delayDownloadMp4: 10000 + #工业相机编码sn + industrialCamera: F223 + #camera3D(ip来标注 + camera3D: 114 # ------------ # -----图片 mp4下载地址 savePath: @@ -95,7 +99,15 @@ scanCodeMode: - 14 # 照片 視頻保存多久 deleteFileDays: 30 - +#灵闪tcp服务地址 intelliBlink: ip: 127.0.0.1 port: 3002 + + +#模版信息 +template: + - id: 1 + name: "模板一" + code: "a1123" + - diff --git a/web/src/main/resources/libs/MvCameraControlWrapper.jar b/web/src/main/resources/libs/MvCameraControlWrapper.jar new file mode 100644 index 0000000..3e2f591 Binary files /dev/null and b/web/src/main/resources/libs/MvCameraControlWrapper.jar differ diff --git a/web/src/main/resources/libs/opencv-480.jar b/web/src/main/resources/libs/opencv-480.jar new file mode 100644 index 0000000..71a14d3 Binary files /dev/null and b/web/src/main/resources/libs/opencv-480.jar differ diff --git a/web/src/main/resources/libs/opencv_java480.dll b/web/src/main/resources/libs/opencv_java480.dll new file mode 100644 index 0000000..42f7a47 Binary files /dev/null and b/web/src/main/resources/libs/opencv_java480.dll differ