在像素矩阵(矩阵)中进行掩膜操作是非常简单的。它的核心思想就是根据提供的一个掩膜矩阵(所谓的核)重新计算每一个像素的值(类似卷积)。这个掩膜矩阵保存着每一个临近像素以及像素本身对新生成的像素值的影响。用数学的观点描述就是我们利用掩膜矩阵保存的权重值进行加权平均后计算新的像素值。
测试场景
让我们思考下图像对比度增强的问题。我们希望用如下的公式去重新计算每一个像素值:
上图第一种标记是以公式的方式,第二种则是用所需要计算的像素×上一个矩阵的形式。尽管两种表实方式都是一样的,然而第二种方式在矩阵较大的时候更加清晰明了。
代码
完整的代码如下:
public class Main {
public static void main(String[] args) {
System.loadLibrary("libs/"+ Core.NATIVE_LIBRARY_NAME);
String filename = "images/lena.jpg";
Mat input = imread(filename);// 申请内存
imshow("input",input);
double t = System.currentTimeMillis();
Mat dst0 = sharpen(input, new Mat());
t = ((double) System.currentTimeMillis() - t) / 1000;
System.out.println("Hand written function time passed in seconds: " + t);
namedWindow("output");
imshow("output",dst0);
waitKey();
Mat kernel= new Mat(3,3,CvType.CV_8S);
int row = 0,col = 0;
kernel.put(row,col,0,-1,0,-1,5,-1,0,-1,0);
t = System.currentTimeMillis();
Mat dst1 = new Mat();
filter2D(input,dst1,input.depth(),kernel);
t = ((double) System.currentTimeMillis()-t)/1000;
imshow("output",dst1);
System.out.println("Built-in filter2D time passed in seconds: " + t);
waitKey();
System.exit(0);
}
public static double saturate(double x) {
return x > 255.0 ? 255.0 : (x < 0.0 ? 0.0 : x);
}
public static Mat sharpen(Mat myImage, Mat Result) {
myImage.convertTo(myImage, CvType.CV_8U);
int nChannels = myImage.channels();
Result.create(myImage.size(), myImage.type());
for (int j = 1; j < myImage.rows() - 1; ++j) {
for (int i = 1; i < myImage.cols() - 1; ++i) {
double[] sum = new double[nChannels];
for (int k = 0; k < nChannels; ++k) {
double top = -myImage.get(j - 1, i)[k];
double bottom = -myImage.get(j + 1, i)[k];
double center = (5 * myImage.get(j, i)[k]);
double left = -myImage.get(j, i - 1)[k];
double right = -myImage.get(j, i + 1)[k];
sum[k] = saturate(top + bottom + center + left + right);
}
Result.put(j, i, sum);
}
}
Result.row(0).setTo(new Scalar(0));
Result.row(Result.rows() - 1).setTo(new Scalar(0));
Result.col(0).setTo(new Scalar(0));
Result.col(Result.cols() - 1).setTo(new Scalar(0));
return Result;
}
}
基础方法
现在我们来看看利用基础的像素操作方式手写掩膜操作以及利用 filter2D()的差别。下面是手写实现的相关代码:
public static double saturate(double x) {
return x > 255.0 ? 255.0 : (x < 0.0 ? 0.0 : x);
}
public static Mat sharpen(Mat myImage, Mat Result) {
myImage.convertTo(myImage, CvType.CV_8U);
int nChannels = myImage.channels();
Result.create(myImage.size(), myImage.type());
for (int j = 1; j < myImage.rows() - 1; ++j) {
for (int i = 1; i < myImage.cols() - 1; ++i) {
double[] sum = new double[nChannels];
for (int k = 0; k < nChannels; ++k) {
double top = -myImage.get(j - 1, i)[k];
double bottom = -myImage.get(j + 1, i)[k];
double center = (5 * myImage.get(j, i)[k]);
double left = -myImage.get(j, i - 1)[k];
double right = -myImage.get(j, i + 1)[k];
sum[k] = saturate(top + bottom + center + left + right);
}
Result.put(j, i, sum);
}
}
Result.row(0).setTo(new Scalar(0));
Result.row(Result.rows() - 1).setTo(new Scalar(0));
Result.col(0).setTo(new Scalar(0));
Result.col(Result.cols() - 1).setTo(new Scalar(0));
return Result;
}
第一步我们要确保输入图像是无符号8位格式:
myImage.convertTo(myImage, CvType.CV_8U);
我们创建一个与输入图像大小一致的输出图像。就跟我们之前在Mat章节学习到的一样,这个大小取决于我们所需存储的图像的通道数。
int nChannels = myImage.channels();
Result.create(myImage.size(), myImage.type());
我们需要提取每个像素的上下左右的图像*-1 然后加上图像本省*5,再将结果放入Result矩阵对应位置。并且需要使用一些方法进行幅度限制(保证结果在0-255之间);
for (int j = 1; j < myImage.rows() - 1; ++j) {
for (int i = 1; i < myImage.cols() - 1; ++i) {
double[] sum = new double[nChannels];
for (int k = 0; k < nChannels; ++k) {
double top = -myImage.get(j - 1, i)[k];
double bottom = -myImage.get(j + 1, i)[k];
double center = (5 * myImage.get(j, i)[k]);
double left = -myImage.get(j, i - 1)[k];
double right = -myImage.get(j, i + 1)[k];
sum[k] = saturate(top + bottom + center + left + right);
}
Result.put(j, i, sum);
}
}
在图像的边缘,由于不存在-索引的像素,所以公式种会出现结果未定义的情况。最简单的办法就是不去计算边缘,并将边缘像素直接定义成0。
Result.row(0).setTo(new Scalar(0));
Result.row(Result.rows() - 1).setTo(new Scalar(0));
Result.col(0).setTo(new Scalar(0));
Result.col(Result.cols() - 1).setTo(new Scalar(0));
filter2D 方法
图像滤波处理是图像处理种最常用的方法之一,OpenCV同样支持此操作。首先我们需要定义一个mask(kernel):
Mat kernel= new Mat(3,3,CvType.CV_8S);
int row = 0,col = 0;
kernel.put(row,col,0,-1,0,-1,5,-1,0,-1,0);
然后调用filter2D方法,并在参数部分分别声明输入输出图像,输入图像通道数,以及所使用的kernel:
filter2D(input,dst1,input.depth(),kernel);
filter2D方法同时还有第五个可选参数设置kernel的中心,第六个参数讲最终的结果附加一个值,第七个参数声明在边界部分的处理方式。
这个方式非常简短,并且高效。通常会远远优于手动编写代码。比如在我们的例子中进行处理,在结果一致的情况下运算速度有一个数量级的差距: