最近在使用OpenCV
处理图片的时候,经常会遇到需要转换图像的情况,网上相关资料比较少,也不全,有时候得费劲老半天才能搞定。
自己踩了坑后,在这里记录下,都是我在项目中遇到的图像转化操作,是一些常用的图像格式转换操作。
具体包括:
nv21、rgba、rgb
转换OpenCV
的Mat
转为Bitmap
Bitmap
转成RGB888
NV21
转成Bitmap
Camera2
中的 android.media.Image
转为 NV21
Android
传递Bitmap
给JNI
,并转为rgba
的Mat
本文的操作都是基于
Activity
横屏的情况下进行的
nv21
是YUV420
格式中的一种,在Android
中,Camera1
获取的摄像头数据,就是NV21
格式的。
rgba、rgb
格式,是不同于YUV
的另一种色彩表示方式,通常我们需要转为RGB
格式,再去做图像检测和处理。
所以在Android
中,nv21
和rgb
的转换,是比较常用、比较普遍的。
这里传入的jbyteArray data_
是nv21
格式,首先转成nv21
的Mat
,然后在通过cv::cvtColor
方法,通过cv::COLOR_YUV2RGBA_NV21
这个参数值,转为rgba
格式的Mat
。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_heiko_myncnnlib_NcnnNativeLib_nv21toARGB(JNIEnv *env, jobject thiz, jbyteArray data_,
jint h, jint w) {
jbyte *data = env->GetByteArrayElements(data_, NULL);
cv::Mat nv21(h + h / 2, w, CV_8UC1, data);
cv::Mat rgba(h, w, CV_8UC4);
//nv21转为rgba格式
cv::cvtColor(nv21, rgba, cv::COLOR_YUV2RGBA_NV21);
//省略了后续无关代码....
//释放资源
env->ReleaseByteArrayElements(data_, data, 0);
}
将nv21
转成rgb
格式的Mat
,这里的COLOR_YUV420sp2RGB
和COLOR_YUV2RGB_NV21
是一样的。
cv::Mat rgba(h, w, CV_8UC3);
//将nv21的数据转为RGB
cv::cvtColor(nv21, rgb, cv::COLOR_YUV420sp2RGB); //也可以传COLOR_YUV2RGB_NV21
cv::Mat rgb(rows, cols, CV_8UC3);
//将rgba转为rgb
cv::cvtColor(rgba, rgb, CV_RGBA2RGB);
在JNI
中,用OpenCV
处理好图像后,得到的结果是Mat
,那么需要将其转为byteArray
,然后传递到Android
层,再转为Bitmap
,显示到ImageView
上。
转成RGBA
相对比较简单,只要将rgba
的Mat
,转为jbyteArray
,传递到Android
层就好。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_heiko_myncnnlib_NcnnNativeLib_nv21toARGB(JNIEnv *env, jobject thiz, jbyteArray data_,
jint h, jint w) {
jbyte *data = env->GetByteArrayElements(data_, NULL);
cv::Mat nv21(h + h / 2, w, CV_8UC1, data);
cv::Mat rgba(h, w, CV_8UC4);
cv::cvtColor(nv21, rgba, cv::COLOR_YUV2RGBA_NV21);
int rows = h;
int cols = w;
jbyteArray byteArray = env->NewByteArray(rows * cols * 4);
env->SetByteArrayRegion(byteArray, 0, rows * cols * 4, reinterpret_cast<jbyte*>(rgba.data));
env->ReleaseByteArrayElements(data_, data, 0);
return byteArray;
}
Android
层进行调用,这里创建Bitmap
的时候,使用的是Bitmap.Config.ARGB_8888
//由于前摄像头放置位置是90度方向的,所以这里height和width对调 (实际上应该是在JNI里进行旋转操作,这里是怎么方便怎么来)
var result = nativeLib.nv21toARGB(data,height,width)
//var result = nativeLib.nv21toARGB(data,width,height)
//byte数组转为ARGB8888的Bitmap
val bitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)
var buffer = ByteBuffer.wrap(result)
bitmap.copyPixelsFromBuffer(buffer)
runOnUiThread {
//显示到Bitmap上
binding.img1.setImageBitmap(bitmap)
}
先来看一下RGB888
转RGB565
的方法
uint16_t *rgb888toRgb565(cv::Mat &rgb, int rows, int cols) {
cv::Vec3b *data = rgb.ptr<cv::Vec3b>(0);
uint16_t *rgb565 = new uint16_t[rows * cols];
for (int i = 0; i < rows * cols; i++) {
int r = data[i][0];
int g = data[i][1];
int b = data[i][2];
rgb565[i] = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
return rgb565;
}
实现JNI
方法,这里传入的data_
是rgb888
格式,然后转成Mat
,再调用rgb888toRgb565
转成rgb565
,最后在转成jbyteArray
返回给Android
层。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_heiko_MyTest_rgb888ToRgb565(JNIEnv *env, jobject thiz, jbyteArray data_,jint w, jint h) {
jbyte *data = env->GetByteArrayElements(data_, NULL);
unsigned char *rgb_data = reinterpret_cast<unsigned char *>(data);
cv::Mat rgb(h, w, CV_8UC3, rgb_data);
int rows = h;
int cols = w;
jbyteArray byteArray = env->NewByteArray(rows * cols * 2);
uint16_t *rgb565 = rgb888toRgb565(rgb, rows, cols);
env->SetByteArrayRegion(byteArray, 0, rows * cols * 2, reinterpret_cast<jbyte *>(rgb565));
env->ReleaseByteArrayElements(data_, data, 0);
return byteArray;
}
Android
层进行调用,这里创建Bitmap
的时候,使用的是Bitmap.Config.RGB_565
//这里的data是RGB888格式,具体看4.x小节
val result : ByteArray = nativeLib.rgb888ToRgb565(data, imageWidth, imageHeight)
//byte数组转为Bitmap
val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.RGB_565)
var buffer = ByteBuffer.wrap(detectResult)
bitmap.copyPixelsFromBuffer(buffer)
runOnUiThread {
//显示到ImageView上
binding.img1.setImageBitmap(bitmap)
}
rgba
也可以先转成rgb565
后,再传递给Android
层,代码如下
uint16_t *rgbaToRgb565(cv::Mat &rgb, int rows, int cols) {
cv::Vec4b *data = rgb.ptr<cv::Vec4b>(0);
uint16_t *rgb565 = new uint16_t[rows * cols];
for (int i = 0; i < rows * cols; i++) {
int r = data[i][0];
int g = data[i][1];
int b = data[i][2];
rgb565[i] = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
return rgb565;
}
Android
中的Bitmap
是ARGB
格式进行存储的,所以我们先取到Bitmap
的像素数组,然后对其进行遍历,分别取到每个像素点的R
、G
、B
数据,赋值到新的ByteArray
里,就得到RGB888
格式的图像数据了。
//解析bytes为bitmap,bytes是jpeg格式的图片流
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
val width: Int = bitmap.width
val height: Int = bitmap.height
val pixels = IntArray(width * height)
//获取像素赋值给 pixels
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
val rgb888 = ByteArray(width * height * 3)
for (i in 0 until width * height) {
// 注意:Android的Bitmap是ARGB格式,而不是RGBA
rgb888[i * 3] = Color.red(pixels[i]).toByte()
rgb888[i * 3 + 1] = Color.green(pixels[i]).toByte()
rgb888[i * 3 + 2] = Color.blue(pixels[i]).toByte()
}
这里的yuv420
的具体格式是NV21
,也就是将NV21
格式转为Bitamp
。
具体操作为先将nv21
的ByteArray
转化为YuvImage
对象,然后压缩为JPEG
格式的ByteArray
,最后通过BitmapFactory.decodeByteArray()
来得到Bitmap
。
fun convertYUV420ToBitmap(
yuv420Data: ByteArray?,
width: Int,
height: Int
): Bitmap {
// 创建YuvImage对象
val yuvImage = YuvImage(yuv420Data, ImageFormat.NV21, width, height, null)
// 创建ByteArrayOutputStream对象
val outputStream = ByteArrayOutputStream()
// 将YuvImage对象压缩为JPEG格式的数据
yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, outputStream)
// 将JPEG数据解码为Bitmap对象
val jpegData = outputStream.toByteArray()
return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
}
Android Camera2
相机中取到的一帧数据是android.media.Image
,我们设置android.graphics.ImageFormat
为ImageFormat.YUV_420_888
,这个格式是YCbCr
的泛化格式,不会具体指明是YU12,YV12,NV12
,或是是NV21
。它能够表示任何4:2:0
的平面和半平面格式,每个分量用8 bits
表示。
这里,我们来将Image
转为NV21
格式。
fun imageToNV21(image: Image): ByteArray {
val planes: Array<Image.Plane> = image.planes
val yBuffer = planes[0].buffer
val uBuffer = planes[1].buffer
val vBuffer = planes[2].buffer
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val yuvData = ByteArray(ySize + uSize + vSize)
yBuffer[yuvData, 0, ySize]
vBuffer[yuvData, ySize, vSize]
uBuffer[yuvData, ySize + vSize, uSize]
return yuvData
}
Android
中,也可以直接向JNI
传递Bitmap
对象,然后在JNI
中,再去对Bitmap进行操作。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_zeekr_ncnnlib_NcnnNativeLib_humanDetectBitmap(JNIEnv *env, jobject thiz, jobject bitmap) {
AndroidBitmapInfo bitmapInfo;
//获取Bitmap的信息
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
int rows = bitmapInfo.height;
int cols = bitmapInfo.width;
void *bitmapPixels;
//获取Bitmap的像素
AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);
//转成rgba的Mat
cv::Mat rgba(rows, cols, CV_8UC4, bitmapPixels);
AndroidBitmap_unlockPixels(env, bitmap);
//省略了后续无关代码
}
关于在JNI中创建Bitmap,并传递到Android层,具体可以看我的这篇文章 : Android JNI/NDK 入门从一到二