年底冲KPI很多项目要结项,临时被拉去开发了一些“很有意思”的项目,感觉自从离开学校以后很长时间都没有再接触过图像处理领域了,有点跟熟悉的陌生人打交道的快感。这里简单记录一下相关代码实现,便于后续归档与复现。
使用纯JavaCV实现图像处理
写在前面:
之前读研的时候在学校偶尔做图像预处理,都是使用的OpenCV图像库,用Python或C++语言进行图像处理编程的时候都很方便。但Java Web的服务端在银河麒麟或CentOS等Linux系统下部署时,该依赖库着实有些水土不服,每次都需要重新编译动态链接库不说,光是离线编译OpenCV库本身就费老劲了。无奈只能撸起袖子用Java重写相关的计算库与方法,想着可否跳过OpenCV封装好的函数,采用原生的写法。
(1)轮廓提取与颜色分割处理
我使用的是org.bytedeco.javacv
依赖库,不需要编译安装opencv,在java环境下会更为便捷,pom.xml
中引入如下依赖:
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.4.1</version>
</dependency>
纯javacv代码如下,需要注意Javacv的语法与Opencv区别还挺大的,有很多接口不再适配。
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacpp.opencv_core;
import static org.bytedeco.javacpp.opencv_core.*;
import static org.bytedeco.javacpp.opencv_highgui.cvShowImage;
import static org.bytedeco.javacpp.opencv_highgui.cvWaitKey;
import static org.bytedeco.javacpp.opencv_imgcodecs.cvLoadImage;
import static org.bytedeco.javacpp.opencv_imgproc.*;
/**
* 颜色分割变换
* rina 2021.11.14
*/
public class colorSelect {
//hsv绿色范围
public static opencv_core.CvScalar g_min = cvScalar(35, 43, 46, 0); //HSV色域
public static opencv_core.CvScalar g_max= cvScalar(77, 255, 220, 0); //HSV色域
public static void main(String[] args) {
//读入图片
IplImage orgImg = cvLoadImage("/opt/project/ColorBbox-OpencvTest/test.png");
//rgb->hsv
IplImage hsv = IplImage.create( orgImg.width(), orgImg.height(), orgImg.depth(), orgImg.nChannels() );
cvCvtColor( orgImg, hsv, CV_BGR2HSV );
IplImage imgThreshold = cvCreateImage(cvGetSize(orgImg), 8, 1);
//阈值化
cvInRangeS(hsv, g_min, g_max, imgThreshold);
//形态学闭处理
IplImage Morphology_result = IplImage.create(orgImg.width(),orgImg.height(), IPL_DEPTH_8U, 1);
IplConvKernel kernelCross = cvCreateStructuringElementEx(21, 21,7,7, CV_SHAPE_RECT);
cvMorphologyEx(imgThreshold, Morphology_result, Morphology_result, kernelCross, MORPH_CLOSE, 1);
//膨胀腐蚀
IplImage erosion_dst = IplImage.create(orgImg.width(),orgImg.height(), IPL_DEPTH_8U, 1);
IplImage dilate_dst = IplImage.create(orgImg.width(),orgImg.height(), IPL_DEPTH_8U, 1);
IplConvKernel kernel=cvCreateStructuringElementEx(3,3,1,1,CV_SHAPE_RECT);
cvErode( Morphology_result, erosion_dst, kernel,3); //腐蚀
cvDilate( erosion_dst, dilate_dst, kernel,4); //膨胀
//查找轮廓并生成轮廓数组, 画出轮廓矩形
CvMemStorage mem = CvMemStorage.create();
CvSeq contours = new CvSeq();
CvSeq ptr = new CvSeq();
cvFindContours(dilate_dst, mem, contours, Loader.sizeof(CvContour.class) , CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE, cvPoint(0,0));
CvRect boundingBox;
int index = 1;
for (ptr = contours; ptr != null; ptr = ptr.h_next()) {
boundingBox = cvBoundingRect(ptr, 0);
cvRectangle( orgImg , cvPoint( boundingBox.x(), boundingBox.y() ),
cvPoint( boundingBox.x() + boundingBox.width(), boundingBox.y() + boundingBox.height()),
cvScalar( 0, 255, 255, 255 ), 2, 0, 0 );
System.out.println("boundingBox_index" + index + ".x : " + boundingBox.x());
System.out.println("boundingBox_index" + index + ".y : " + boundingBox.y());
System.out.println("boundingBox_index" + index + ".width : " + boundingBox.width());
System.out.println("boundingBox_index" + index + ".height : " + boundingBox.height());
index++;
}
cvShowImage( "Contours", orgImg );
cvWaitKey(0);
}
}
使用七巧板图像进行测试,测试图像如下,这里选择的是HSV色域中的绿色区域:
阈值化后,再经过膨胀腐蚀处理后的效果如下所示:
对颜色轮廓进行提取后,进行颜色区域的标注,标注Bbox效果如下所示,这里将绿色区域都框出来了:
注意cvScalar()
的参数为HSV色域值,而不是RBG或BGR。
HSV(Hue, Saturation, Value)
是根据颜色的直观特性由A. R. Smith在1978年创建的一种颜色空间, 也称六角锥体模型(Hexcone Model)。这个模型中颜色的参数分别是:色调(H),饱和度(S),亮度(V)。
色调H:用角度度量,取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。它们的补色是:黄色为60°,青色为180°,品红为300°;
饱和度S:取值范围为0.0~1.0;
亮度V:取值范围为0.0(黑色)~1.0(白色)。
对于基本色中对应的HSV分量需要给定一个严格的范围,下面是一些颜色的模糊范围:
H: 0— 180
S: 0— 255
V: 0— 255
(2)离焦处理
Google到了一个牛逼文档,里面图像处理的例子写的挺齐全的,可以作为参考:http://www.jhlabs.com/ip/blurring.html
另外离焦(高斯模糊)的实现原理参考了这篇大牛技术贴:https://www.cnblogs.com/invisible2/p/9177018.html
下面给出我离焦处理的工具类详细代码:
/**
* 离焦 高斯模糊变换
* rina 2021.11.24
*/
public class GaussianBlurUtils {
/**
* 高斯模糊
* 参考:https://www.cnblogs.com/invisible2/p/9177018.html
* @param imagePath
* @param radius 模糊半径
* @param outPath
* @return
*/
public static void gaussianBlurTrans(String imagePath, int radius, String outPath) throws IOException {
File file = new File(imagePath);
BufferedImage src = ImageIO.read(file);
int width = src.getWidth();
int height = src.getHeight();
int[] pixels = new int[width * height];
int[] outPixels = new int[width * height];
src.getRGB(0, 0, width, height, pixels, 0, width);
// 均值滤波使用的卷积模板半径,这里使用5*5均值,所以半径使用2
int r = 0, g = 0, b = 0;
// 获取每个像素点的高斯权重
double[][] weights = getGaussianWeight(radius);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int rSum = 0;
int gSum = 0;
int bSum = 0;
for (int i = -radius; i <= radius; i++) {
int roffset = row + i;
roffset = (roffset < 0) ? 0 : (roffset >= height ? height - 1 : roffset);
for (int j = -radius; j <= radius; j++) {
int coffset = col + j;
coffset = (coffset < 0) ? 0 : (coffset >= width ? width - 1 : coffset);
int pixel = pixels[roffset * width + coffset];
r = (pixel >> 16) & 0XFF;
g = (pixel >> 8) & 0xff;
b = pixel & 0xff;
rSum += r * weights[i + radius][j + radius];
gSum += g * weights[i + radius][j + radius];
bSum += b * weights[i + radius][j + radius];
}
}
r = rSum;
g = gSum;
b = bSum;
outPixels[row * width + col] = (255 << 24) | (clamp(r) << 16) | (clamp(g) << 8) | clamp(b);
}
}
BufferedImage dest = new BufferedImage(width, height, src.getType());
dest.setRGB(0, 0, width, height, outPixels, 0, width);
File newFile = new File(outPath);
ImageIO.write(dest, "jpg", newFile);
}
/**
* 获取(2*r+1)*(2*r+1)图像(半径为r)的高斯权重
* 以图像中心为坐标原点来计算所有点的权重
* 离中心点越近,权重越大
* @param r 半径
* @return 图像所有点的权重
*/
public static double[][] getGaussianWeight(int r) {
int n = 2*r+1;
double[][] a = new double[n][n];
double totalWeight = 0;
// 计算每个像素点的权重
for (int i=0; i<n; i++) {
for (int j=0; j<n; j++) {
int x = j - r;
int y = i - r;
a[i][j] = getGaussianFunction(x, y);
totalWeight += a[i][j];
}
}
// 让它们的权重之和等于1
for (int i=0; i<n; i++) {
for (int j=0; j<n; j++) {
a[i][j] = a[i][j] / totalWeight;
}
}
return a;
}
/**
* 高斯函数,根据坐标计算每个点的权重,3*3图像的原点默认是中间的那个点
* 参考:https://www.cnblogs.com/invisible2/p/9177018.html
* @param x 横坐标
* @param y 纵坐标
* @return
*/
public static double getGaussianFunction(int x, int y) {
double a = 1.5;
double b = -(x*x + y*y)/(2*a*a);
double c = Math.exp(b)/(2*Math.PI*a*a);
return c;
}
/**
* 如果像素点的值超过了0-255的范围,予以调整
* @param value 输入值
* @return 输出值
*/
private static int clamp(int value) {
return value > 255 ? 255 : (Math.max(value, 0));
}
}
radius
模糊半径可调,依旧是这张七巧板测试图像,离焦模糊效果如下:
(3)运动模糊处理
运动模糊这里实现了线性模糊,旋转模糊,以及含有畸变效果的尺度模糊。
下面给出运动模糊处理的工具类详细代码,定义变量为:
float centreX; //X方向系数
float centreY; //Y方向系数
float distance; //这里设置运动距离
float angle; //运动方向(角度)
float rotation; //旋转角度
float zoom; //尺度
/**
* 运动模糊变换
* rina 2021.11.24
*/
public class MotionBlurUtils {
private static int log2( int n ) {
int m = 1;
int log2n = 0;
while (m < n) {
m *= 2;
log2n++;
}
return log2n;
}
public static void MotionBlurTrans(String imagePath, String outPath, float centreX, float centreY,float distance, float angle, float rotation, float zoom) throws IOException {
File file = new File(imagePath);
BufferedImage ImageBuffer = ImageIO.read(file);
BufferedImage ResultImageBuffer = ImageBuffer;
float cx = (float) ImageBuffer.getWidth() * centreX;
float cy = (float) ImageBuffer.getHeight() * centreY;
float imageRadius = (float) Math.sqrt( cx*cx + cy*cy );
float translateX = (float) (distance * Math.cos( angle ));
float translateY = (float) (distance * -Math.sin( angle ));
float scale = zoom;
float rotate = rotation;
float maxDistance = distance + Math.abs(rotation*imageRadius) + zoom*imageRadius;
int steps = log2((int)maxDistance);
translateX /= maxDistance;
translateY /= maxDistance;
scale /= maxDistance;
rotate /= maxDistance;
if ( steps == 0 ) {
File newFile = new File(outPath);
ImageIO.write(ResultImageBuffer, "jpg", newFile);
return;
}
BufferedImage tmp = new BufferedImage(ImageBuffer.getWidth(), ImageBuffer.getHeight(), ImageBuffer.getType());
for ( int i = 0; i < steps; i++ ) {
Graphics2D g = tmp.createGraphics();
g.drawImage( ResultImageBuffer, null, null );
g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR );
g.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.5f ) );
g.translate( cx+translateX, cy+translateY );
g.scale( 1.0001+scale, 1.0001+scale );
if ( rotation != 0 )
g.rotate( rotate );
g.translate( -cx, -cy );
g.drawImage( tmp, null, null );
g.dispose();
ResultImageBuffer = tmp;
translateX *= 2;
translateY *= 2;
scale *= 2;
rotate *= 2;
}
File newFile = new File(outPath);
ImageIO.write(ResultImageBuffer, "jpg", newFile);
}
}
还是这张七巧板测试图像,当不调整运动方向角度时,运动模糊为水平运动模糊,其水平线性运动模糊的效果如下:
当调整运动方向角度时,运动模糊为沿着该角度方向的线性模糊,例如设置angle=10
,效果如下,可以发现模糊方向是朝着右上角10度方向倾斜模糊的:
当调整旋转角度时,其旋转模糊效果如下所示:
(4)灰度化处理
参考了一下这篇技术贴:
https://www.cnblogs.com/deng-c-q/p/8710252.html
图片灰度化方式这里分为四种:
(1)最大值法(取颜色RGB中的最大值作为灰度值)
(2)最小值法(取颜色RGB的最小值作为灰度值)
(3)均值法(取颜色的RGB的平均值作为灰度值)
(4)加权法
下面给出灰度化处理的工具类详细代码,如下所示:
/**
* 图片灰度化变换
* rina 2021.11.24
*/
public class GrayTransUtils {
/**
* 颜色分量转换为RGB值
* @param alpha
* @param red
* @param green
* @param blue
* @return
*/
private static int colorToRGB(int alpha, int red, int green, int blue) {
int newPixel = 0;
newPixel += alpha;
newPixel = newPixel << 8;
newPixel += red;
newPixel = newPixel << 8;
newPixel += green;
newPixel = newPixel << 8;
newPixel += blue;
return newPixel;
}
/**
* 图片灰度化的方法
* @param status 灰度化方法的种类,1表示最大值法,2表示最小值法,3表示均值法,4加权法
* @param image 需要灰度化的图片
* @throws IOException
*/
public static BufferedImage grayImageTrans(int status, BufferedImage image) throws IOException {
//File file = new File(imagePath);
//BufferedImage image = ImageIO.read(file);
int width = image.getWidth();
int height = image.getHeight();
BufferedImage grayImage = new BufferedImage(width, height, image.getType());
//BufferedImage grayImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
int color = image.getRGB(i, j);
final int r = (color >> 16) & 0xff;
final int g = (color >> 8) & 0xff;
final int b = color & 0xff;
int gray = 0;
if(status == 1){
gray = getBigger(r, g, b);//最大值法灰度化
}else if(status==2){
gray = getSmall(r, g, b);//最小值法灰度化
}else if(status==3){
gray = getAvg(r, g, b);//均值法灰度化
}else if(status==4){
gray = (int) (0.3 * r + 0.59 * g + 0.11 * b);//加权法灰度化
}
//System.out.println("像素坐标:" + " x=" + i + " y=" + j + " 灰度值=" + gray);
grayImage.setRGB(i, j, colorToRGB(0, gray, gray, gray));
}
}
//File newFile = new File(outPath);
//ImageIO.write(grayImage, "jpg", newFile);
return grayImage;
}
//比较三个数的大小取最大数
public static int getBigger(int x,int y,int z){
if(x>=y&&x>=z){
return x;
}else if(y>=x&&y>=z){
return y;
}else if(z>=x&&z>=y){
return z;
}else{
return 0;
}
}
//比较三个数的大小取最小数
public static int getSmall(int x,int y,int z){
if(x<=y&&x<=z){
return x;
}else if(y>=x&&y>=z){
return y;
}else if(z>=x&&z>=y){
return z;
}else{
return 0;
}
}
//均值法
public static int getAvg(int x,int y,int z){
int avg=(x+y+z)/3;
return avg;
}
}
最后给出这四种灰度二值化处理的效果图,个人感觉第三种效果更好一些(依旧是这张七巧板测试图例,有一丢丢看吐了…):
每天都想午休但睡不着的乔木小姐
2021.11.25