Sunday, July 15, 2007

Ahhh - CIFilter ... bliss

So, after ranting and raving about QT based effects I decided to chuck them and go for CoreVideo. After getting the movie export working using a procedure based approach (so that I can mess with the frames before they are compressed) I turned to CIFilter.


Oh what joy. Ahhhh. Awesome.


So instead of having to write a billion lines of code to find out what effects exist, you can just do this:


NSArray *filters = [CIFilter filterNamesInCategories:[NSArray arrayWithObject:kCICategoryBlur]];


Which will get you all the blur filters. The really nice thing about CIFilter is the metadata. It makes it reasonably simple to create a user interface on the fly, given any CIFilter attributes. I won't go over the whole thing here as it's well documented elsewhere suffice too say that you've got all the info you need to build a UI dynamically.


When I was still considering the use of QTEffects for Squish I had originally thought that I might hard code a few commonly used filter interfaces such as Gamma, Brightness/Contrast/Saturation, and so on. The primary reason was that Squish isn't a video editor. It doesn't handle transitions and it's not intended to be able to insert complex filter overlays onto video - the type of thing that iMovie/FCP are good at. But when I saw the list of filters, I thought "hmm. that's quite a few." and realized I was going to have to be a bit smarter.


So Squish now builds it's filter inspector based on the properties of the filter you're looking at. One part I really liked, which is a mix of standard NSObject and the CIFilter interface was the elegant way by which all filter attributes can be extracted/and or set on the filter. In three words: Key Value Coding.


For example - lets say you want to get a mutable dictionary of all filter attributes that matter to you, change some values and the set the filter values again so that you can observe the result. Step one - get the attributes that matter:


CIFilter *filter = [CIFilter filterWithName:effectName];
[filter setDefaults];
NSDictionary *inputKeyDictionary = [filter dictionaryWithValuesForKeys:[filter inputKeys]];
NSMutableDictionary *attrs = [NSMutableDictionary dictionaryWithDictionary:inputKeyDictionary];

Er, well - that was simple :-) Not only are they easy to get but they will have sensible defaults due to the setDefaults message. So what if you've now modified the attributes and want to set them back? Again, so so trivial since the CIFilter simply uses the existing KVC methods and we already have all of the input attributes. We can set them back on the filter like so:

CIFilter *aFilter = [CIFilter filterWithName:effectName];
[aFilter setValuesForKeysWithDictionary: attrs];

Anyone seasoned in Cocoa will see the above as trivial. It is, certainly. But it's such a joy to use having just jumped off the run away train that is QT Atoms that I feel the need to write something. It's also nice to praise a sensible elegant API when you see it.


What this all means in reality is that I've been able to construct a full working CoreImage effects based filtering mechanism in about 3 days total. That's pretty impressive if you ask me (Ok, yes, it'd be nice if there were a standard inspector available, but I'll live) - and getting it working with the visual context of QT was a breeze.


Having wrapped up the filter code, it was then possible to apply a filter to a frame by simply calling one helper method (whose implementation isn't that complex). In fact; there's more code to get the bitmap data out (so that I can hand it to QT) that there is to perform the filtering!

CVPixelBufferRef ref = [currentMovieFrame pixelBufferRef];
CVReturn lockResult = CVPixelBufferLockBaseAddress(ref, 0);
if(lockResult == kCVReturnSuccess) {
void *dataPtr = CVPixelBufferGetBaseAddress(ref);
if([parameters effectStack]) {
CIImage *image = [[CIImage imageWithCVImageBuffer:ref] autorelease];
CIFilter *filter = [CIFilterFactory generateFromEffectStack:image effectStack:[parameters effectStack]];
if(filter) {
CIImage *outImage = [filter valueForKey:@"outputImage"];
int width = CVPixelBufferGetWidth(ref);
int height = CVPixelBufferGetHeight(ref);
int bytesPerRow = CVPixelBufferGetBytesPerRow(ref);
CGColorSpaceRef cSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef bitmapCtx = CGBitmapContextCreate(dataPtr, width, height, 8, bytesPerRow, cSpace, kCGImageAlphaPremultipliedFirst);
CIContext *ciContext = [CIContext contextWithCGContext:bitmapCtx options:nil];
CGImageRef cgImage = [ciContext createCGImage:outImage fromRect:[outImage extent]];
CGContextDrawImage(bitmapCtx, [outImage extent], cgImage);

CGContextRelease(bitmapCtx);
CGImageRelease(cgImage);
CGColorSpaceRelease(cSpace);
}
}
long dataSize = CVPixelBufferGetDataSize(ref);
(*outputImageDescription)->dataSize = dataSize;
(*outputImageDescription)->depth = 32;
CVPixelBufferUnlockBaseAddress(ref, 0);

// etc etc - all QT based stuff from here on


Before you scream - Squish isn't going to use the method above - it's quite ineffient (it creates a bitmap and throws it away, for every video frame). However; it's simple to read - and good for demonstrating the technique.

CIFilter *filter = [CIFilterFactory generateFromEffectStack:image effectStack:[parameters effectStack]];

The above gets us a fully initialized filter chain (In Squish, filters can be layered and thus are chained together). We simply get the output image and go through the steps above to extract some bitmap data for use with QT. Pretty impressive if you ask me.


In closing - it's been a week of productivity because an API exists that a) has been thought out, b) integrates well with CoreGraphics and c) "just works". It's really nice when this happens!

Monday, July 9, 2007

Quicktime Atoms

Rant mode: Disabled
Here we are. Slighly cold day, working away on Squish in some attempt to extract various bits of information about the selected compression fromat, its frame rate, audio properties etc etc. I want to do this so I can show users an "executive summary" of what their compression preset is all about - without them having to enter the QT settings dialogs to find out. Of course all these things are stored in something called a QTAtomContainer.

OK. I can handle that. How hard can it be?

... a few hours pass ...

Rant mode: Enabled

I have now discovered the source of all woe in the world. QuickTime Atom Containers, Atoms, Atom IDs, Atom anythings. grrr. I can understand that this is a legacy carry over, from a time perhaps when space efficiency was important - but I can't think of anything more painful right now than using these things... aside from perhaps trying to cut my leg off using the back edge of a rusty razor blade.

Don't get me wrong here. I like QuickTime (the new parts). Rather, I like what QuickTime can DO for me. Lots. It's pretty cool. But these 1980's C API's are doing my HEAD IN. Every time I come to examine these APIs and use them, I cringe.... lets demonstrate the joy:

So - I want to get some details about a QTEffect.
Lets say I want to get the name of said effect - and that I've already gone through a bunch of pain to get the effect description QTAtomContainer. I can get the name by simply doing:

  
QTAtom effectNameAtom = QTFindChildByIndex(container, kParentAtomIsContainer, kEffectNameAtom, effectIndex, nil);
long dataSize;
Ptr atomData;
OSErr err = QTGetAtomDataPtr(container, effectNameAtom, &dataSize, &atomData);
if(err != noErr) {
[NSException raise:@"Effects" format:@"Can't get name of the effect"];
} else {
[self setName:[NSString stringWithCString:atomData length:dataSize]];
}


Easy huh? OK. Lets say I wanted to create an effect - and apply that effect to a video. I'll make it simple too - lets say that effect is to operate on just the existing video - it's not a transition. Righteo then. Lets begin.

Well, we need to setup a effect description ... simple enough:
 
QTAtomContainer effectsDescription;
QTNewAtomContainer(&effectsDescription);
QTInsertChild(effectsDescription,
kParentAtomIsContainer,
kParameterWhatName,
kParameterWhatID,
0,
sizeof(effectCode),
&effectCode,
nil);
// EndianU32_NtoB(3);
long value = 3;

QTInsertChild(effectsDescription,
kParentAtomIsContainer,
'ksiz',
1,
0,
sizeof(value),
&value,
nil);

if(effectCode == kEdgeDetectImageFilterType) {
value = 1;
QTInsertChild(effectsDescription,
kParentAtomIsContainer,
'colz',
1,
0,
sizeof(value),
&value,
nil);
}
OSType myType = [self sourceName];
QTInsertChild(effectsDescription, kParentAtomIsContainer, kEffectSourceName, 1, 0, sizeof(myType), &myType, NULL);


Er, yay. But wait - that's not all! We need also to add this as a sample to a new track. This is how we tell QT that's it can apply this effect.

 
Movie sourceMovie = GetTrackMovie(sourceTrack);

// Now add an effect
NSRect trackSize = [self trackRect:sourceTrack];
theEffectsTrack = NewMovieTrack(sourceMovie, IntToFixed(trackSize.size.width), IntToFixed(trackSize.size.height), 0);
if(theEffectsTrack == nil) {
[self log:@" ** Unable to create the effects track"];
return;
}

[self log:@"Created a new effects track with %@", NSStringFromSize(trackSize.size)];
theEffectsMedia = NewTrackMedia(theEffectsTrack, VideoMediaType, GetMovieTimeScale(sourceMovie), nil, 0);
if(theEffectsMedia == nil) {
[self log:@"Unable to add the new media to the video, for the effect track"];
} else {
[self log:@"Added effects media, with duration %lld", GetMovieDuration(sourceMovie)];
}

ImageDescriptionHandle sampleDescription = [self createEffectImageDescription:effectCode size:trackSize.size];
BeginMediaEdits(theEffectsMedia);
// Add the sample to the media
TimeValue sampleTime = 0;
OSStatus err = noErr;
BAILSETERR(AddMediaSample(theEffectsMedia,
(Handle) effectsDescription,
0,
GetHandleSize((Handle) effectsDescription),
GetMovieDuration(sourceMovie),
(SampleDescriptionHandle) sampleDescription,
1,
0,
&sampleTime));
// End the media editing session
EndMediaEdits(theEffectsMedia);
BAILSETERR(InsertMediaIntoTrack(theEffectsTrack, 0, sampleTime, GetMediaDuration(theEffectsMedia), fixed1));

...

- (ImageDescriptionHandle) createEffectImageDescription:(OSType)effectCode size:(NSSize)effectSize {
ImageDescriptionHandle sampleDescription = nil;
[self log:@"Creating sample description for effect %@", [NSString osTypeToString:effectCode]];
MakeImageDescriptionForEffect(effectCode, &sampleDescription);
(**sampleDescription).vendor = kAppleManufacturer;
(**sampleDescription).temporalQuality = codecNormalQuality;
(**sampleDescription).spatialQuality = codecNormalQuality;
(**sampleDescription).width = effectSize.width;
(**sampleDescription).height = effectSize.height;
return sampleDescription;
}



Whew. But guess what? YES! There's MORE! It's all well and good to describe an effect, but we also need to tell QT about how to link it all together. Should be simple enough:

 
OSErr err = noErr;
QTNewAtomContainer(&inputMap);

long refIndex;
QTAtom inputAtom;

// Add a reference to the video track
BAILSETERR(AddTrackReference(theEffectsTrack, sourceTrack, kTrackModifierReference, &refIndex));

// Add a reference into the input map
QTInsertChild(inputMap, kParentAtomIsContainer, kTrackModifierInput, refIndex, 0, 0, nil, &inputAtom);

OSType type = [self safeOStype:kTrackModifierTypeImage];
QTInsertChild(inputMap, inputAtom, kTrackModifierType, 1, 0, sizeof(type), &type, nil);

type = [self sourceName];
QTInsertChild(inputMap, inputAtom, kEffectDataSourceType, 1, 0, sizeof(type), &type, nil);

BAILSETERR(SetMediaInputMap(theEffectsMedia, inputMap));


Er, wow.
Now it's slightly unfair of me to complain here. A couple of things are going on that simple CoreImage Filtering can't match.

1) It's applying the filter to a video - not just a frame.
2) The API is allowing us to choose the source video track, so effects can be layered "easily".

However. It doesn't excuse the verbosity of this entire API/method. Every time I come to use this tech I'm absolutely Stunned (yes, stunned with a capital S) at how much work I have to do. Why couldn't we have an API like so? (OK, I'm intentionally ignoring everyone who doesn't use Objective-C here)...


QTEffect *effect = [[QTEffect alloc] initWithName:kQTBlurEffect];
NSArray *defaultParameters = [effect defaults];

// ... go off and modify these - whatever.
// In my example above they are hard coded).

[effect setParameters:newParameters];
[effect applyTo:myQtMovie];


I guess my rant is really that there appears to be no other way to do this "simple" task. Simple? OK, it's not simple. Application of video effects must be reasonably tricky. But it's now "simple" compared to other APIs available to us right now. CoreImage Filtering is a great example. It provides the paramter metadata and defaults as easily understood NSDictionary instances. Bam. Done. I spent a considerable time trying to extract the presets from a QTEffect but with no joy. I know the stuff is there, but it's soooo cryptic. Ghaaa.

I can only assume that QTEffects is dead. It's certainly not been kept up to date (even though underneath it I think has been udpated) from an API point of view. In fact for Squish I've taken two steps back now. Why would I use QTEffects (even though they do what I want) when CoreImage seems the future? Yes, I'm going to have to rewrite my recompression engine (because the change away from the method I'm using now so that I can use CoreImage is actually quite major. QT has a very cool function ConvertMovieToDataRef that can magically (given the right input) transcode one movie to another) - but I can't see why I would want to stay with API's like QTAtom and QTEffects. The pain is too great.

I guess I'd hoped that QTEffect would be a neat way for me to have effects applied to the video without having to unpack each source video frame, perform the effect myself and then recompress the video myself. It seems it is not to be. Ah well, I guess you don't get the best of both worlds (the cools stuff of QT, with the cool stuff of Core*) without doing some work.

Roll on Leopard.