Обрезать захваченное изображение точно так, как оно выглядит в AVCaptureVideoPreviewLayer

У меня есть приложение для фото, которое использует AV Foundation. Я установил слой предварительного просмотра, используя AVCaptureVideoPreviewLayer, который занимает верхнюю половину экрана. Поэтому, когда пользователь пытается сделать свою фотографию, все, что они могут видеть, это то, что видит верхняя половина экрана.

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

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

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

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

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

Вот моя реализация viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];

    AVCaptureSession *session =[[AVCaptureSession alloc]init];
    [session setSessionPreset:AVCaptureSessionPresetPhoto];

    AVCaptureDevice *inputDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    NSError *error = [[NSError alloc]init];
    AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:inputDevice error:&error];

    if([session canAddInput:deviceInput])
        [session addInput:deviceInput];

    CALayer *rootLayer = [[self view]layer];
    [rootLayer setMasksToBounds:YES];

    _previewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:session];
    [_previewLayer setFrame:CGRectMake(0, 0, rootLayer.bounds.size.width, rootLayer.bounds.size.height/2)];
    [_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];

    [rootLayer insertSublayer:_previewLayer atIndex:0];

    _stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
    [session addOutput:_stillImageOutput];

    [session startRunning];
    }

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

-(IBAction)stillImageCapture {
    AVCaptureConnection *videoConnection = nil;
    for (AVCaptureConnection *connection in _stillImageOutput.connections){
        for (AVCaptureInputPort *port in [connection inputPorts]){
            if ([[port mediaType] isEqual:AVMediaTypeVideo]){
                videoConnection = connection;
                break;
            }
        }
        if (videoConnection) {
            break;
        }
    }

    NSLog(@"about to request a capture from: %@", _stillImageOutput);

    [_stillImageOutput captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        if(imageDataSampleBuffer) {
            NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];

            UIImage *image = [[UIImage alloc]initWithData:imageData];
            CALayer *subLayer = [CALayer layer];
            subLayer.frame = _previewLayer.frame;
            image = [self rotate:image andOrientation:image.imageOrientation];

            //Below is the crop that is sort of working for me, but as you can see I am manually entering in values and just guessing and it still does not look perfect.
            CGRect cropRect = CGRectMake(0, 650, 3000, 2000);
            CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], cropRect);

            subLayer.contents = (id)[UIImage imageWithCGImage:imageRef].CGImage;
            subLayer.frame = _previewLayer.frame;

            [_previewLayer addSublayer:subLayer];
        }
    }];
}

Ответ 1

Посмотрите на AVCaptureVideoPreviewLayer

-(CGRect)metadataOutputRectOfInterestForRect:(CGRect)layerRect

Этот метод позволяет легко преобразовать видимый CGRect вашего слоя в фактический выход камеры.

Одно предостережение: физическая камера не установлена "сверху вверх", а поворачивается на 90 градусов по часовой стрелке. (Итак, если вы держите iPhone - Главная кнопка справа, камера на самом деле вверху вверх).

Помня об этом, вам нужно преобразовать CGRect, указанный выше методом, чтобы обрезать изображение точно на экране.

Пример:

CGRect visibleLayerFrame = THE ACTUAL VISIBLE AREA IN THE LAYER FRAME
CGRect metaRect = [self.previewView.layer metadataOutputRectOfInterestForRect:visibleLayerFrame];


CGSize originalSize = [originalImage size];

if (UIInterfaceOrientationIsPortrait(_snapInterfaceOrientation)) {
    // For portrait images, swap the size of the image, because
    // here the output image is actually rotated relative to what you see on screen.

    CGFloat temp = originalSize.width;
    originalSize.width = originalSize.height;
    originalSize.height = temp;
}


// metaRect is fractional, that why we multiply here

CGRect cropRect;

cropRect.origin.x = metaRect.origin.x * originalSize.width;
cropRect.origin.y = metaRect.origin.y * originalSize.height;
cropRect.size.width = metaRect.size.width * originalSize.width;
cropRect.size.height = metaRect.size.height * originalSize.height;

cropRect = CGRectIntegral(cropRect);

Это может быть немного запутанным, но что заставило меня понять, что это так:

Удерживайте устройство "Главная кнопка вправо" → Вы увидите, что ось x фактически лежит вдоль "высоты" вашего iPhone, а ось y лежит вдоль "ширины" вашего iPhone. Поэтому для портретных изображений вам нужно поменять размер;)

Ответ 2

@Cabus имеет решение, которое работает, и вы должны проголосовать за его ответ. Тем не менее, я сделал свою собственную версию в Swift со следующим:

// The image returned in initialImageData will be larger than what
//  is shown in the AVCaptureVideoPreviewLayer, so we need to crop it.
let image : UIImage = UIImage(data: initialImageData)!

let originalSize : CGSize
let visibleLayerFrame = self.previewView!.bounds // THE ACTUAL VISIBLE AREA IN THE LAYER FRAME

// Calculate the fractional size that is shown in the preview
let metaRect : CGRect = (self.videoPreviewLayer?.metadataOutputRectOfInterestForRect(visibleLayerFrame))!
if (image.imageOrientation == UIImageOrientation.Left || image.imageOrientation == UIImageOrientation.Right) {
    // For these images (which are portrait), swap the size of the
    // image, because here the output image is actually rotated
    // relative to what you see on screen.
    originalSize = CGSize(width: image.size.height, height: image.size.width)
}
else {
    originalSize = image.size
}

// metaRect is fractional, that why we multiply here.
let cropRect : CGRect = CGRectIntegral(
        CGRect( x: metaRect.origin.x * originalSize.width,
                y: metaRect.origin.y * originalSize.height,
                width: metaRect.size.width * originalSize.width,
                height: metaRect.size.height * originalSize.height))

let finalImage : UIImage = 
    UIImage(CGImage: CGImageCreateWithImageInRect(image.CGImage, cropRect)!, 
        scale:1, 
        orientation: image.imageOrientation )

Ответ 3

Здесь @Эрик Аллен отвечает в Swift 3:

let originalSize: CGSize
let visibleLayerFrame = self?.photoView.bounds

// Calculate the fractional size that is shown in the preview
let metaRect = (self?.videoPreviewLayer?.metadataOutputRectOfInterest(for: visibleLayerFrame ?? CGRect.zero)) ?? CGRect.zero

if (image.imageOrientation == UIImageOrientation.left || image.imageOrientation == UIImageOrientation.right) {
    // For these images (which are portrait), swap the size of the
    // image, because here the output image is actually rotated
    // relative to what you see on screen.
    originalSize = CGSize(width: image.size.height, height: image.size.width)
} else {
    originalSize = image.size
}

let cropRect: CGRect = CGRect(x: metaRect.origin.x * originalSize.width, y: metaRect.origin.y * originalSize.height, width: metaRect.size.width * originalSize.width, height: metaRect.size.height * originalSize.height).integral

if let finalCgImage = image.cgImage?.cropping(to: cropRect) {
    let finalImage = UIImage(cgImage: finalCgImage, scale: 1.0, orientation: image.imageOrientation)

    // User your image...
}