一、概述
在现实世界中,我们(人类)看到的是图像,而当我们让数字设备“看到”时,我们正在记录图像中每个点的值。
如上图所示,数字设备看到的是一个矩阵,其中包含所有像素的值。最终,在计算机世界中,所有图像都可以简化为数值矩和矩阵信息。
OpenCV的core模块定义了如何在内存中存储图像,还包括矩阵、向量、点等一些基础操作的定义。
2.基础镜像容器
OpenCV定义了Mat类作为基本图像容器,此外Mat还可以只单纯地表示一个矩阵。Mat由两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同矩阵可以是不同的维数)的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同。例如,一个RGB的图片,其Mat对象的矩阵就是一个分别存储R、G、B通道值的三维矩阵。
在视觉算法中经常需要传递图片、拷贝图片等操作,每次都拷贝矩阵开销较大,因此OpenCV采用了引用计数机制,让每个Mat对象有自己的信息头,但共享同一个矩阵,拷贝构造函数则只拷贝信息头和矩阵指针,而不拷贝矩阵。
2.1 创建Mat
创建一个Mat的方法如下:
/**
* @param rows行;对应bitmap的高
* @param clos列;对应bitmap的宽
* @param 颜色空间&数据类型CvType
* @param 矩阵的数据
**/
Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP);
在Android OpenCV基础(一、OpenCV入门)中,我们已经看到过从bitmap对象创建Mat的方法:
void *pixels = 0;
AndroidBitmapInfo info;
// 获取bitmap的信息
AndroidBitmap_getInfo(env, bitmap, &info);
// 获取bitmap的像素值
AndroidBitmap_lockPixels(env, bitmap, &pixels);
cv::Mat rgbMat(info.height, info.width, CV_8UC4, pixels);
2.2 拷贝Mat
在第 1 章中,提到了复制构造函数只复制标题和矩阵指针,而不复制矩阵。以下操作都不会复制矩阵:
Mat B(A); // 使用拷贝构造函数
C = A; // 赋值运算符
如果确实需要复制矩阵本身,可以通过两种方式进行:
cv::Mat tmp(info.height, info.width, CV_8UC4, pixels);
cv::Mat dst;
// 方法1:copyTo
tmp.copyTo(dst);
// 方法2:clone
dst = tmp.clone
2.3 CvType
在Mat的构造函数中,需要传入的CvType是OpenCV内置的类型,格式含义如下:
// CvType含义:[每个颜色所占位数][是否带符号][基本数据类型][每个颜色的通道数]
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
// 例如CV_8UC4表示:每个颜色占8位,用unsigned char表示,每个颜色有4个通道(R、G、B、A)
cv::Mat rgbMat(info.height, info.width, CV_8UC4, pixels);
2.3.1 颜色空间
颜色空间是指对于给定的颜色,颜色元素如何组合以对其进行编码。常见的有:
- RGB:用红色Red、绿色Green和蓝色Blue作为基本色,是最常见的,这是因为人眼采用相似的工作机制,它也被显示设备所采用。
- RGBA:在RGB的基础上加入了透明度Alpha。
- YCrCb:在JPEG图像格式中广泛使用。
- YUV:用于Android相机的颜色空间,”Y”表示明亮度(Luminance或Luma,也就是灰度值);而”U”和”V”表示色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
2.3.2 数据类型
数据类型是指每个元素如何存储,存储的方式决定了颜色在其定义域上能够控制的精度。例如,在RGB空间,如果对于单个的R、G、B用char存储,char占8位,那么RGB就可以表示出1600万种可能的颜色(256 * 256 * 256)。
如果使用更多的类型存储(比如32位的float)或64位的double),则能给出更加精细的颜色分辨能力,单也会增加图像所占的内存空间。
三、Bitmap与Mat
3.1 Bitmap转Mat
void bitmapToMat(JNIEnv *env, jobject bitmap, cv::Mat &dst) {
AndroidBitmapInfo info;
void *pixels = 0;
try {
CV_Assert(AndroidBitmap_getInfo(env, bitmap, &info) >= 0);
CV_Assert(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 ||
info.format == ANDROID_BITMAP_FORMAT_RGB_565);
CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0);
CV_Assert(pixels);
dst.create(info.height, info.width, CV_8UC4);
if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
cv::Mat tmp(info.height, info.width, CV_8UC4, pixels);
tmp.copyTo(dst);
} else {
cv::Mat tmp(info.height, info.width, CV_8UC2, pixels);
cvtColor(tmp, dst, CV_BGR5652RGBA);
}
AndroidBitmap_unlockPixels(env, bitmap);
return;
}catch (...) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code {nBitmapToMat}");
return;
}
}
3.2 Mat转Bitmap
/**
* 创建Bitmap对象
*/
jobject createBitmap(JNIEnv *env, int width, int height, std::string config) {
jclass bitmapConfig = env->FindClass("android/graphics/Bitmap$Config");
jfieldID configFieldID = env->GetStaticFieldID(bitmapConfig, config.c_str(),
"Landroid/graphics/Bitmap$Config;");
jobject rgb565Obj = env->GetStaticObjectField(bitmapConfig, configFieldID);
jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmapMethodID = env->GetStaticMethodID(bitmapClass,"createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jobject bitmapObj = env->CallStaticObjectMethod(bitmapClass, createBitmapMethodID,
width, height, rgb565Obj);
env->DeleteLocalRef(bitmapConfig);
env->DeleteLocalRef(bitmapClass);
return bitmapObj;
}
/**
* Mat转Bitmap
*/
void matToBitmap(JNIEnv *env, cv::Mat &src, jobject bitmap) {
AndroidBitmapInfo info;
void *pixels = 0;
try {
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
return;
}
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 &&
info.format != ANDROID_BITMAP_FORMAT_RGB_565) {
return;
}
if (src.dims != 2 || info.height != (uint32_t) src.rows ||
info.width != (uint32_t) src.cols) {
return;
}
if (src.type() != CV_8UC1 && src.type() != CV_8UC3 && src.type() != CV_8UC4) {
return;
}
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
return;
}
if (pixels == 0) {
return;
}
if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
cv::Mat tmp(info.height, info.width, CV_8UC4, pixels);
if (src.type() == CV_8UC1) {
cvtColor(src, tmp, CV_GRAY2RGBA);
} else if (src.type() == CV_8UC3) {
cvtColor(src, tmp, CV_RGB2RGBA);
} else if (src.type() == CV_8UC4) {
src.copyTo(tmp);
}
} else {
// info.format == ANDROID_BITMAP_FORMAT_RGB_565
cv::Mat tmp(info.height, info.width, CV_8UC2, pixels);
if (src.type() == CV_8UC1) {
cvtColor(src, tmp, CV_GRAY2BGR565);
} else if (src.type() == CV_8UC3) {
cvtColor(src, tmp, CV_RGB2BGR565);
} else if (src.type() == CV_8UC4) {
cvtColor(src, tmp, CV_RGBA2BGR565);
}
}
AndroidBitmap_unlockPixels(env, bitmap);
return;
} catch (const cv::Exception &e) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("org/opencv/core/CvException");
if (!je) je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, e.what());
return;
} catch (...) {
AndroidBitmap_unlockPixels(env, bitmap);
jclass je = env->FindClass("java/lang/Exception");
env->ThrowNew(je, "Unknown exception in JNI code {nMatToBitmap}");
return;
}
}
四、图片变换
一般而言,图像处理算子是指获取一个或多个输入图像并产生输出图像的函数。图像变换可分为以下两种:
- 点算子(像素变换):这类算子只根据输入的像素值计算对应的输出像素值(有时可以添加一些全局信息或参数)。常用算子包括亮度和对比度调整,以及颜色校正和变换。
- 邻域(基于区域)算子:这类算子根据输入像素值及其周围像素值计算对应的输出像素值。常见的算子包括核函数、过滤器等。
4.1 图片亮化
首先声明JNI接口如下:
public class OpenCVSample {
static {
try {
System.loadLibrary("native-lib");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
public static native Bitmap lightenBitmap(Bitmap bitmap);
}
然后在native层实现如下点算子计算逻辑::
其中 i 和 j 表示第 i 行第 j 列的像素点,alpha和beta是参数,程序中我们使用alpha = 2.2,beta=50,实现如下:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_bc_sample_OpenCVSample_lightenBitmap(
JNIEnv *env,
jclass thiz, jobject bitmap) {
cv::Mat rgbMat;
bitmapToMat(env, bitmap, rgbMat);
// 创建一个同样大小的结果Mat
cv::Mat dst = cv::Mat(rgbMat.size(), rgbMat.type());
/// 执行运算 new_image(i,j) = alpha*image(i,j) + beta
float alpha = 2.2f;
int beta = 50;
for (int y = 0; y < rgbMat.rows; y++) {
for (int x = 0; x < rgbMat.cols; x++) {
for (int c = 0; c < rgbMat.channels(); c++) {
// Vec4b因为我们的使用的是RGBA通道,如果RGB则是Vec3b
// 实现计算逻辑
dst.at<cv::Vec4b>(y, x)[c] = cv::saturate_cast<uchar>(
alpha * (rgbMat.at<cv::Vec4b>(y, x)[c]) + beta);
}
}
}
jobject sharpenBitmap = createBitmap(env, dst.cols, dst.rows, "ARGB_8888");
matToBitmap(env, dst, sharpenBitmap);
return sharpenBitmap;
}
运行结果如下:
4.2 图片锐化
锐化是一个简单的邻域算子。与OpenGL实现锐化原理类似,锐化其实就是根据掩码矩阵(也称作核)重新计算图像中每个像素的值。
这次我们以如下矩阵作为锐化的kenal核函数:
首先声明JNI接口如下:
public class OpenCVSample {
static {
try {
System.loadLibrary("native-lib");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
public static native Bitmap sharpenBitmap(Bitmap bitmap);
}
然后在native层实现如下邻域算子计算逻辑,先把矩阵中心的元素(上面的例子中是(0,0)位置的元素,也就是5)对齐到要计算的目标像素上,再把邻域像素值和相应的矩阵元素值的乘积加起来。:
extern "C"
JNIEXPORT jobject JNICALL
Java_com_bc_sample_OpenCVSample_sharpenBitmap(
JNIEnv *env,
jclass thiz, jobject bitmap) {
cv::Mat rgbMat;
bitmapToMat(env, bitmap, rgbMat);
cv::Mat dst;
sharpen(rgbMat, dst);
jobject sharpenBitmap = createBitmap(env, dst.cols, dst.rows, "ARGB_8888");
matToBitmap(env, dst, sharpenBitmap);
return sharpenBitmap;
}
/**
* 锐化核函数实现
**/
void sharpen(const cv::Mat &myImage, cv::Mat &Result) {
CV_Assert(myImage.depth() == CV_8U); // 仅接受uchar图像
Result.create(myImage.size(), myImage.type());
const int nChannels = myImage.channels();
for (int j = 1; j < myImage.rows - 1; ++j) {
// 矩阵中的当前点
const uchar *previous = myImage.ptr<uchar>(j - 1);
// 矩阵中当前点的前一个点(当前列-1)
const uchar *current = myImage.ptr<uchar>(j);
// 矩阵中当前点的下一个点(当前列+1)
const uchar *next = myImage.ptr<uchar>(j + 1);
uchar *output = Result.ptr<uchar>(j);
for (int i = nChannels; i < nChannels * (myImage.cols - 1); ++i) {
*output++ = cv::saturate_cast<uchar>(5 * current[i]
- current[i - nChannels] - current[i + nChannels] -
previous[i] - next[i]);
// 或者使用其他锐化核函数
// *output++ = cv::saturate_cast<uchar>(9 * current[i]
// - current[i - nChannels] - current[i + nChannels]
// -previous[i] - previous[i - nChannels] - previous[i + nChannels]
// -next[i] - next[i - nChannels] - next[i + nChannels]);
}
}
// 不对边界点使用掩码,直接把它们设为0
Result.row(0).setTo(cv::Scalar(0)); // 上边界
Result.row(Result.rows - 1).setTo(cv::Scalar(0)); // 下边界
Result.col(0).setTo(cv::Scalar(0)); // 左边界
Result.col(Result.cols - 1).setTo(cv::Scalar(0));// 右边界
}
运行结果如下所示,左边是APP运行结果,右边是放大后的对比:
4.3 imgproc 模块
imgproc模块提供了许多图片处理的API,实际开发中可以直接调用OpenCV提供的图片处理API,我们将在下一章介绍imgproc模块。上述图片处理都可以在imgproc模块找到对应API:
// 滤波器
CVAPI(void) cvFilter2D( const CvArr* src, CvArr* dst, const CvMat* kernel,
CvPoint anchor CV_DEFAULT(cvPoint(-1,-1)));
The End
欢迎关注我,一起解锁更多技能:BC的掘金主页~💐BC的CSDN主页~💐💐
OpenCV官网:https://opencv.org/releases/
OpenCV github:https://github.com/opencv/opencv/tree/master
LearnOpenCV学习资料
OpenCV 4.5.5官方文档
OpenCV 2.3.2官方文档
文章出处登录后可见!