这是我图像处理大作业完成后摸出来的第二篇博客,上期已经简单介绍过如何在springboot环境下使用opencv,这期就承接上回简单讲讲Java版opencv的基本操作。
在我的理解中,关于opencv的基本操作差不多就是旋转,水平镜像翻转,放大缩小,以及裁剪绘制等。都是一些没什么技术含量的操作,大都是调用函数即可完成,有过opencv基础的同学肯定可以很快掌握。
但是在讲解之前,有一点需要和大家提前探讨,那就是java版opencv对几个核心库的封装,c版opencv中只需要cv.什么就可以调用各类函数,但是在java中对其中部分函数中了重新封装(大部分没变)。
core中依旧是核心功能模块,尤其是底层数据结构和算法函数,如Mat,Core等。
imgproc,图像处理模块,基本上需要用的图像处理功能都在这个里面。
highgui,高层GUI显示模块,提供了各位图形和媒体接口,常用的imshow就在里面。
imgcodecs,这个库我没具体搞懂什么意思,但是imread封装在里面。
calib3d,相机校准和三维重建相关的内容。
features2d,2D功能框架 ,包含兴趣点检测子、描述子以及兴趣点匹配框架。
ml,机器学习模块。
objdetect,物体检测模块。
video,视频分析组件。
当然以上介绍了很多,实际上重新封装的也只有imgcodecs这个库,其余的貌似和opencv没太多区别,也不排除imgcodecs在新版opencv中就已经存在了,因为我用的是3.x的,至少那个时候没有。对于基本图像处理实际需要的就是最开始介绍了三个库。
由于我是使用SpringBoot单间前后端分离项目,所以我在处理完图片封装为Mat之后还需要将其转化为Base64格式发送给前端的React,之后的代码中会涉及到几个函数【Mat转image再转bufferedimage】,bufferedimage是java自己的图片保存格式,类似于matlab中的变量区,可以保存图片的一些基本信息。这里放的两个转换函数经过验证,可以直接使用,写的很简单,可以根据实际需求微调。
public class Converter {
public static Map<String,Object> BufferedImageToBase64(BufferedImage bufferedImage) throws IOException {
Map<String,Object> result = new HashMap<>();
//将图片转换成字符串给前端
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", stream);
Base64.Encoder encoder = Base64.getEncoder();
String base64 = encoder.encodeToString(stream.toByteArray());
stream.flush();
stream.close();
//封装数据
result.put("image", "data:image/png;base64,"+base64);
return result;
}
public static BufferedImage ImageToBufferedImage(Image image){
if (image instanceof BufferedImage) {
return (BufferedImage)image;
}
image = new ImageIcon(image).getImage();
BufferedImage bimage = null;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
try {
int transparency = Transparency.OPAQUE;
GraphicsDevice gs = ge.getDefaultScreenDevice();
GraphicsConfiguration gc = gs.getDefaultConfiguration();
bimage = gc.createCompatibleImage(
image.getWidth(null), image.getHeight(null), transparency);
} catch (HeadlessException e) {
}
if (bimage == null) {
int type = BufferedImage.TYPE_INT_RGB;
bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), type);
}
Graphics g = bimage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return bimage;
}
}
我的图片读取使用了swing组件调用文件选择器,简单编写的画不需要这么复杂,直接将需要的文件路径传过来就行,至于简单的系统组件,我放到之后的几期中再讲。
@PostMapping("open")
public Map<String,Object> open(HttpServletResponse response) throws IOException {
response.setContentType("application/octet-stream; charset=utf-8");
response.setContentType("image/png");
/*打开文件选择器*/
FileChooser f = new FileChooser();
String imgPath = f.getPath();
System.out.println(imgPath);
/*转换成BufferedImage*/
BufferedImage read = ImageIO.read(new FileInputStream(imgPath));
/*存一份到OpenCV里面*/
MatImage.matImage=imread(imgPath);
History.AllMethod(MatImage.matImage);
System.out.println(MatImage.matImage);
/*封装base64转给前端*/
return Converter.BufferedImageToBase64(read);
}
上面的代码是我controller里面具体的写法,因为需要发送给前端显示所以读了两份,其实最好的写法是Mat读第一次,读好了之后直接存到后端里面做备份(这样就不用前端往后端发送需要操作的图片了,直接后端操作好了让前端显示就行)。备份好了直接讲Mat转base64发过去,我上面的写法臃肿了。
如果是直接想要看读取和显示的话一下这种写法就够了
Mat image = Imgcodecs.imread("D:/test.bmp");
HighGui.imshow("yt",image);
HighGui.waitKey(13);
imshow是单独开个小窗口,如果是web项目,建议参考上面的话修改第一段代码。
旋转任意角度
/**
* 逆时针旋转,但是图片宽高不用变,此方法与rotateLeft和rotateRight不兼容
*
* @param src 源
* @param angele 旋转的角度
* @return 旋转后的对象
*/
public static Mat rotate(Mat src, double angele) {
Mat dst = src.clone();
org.opencv.core.Point center = new org.opencv.core.Point(src.width() / 2.0, src.height() / 2.0);
Mat affineTrans = Imgproc.getRotationMatrix2D(center, angele, 1.0);
Imgproc.warpAffine(src, dst, affineTrans, dst.size(), Imgproc.INTER_NEAREST);
return dst;
}
旋转90°,垂直、水平翻转
@RequestMapping("spin")
public Map<String,Object> spin(HttpServletResponse response,
@RequestParam("key") String key) throws IOException {
response.setContentType("application/octet-stream; charset=utf-8");
response.setContentType("image/png");
/*从constants中取Mat*/
Mat mat = MatImage.matImage;
Mat temp = new Mat();
Mat result = new Mat();
switch (key) {
case "0": {
/*顺时针90*/
Core.transpose(mat, temp);
Core.flip(temp, result, 1);
/*更改存储的图片*/
MatImage.matImage = result;
History.AllMethod(MatImage.matImage);
Image image = toBufferedImage(result);
BufferedImage bufferedImage = Converter.ImageToBufferedImage(image);
return Converter.BufferedImageToBase64(bufferedImage);
}
case "1": {
/*逆时针90*/
Core.transpose(mat, temp);
Core.flip(temp, result, 0);
/*更改存储的图片*/
MatImage.matImage = result;
History.AllMethod(MatImage.matImage);
Image image = toBufferedImage(result);
BufferedImage bufferedImage = Converter.ImageToBufferedImage(image);
return Converter.BufferedImageToBase64(bufferedImage);
}
case "2": {
/*转180*/
Core.flip(mat, result, -1);
/*更改存储的图片*/
MatImage.matImage = result;
History.AllMethod(MatImage.matImage);
Image image = toBufferedImage(result);
BufferedImage bufferedImage = Converter.ImageToBufferedImage(image);
return Converter.BufferedImageToBase64(bufferedImage);
}
case "3":{
/*水平翻转*/
Core.flip(mat,result,1);
MatImage.matImage=result;
History.AllMethod(MatImage.matImage);
Image image = toBufferedImage(result);
BufferedImage bufferedImage = Converter.ImageToBufferedImage(image);
return Converter.BufferedImageToBase64(bufferedImage);
}
case "4":{
/*垂直翻转*/
Core.flip(mat,result,0);
MatImage.matImage=result;
History.AllMethod(MatImage.matImage);
Image image = toBufferedImage(result);
BufferedImage bufferedImage = Converter.ImageToBufferedImage(image);
return Converter.BufferedImageToBase64(bufferedImage);
}
default:
return null;
}
}
上面是springboot中的写法,翻译成普通maven也很好理解,随便讲一个case里面的内容扒出来,将Core.flip后面的全删掉加imshow和waitkey即可。
因为实际需要的就是Core中的flip,接受的参数为mat(需要操作图片),res(输出图片),type(操作方法)。当第三个参数type为0的时候顺时针90°,1的时候逆时针90°。再配合transpose旋转,很容易理解道理做了什么操作。
Rect rect = new Rect(x,y,ex-x,ey-y);
Mat result = new Mat(mat,rect);
Rect是core中封装的内容,表示矩形框,这里输入的是左上角第一个点坐标和width,height。传入Mat之后就可以实现裁剪,其实就是重新定义了一个对象,但是限定其范围了。
说到裁剪,就不得不说到前端技术,这里也是我参考了很多文献和教程整出的手动裁剪框,基于React和js实现。