背景
项目中需要制作设备面板图,在编辑工具中根据底图上插槽的位置人工进行插槽大小和位置的编辑,当设备底图中插槽比较少的时候耗时还不长,但很多设备上的插槽动则五六十个,人工拖拽生成比较繁琐,耗时也很长,拖拽位置还不精确,在此使用opencv进行插槽位置的自动读取,以减少工作量。
OpenCv的安装和使用(Windows,Idea)
-
从OpenCv官网下载Windows的安装包,并进行安装
-
把安装目录\build\java下的opencv-411.jar 添加到本地maven库
mvn install:install-file -DgroupId=com.acts -DartifactId=opencv -Dversion=4.11 -Dpackaging=jar -Dfile=安装目录\build\java\opencv-411.jar
-
在项目中通过maven引入OpenCv的jar包进行使用
<dependency> <groupId>com.acts</groupId> <artifactId>opencv</artifactId> <version>4.11</version> </dependency>
-
使用opevCv时,需先使用以下代码检测环境,否则不能正常运行
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
System.out.println("opencv = " + Core.VERSION);
- 在Idea的项目或者测试用例启动时,VM options中配置OpenCv依赖的dll文件路径
-Djava.library.path=D:\opencv\build\java\x64
如需在Junit中进行图片中间过程查看,还需加入-Djava.awt.headless=false
常用类简介
OpenCv中的功能很多,这里只是简单列举,完整功能可以参考官网的介绍
Imgcodecs 图片读取、写入
- imread 图片读取
- imwrite 图片写入
Imgproc 图片处理
- accumulate 图像累加
- cvtColor 颜色空间转换,如转换为灰度图片
- Canny 边缘检测
- threshold 二值化
- boundingRect 检测到的边缘转化为矩形
- contourArea 计算面积
- approxPolyDP 用多边形包围轮廓,可以得到严格的矩形
HighGui GUI展示
- imshow 显示图片
- waitKey 等待输入
实现过程
实现思路
图片处理其实也是属于一种数据挖掘的过程,我们从一张图片中挖掘所需的数据,图片处理流程如下:
- 数据预处理
- 图片数据归一化(减少运算量,降低实现难度)
- 数据清洗(去除噪点、干扰数据等)
- 数据计算
- 边缘检测(计算边缘数据)
- 边缘数据清洗(过滤干扰结果)
- 迭代
- 结果返回
实现过程
-
数据预处理
- 导入的底图图片为jpg或者png图片,一般是带有多种颜色的(GRB)的图片,而我们所需提取的槽位结果只需要槽位的定位,而无需槽位的颜色数据,所以第一步需要把彩色图片变为灰度图片。
在这里使用Imgproc.cvtColor
方法,把图片转为灰度图片。
//转为灰度 Imgproc.cvtColor(imread, desc, Imgproc.COLOR_BGR2GRAY);
- 导入的底图图片为jpg或者png图片,一般是带有多种颜色的(GRB)的图片,而我们所需提取的槽位结果只需要槽位的定位,而无需槽位的颜色数据,所以第一步需要把彩色图片变为灰度图片。
彩色图片每个像素由RGB三种颜色合并而成,如红色为255,0,0分别代表每种颜色的强度,所以RGB图片可以拆分为三个颜色通道,在OpenCv中,颜色的表示与Web略微由区别,OpenCv中的颜色顺序为BGR,所以红色的值为0,0,255。
如果三种颜色的值相同,则代表了灰度的强度,如0,0,0(黑色),150,150,150(某种灰度的灰色),255,255,255(白色)
- 图片转为灰度图片后,需要计算的值还是比较多的,因为图片中每个像素的灰度值不一致,如果要简化运算,还可以进行二值化处理,二值化处理可以设定一个阈值,把大于阈值的数值变为极值,运算过程和三元表达式一致
当前值>阈值?0:极值
,OpenCv已经封装了二值化的处理方法,在这里可以使用Imgproc.threshold
方法,把图片二值化;同时还需裁剪掉最外层的边框,以减少干扰元素,此处本应该去除图片中的数字,但是比较复杂,此处在最后的边缘查找结果中进行过滤。
//去除边框的线条干扰
int cutNum = 23;
Mat submat = desc.submat(cutNum,desc.rows()-cutNum,cutNum,desc.cols()-cutNum);
//写入临时文件,读取后再删除,避免直接添加边框图片内容又恢复的问题
Imgcodecs.imwrite("temp.jpg", submat);
Mat temp = Imgcodecs.imread("temp.jpg");
try {
Files.delete(Paths.get("temp.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
//减去的边缘再填充回来,避免生成的坐标变位
Core.copyMakeBorder(temp, temp, cutNum, cutNum, cutNum, cutNum, Core.BORDER_CONSTANT,new Scalar(0,0,0));
//图像反向二值化,去除噪点
Imgproc.threshold(desc,desc, 187, 255, Imgproc.THRESH_BINARY_INV|Imgproc.THRESH_OTSU);
- 二值化后,为了让边缘检测的准确度增加,避免边缘检测后的矩形不闭合的情况,还需对二值化后的线条进行加粗,加粗使用
Imgproc.morphologyEx
方法
//像素点加粗到sizeW*sizeH,让矩形闭合
Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(sizeW,sizeH));
Imgproc.morphologyEx(desc, desc, Imgproc.MORPH_GRADIENT, element);
至此,图片预处理工作完成,下面开始槽位的提取
-
数据计算
- 边缘检测(提取所需数据)
边缘检测在OpenCv中有现成的方法可以使用,边缘检测会设计到两个阈值,第一个阈值设置边缘的强度,第二个阈值是在查找到符合强度阈值的边缘时,再往外找8个像素点,如果在外面8个像素点中找到符合第二个阈值的值,则也会算为强阈值。
边缘检测的结果为检测到边缘的线条,所以在边缘检测完成后,还需查找检测到的边缘,组合成矩形。
//检测边缘 Mat temp = new Mat();//边缘检测结果 Imgproc.Canny(desc, temp,thresholdStrength,thresholdWeakness); List<MatOfPoint> list = new ArrayList<>(); Mat hierarchy = new Mat(); //查找边缘 Imgproc.findContours(temp, list,hierarchy,Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);//查找边缘为封闭的矩形
- 边缘数据清洗(过滤干扰结果)
上一步的边缘查找结果(矩形)会查找到图片中所有符合矩形结果的边缘,里面还是会有干扰数据的,在此我们还需通过计算矩形的面积,过滤掉不符合我们要求的矩形。
/* 遍历找到的矩形边缘,当边缘大小超过一定值的时候,记录找到边缘的坐标和宽高 */ List<Rect> rects = new ArrayList<>(); for (int i = 0; i < list.size(); i++) {//遍历查找到的边缘 MatOfPoint mp = list.get(i); double v = Imgproc.contourArea(mp);//获取边缘大小 if(v>boundFilter) { Rect rect = Imgproc.boundingRect(mp);//边缘转化为矩形 rects.add(rect); } }
- 边缘检测(提取所需数据)
至此,所有槽位的坐标和大小已经获取完毕,可以渲染到原图上查看一下查找的结果
//展示
HighGui.imshow("原图", imread);
HighGui.imshow("处理后", desc);
Mat out = new Mat(imread.size(),imread.type());
imread.copyTo(out);
rects.forEach(rect->{
Imgproc.rectangle(out, rect, new Scalar(0,0,255));
});
HighGui.imshow("边缘检测", out);
HighGui.waitKey(0);
- 迭代
实现过程中,需要不停迭代像素加粗的值、边缘检测的阈值,以达到最佳匹配效果。
项目部署
部署过程和原有springboot项目类似,只是在部署的文件夹中需加入opencv的dll文件,同时在启动时需指定dll文件的路径
部署文件夹路径如下
- dir
- image_manager_demo-0.0.1-SNAPSHOT.jar
- opencv
- x64
- opencv_java411.dll
- x86
- opencv_java411.dll
- x64
启动命令为
java -jar -Djava.library.path=./opencv/x64 image_manager_demo-0.0.1-SNAPSHOT.jar
处理的完整代码如下
package com.bili.image_manager_demo.service;
import com.bili.image_manager_demo.dto.DevicePanelModelDto;
import com.bili.image_manager_demo.dto.PanelModel;
import org.opencv.core.*;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* image_manager_demo
*
* @author bili
* @date 2019/8/13
*/
@Service("cvImageManagerService")
public class CvImageManagerServiceImpl implements ImageManagerService {
@Autowired
private MongoTemplate mt;
@Override
public List<Rect> getAllRectangualObj(Path imagePath, int sizeW,int sizeH,int thresholdStrength,int thresholdWeakness,int boundFilter) {
//opencv环境检查
environmentalInspection();
//读取图片
Mat imread = Imgcodecs.imread(imagePath.toString());
//图片预处理
Mat desc = imagePreprocessing(imread,sizeW,sizeH);
//检测边缘
Mat temp = new Mat();//边缘检测结果
Imgproc.Canny(desc, temp,thresholdStrength,thresholdWeakness);
List<MatOfPoint> list = new ArrayList<>();
Mat hierarchy = new Mat();
//查找边缘
Imgproc.findContours(temp, list,hierarchy,Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);//查找边缘为封闭的矩形
/*
遍历找到的矩形边缘,当边缘大小超过一定值的时候,记录找到边缘的坐标和宽高
*/
List<Rect> rects = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {//遍历查找到的边缘
MatOfPoint mp = list.get(i);
double v = Imgproc.contourArea(mp);//获取边缘大小
if(v>boundFilter) {
Rect rect = Imgproc.boundingRect(mp);//边缘转化为矩形
rects.add(rect);
}
}
//展示
HighGui.imshow("原图", imread);
HighGui.imshow("处理后", desc);
Mat out = new Mat(imread.size(),imread.type());
imread.copyTo(out);
rects.forEach(rect->{
Imgproc.rectangle(out, rect, new Scalar(0,0,255));
});
HighGui.imshow("边缘检测", out);
HighGui.waitKey(0);
return rects;
}
/**
* 图片预处理
* @param imread 图片对象
* @param sizeW 像素点加粗宽度
* @param sizeH 像素点加粗高度
* @return 处理后的对象
*/
private Mat imagePreprocessing(Mat imread,int sizeW,int sizeH) {
Mat desc = new Mat();
//转为灰度
Imgproc.cvtColor(imread, desc, Imgproc.COLOR_BGR2GRAY);
//图像反向二值化,去除噪点
Imgproc.threshold(desc,desc, 187, 255, Imgproc.THRESH_BINARY_INV|Imgproc.THRESH_OTSU);
//像素点加粗到sizeW*sizeH,让矩形闭合
Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(sizeW,sizeH));
Imgproc.morphologyEx(desc, desc, Imgproc.MORPH_GRADIENT, element);
//去除边框的线条干扰
int cutNum = 23;
Mat submat = desc.submat(cutNum,desc.rows()-cutNum,cutNum,desc.cols()-cutNum);
//写入临时文件,读取后再删除,避免直接添加边框图片内容又恢复的问题
Imgcodecs.imwrite("temp.jpg", submat);
Mat temp = Imgcodecs.imread("temp.jpg");
try {
Files.delete(Paths.get("temp.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
//减去的边缘再填充回来,避免生成的坐标变位
Core.copyMakeBorder(temp, temp, cutNum, cutNum, cutNum, cutNum, Core.BORDER_CONSTANT,new Scalar(0,0,0));
return temp;
}
/**
* opencv 环境检查
*/
private void environmentalInspection() {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
System.out.println("opencv = " + Core.VERSION);
}
}