Эффективное внедрение корневого веб-камеры Java Native Interface

Я работаю над проектом, который берет видеовход с веб-камеры и отображает области движения для пользователя. Моя попытка "бета" в этом проекте состояла в том, чтобы использовать Java Media Framework для извлечения веб-камеры. Благодаря некоторым функциям утилиты JMF удобно возвращает фреймы веб-камер в качестве BufferedImages, и я создал значительную часть структуры для обработки. Однако вскоре я понял, что JMF не поддерживается Sun/Oracle больше, а некоторые из более высоких разрешений веб-камеры (720p) недоступны через интерфейс JMF.

Я хотел бы продолжить обработку фреймов как BufferedImages и использовать OpenCV (С++) для источника видеопотока. Используя только OpenCV-структуру, я обнаружил, что OpenCV отлично справляется с эффективным возвратом фреймов веб-камер высокого разрешения и рисует их на экране.

Я полагал, что было бы довольно просто передать эти данные на Java и добиться такой же эффективности. Я только что закончил писать JNI DLL, чтобы скопировать эти данные в BufferedImage и вернуть его на Java. Тем не менее, я нахожу, что количество копий данных, которые я делаю, действительно затрудняет работу. Я нацелен на 30 FPS, но для копирования копий данных из массива char, возвращаемого OpenCV, в Java BufferedImage требуется примерно 100 мс. Вместо этого я вижу около 2-5 FPS.

При возврате захвата кадра OpenCV предоставляет указатель на массив 1D char. Эти данные должны быть предоставлены Java, и, видимо, у меня нет времени на их копирование.

Мне нужно лучшее решение для захвата этих кадров в BufferedImage. Несколько решений, которые я рассматриваю, ни один из которых, я думаю, не очень хорош (достаточно уверен, что они также будут работать плохо):

(1) Переопределите BufferedImage и верните данные пикселей из различных методов BufferedImage, выполнив собственные вызовы в DLL. (Вместо того, чтобы делать копирование массива сразу, я возвращаю отдельные пиксели по запросу вызывающего кода). Обратите внимание, что для вызова кода обычно требуется все пиксели изображения, чтобы нарисовать изображение или обработать его, поэтому эта индивидуальная операция захвата пикселей будет реализована в 2D-контуре.

(2) Попросите BufferedImage использовать java.nio.ByteBuffer для прямого доступа к данным в массиве char, возвращаемом OpenCV. Поблагодарили бы за какие-либо советы относительно того, как это делается.

(3) Делайте все на С++ и забывайте Java. Хорошо, да, это звучит как наиболее логичное решение, однако у меня не будет времени, чтобы начать этот многомесячный проект с нуля.

На данный момент мой код JNI был написан для возврата BufferedImage, однако на данный момент я готов принять возврат массива 1D char, а затем поместить его в BufferedImage.

Кстати... вопрос здесь: Каков наиболее эффективный способ копирования массива данных изображения 1D char в BufferedImage?

Предоставляется (неэффективный) код, который я использую для исходного изображения из OpenCV и копируем в BufferedImage:

JNIEXPORT jobject JNICALL Java_graphicanalyzer_ImageFeedOpenCV_getFrame
  (JNIEnv * env, jobject jThis, jobject camera)
{
 //get the memory address of the CvCapture device, the value of which is encapsulated in the camera jobject
 jclass cameraClass = env->FindClass("graphicanalyzer/Camera");
 jfieldID fid = env->GetFieldID(cameraClass,"pCvCapture","I");

 //get the address of the CvCapture device
 int a_pCvCapture = (int)env->GetIntField(camera, fid);

 //get a pointer to the CvCapture device
    CvCapture *capture = (CvCapture*)a_pCvCapture;

 //get a frame from the CvCapture device
 IplImage *frame = cvQueryFrame( capture );

 //get a handle on the BufferedImage class
 jclass bufferedImageClass = env->FindClass("java/awt/image/BufferedImage");
 if (bufferedImageClass == NULL)
 {
  return NULL;
 }

 //get a handle on the BufferedImage(int width, int height, int imageType) constructor
 jmethodID bufferedImageConstructor = env->GetMethodID(bufferedImageClass,"<init>","(III)V");

 //get the field ID of BufferedImage.TYPE_INT_RGB
 jfieldID imageTypeFieldID = env->GetStaticFieldID(bufferedImageClass,"TYPE_INT_RGB","I");

 //get the int value from the BufferedImage.TYPE_INT_RGB field
 jint imageTypeIntRGB = env->GetStaticIntField(bufferedImageClass,imageTypeFieldID);

 //create a new BufferedImage
 jobject ret = env->NewObject(bufferedImageClass, bufferedImageConstructor, (jint)frame->width, (jint)frame->height, imageTypeIntRGB);

 //get a handle on the method BufferedImage.getRaster()
 jmethodID getWritableRasterID = env->GetMethodID(bufferedImageClass, "getRaster", "()Ljava/awt/image/WritableRaster;");

 //call the BufferedImage.getRaster() method
 jobject writableRaster = env->CallObjectMethod(ret,getWritableRasterID);

 //get a handle on the WritableRaster class
 jclass writableRasterClass = env->FindClass("java/awt/image/WritableRaster");

 //get a handle on the WritableRaster.setPixel(int x, int y, int[] rgb) method
 jmethodID setPixelID = env->GetMethodID(writableRasterClass, "setPixel", "(II[I)V"); //void setPixel(int, int, int[])

 //iterate through the frame we got above and set each pixel within the WritableRaster
 jintArray rgbArray = env->NewIntArray(3);
 jint rgb[3];
 char *px;
 for (jint x=0; x < frame->width; x++)
 {
  for (jint y=0; y < frame->height; y++)
  {
   px = frame->imageData+(frame->widthStep*y+x*frame->nChannels);
   rgb[0] = abs(px[2]);  // OpenCV returns BGR bit order
   rgb[1] = abs(px[1]);  // OpenCV returns BGR bit order
   rgb[2] = abs(px[0]);  // OpenCV returns BGR bit order
   //copy jint array into jintArray
   env->SetIntArrayRegion(rgbArray,0,3,rgb); //take values in rgb and move to rgbArray
   //call setPixel()  this is a copy operation
   env->CallVoidMethod(writableRaster,setPixelID,x,y,rgbArray);
  }
 }

 return ret;  //return the BufferedImage
}

Ответ 1

Удалось ускорить процесс с помощью NIO ByteBuffer.

На стороне С++ JNI...

JNIEXPORT jobject JNICALL Java_graphicanalyzer_ImageFeedOpenCV_getFrame
  (JNIEnv * env, jobject jThis, jobject camera)
{
    //...

    IplImage *frame = cvQueryFrame(pCaptureDevice);

    jobject byteBuf = env->NewDirectByteBuffer(frame->imageData, frame->imageSize);

    return byteBuf;
}

и на стороне Java...

void getFrame(Camera cam)
{
    ByteBuffer frameData = cam.getFrame();   //NATIVE call

    byte[] imgArray = new byte[frame.data.capacity()];
    frameData.get(imgArray); //although it seems like an array copy, this call returns very quickly
    DataBufferByte frameDataBuf = new DataBufferByte(imgArray,imgArray.length);

    //determine image sample model characteristics
    int dataType = DataBuffer.TYPE_BYTE;
    int width = cam.getFrameWidth();
    int height = cam.getFrameHeight();
    int pixelStride = cam.getPixelStride();
    int scanlineStride = cam.getScanlineStride();
    int bandOffsets = new int[] {2,1,0};  //BGR

    //create a WritableRaster with the DataBufferByte
    PixelInterleavedSampleModel pism = new PixelInterleavedSampleModel
    (
        dataType,
        width,
        height,
        pixelStride,
        scanlineStride,
        bandOffsets
    );
    WritableRaster raster = new ImgFeedWritableRaster( pism, frameDataBuf, new Point(0,0) );

    //create the BufferedImage
    ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
    ComponentColorModel cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
    BufferedImage newImg = new BufferedImage(cm,raster,false,null);

    handleNewImage(newImg);
}

Используя java.nio.ByteBuffer, я могу быстро обратиться к массиву char, возвращенному кодом OpenCV, без (видимо) делая много ужасного копирования массивов.

Ответ 2

Есть еще один вариант, если вы хотите сделать свой код очень быстрым и по-прежнему использовать Java. У инструментария AWT для окон есть прямой собственный интерфейс, который можно использовать для рисования на поверхности AWT с использованием C или С++. Таким образом, не нужно было ничего копировать на Java, поскольку вы могли бы визуализировать непосредственно из буфера на C или С++. Я не уверен в специфике того, как это сделать, потому что я не смотрел на это некоторое время, но я знаю, что он включен в стандартный дистрибутив JRE. Используя этот метод, вы могли бы подойти к пределу FPS камеры, если хотите, а не пытаться достичь 30 FPS.

Если вы хотите продолжить исследование, я бы начал здесь и здесь.

Счастливое программирование!

Ответ 3

Я бы построил массив RGB int, необходимый для BufferedImage, а затем использовал один вызов

 void setRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) 

чтобы сразу установить весь массив данных изображения. Или, по крайней мере, большие его части.

Без учета времени, я бы заподозрил, что он-пиксел вызывает

env->SetIntArrayRegion(rgbArray,0,3,rgb);
env->CallVoidMethod(writableRaster,setPixelID,x,y,rgbArray);

которые берут львиную долю времени.

РЕДАКТИРОВАТЬ: Вероятнее всего, это вызовы метода, а не манипулирование памятью, которая сама по себе занимает время. Поэтому создайте данные в коде JNI и скопируйте их в блоки или удалите один из них на образ Java. После создания и привязки Java int [] вы можете получить к нему доступ через собственные указатели. Затем один вызов setRGB скопирует массив в ваше изображение.

Примечание. Вам по-прежнему приходится копировать данные хотя бы один раз, но все пиксели в одном ударе через один вызов функции будут значительно более эффективными, чем их индивидуальное выполнение с помощью 2-х вызовов функций.

ИЗМЕНИТЬ 2:

Пересматривая мой код JNI, я использовал только массивы байтов, но принципы одинаковы для массивов int. Использование:

NewIntArray

для создания массива int и

GetIntArrayElements

чтобы связать его и получить указатель, а когда вы закончите,

ReleaseIntArrayElements

чтобы освободить его, не забывая использовать флаг для копирования данных обратно в кучу памяти Java.

Затем вы можете использовать дескриптор массива Java int для вызова функции setRGB.

Помните также, что на самом деле это устанавливает RGBA-пиксели, поэтому 4 канала, включая альфа, а не только три (имена RGB в Java, похоже, предшествуют альфа-каналу, но большинство так называемых методов совместимы с 32-битным значением).

Ответ 4

В качестве вторичного рассмотрения, если единственная разница между массивом данных изображения, возвращаемым OpenCV и тем, что требуется Java, - это BGR vs RGB, тогда

px = frame->imageData+(frame->widthStep*y+x*frame->nChannels);
rgb[0] = abs(px[2]);  // OpenCV returns BGR bit order
rgb[1] = abs(px[1]);  // OpenCV returns BGR bit order
rgb[2] = abs(px[0]);  // OpenCV returns BGR bit order

- относительно неэффективный способ их преобразования. Вместо этого вы можете сделать что-то вроде:

uint32 px = frame->imageData+(frame->widthStep*y+x*frame->nChannels);
javaArray[ofs]=((px&0x00FF0000)>>16)|(px&0x0000FF00)|((px&0x000000FF)<<16);

(обратите внимание, что мой код C ржавый, поэтому это может быть не совсем корректно, но оно показывает, что нужно).