I'm having trouble getting a rendered video's colors to match the source content's colors. I'm rendering images into a CGContext, converting the backing data into a CVPixelBuffer and appending that as a frame to an AVAssetWriterInputPixelBufferAdaptor. This causes slight color differences between the images that I'm drawing into the CGContext and the resulting video file.
It seems like there are 3 things that need to be addressed:
tell AVFoundation what colorspace the video is in.
make the AVAssetWriterInputPixelBufferAdaptor and the CVPixelBuffers I append to it match that color space.
use the same colorspace for the CGContext.
The documentation is terrible, so I'd appreciate any guidance on how to do these things or if there is something else I need to do to make the colors be preserved throughout this entire process.
Full code:
AVAssetWriter *_assetWriter;
AVAssetWriterInput *_assetInput;
AVAssetWriterInputPixelBufferAdaptor *_assetInputAdaptor;
NSDictionary *outputSettings = #{ AVVideoCodecKey :AVVideoCodecH264,
AVVideoWidthKey :#(outputWidth),
AVVideoHeightKey:#(outputHeight)};
_assetInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:outputSettings];
NSDictionary *bufferAttributes = #{å(NSString*)kCVPixelBufferPixelFormatTypeKey:#(kCVPixelFormatType_32ARGB)};
_assetInputAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetInput
sourcePixelBufferAttributes:bufferAttributes];
_assetWriter = [AVAssetWriter assetWriterWithURL:aURL fileType:AVFileTypeMPEG4 error:nil];
[_assetWriter addInput:_assetInput];
[_assetWriter startWriting];
[_assetWriter startSessionAtSourceTime:kCMTimeZero];
NSInteger bytesPerRow = outputWidth * 4;
long size = bytesPerRow * outputHeight;
CGColorSpaceRef srgbSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
UInt8 *data = (UInt8 *)calloc(size, 1);
CGContextRef ctx = CGBitmapContextCreateWithData(data, outputWidth, outputHeight, 8, bytesPerRow, srgbSpace, kCGImageAlphaPremultipliedFirst, NULL, NULL);
// draw everything into ctx
CVPixelBufferRef pixelBuffer;
CVPixelBufferCreateWithBytes(kCFAllocatorSystemDefault,
outputWidth, outputHeight,
k32ARGBPixelFormat,
data,
bytesPerRow,
ReleaseCVPixelBufferForCVPixelBufferCreateWithBytes,
NULL,
NULL,
&pixelBuffer);
NSDictionary *pbAttachements = #{(id)kCVImageBufferCGColorSpaceKey : (__bridge id)srgbSpace};
CVBufferSetAttachments(pixelBuffer, (__bridge CFDictionaryRef)pbAttachements, kCVAttachmentMode_ShouldPropagate);
[_assetInputAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:CMTimeMake(0, 60)];
CGColorSpaceRelease(srgbSpace);
[_assetInput markAsFinished];
[_assetWriter finishWritingWithCompletionHandler:^{}];
This is quite a confusing subject and the Apple docs really do not help all that much. I am going to describe the solution I have settled on based on using the BT.709 colorspace, I am sure someone will have an objection based on Colorimetric correctness and the weirdness of various video standards, but this is complex topic. First off, don't use kCVPixelFormatType_32ARGB as the pixel type. Always pass kCVPixelFormatType_32BGRA instead, since BGRA is the native pixel layout on both MacOSX and iPhone hardware and it BGRA is just faster. Next, when you create a CGBitmapContext to render into use the BT.709 colorspace (kCGColorSpaceITUR_709). Also, don't render into a malloc() buffer, render directly into the CoreVideo pixel buffer by creating a bitmap context over the same memory, CoreGraphics will handle the colorspace and gamma conversion from whatever your input image is to BT.709 and its associated gamma. Then you need to tell AVFoundation the colorspace of the pixel buffer, do that by making an ICC profile copy and setting the kCVImageBufferICCProfileKey on the CoreVideo pixel buffer. That takes care of your issues 1 and 2, you do not need to have input images in this same colorspace with this approach. Now, this is of course complex and actual working source code (yes actually working) is hard to come by. Here is a github link to a small project that does these exact steps, the code is BSD licensed, so feel free to use it. Note specifically the H264Encoder class which wraps all this horror up into a reusable module. You can find calling code in encode_h264.m, it is a little MacOSX command line util to encode PNG to M4V. Also attached 3 keys Apple docs related to this subject 1, 2, 3.
MetalBT709Decoder
Related
I am developing an OS X app that uses custom Core Image filters to attain a particular effect: Set one image's luminance as another image's alpha channel. There are filters to use an image as a mask for another but they require a third -background- image; I need to output an image with transparent parts, no background set.
As explained in Apple's documentation, I wrote the kernel code and tested it in QuartzComposer; it works as expected.
The kernel code is:
kernel vec4 setMask(sampler src, sampler mask)
{
vec4 color = sample(src, samplerCoord(src));
vec4 alpha = sample(mask, samplerCoord(mask));
color.a = alpha.r;
// (mask image is grayscale; any channel colour will do)
return color;
}
But when I try to use the filter from my code (either packaging it as an image unit or directly from the app source), the output image turns out to have the following 'undefined'(?) extent:
extent CGRect origin=(x=-8.988465674311579E+307, y=-8.988465674311579E+307) size=(width=1.797693134862316E+308, height=1.797693134862316E+308)
and further processing (convert to NSImage bitmap representation, write to file, etc.) fails. The filter itself loads perfectly (not nil) and the output image it produces isn't nil either, just has an invalid rect.
EDIT: Also, I copied the exported image unit (plugin), to both /Library/Graphics/Image Units and ~/Library/Graphics/Image Units, so that it appears in QuartzComposer's Patch Library, but when I connect it to the source images and Billboard renderer, nothing is drawn (transparent background).
Am I missing something?
EDIT: Looks like I assumed to much about the default behaviour of -[CIFilter apply:].
My filter subclass code's -outputImage implementation was this:
- (CIImage*) outputImage
{
CISampler* src = [CISampler samplerWithImage:inputImage];
CISampler* mask = [CISampler samplerWithImage:inputMaskImage];
return [self apply:setMaskKernel, src, mask, nil];
}
So I tried and changed it to this:
- (CIImage*) outputImage
{
CISampler* src = [CISampler samplerWithImage:inputImage];
CISampler* mask = [CISampler samplerWithImage:inputMaskImage];
CGRect extent = [inputImage extent];
NSDictionary* options = #{ kCIApplyOptionExtent: #[#(extent.origin.x),
#(extent.origin.y),
#(extent.size.width),
#(extent.size.height)],
kCIApplyOptionDefinition: #[#(extent.origin.x),
#(extent.origin.y),
#(extent.size.width),
#(extent.size.height)]
};
return [self apply:setMaskKernel arguments:#[src, mask] options:options];
}
...and now it works!
How are you drawing it? And what does your CIFilter code look like? You'll need to provide a kCIApplyOptionDefinition most likely when you call apply: in outputImage.
Alternatively, you can also change how you are drawing the image, using CIContext's drawImage:inRect:fromRect.
I load an image (NSImage) from the disk, and draw it to an NSImageView on MAC, No problem image looks fine and clear.
After drawing it to the NSImageView, I call the function below with the same image, then draw the returned value to the same NSImageView. The resulting image is extremly blurry, even if all I do is lockFocus and UnlockFocus without doing anything else.
-(NSImage*)addTarget:(NSImage*)image
{
[image lockFocus]; // this image is sharp and clear
[image unlockFocus];
return image; // this image is extremely blurry
}
Anybody knows why or how to fix that?
thanks
rough
So as doing some research I realized that this is related to Retina displays. I guess locking focus will always draw at bestRepresentation which if there is a retina display attached anywhere to the computer, it will render based on that scale factor. So in order to get this to maintain proper dimensions and DPI I created a method that iterates through all screens and returns the largest backingScaleFactor
func maximumScaleFactor(screen: NSScreen) -> CGFloat {
var max: CGFloat = 0
for s in NSScreen.screens()! {
if s.backingScaleFactor > max { max = s.backingScaleFactor }
}
return max / screen.backingScaleFactor
}
Then for the 'NSImage' I did the following
-(NSImage*)addTarget:(NSImage*)image
let scale = self.maximumScaleFactor(currentScreen)
let originalSize = image.size
//cut the image size in half
var size = image.size
size.x /= scale
size.y /= scale
image.lockFocus()
//do whatever drawing you need here
image.unlockFocus()
//set the image back to its original size
image.size = originalSize
return image
}
So far this has worked well for me and the image quality subjectively appears the same to me.
To fix your problem is difficult because you don't say what you want to achieve. Why did you write that method, why do you call it, what do you expect this method does? You said: all I do is lockFocus and UnlockFocus without doing anything else. Indeed it looks like calling lockFocus unlockFocus (and nothing between) does nothing. But that is wrong. [image lockFocus] alone changes image dramatically. An NSImage object contains zero, one (in most cases) or more (icon or some TIFFs) object of class NSImageRep. A call of lockFocus on this image selects an NSImageRep, that is best suited for depicting on the screen. Then it computes how many pixels (but now for a screen) it needs to render the image with the given size but with a resolution of only 72 dpi (or 144 dpi for retina screens). And then removes the NSImageRep from the list of representations and creates instead a new NSImageRep. In former OS-versions (before 10.6) an NSCachedImageRep was created. But now an NSCGImageSnapshotRep is created which under the hood is a CGImage. Make a
NSLog(#" image is:\n%#", image );
before lockFocus and one after the call of unlockFocus and the you will see what happens: for a high resolution image the number of pixels will go down, which is a nothing else than a reduction in quality. And that makes your image blurry.
I have being study about OpenGL ES for iOS.
I wonder that data of YUV format is can display without converting RGB.
In most, the yuv data have to convert RGB for display. But, converting process is very slow, Then, that is not display smoothly.
So, I would like to try to dispaly YUV data without convert to RGB.
Is it possible? If possible, what can I do?
Please, let me give a advice.
I think it is not possible in OpenGL ES to display YUV data without convert to RGB data.
You can do this very easily using OpenGL ES 2.0 shaders. I use this technique for my super-fast iOS camera app SnappyCam. The fragment shader would perform the matrix multiplication to take you from YCbCr ("YUV") to RGB. You can have each {Y, Cb, Cr} channel in a separate GL_LUMINANCE texture, or combine the {Cb, Cr} textures together using a GL_LUMINANCE_ALPHA texture if your chrominance data is already interleaved (Apple call this a bi-planar format).
See my related answer to the question YUV to RGBA on Apple A4, should I use shaders or NEON? here on StackOverflow.
You may also do this using the fixed rendering pipeline of ES 1.1, but I haven't tried it. I would look toward the the texture blending functions, e.g. given in this OpenGL Texture Combiners Wiki Page.
Is you are looking solution for IOS, iPhone application then there are solution for that.
This is way convert CMSampleBufferRef to UIImage when video pixel type is set as kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
-(UIImage *) imageFromSamplePlanerPixelBuffer:(CMSampleBufferRef) sampleBuffer{
#autoreleasepool {
// Get a CMSampleBuffer's Core Video image buffer for the media data
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// Get the number of bytes per row for the plane pixel buffer
void *baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
// Get the number of bytes per row for the plane pixel buffer
size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
// Get the pixel buffer width and height
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// Create a device-dependent gray color space
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
// Create a bitmap graphics context with the sample buffer data
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
bytesPerRow, colorSpace, kCGImageAlphaNone);
// Create a Quartz image from the pixel data in the bitmap graphics context
CGImageRef quartzImage = CGBitmapContextCreateImage(context);
// Unlock the pixel buffer
CVPixelBufferUnlockBaseAddress(imageBuffer,0);
// Free up the context and color space
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// Create an image object from the Quartz image
UIImage *image = [UIImage imageWithCGImage:quartzImage];
// Release the Quartz image
CGImageRelease(quartzImage);
return (image);
}
}
If you are looking for Mobile devices then i can provide others to.
I'm trying to extract pixel data from individual frames of a QT Movie.
I think I need to use CV, because QTKit and NSImage would
be too slow...
I need to compare each pixel of the image in the buffer (CVImageBufferRef) containing the current frame of the webcam (iSight).
So I need speed.
Sorry for my bad english language, I'm Italian.
See the code in the question how to convert a CVImageBufferRef to UIImage, which is a bigger question but covers the same ground. Here's the base code you'd need (from the OP of that question):
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
/*Lock the image buffer*/
CVPixelBufferLockBaseAddress(imageBuffer,0);
/*Get information about the image*/
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
/*We unlock the image buffer*/
CVPixelBufferUnlockBaseAddress(imageBuffer,0);
Be sure to read the accepted answer for a fuller explanation of the data format.
i'm handing off to this method and using the resultant CGImageRef adding it to a CGImageDestinationRef that is setup for finalizing as a kUTTypeJPEG:
- (CGImageRef)scaleImage:(CGImageRef)fullImageRef originalSize:(CGSize)originalSize scale:(CGFloat)scale;
{
CGSize imageSize = originalSize;
CGRect scaledRect = CGRectMake(0.0, 0.0, imageSize.width * scale, imageSize.height * scale);
CGColorSpaceRef colorSpace = CGImageGetColorSpace(fullImageRef);
CGContextRef context = CGBitmapContextCreate(NULL, scaledRect.size.width, scaledRect.size.height, 8, 0, colorSpace, CGImageGetAlphaInfo(fullImageRef));
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGImageRef subImage = CGImageCreateWithImageInRect(fullImageRef, scaledRect);
CGContextDrawImage(context, scaledRect, subImage); // offscreen
CGImageRef scaledImage = CGBitmapContextCreateImage(context);
CGImageRelease(subImage);
subImage = NULL;
subImage = scaledImage;
CGImageRelease(scaledImage);
CGContextFlush(context);
CGContextRelease(context);
return subImage;
}
i need to be sure that the resulting JPEG is the best possible color match for the original.
what i am unclear about is the actual role of CGImageAlphaInfo constant for my purposes.
i've read Quartz docs, and also the excellent Programming With Quartz book (by Gelphman and Laden), but i'm still not sure of my methods.
i've set the property kCGImageDestinationLossyCompressionQuality to 1.0 in the jpeg's property dictionary, along with capturing other properties.
should i be doing something more to maintain the color integrity of the original?
this is macos, and using gc.
It doesn't much matter, because JPEG doesn't support alpha (at least, not without external masking or an extension), so you can expect that CGImageDestination will throw away the alpha channel at the export stage.
I would try the original image's alpha info first, and if I can't create the bitmap context that way, I would use either kCGImageAlphaNoneSkipFirst or kCGImageAlphaNoneSkipLast. For other destination file formats, of course, I'd use one of the alpha-with-premultiplied-colors pixel formats.
This page has the full list of combinations supported by CGBitmapContext.