Как напрямую повернуть изображение CVImageBuffer в IOS 4 без преобразования в UIImage?

Я использую OpenCV 2.2 на iPhone для обнаружения лиц. Я использую AVCaptureSession в IOS 4, чтобы получить доступ к потоку камеры, как показано в следующем коде.

Моя проблема заключается в том, что видеокадры поступают как объекты CVBufferRef (указатели на CVImageBuffer) и имеют ориентацию в виде ландшафта, шириной 480 пикселей и высотой 300 пикселей. Это нормально, если вы держите телефон боком, но когда телефон находится в вертикальном положении, я хочу повернуть эти рамки на 90 градусов по часовой стрелке, чтобы OpenCV мог правильно найти лица.

Я мог преобразовать CVBufferRef в CGImage, затем в UIImage, а затем повернуть, как это делает этот человек: Повернуть CGImage, взятый из видеокадра

Однако при этом расходуется много ресурсов процессора. Я ищу более быстрый способ повернуть входящие изображения, в идеале используя графический процессор для этой обработки, если это возможно.

Любые идеи?

Ян

Пример кода:

 -(void) startCameraCapture {
  // Start up the face detector

  faceDetector = [[FaceDetector alloc] initWithCascade:@"haarcascade_frontalface_alt2" withFileExtension:@"xml"];

  // Create the AVCapture Session
  session = [[AVCaptureSession alloc] init];

  // create a preview layer to show the output from the camera
  AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:session];
  previewLayer.frame = previewView.frame;
  previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

  [previewView.layer addSublayer:previewLayer];

  // Get the default camera device
  AVCaptureDevice* camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

  // Create a AVCaptureInput with the camera device
  NSError *error=nil;
  AVCaptureInput* cameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:camera error:&error];
  if (cameraInput == nil) {
   NSLog(@"Error to create camera capture:%@",error);
  }

  // Set the output
  AVCaptureVideoDataOutput* videoOutput = [[AVCaptureVideoDataOutput alloc] init];
  videoOutput.alwaysDiscardsLateVideoFrames = YES;

  // create a queue besides the main thread queue to run the capture on
  dispatch_queue_t captureQueue = dispatch_queue_create("catpureQueue", NULL);

  // setup our delegate
  [videoOutput setSampleBufferDelegate:self queue:captureQueue];

  // release the queue.  I still don't entirely understand why we're releasing it here,
  // but the code examples I've found indicate this is the right thing.  Hmm...
  dispatch_release(captureQueue);

  // configure the pixel format
  videoOutput.videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
          [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA], 
          (id)kCVPixelBufferPixelFormatTypeKey,
          nil];

  // and the size of the frames we want
  // try AVCaptureSessionPresetLow if this is too slow...
  [session setSessionPreset:AVCaptureSessionPresetMedium];

  // If you wish to cap the frame rate to a known value, such as 10 fps, set 
  // minFrameDuration.
  videoOutput.minFrameDuration = CMTimeMake(1, 10);

  // Add the input and output
  [session addInput:cameraInput];
  [session addOutput:videoOutput];

  // Start the session
  [session startRunning];  
 }

 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
  // only run if we're not already processing an image
  if (!faceDetector.imageNeedsProcessing) {

   // Get CVImage from sample buffer
   CVImageBufferRef cvImage = CMSampleBufferGetImageBuffer(sampleBuffer);

   // Send the CVImage to the FaceDetector for later processing
   [faceDetector setImageFromCVPixelBufferRef:cvImage];

   // Trigger the image processing on the main thread
   [self performSelectorOnMainThread:@selector(processImage) withObject:nil waitUntilDone:NO];
  }
 }

person Ian Charnas    schedule 25.12.2010    source источник


Ответы (4)


vImage - довольно быстрый способ сделать это. Однако требуется ios5. Вызов говорит ARGB, но он работает для BGRA, который вы получаете из буфера.

Это также имеет то преимущество, что вы можете вырезать часть буфера и повернуть ее. См. мой ответ здесь

- (unsigned char*) rotateBuffer: (CMSampleBufferRef) sampleBuffer
{
 CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
 CVPixelBufferLockBaseAddress(imageBuffer,0);

 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
 size_t width = CVPixelBufferGetWidth(imageBuffer);
 size_t height = CVPixelBufferGetHeight(imageBuffer);
 size_t currSize = bytesPerRow*height*sizeof(unsigned char); 
 size_t bytesPerRowOut = 4*height*sizeof(unsigned char); 

 void *srcBuff = CVPixelBufferGetBaseAddress(imageBuffer); 
 unsigned char *outBuff = (unsigned char*)malloc(currSize);  

 vImage_Buffer ibuff = { srcBuff, height, width, bytesPerRow};
 vImage_Buffer ubuff = { outBuff, width, height, bytesPerRowOut};

 uint8_t rotConst = 1;   // 0, 1, 2, 3 is equal to 0, 90, 180, 270 degrees rotation

 vImage_Error err= vImageRotate90_ARGB8888 (&ibuff, &ubuff, NULL, rotConst, NULL,0);
 if (err != kvImageNoError) NSLog(@"%ld", err);

 return outBuff;
}
person Sten    schedule 07.09.2012
comment
Я использовал нечто подобное, чтобы управлять отдельными кадрами sampleBuffer видео перед записью его в файл. Несколько замечаний: прототип функции vImageRotate... изменился, и мой вызов выглядит как vImageRotate90_ARGB8888(&inbuff, &outbuff, rotationConstant, bgColor, 0); (где uint8_t bgColor[4] = {0, 0, 0, 0};). И вам нужно вручную создать CVPixelBufferRef, чтобы передать полученные данные изображения в AVAssetWriterInputPixelBufferAdaptor. Только не забудьте создать CVPixelBufferReleaseBytesCallback, чтобы освободить буфер данных, заблокированный в этой функции. - person Mr. T; 28.05.2014

Если вы повернетесь с упором на 90 градусов, вы можете просто сделать это в памяти. Вот пример кода, который просто копирует данные в новый буфер пикселей. Выполнение ротации методом грубой силы должно быть прямым.

- (CVPixelBufferRef) rotateBuffer: (CMSampleBufferRef) sampleBuffer
{
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CVPixelBufferLockBaseAddress(imageBuffer,0);

    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);

    void *src_buff = CVPixelBufferGetBaseAddress(imageBuffer);

    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             nil];

    CVPixelBufferRef pxbuffer = NULL;
    //CVReturn status = CVPixelBufferPoolCreatePixelBuffer (NULL, _pixelWriter.pixelBufferPool, &pxbuffer);
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width,
                                          height, kCVPixelFormatType_32BGRA, (CFDictionaryRef) options, 
                                          &pxbuffer);

    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *dest_buff = CVPixelBufferGetBaseAddress(pxbuffer);
    NSParameterAssert(dest_buff != NULL);

    int *src = (int*) src_buff ;
    int *dest= (int*) dest_buff ;
    size_t count = (bytesPerRow * height) / 4 ;
    while (count--) {
        *dest++ = *src++;
    }

    //Test straight copy.
    //memcpy(pxdata, baseAddress, width * height * 4) ;
    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    return pxbuffer;
}

Затем вы можете использовать AVAssetWriterInputPixelBufferAdaptor, если вы записываете это обратно в AVAssetWriterInput.

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

Редактировать. Для этого вы можете использовать графический процессор. Похоже, что передается много данных. В CVPixelBufferRef есть ключ kCVPixelBufferOpenGLCompatibilityKey. Я предполагаю, что вы можете создать изображение, совместимое с OpenGL, из CVImageBufferRef (который является всего лишь ссылкой на буфер пикселей) и пропустить его через шейдер. Опять же, перебор IMO. Вы можете увидеть, есть ли в BLAS или LAPACK "неуместные" методы транспонирования. Если они это сделают, вы можете быть уверены, что они оптимизированы.

90 CW, где new_width = width ... Это даст вам портретное изображение.

for (int i = 1; i <= new_height; i++) {
    for (int j = new_width - 1; j > -1; j--) {
        *dest++ = *(src + (j * width) + i) ;
    }
}
person Steve McFarlin    schedule 02.01.2011
comment
Стив, спасибо за ответ. В настоящее время я использую методы транспонирования и переворота из OpenCV, это самый быстрый из всех методов поворота изображения, которые я пробовал. Я обнаружил, что, хотя я могу передать это в OpenGL, если я не смогу выполнять всю обработку изображений (включая обнаружение лиц) в OpenGL, тогда я не получу гигантского прироста производительности. Пока я буду придерживаться различных комбинаций transpose () и flip () для поворота с шагом 90 градусов. Я считаю, что на этот вопрос дан ответ, поскольку я считаю, что вы дали наилучший ответ в рамках ограничений. - person Ian Charnas; 05.01.2011
comment
Хорошая идея с использованием opencv для поворота изображения ... Я раздвоил проект niw и добавил отслеживание лиц в реальном времени ... Я, вероятно, уберу его позже, но, по крайней мере, он дает отправную точку для тех, кто ищет полное решение - github.com/gitaaron/iphone_opencv_test - person surtyaar; 20.03.2011
comment
Я использовал прямое копирование данных для поворота изображения, и он отлично работал с низким разрешением (AVCaptureSessionPresetLow), но когда я попробовал его с AVCaptureSessionPresetMedium, изображение испортилось. Я, наверное, упустил что-то глупое ... Кто-нибудь знает, в чем может быть проблема? - person Ilya K.; 23.07.2012
comment
Честно говоря, этот метод очень медленный. Гораздо эффективнее протолкнуть изображение через шейдер OpenGL для вращения. Вы можете изменить матрицу модели / вида для вращения (и масштабирования при необходимости). Вы также можете сделать это через CoreImage. Взгляните на проект Брэда Ларсона GPUImage на GitHub. - person Steve McFarlin; 23.07.2012
comment
Да, я знаю, что это медленно, но в данном случае я просто хочу понять (из любопытства), почему с изображением более высокого разрешения (480x360 против 192x144) тот же алгоритм не работает. Можете ли вы помочь мне с этим? - person Ilya K.; 24.07.2012
comment
Я не понимаю, почему это не сработает. Приведенный выше код выглядит не зависящим от разрешения. Имейте в виду, что ширина и высота меняются местами. Почему он работает с низким разрешением, а не с более высоким, мне непонятно. Вы отправляете или запускаете ротацию в потоке, отличном от того, в котором работает AVCaptureSession? - person Steve McFarlin; 27.07.2012

Может быть, проще просто настроить ориентацию видео так, как вы хотите:

connection.videoOrientation = AVCaptureVideoOrientationPortrait

Таким образом, вам вообще не нужно использовать этот трюк с вращением

person nikwest    schedule 25.03.2015
comment
Этот метод не поворачивает буферы изображений физически. - person ozz; 19.09.2016
comment
Какой вид объекта является подключением? - person Parth Patel; 20.03.2020

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

Мне нужно было извлечь необработанные данные изображения из буфера изображений формата YCbCr, доставленного камерой iPhone (получено из [AVCaptureVideoDataOutput.availableVideoCVPixelFormatTypes firstObject]), отбросив такую ​​информацию, как заголовки, метаинформацию и т. Д., Чтобы передать ее для дальнейшей обработки.

Кроме того, мне нужно было извлечь только небольшую область в центре захваченного видеокадра, поэтому потребовалось некоторое кадрирование.

Мои условия позволяли снимать видео только в любой альбомной ориентации, но когда устройство расположено в альбомной левой ориентации, изображение доставляется перевернутым, поэтому мне нужно было перевернуть его по обеим осям. В случае, если изображение перевернуто, моя идея заключалась в том, чтобы скопировать данные из буфера исходного изображения в обратном порядке и перевернуть байты в каждой строке считывать данные, чтобы перевернуть изображение по обеим осям. Эта идея действительно работает, и, поскольку мне все равно нужно было скопировать данные из исходного буфера, кажется, что при чтении с начала или с конца не так много потери производительности (конечно, большее изображение = более длительная обработка, но я имею дело с действительно небольшими числами) .

Я хотел бы знать, что другие думают об этом решении, и, конечно, несколько советов, как улучшить код:

/// Lock pixel buffer
CVPixelBufferLockBaseAddress(imageBuffer, 0);

/// Address where image buffer starts
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer);

/// Read image parameters
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);

/// See whether image is flipped upside down
BOOL isFlipped = (_previewLayer.connection.videoOrientation == AVCaptureVideoOrientationLandscapeLeft);

/// Calculate cropping frame. Crop to scanAreaSize (defined as CGSize constant elsewhere) from the center of an image
CGRect cropFrame = CGRectZero;
cropFrame.size = scanAreaSize;
cropFrame.origin.x = (width / 2.0f) - (scanAreaSize.width / 2.0f);
cropFrame.origin.y = (height / 2.0f) - (scanAreaSize.height / 2.0f);

/// Update proportions to cropped size
width = (size_t)cropFrame.size.width;
height = (size_t)cropFrame.size.height;

/// Allocate memory for output image data. W*H for Y component, W*H/2 for CbCr component
size_t bytes = width * height + (width * height / 2);

uint8_t *outputDataBaseAddress = (uint8_t *)malloc(bytes);

if(outputDataBaseAddress == NULL) {

    /// Memory allocation failed, unlock buffer and give up
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

    return NULL;
}

/// Get parameters of YCbCr pixel format
CVPlanarPixelBufferInfo_YCbCrBiPlanar *bufferInfo = (CVPlanarPixelBufferInfo_YCbCrBiPlanar *)baseAddress;

NSUInteger bytesPerRowY = EndianU32_BtoN(bufferInfo->componentInfoY.rowBytes);
NSUInteger offsetY = EndianU32_BtoN(bufferInfo->componentInfoY.offset);

NSUInteger bytesPerRowCbCr = EndianU32_BtoN(bufferInfo->componentInfoCbCr.rowBytes);
NSUInteger offsetCbCr = EndianU32_BtoN(bufferInfo->componentInfoCbCr.offset);

/// Copy image data only, skipping headers and metadata. Create single buffer which will contain Y component data
/// followed by CbCr component data.

/// Process Y component
/// Pointer to the source buffer
uint8_t *src;

/// Pointer to the destination buffer
uint8_t *destAddress;

/// Calculate crop rect offset. Crop offset is number of rows (y * bytesPerRow) + x offset.
/// If image is flipped, then read buffer from the end to flip image vertically. End address is height-1!
int flipOffset = (isFlipped) ? (int)((height - 1) * bytesPerRowY) : 0;

int cropOffset = (int)((cropFrame.origin.y * bytesPerRowY) + flipOffset + cropFrame.origin.x);

/// Set source pointer to Y component buffer start address plus crop rect offset
src = baseAddress + offsetY + cropOffset;

for(int y = 0; y < height; y++) {

    /// Copy one row of pixel data from source into the output buffer.
    destAddress = (outputDataBaseAddress + y * width);

    memcpy(destAddress, src, width);

    if(isFlipped) {

        /// Reverse bytes in row to flip image horizontally
        [self reverseBytes:destAddress bytesSize:(int)width];

        /// Move one row up
        src -= bytesPerRowY;
    }
    else {

        /// Move to the next row
        src += bytesPerRowY;
    }
}

/// Calculate crop offset for CbCr component
flipOffset = (isFlipped) ? (int)(((height - 1) / 2) * bytesPerRowCbCr) : 0;
cropOffset = (int)((cropFrame.origin.y * bytesPerRowCbCr) + flipOffset + cropFrame.origin.x);

/// Set source pointer to the CbCr component offset + crop offset
src = (baseAddress + offsetCbCr + cropOffset);

for(int y = 0; y < (height / 2); y++) {

    /// Copy one row of pixel data from source into the output buffer.
    destAddress = (outputDataBaseAddress + (width * height) + y * width);

    memcpy(destAddress, src, width);

    if(isFlipped) {

        /// Reverse bytes in row to flip image horizontally
        [self reverseBytes:destAddress bytesSize:(int)width];

        /// Move one row up
        src -= bytesPerRowCbCr;
    }
    else {

        src += bytesPerRowCbCr;
    }
}

/// Unlock pixel buffer
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

/// Continue with image data in outputDataBaseAddress;
person Matthes    schedule 03.06.2015