cockscomblog?

cockscomb on hatena blog

iOS で写真.app による編集を CIFilter で再現

AssetsLibrary.framework でカメラロールから生の画像データが取り出せるほか、ただ UIImagePickerController を使うのに比べ柔軟にいろいろなことができる。それで、そのままふつうに使っていて遭遇する問題のひとつが、iOS 標準の写真アプリによる画像の加工に対応できないことであった。

「写真.app」では、カメラロールの写真に対しトリミングや自動補正、赤目補正などの加工を行うことができる。UIImagePickerControllerDelegate- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info メソッドで UIImagePickerControllerEditedImageinfo から取り出せば、この加工後の画像を得ることができる。

AssetsLibrary.framework を使って得られる画像には、写真.appで行った編集が反映されていない。写真.app による編集は AdobeXMP フォーマットによってメタデータとして格納されている。これを再現するのは非常に困難だと考えられていた。

しかし CoreImage.frameworkCIFilter クラスに + (NSArray)filterArrayFromSerializedXMP:(NSData )xmpData inputImageExtent:(CGRect)extent error:(NSError **)outError というメソッドがあると id:ninjinkun さんにより発見される。CIFilter は一連のフィルターによる効果を XMP フォーマットでシリアライズ/デシリアライズする仕組みを備えていた。

  • + (NSData*)serializedXMPFromFilters:(NSArray *)filters inputImageExtent:(CGRect)extent
  • + (NSArray*)filterArrayFromSerializedXMP:(NSData *)xmpData inputImageExtent:(CGRect)extent error:(NSError **)outError

この CIFilter のふたつのクラスメソッドを用いることで XMP データと CIFilter の配列を相互に変換可能である。(ちなみに OS X にはない)

ただしこれは iOS 6 以降でしか使えない。iOS 5 もある程度考慮したコードは以下のようになるはず。

ALAssetRepresentation *representation = imageAsset.defaultRepresentation;

    NSString *XMPString = representation.metadata[@"AdjustmentXMP"];
    NSData *XMP = [XMPString dataUsingEncoding:NSUTF8StringEncoding];

    UIImage *editedImage;
    if (XMP && [[CIFilter class] respondsToSelector:@selector(filterArrayFromSerializedXMP:inputImageExtent:error:)]) {
        CIImage *image = [CIImage imageWithCGImage:representation.fullResolutionImage];

        NSError *error = nil;
        NSArray *filters = [CIFilter filterArrayFromSerializedXMP:XMP inputImageExtent:image.extent error:&error];
        if (error) {
            NSLog(@"Error during CIFilter creation: %@", error);
        }

        CIContext *context = [CIContext contextWithOptions:nil];

        for (CIFilter *filter in filters) {
            [filter setValue:image forKey:kCIInputImageKey];
            image = filter.outputImage;
        }

        editedImage = [UIImage imageWithCGImage:[context createCGImage:image fromRect:image.extent]];
    }
    else {
        editedImage = [UIImage imageWithCGImage:representation.fullResolutionImage scale:representation.scale orientation:(UIImageOrientation)representation.orientation];
    }

注意点があり、この方法では赤目補正が効かない。赤目補正のフィルターは CIRedEyeCorrections という名前であるはずだが、Core Image Filter Reference には記載されていない。何らかの理由で使用できない状態に置かれているものとみられ、写真.app でのみ使えているようである。

適当に自動補正してトリミングした写真を再現するときの CIFilter の名前を以下に列挙する。

CIVibrance
CIToneCurve
CIHighlightShadowAdjust
CIAffineTransform
CICrop

このようなフィルターが写真.app では使われているようだ。

以上の方法で、iOS 6 以降については問題が解決され、AssetsLibrary.framework を安心して使うことができる。

ということで、画像アップロードの際に写真.appでのトリミング操作が反映されるようになったはてなブログiPhoneアプリどうぞご利用ください。

追記 (2013/12/11)

iOS 7のシミュレータで試したところ赤目補正が効いていた。

ログの様子

CIRedEyeCorrections: inputImage=nil inputCameraModel=nil inputCorrectionInfo=(
        {
        averageSkinLuminance = "0.3176471";
        bitmaskThreshold = "0.07450981";
        bitmaskX = "0.2054794";
        bitmaskY = "0.4444444";
        cornealReflectionThreshold = "0.8941177";
        cornealReflectionX = "0.2100457";
        cornealReflectionY = "0.4461806";
        existingPupilAverage = "0.2156863";
        existingPupilHigh = "0.4470589";
        existingPupilLow = 0;
        existingPupilMedium = "0.1843137";
        finalEyeCase = 0;
        forceCase = 0;
        fullImageHeight = 576;
        fullImageWidth = 438;
        imageOrientation = 1;
        imageSignalToNoiseRatio = 20;
        imageSpecialValue = 0;
        interocularDistance = 0;
        pointX = "0.2009132";
        pointY = "0.4461806";
        pupilShadeAlignment = 0;
        pupilShadeAverage = 0;
        pupilShadeHigh = 0;
        pupilShadeLow = 0;
        pupilShadeMedium = 0;
        repairRectangleMaximumX = "0.2465753";
        repairRectangleMaximumY = "0.421875";
        repairRectangleMinimumX = "0.1780822";
        repairRectangleMinimumY = "0.4704861";
        searchRectangleMaximumX = "0.3013698";
        searchRectangleMaximumY = "0.3697917";
        searchRectangleMinimumX = "0.1050228";
        searchRectangleMinimumY = "0.5190972";
        size = "0.01185013";
        snappedX = "0.2009132";
        snappedY = "0.4461806";
    },
        {
        averageSkinLuminance = "0.3529412";
        bitmaskThreshold = "0.007843138";
        bitmaskX = "0.4611872";
        bitmaskY = "0.46875";
        cornealReflectionThreshold = "0.9333334";
        cornealReflectionX = "0.4611872";
        cornealReflectionY = "0.4652778";
        existingPupilAverage = "0.1764706";
        existingPupilHigh = "0.4117647";
        existingPupilLow = 0;
        existingPupilMedium = "0.1490196";
        finalEyeCase = 0;
        forceCase = 0;
        fullImageHeight = 576;
        fullImageWidth = 438;
        imageOrientation = 1;
        imageSignalToNoiseRatio = 20;
        imageSpecialValue = 0;
        interocularDistance = 0;
        pointX = "0.4657534";
        pointY = "0.4704861";
        pupilShadeAlignment = 0;
        pupilShadeAverage = 0;
        pupilShadeHigh = 0;
        pupilShadeLow = 0;
        pupilShadeMedium = 0;
        repairRectangleMaximumX = "0.4931507";
        repairRectangleMaximumY = "0.4392361";
        repairRectangleMinimumX = "0.4292237";
        repairRectangleMinimumY = "0.4913194";
        searchRectangleMaximumX = "0.56621";
        searchRectangleMaximumY = "0.3940972";
        searchRectangleMinimumX = "0.369863";
        searchRectangleMinimumY = "0.5434028";
        size = "0.0124749";
        snappedX = "0.4657534";
        snappedY = "0.4704861";
    }
)>