I am developing an Android application using Java. In my application, I am doing some image processing. So I am using c++ and Open CV for it and calling the c++ function through JNI. I am trying to convert equirectangular/spherical image to cubemap image.
I found this link for conversion, https://code.i-harness.com/en/q/1c4dbae/. I am passing Mat from Java and trying to return the converted image back to Java.
This is my C++ code
#include <jni.h>
#include <string>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
using namespace std;
using namespace cv;
extern "C"
JNIEXPORT jstring
JNICALL
Java_media_memento_memento_VRPhotoSphereActivity_convertEquiRectToCubeMap(
JNIEnv *env,
jobject /* this */, jlong addrMat, jlong addrNewMat) {
Mat& mat = *(Mat*)addrMat;
Mat& newMat = *(Mat*)addrNewMat;
newMat.create(mat.rows, mat.cols, mat.type());
memcpy(newMat.data, mat.data , sizeof(mat.data) -1);
//EquiRec to Cubemap conversion starts from here
float faceTransform[6][2] =
{
{0, 0},
{M_PI / 2, 0},
{M_PI, 0},
{-M_PI / 2, 0},
{0, -M_PI / 2},
{0, M_PI / 2}
};
//conversion ends here
const Mat &in= mat;
Mat face = newMat;
int faceId = 0;
const int width = -1;
const int height = -1;
float inWidth = in.cols;
float inHeight = in.rows;
// Allocate map
Mat mapx(height, width, CV_32F);
Mat mapy(height, width, CV_32F);
// Calculate adjacent (ak) and opposite (an) of the
// triangle that is spanned from the sphere center
//to our cube face.
const float an = sin(M_PI / 4);
const float ak = cos(M_PI / 4);
const float ftu = faceTransform[faceId][0];
const float ftv = faceTransform[faceId][1];
// For each point in the target image,
// calculate the corresponding source coordinates.
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
// Map face pixel coordinates to [-1, 1] on plane
float nx = (float)y / (float)height - 0.5f;
float ny = (float)x / (float)width - 0.5f;
nx *= 2;
ny *= 2;
// Map [-1, 1] plane coords to [-an, an]
// thats the coordinates in respect to a unit sphere
// that contains our box.
nx *= an;
ny *= an;
float u, v;
// Project from plane to sphere surface.
if(ftv == 0) {
// Center faces
u = atan2(nx, ak);
v = atan2(ny * cos(u), ak);
u += ftu;
} else if(ftv > 0) {
// Bottom face
float d = sqrt(nx * nx + ny * ny);
v = M_PI / 2 - atan2(d, ak);
u = atan2(ny, nx);
} else {
// Top face
float d = sqrt(nx * nx + ny * ny);
v = -M_PI / 2 + atan2(d, ak);
u = atan2(-ny, nx);
}
// Map from angular coordinates to [-1, 1], respectively.
u = u / (M_PI);
v = v / (M_PI / 2);
// Warp around, if our coordinates are out of bounds.
while (v < -1) {
v += 2;
u += 1;
}
while (v > 1) {
v -= 2;
u += 1;
}
while(u < -1) {
u += 2;
}
while(u > 1) {
u -= 2;
}
// Map from [-1, 1] to in texture space
u = u / 2.0f + 0.5f;
v = v / 2.0f + 0.5f;
u = u * (inWidth - 1);
v = v * (inHeight - 1);
// Save the result for this pixel in map
mapx.at<float>(x, y) = u;
mapy.at<float>(x, y) = v;
}
}
// Recreate output image if it has wrong size or type.
if(face.cols != width || face.rows != height ||
face.type() != in.type()) {
face = Mat(width, height, in.type());
}
// Do actual resampling using OpenCV's remap
Mat i = in;
Mat f = face;
remap(i, f, mapx, mapy,
CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));
//send the image back here. For now the feature is not implemented yet.
std::string hello = "Spherical equirectangular photo converted to cubemap face photo";
return env->NewStringUTF(hello.c_str());
}
When I tried to run my application, it is giving me this compilation error.
Error:(146) undefined reference to `cv::remap(cv::_InputArray const&, cv::_OutputArray const&, cv::_InputArray const&, cv::_InputArray const&, int, int, cv::Scalar_<double> const&)'
This is the screenshot.
How can I fix that error?
Edit
Actually, it is throwing error starting from this line
Mat mapx(height, width, CV_32F);
Mat mapy(height, width, CV_32F);
This is the screenshot
Related
There is a comment on here, on a stackflow post that answers the question of how to achieve super fast median calculation on opencv. The question is here:
super fast median of matrix in opencv (as fast as matlab)
The problem is that the code is on C/C++ and I can't find a way to convert this to opencv for java or C#, since I'm trying to port this to OpenCV for Xamarin.Forms
Anyone knows how to conver this?
double medianMat(cv::Mat Input, int nVals) {
// COMPUTE HISTOGRAM OF SINGLE CHANNEL MATRIX
float range[] = {
0,
nVals
};
const float * histRange = {
range
};
bool uniform = true;
bool accumulate = false;
cv::Mat hist;
calcHist( & Input, 1, 0, cv::Mat(), hist, 1, & nVals, & histRange, uniform, accumulate);
// COMPUTE CUMULATIVE DISTRIBUTION FUNCTION (CDF)
cv::Mat cdf;
hist.copyTo(cdf);
for (int i = 1; i <= nVals - 1; i++) {
cdf.at < float > (i) += cdf.at < float > (i - 1);
}
cdf /= Input.total();
// COMPUTE MEDIAN
double medianVal;
for (int i = 0; i <= nVals - 1; i++) {
if (cdf.at < float > (i) >= 0.5) {
medianVal = i;
break;
}
}
return medianVal / nVals;
}
UPDATE:
As an example, this is my tried method on c# to calculate the mediam of a grey scale Mat. It fails, can you see what I did wrong?
private static int Median2(Mat Input)
{
// COMPUTE HISTOGRAM OF SINGLE CHANNEL MATRIX
MatOfInt mHistSize = new MatOfInt(256);
MatOfFloat mRanges = new MatOfFloat(0f, 256f);
MatOfInt channel = new MatOfInt(0); // only 1 channel for grey
Mat temp = new Mat();
Mat hist = new Mat();
Imgproc.CalcHist(Java.Util.Arrays.AsList(Input).Cast<Mat>().ToList(), channel, temp, hist, mHistSize, mRanges);
float[] cdf = new float[(int)hist.Total()];
hist.Get(0, 0, cdf);
for (int i = 1; i < 256; i++)
{
cdf[i] += cdf[i - 1];
cdf[i - 1] = cdf[i - 1] / Input.Total();
}
// COMPUTE CUMULATIVE DISTRIBUTION FUNCTION (CDF)
float total = (float)Input.Total();
// COMPUTE MEDIAN
double medianVal=0;
for (int i = 0; i < 256; i++)
{
if (cdf[i] >= 0.5) {
medianVal = i;
break;
}
}
return (int)(medianVal / 256);
}
I am trying to floodfill a bitmap using Renderscript. and my renderscript file progress.rs is
#pragma version(1)
#pragma rs java_package_name(com.intel.sample.androidbasicrs)
rs_allocation input;
int width;
int height;
int xTouchApply;
int yTouchApply;
static int same(uchar4 pixel, uchar4 in);
uchar4 __attribute__((kernel)) root(const uchar4 in, uint32_t x, uint32_t y) {
uchar4 out = in;
rsDebug("Process.rs : image width: ", width);
rsDebug("Process.rs : image height: ", height);
rsDebug("Process.rs : image pointX: ", xTouchApply);
rsDebug("Process.rs : image pointY: ", yTouchApply);
if(xTouchApply >= 0 && xTouchApply < width && yTouchApply >=0 && yTouchApply < height){
// getting touched pixel
uchar4 pixel = rsGetElementAt_uchar4(input, xTouchApply, yTouchApply);
rsDebug("Process.rs : getting touched pixel", 0);
// resets the pixel stack
int topOfStackIndex = 0;
// creating pixel stack
int pixelStack[width*height];
// Pushes the touched pixel onto the stack
pixelStack[topOfStackIndex] = xTouchApply;
pixelStack[topOfStackIndex+1] = yTouchApply;
topOfStackIndex += 2;
//four way stack floodfill algorithm
while(topOfStackIndex>0){
rsDebug("Process.rs : looping while", 0);
// Pops a pixel from the stack
int x = pixelStack[topOfStackIndex - 2];
int y1 = pixelStack[topOfStackIndex - 1];
topOfStackIndex -= 2;
while (y1 >= 0 && same(rsGetElementAt_uchar4(input, x, y1), pixel)) {
y1--;
}
y1++;
int spanLeft = 0;
int spanRight = 0;
while (y1 < height && same(rsGetElementAt_uchar4(input, x, y1), pixel)) {
rsDebug("Process.rs : pointX: ", x);
rsDebug("Process.rs : pointY: ", y1);
float3 outPixel = dot(f4.rgb, channelWeights);
out = rsPackColorTo8888(outPixel);
// conditions to traverse skipPixels to check threshold color(Similar color)
if (!spanLeft && x > 0 && same(rsGetElementAt_uchar4(input, x - 1, y1), pixel)) {
// Pixel to the left must also be changed, pushes it to the stack
pixelStack[topOfStackIndex] = x - 1;
pixelStack[topOfStackIndex + 1] = y1;
topOfStackIndex += 2;
spanLeft = 1;
} else if (spanLeft && !same(rsGetElementAt_uchar4(input, x - 1, y1), pixel)) {
// Pixel to the left has already been changed
spanLeft = 0;
}
// conditions to traverse skipPixels to check threshold color(Similar color)
if (!spanRight && x < width - 1 && same(rsGetElementAt_uchar4(input, x + 1, y1), pixel)) {
// Pixel to the right must also be changed, pushes it to the stack
pixelStack[topOfStackIndex] = x + 1;
pixelStack[topOfStackIndex + 1] = y1;
topOfStackIndex += 2;
spanRight = 1;
} else if (spanRight && x < width - 1 && !same(rsGetElementAt_uchar4(input, x + 1, y1), pixel)) {
// Pixel to the right has already been changed
spanRight = 0;
}
y1++;
}
}
}
return out;
}
static int same(uchar4 px, uchar4 inPx){
int isSame = 0;
if((px.r == inPx.r) && (px.g == inPx.g) && (px.b == inPx.b) && (px.a == inPx.a)) {
isSame = 1;
// rsDebug("Process.rs : matching pixel: ", isSame);
} else {
isSame = 0;
}
// rsDebug("Process.rs : matching pixel: ", isSame);
return isSame;
}
and my Activity's code is:
inputBitmap = Bitmap.createScaledBitmap(inputBitmap, displayWidth, displayHeight, false);
// Create an allocation (which is memory abstraction in the RenderScript)
// that corresponds to the inputBitmap.
allocationIn = Allocation.createFromBitmap(
rs,
inputBitmap,
Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT
);
allocationOut = Allocation.createTyped(rs, allocationIn.getType());
int imageWidth = inputBitmap.getWidth();
int imageHeight = inputBitmap.getHeight();
script.set_width(imageWidth);
script.set_height(imageHeight);
script.set_input(allocationIn);
//....
//....
// and my onTouchEvent Code is
script.set_xTouchApply(xTouchApply);
script.set_yTouchApply(yTouchApply);
// Run the script.
script.forEach_root(allocationIn, allocationOut);
allocationOut.copyTo(outputBitmap);
when I touched bitmap it is showing Application not responding. It is because of root method is calling for every pixels. How can I optimize this code. And how can I compare two uchar4 variables in Renderscript? How can I improve my same method? Or How can I find similar neighbor pixels using threshold value? I got stuck. Please guys help me.
I don't have much knowledge of c99 programming language and Renderscript. Can you guys debug my renderscript code. and please tell me what's wrong in this code. Or can I improve this renderscript code to floodfill the bitmap. Any help will be appreciated And sorry for my poor English ;-) . Thanks
Renderscript is Android's front-end to GPU-instructions. And it is extremely good if you want to perform operations on each pixel because it uses the massive GPU-parallelism-capabilities. So, you can run an operation on each pixel. For this purpose, you start a program in Renderscript with sth like "for all pixels, do the following".
The flood fill algorithm though cannot run in such a parallel environment because you only know which pixel to paint after painting another pixel before it. This is not only true for renderscript but all GPU-related libraries, like CUDA or others.
I want to rotate YUV420SP image by 90 counter clockwise. Image size is 640*480, so the rotated image size becomes 480*640 which i don't want, So i want to extract 480*480 data (or any other square size) and rotate that data.
I have seen : Rotate an YUV byte array on Android
But this answer rotates 90 clockwise.
Can somebody suggest some function which rotates YUV420Sp data by 90(counter clockwise) or by 270 degrees (clockwise) without changing image dimensions.
OK, here's my native code that evolved after much banging of the head.
My difficulty was that I didn't understand planar image formats until I saw this and this:
Here are the 2 functions I eventually wrote:
// rotate luma image plane 90*
//
// (dst direction)
// ------>
// dst -> +-------------+
// |^ |
// |^ (base dir) |
// |^ |
// base -> +-------------+ <- endp
//
//////////////////////////////////////////////////////////
void rotateLumaPlane90(const unsigned char *src, unsigned char *dst,
size_t size, size_t width, size_t height)
{
const unsigned char *endp;
const unsigned char *base;
int j;
endp = src + size;
for (base = endp - width; base < endp; base++) {
src = base;
for (j = 0; j < height; j++, src -= width)
{
*dst++ = *src;
}
}
}
//
// nv12 chroma plane is interleaved chroma values that map
// from one pair of chroma to 4 pixels:
//
// Y1 Y2 Y3 Y4
// Y5 Y6 Y7 Y8 U1,V1 -> chroma values for block Y1 Y2
// Y9 Ya Yb Yc Y5 Y6
// Yd Ye Yf Yg
// ----------- U2,V2 -> chroma values for block Y3 Y4
// U1 V1 U2 V2 Y7 Y8
// U3 V3 U4 V4
//
//////////////////////////////////////////////////////////
void rotateChromaPlane90(const unsigned char *src, unsigned char *dst,
size_t size, size_t width, size_t height)
{
// src will start at upper right, moving down to bottom
// then left 1 col and down...
//
// dest will start at end and go to 0
int row = 0;
int col = (int) width;
int src_offset = col - 1;
int dst_offset = (int) size - 2;
while (src_offset >= 0)
{
dst[dst_offset] = src[src_offset];
dst[dst_offset+1] = src[src_offset+1];
dst_offset -= 2;
src_offset += width;
row++;
if (row >= height) {
col -= 2;
src_offset = col;
row = 0;
}
}
}
And here is a sample of me calling these funcs from android native:
// first rotate the Y plane
rotateLumaPlane90((unsigned char *) encode_buffer,
rotate_buffer,
yPlaneSize,
gInputWidth,
gInputHeight);
// now rotate the U and V planes
rotateChromaPlane90((unsigned char *) encode_buffer + yPlaneSize,
rotate_buffer + yPlaneSize,
yPlaneSize / 2,
gInputWidth,
gInputHeight/2);
Notice the last param to the rotateChromaPlane90 is the height of the original image/2. I should probably just change the chroma rotate function to make that less error-prone.
When flipped to the back facing camera I then found I needed to rotate 90* in the opposite direction (or 270*) so I also have a 270* variation as:
// rotate luma image plane 270*
//
// +-------------+
// |^ |
// |^ (base dir) |
// |^ |
// base -> +-------------+ <- endp
// ^
// <---------- |
// (dst dir) dst
//
//////////////////////////////////////////////////////////
void rotateLumaPlane270(unsigned char *src,
register unsigned char *dst,
int size, int width, int height)
{
unsigned char *endp;
register unsigned char *base;
int j;
endp = src + size;
dst = dst + size - 1;
for (base = endp - width; base < endp; base++) {
src = base;
for (j = 0; j < height; j++, src -= width)
{
*dst-- = *src;
}
}
}
//
// nv21 chroma plane is interleaved chroma values that map
// from one pair of chroma to 4 pixels:
//
// Y1 Y2 Y3 Y4
// Y5 Y6 Y7 Y8 U1,V1 -> chroma values for block Y1 Y2
// Y9 Ya Yb Yc Y5 Y6
// Yd Ye Yf Yg
// ----------- U2,V2 -> chroma values for block Y3 Y4
// U1 V1 U2 V2 Y7 Y8
// U3 V3 U4 V4
//
//////////////////////////////////////////////////////////
void rotateChromaPlane270(unsigned char *src,
register unsigned char *dst,
int size, int width, int height)
{
// src will start at upper right, moving down to bottom
// then left 1 col and down...
//
// dest will start at 0 and go til end
int row = 0;
int col = width;
int src_offset = col - 1;
int dst_offset = 0;
while (src_offset > 0)
{
dst[dst_offset++] = src[src_offset];
dst[dst_offset++] = src[src_offset+1];
src_offset += width;
row++;
if (row >= height) {
col -= 2;
src_offset = col;
row = 0;
}
}
}
I've just started to play around with NDK to explore the sweet performance boost that I've been promised. To get a feel for the difference, I tried a dumb number-crunching task (render a Mandelbrot set to a bitmap) and compared it to a Java version of the same code. To my big surprise, the C version is significantly slower (5.0 seconds vs 1.6 on my HTC One, on average). Even stranger, the cost isn't because of the overhead of making a native call, but it's the actual number-crunching that takes longer.
This can't be right, can it? What did I miss?
C version (debug timer code removed):
const int MAX_ITER = 63;
const float MAX_DEPTH = 16;
static uint16_t rgb565(int red, int green, int blue)
{
return (uint16_t)(((red << 8) & 0xf800) | ((green << 2) & 0x03e0) | ((blue >> 3) & 0x001f));
}
float zAbs(float re, float im) {
return re*re + im*im;
}
int depth(float cRe, float cIm) {
int i=0;
float re, im;
float zRe = 0.0f;
float zIm = 0.0f;
while ((zAbs(zRe, zIm) < MAX_DEPTH) && (i < MAX_ITER)) {
re = zRe * zRe - zIm * zIm + cRe;
im = 2.0f * zRe * zIm + cIm;
zRe = re;
zIm = im;
i++;
}
return i;
}
extern "C"
void Java_com_example_ndktest_MainActivity_renderFractal(JNIEnv* env, jobject thiz, jobject bitmap, float re0, float im0, float b)
{
AndroidBitmapInfo info;
void* pixels;
int ret;
long t0 = currentTimeInMilliseconds();
if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
if (info.format != ANDROID_BITMAP_FORMAT_RGB_565) {
LOGE("Bitmap format is not RGB_565 !");
return;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
int w = info.width;
int h = info.height;
float re, im;
int z = 0;
uint16_t* px = (uint16_t*)pixels;
for(int y=0; y<h; y++) {
im = im0 + b*((float)y/(float)h);
for(int x=0; x<info.width; x++) {
re = re0 + b*((float)x/(float)w);
z = depth(re, im);
px[y*w + x] = rgb565(0, z*4, z * 16);
}
}
AndroidBitmap_unlockPixels(env, bitmap);
}
Java version:
private static final int MAX_ITER = 63;
private static final float MAX_DEPTH = 16;
static int rgb565(int red, int green, int blue)
{
return ((red << 8) & 0xf800) | ((green << 2) & 0x03e0) | ((blue >> 3) & 0x001f);
}
static float zAbs(float re, float im) {
return re*re + im*im;
}
static int depth(float cRe, float cIm) {
int i=0;
float re, im;
float zRe = 0.0f;
float zIm = 0.0f;
while ((zAbs(zRe, zIm) < MAX_DEPTH) && (i < MAX_ITER)) {
re = zRe * zRe - zIm * zIm + cRe;
im = 2.0f * zRe * zIm + cIm;
zRe = re;
zIm = im;
i++;
}
return i;
}
static void renderFractal(Bitmap bitmap, float re0, float im0, float b)
{
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] pixels = new int[w * h];
bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
float re, im;
int z = 0;
for(int y=0; y<h; y++) {
im = im0 + b*((float)y/(float)h);
for(int x=0; x<w; x++) {
re = re0 + b*((float)x/(float)w);
z = depth(re, im);
pixels[y*w + x] = rgb565(0, z*4, z * 16);
}
}
bitmap.setPixels(pixels, 0, w, 0, 0, w, h);
}
As noted in the comments, this was because the NDK code was built for the armeabi target rather than the armeabi-v7a target. The former is intended to work across a broad range of hardware, including devices without floating-point hardware, so it does all floating-point calculations in software.
Building for armeabi-v7a enables the VFP instructions, so anything that relies heavily on floating point calculations will speed up dramatically.
If you build exclusively for armeabi-v7a, you will exclude a fairly broad selection of devices, even relatively recent ones (e.g. the Samsung Galaxy Ace). These devices have VFP support, but the CPU is based on the ARMv6 instruction set rather than ARMv7. There is no "pre-ARMv7 CPU with VFP" build target, so you have to build for armeabi, or use custom build rules and careful selection of supported devices.
At the other end of the spectrum, you may get a small performance boost by specifying hard-float ABI within your armeabi-v7a library (-mhard-float -- requires NDK r9b).
FWIW, one of the selling points of just-in-time compilers like the one in Dalvik is that they can recognize the system capabilities and adapt code generation appropriately.
My Problem is: I've set up a camera in Android and receive the preview data by using an onPreviewFrame-listener which passes me an byte[] array containing the image data in the default android YUV-format (device does not support R5G6B5-format). Each pixel consists of 12bits which makes the thing a little tricky. Now what I want to do is converting the YUV-data into ARGB-data in order to do image processing with it. This has to be done with renderscript, in order to maintain a high performance.
My idea was to pass two pixels in one element (which would be 24bits = 3 bytes) and then return two ARGB pixels. The problem is, that in Renderscript a u8_3 (a 3dimensional 8bit vector) is stored in 32bit, which means that the last 8 bits are unused. But when copying the image data into the allocation all of the 32bits are used, so the last 8bit get lost. Even if I used a 32bit input data, the last 8bit are useless, because they're only 2/3 of a pixel. When defining an element consisting a 3-byte-array it actually has a real size of 3 bytes. But then the Allocation.copyFrom()-method doesn't fill the in-Allocation with data, argueing it doesn't has the right data type to be filled with a byte[].
The renderscript documentation states, that there is a ScriptIntrinsicYuvToRGB which should do exactly that in API Level 17. But in fact the class doesn't exist. I've downloaded API Level 17 even though it seems not to be downloadable any more. Does anyone have any information about it? Does anyone have ever tried out a ScriptIntrinsic?
So in conclusion my question is: How to convert the camera data into ARGB data fast, hardwareaccelerated?
That's how to do it in Dalvik VM (found the code somewhere online, it works):
#SuppressWarnings("unused")
private void decodeYUV420SP(int[] rgb, byte[] yuv420sp, int width, int height) {
final int frameSize = width * height;
for (int j = 0, yp = 0; j < height; j++) {
int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
for (int i = 0; i < width; i++, yp++) {
int y = (0xff & ((int) yuv420sp[yp])) - 16;
if (y < 0)
y = 0;
if ((i & 1) == 0) {
v = (0xff & yuv420sp[uvp++]) - 128;
u = (0xff & yuv420sp[uvp++]) - 128;
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
if (r < 0)
r = 0;
else if (r > 262143)
r = 262143;
if (g < 0)
g = 0;
else if (g > 262143)
g = 262143;
if (b < 0)
b = 0;
else if (b > 262143)
b = 262143;
rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
}
}
}
I'm sure you will find the LivePreview test application interesting ... it's part of the Android source code in the latest Jelly Bean (MR1). It implements a camera preview and uses ScriptIntrinsicYuvToRgb to convert the preview data with Renderscript. You can browse the source online here:
LivePreview
I was not able to get running ScriptInstrinsicYuvToRgb, so I decided to write my own RS solution.
Here's ready script (named yuv.rs):
#pragma version(1)
#pragma rs java_package_name(com.package.name)
rs_allocation gIn;
int width;
int height;
int frameSize;
void yuvToRgb(const uchar *v_in, uchar4 *v_out, const void *usrData, uint32_t x, uint32_t y) {
uchar yp = rsGetElementAtYuv_uchar_Y(gIn, x, y) & 0xFF;
int index = frameSize + (x & (~1)) + (( y>>1) * width );
int v = (int)( rsGetElementAt_uchar(gIn, index) & 0xFF ) -128;
int u = (int)( rsGetElementAt_uchar(gIn, index+1) & 0xFF ) -128;
int r = (int) (1.164f * yp + 1.596f * v );
int g = (int) (1.164f * yp - 0.813f * v - 0.391f * u);
int b = (int) (1.164f * yp + 2.018f * u );
r = r>255? 255 : r<0 ? 0 : r;
g = g>255? 255 : g<0 ? 0 : g;
b = b>255? 255 : b<0 ? 0 : b;
uchar4 res4;
res4.r = (uchar)r;
res4.g = (uchar)g;
res4.b = (uchar)b;
res4.a = 0xFF;
*v_out = res4;
}
Don't forget to set camera preview format to NV21:
Parameters cameraParameters = camera.getParameters();
cameraParameters.setPreviewFormat(ImageFormat.NV21);
// Other camera init stuff: preview size, framerate, etc.
camera.setParameters(cameraParameters);
Allocations initialization and script usage:
// Somewhere in initialization section
// w and h are variables for selected camera preview size
rs = RenderScript.create(this);
Type.Builder tbIn = new Type.Builder(rs, Element.U8(rs));
tbIn.setX(w);
tbIn.setY(h);
tbIn.setYuvFormat(ImageFormat.NV21);
Type.Builder tbOut = new Type.Builder(rs, Element.RGBA_8888(rs));
tbOut.setX(w);
tbOut.setY(h);
inData = Allocation.createTyped(rs, tbIn.create(), Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT & Allocation.USAGE_SHARED);
outData = Allocation.createTyped(rs, tbOut.create(), Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT & Allocation.USAGE_SHARED);
outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
yuvScript = new ScriptC_yuv(rs);
yuvScript.set_gIn(inData);
yuvScript.set_width(w);
yuvScript.set_height(h);
yuvScript.set_frameSize(previewSize);
//.....
Camera callback method:
public void onPreviewFrame(byte[] data, Camera camera) {
// In your camera callback, data
inData.copyFrom(data);
yuvScript.forEach_yuvToRgb(inData, outData);
outData.copyTo(outputBitmap);
// draw your bitmap where you want to
// .....
}
For anyone who didn't know, RenderScript is now in the Android Support Library, including intrinsics.
http://android-developers.blogspot.com.au/2013/09/renderscript-in-android-support-library.html
http://android-developers.blogspot.com.au/2013/08/renderscript-intrinsics.html
We now have the new renderscript-intrinsics-replacement-toolkit to do it. First, build and import the renderscript module to your project and add it as a dependency to your app module. Then, go to Toolkit.kt and add the following:
fun toNv21(image: Image): ByteArray? {
val nv21 = ByteArray((image.width * image.height * 1.5f).toInt())
return if (!nativeYuv420toNv21(
nativeHandle,
image.width,
image.height,
image.planes[0].buffer, // Y buffer
image.planes[1].buffer, // U buffer
image.planes[2].buffer, // V buffer
image.planes[0].pixelStride, // Y pixel stride
image.planes[1].pixelStride, // U/V pixel stride
image.planes[0].rowStride, // Y row stride
image.planes[1].rowStride, // U/V row stride
nv21
)
) {
null
} else nv21
}
private external fun nativeYuv420toNv21(
nativeHandle: Long,
imageWidth: Int,
imageHeight: Int,
yByteBuffer: ByteBuffer,
uByteBuffer: ByteBuffer,
vByteBuffer: ByteBuffer,
yPixelStride: Int,
uvPixelStride: Int,
yRowStride: Int,
uvRowStride: Int,
nv21Output: ByteArray
): Boolean
Now, go to JniEntryPoints.cpp and add the following:
extern "C" JNIEXPORT jboolean JNICALL Java_com_google_android_renderscript_Toolkit_nativeYuv420toNv21(
JNIEnv *env, jobject/*thiz*/, jlong native_handle,
jint image_width, jint image_height, jobject y_byte_buffer,
jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride,
jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride,
jbyteArray nv21_array) {
auto y_buffer = static_cast<jbyte*>(env->GetDirectBufferAddress(y_byte_buffer));
auto u_buffer = static_cast<jbyte*>(env->GetDirectBufferAddress(u_byte_buffer));
auto v_buffer = static_cast<jbyte*>(env->GetDirectBufferAddress(v_byte_buffer));
jbyte* nv21 = env->GetByteArrayElements(nv21_array, nullptr);
if (nv21 == nullptr || y_buffer == nullptr || u_buffer == nullptr
|| v_buffer == nullptr) {
// Log this.
return false;
}
RenderScriptToolkit* toolkit = reinterpret_cast<RenderScriptToolkit*>(native_handle);
toolkit->yuv420toNv21(image_width, image_height, y_buffer, u_buffer, v_buffer,
y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride,
nv21);
env->ReleaseByteArrayElements(nv21_array, nv21, 0);
return true;
}
Go to YuvToRgb.cpp and add the following:
void RenderScriptToolkit::yuv420toNv21(int image_width, int image_height, const int8_t* y_buffer,
const int8_t* u_buffer, const int8_t* v_buffer, int y_pixel_stride,
int uv_pixel_stride, int y_row_stride, int uv_row_stride,
int8_t *nv21) {
// Copy Y channel.
for(int y = 0; y < image_height; ++y) {
int destOffset = image_width * y;
int yOffset = y * y_row_stride;
memcpy(nv21 + destOffset, y_buffer + yOffset, image_width);
}
if (v_buffer - u_buffer == sizeof(int8_t)) {
// format = nv21
// TODO: If the format is VUVUVU & pixel stride == 1 we can simply the copy
// with memcpy. In Android Camera2 I have mostly come across UVUVUV packaging
// though.
}
// Copy UV Channel.
int idUV = image_width * image_height;
int uv_width = image_width / 2;
int uv_height = image_height / 2;
for(int y = 0; y < uv_height; ++y) {
int uvOffset = y * uv_row_stride;
for (int x = 0; x < uv_width; ++x) {
int bufferIndex = uvOffset + (x * uv_pixel_stride);
// V channel.
nv21[idUV++] = v_buffer[bufferIndex];
// U channel.
nv21[idUV++] = u_buffer[bufferIndex];
}
}
}
Finally, go to RenderscriptToolkit.h and add the following:
/**
* https://blog.minhazav.dev/how-to-use-renderscript-to-convert-YUV_420_888-yuv-image-to-bitmap/#tobitmapimage-image-method
* #param image_width width of the image you want to convert to byte array
* #param image_height height of the image you want to convert to byte array
* #param y_buffer Y buffer
* #param u_buffer U buffer
* #param v_buffer V buffer
* #param y_pixel_stride Y pixel stride
* #param uv_pixel_stride UV pixel stride
* #param y_row_stride Y row stride
* #param uv_row_stride UV row stride
* #param nv21 the output byte array
*/
void yuv420toNv21(int image_width, int image_height, const int8_t* y_buffer,
const int8_t* u_buffer, const int8_t* v_buffer, int y_pixel_stride,
int uv_pixel_stride, int y_row_stride, int uv_row_stride,
int8_t *nv21);
You are now ready to harness the full power of renderscript. Below, I am providing an example with the ARCore Camera Image object (replace the first line with whatever code gives you your camera image):
val cameraImage = arFrame.frame.acquireCameraImage()
val width = cameraImage.width
val height = cameraImage.height
val byteArray = Toolkit.toNv21(cameraImage)
byteArray?.let {
Toolkit.yuvToRgbBitmap(
byteArray,
width,
height,
YuvFormat.NV21
).let { bitmap ->
saveBitmapToDevice(
name,
session,
bitmap,
context
)}}