Отображение созданного снимка экрана UIImage не отображается в UIImageView (только для устройства)

Я пытаюсь сохранить буфер OpenGL (что в данный момент отображается на виде) в библиотеку фотографий устройства. Фрагмент кода ниже отлично работает на симуляторе. Но для реального устройства это сбой. Я полагаю, что может возникнуть проблема с тем, как я создаю UIImage, захваченный с экрана.

  • Эта операция инициируется с помощью метода дескриптора события IBAction.
  • Функцией, которую я использую для сохранения изображения, является UIImageWriteToSavedPhotosAlbum (я недавно изменил это на writeImageToSavedPhotosAlbum ALAssetsLibrary).
  • Я гарантировал, что мое приложение имеет доступ к библиотеке фотографий.
  • Я также убедился, что мой CGImageRed определен глобально (определен в верхней части файла), а мой UIImage является (неатомарным, сохраняемым) свойством.

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

Ниже приведен соответствующий фрагмент кода (позвоните, чтобы сохранить в библиотеке фотографий):

-(void)TakeImageBufferSnapshot:(CGSize)dimensions
{
   NSLog(@"TakeSnapShot 1 : (%f, %f)", dimensions.width, dimensions.height);
   NSInteger size = dimensions.width * dimensions.height * 4;
   GLubyte *buffer = (GLubyte *) malloc(size);
   glReadPixels(0, 0, dimensions.width, dimensions.height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
   GLubyte *buffer2 = (GLubyte *) malloc(size);
   int height = (int)dimensions.height - 1;
   int width = (int)dimensions.width;

   for(int y = 0; y < dimensions.height; y++)
   {
       for(int x = 0; x < dimensions.width * 4; x++)
       {
           buffer2[(height - 1 - y) * width * 4 + x] = buffer[y * 4 * width + x];
       }
   }

   NSLog(@"TakeSnapShot 2");

   // make data provider with data.
   CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer2, size, NULL);

   if (buffer) free(buffer);
   if (buffer2) free(buffer2);

   // prep the ingredients
   int bitsPerComponent = 8;
   int bitsPerPixel = 32;
   int bytesPerRow = 4 * self.view.bounds.size.width;
   CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
   CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
   CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;

   NSLog(@"TakeSnapShot 3");

   // make the cgimage
   g_savePhotoImageRef = CGImageCreate(dimensions.width, dimensions.height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

   NSLog(@"TakeSnapShot 4");

   // then make the uiimage from that
   self.savePhotoImage = [UIImage imageWithCGImage:g_savePhotoImageRef];

   CGColorSpaceRelease(colorSpaceRef);
   CGDataProviderRelease(provider);
}

-(void)SaveToPhotoAlbum
{
   ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
   NSLog(@"Authorization status: %d", status);

   if (status == ALAuthorizationStatusAuthorized)
   {
       [self TakeImageBufferSnapshot:self.view.bounds.size];

       // UPDATED - DO NOT proceed to save to album below.
       // Instead, set the created image to a UIImageView IBOutlet.
       // On the simulator this shows the screen/buffer captured image (as expected) -
       // but on the device (ipad) this doesnt show anything and the app crashes.
       self.testImageView.image = self.savePhotoImage;
       return;

       NSLog(@"Saving to photo album...");
       UIImageWriteToSavedPhotosAlbum(self.savePhotoImage,
                                   self,
                                       @selector(photoAlbumImageSave:didFinishSavingWithError:contextInfo:),
                                   nil);
   }
   else
   {
       UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Access is denied"
                                                    message:@"Allow access to your Photos library to save this image."
                                                   delegate:nil
                                          cancelButtonTitle:@"Close"
                                          otherButtonTitles:nil, nil];
       [alert show];
   }
}

- (void)photoAlbumImageSave:(UIImage *)image didFinishSavingWithError:(NSError *)error     contextInfo:(void *)context
{
   self.savePhotoImage = nil;
   CGImageRelease(g_savePhotoImageRef);

   if (error)
   {
       NSLog(@"Error saving photo to albums: %@", error.description);
   }
   else
   {
       NSLog(@"Saved to albums!");
   }
}

* Обновление * Я думаю, что мне удалось сузить мою проблему. Я начал заниматься методом проб и ошибок, где после комментирования строк кода запускаю приложение (на устройстве), чтобы сузить кругозор. Похоже, у меня могут быть проблемы с функцией TakeImageBufferSnapshot, которая берет экранный буфер (используя glReadPixels) и создает CGImageRef. Теперь, когда я пытаюсь создать из этого UIImage (используя метод [UIImage imageWithCGImage:], похоже, именно поэтому приложение аварийно завершает работу. Если я закомментирую эту строку, кажется, что проблемы нет (кроме того факта, что У меня нет ссылки на UIImage).

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

2 ответа

Решение

Во-первых, я должен отметить, что glReadPixels() может не вести себя так, как вы ожидаете. Если вы попытаетесь использовать его для чтения с экрана после -presentRenderbuffer: был вызван, результаты не определены. На iOS 6.0+ это возвращает черное изображение, например. Вам нужно либо использовать glReadPixels() прямо перед тем, как контент будет представлен на экране (моя рекомендация) или включите сохранение резервной копии для вашего контекста OpenGL ES (что имеет неблагоприятные последствия для производительности).

Во-вторых, нет необходимости в двух буферах. Вы можете захватить непосредственно в один и использовать его для создания вашего CGImageRef.

К вашей основной проблеме, проблема заключается в том, что вы освобождаете свой необработанный байтовый буфер, в то время как ваш CGImageRef / UIImage все еще полагается на него. Это вытащит коврик из-под вашего UIImage и приведет к повреждению / разрушению изображения, которое вы видите. Чтобы учесть это, вам необходимо установить функцию обратного вызова, которая будет запускаться при освобождении вашего CGDataProvider. Вот как я делаю это в моей среде GPUImage:

rawImagePixels = (GLubyte *)malloc(totalBytesForImage);
glReadPixels(0, 0, (int)currentFBOSize.width, (int)currentFBOSize.height, GL_RGBA, GL_UNSIGNED_BYTE, rawImagePixels);
dataProvider = CGDataProviderCreateWithData(NULL, rawImagePixels, totalBytesForImage, dataProviderReleaseCallback);
cgImageFromBytes = CGImageCreate((int)currentFBOSize.width, (int)currentFBOSize.height, 8, 32, 4 * (int)currentFBOSize.width, defaultRGBColorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaLast, dataProvider, NULL, NO, kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);

Функция обратного вызова принимает эту форму:

void dataProviderReleaseCallback (void *info, const void *data, size_t size)
{
    free((void *)data);
}

Эта функция будет вызываться только тогда, когда UIImage, содержащий ваш CGImageRef (и, соответственно, CGDataProvider) будет освобожден. До этого момента буфер, содержащий ваши байты изображения, остается.

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

Хорошо - из моего опыта вы не можете просто захватить пиксели, которые находятся в буфере прямо сейчас, вам нужно восстановить правильный контекст, нарисовать и захватить ТО, прежде чем наконец освободить контекст

=> Это в основном верно для устройства и ios6 в частности

EAGLContext* previousContext = [EAGLContext currentContext];
[EAGLContext setCurrentContext: self.context];

    [self fillBuffer:sender];

    //GRAB the pixels here

    [EAGLContext setCurrentContext:previousContext];

в качестве альтернативы (вот как я это делаю) создайте новый FrameBuffer, заполните THAT и получите пиксели из THERE

GLuint rttFramebuffer;
glGenFramebuffers(1, &rttFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, rttFramebuffer);

[self fillBuffer:self.displayLink];

size_t size = viewportHeight * viewportWidth * 4;
GLubyte *pixels = malloc(size*sizeof(GLubyte));
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(0, 0, viewportWidth, viewportHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

// Restore the original framebuffer binding
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glDeleteFramebuffers(1, &rttFramebuffer);

size_t bitsPerComponent = 8;
size_t bitsPerPixel = 32;
size_t bytesPerRow = viewportWidth * bitsPerPixel / bitsPerComponent;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, pixels, size, ImageProviderReleaseData);
CGImageRef cgImage = CGImageCreate(viewportWidth, viewportHeight, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, provider, NULL, true, kCGRenderingIntentDefault);
CGDataProviderRelease(provider);

UIImage *image = [UIImage imageWithCGImage:cgImage scale:self.contentScaleFactor orientation:UIImageOrientationDownMirrored];
CGImageRelease(cgImage);
CGColorSpaceRelease(colorSpace);

Изменить: удален вызов настоящего буфера

Другие вопросы по тегам