ラベル メタデータ の投稿を表示しています。 すべての投稿を表示
ラベル メタデータ の投稿を表示しています。 すべての投稿を表示

2017年11月5日日曜日

画像のメタデータを解析

画像のメタデータ、つまりEXIFの撮影データや、GPSの位置情報などを取り出す。

実際のメタデータはCIImageのpropertiesプロパティで取れる。
辞書の中の一部に配列が入れ子になってる。
let ciImage = CIImage(contentsOf: fileURL)
let metadata:[String:Any] = ciImage!.properties

辞書の中身は以下のよう。
{ }が辞書、( )が配列。
ISOSpeedRatingsのように要素1個の配列に入ってる場合もあるので注意。
辞書のKeyはColorModelとかの文字列でもいいが、kCGImagePropertyColorModelなどの定数があるのでそれを使うといい。ただし、CFStringなのでStringにキャストして使う。
“ ”で囲まれた数値は最初にNSNumberにキャストすると取り出せる場合が多い。
値がなくてnilの場合もあるので、Optionalとして、nilチェックもする。

辞書のトップレベル

辞書のトップレベルならそのままkeyで取れる。
let colorModel = metadata[kCGImagePropertyColorModel as String] as! String

辞書の2ndレベル

2ndレベル以降だと、一度入れ子になった辞書や配列を読み出さないといけない。
以下ではExifの中のExposureTime(シャッター速度)を取り出す。
let exifDict = metadata[kCGImagePropertyExifDictionary as String] as? [String:Any]
if let m = exifDict?[kCGImagePropertyExifExposureTime as String] as? NSNumber {
    let exposureTime = m.floatValue
}

つづいてISOSpeedRatings(ISO感度)を取り出す。
辞書の中の要素一個の配列に入ってる。
if let ISORateArr = exifDict?[kCGImagePropertyExifISOSpeedRatings as String] as? [Any] {
    if let rate = ISORateArr[0] as? NSNumber {
        let ISORate = exifDict[kCGImagePropertyExifISOSpeedRatings as String] = rate.stringValue
    }
}

メタデータの中身の例

{
    ColorModel = RGB;
    DPIHeight = 72;
    DPIWidth = 72;
    Depth = 8;
    Orientation = 1;
    PixelHeight = 2448;
    PixelWidth = 3264;
    ProfileName = "sRGB IEC61966-2.1";
    "{Exif}" =     {
        ApertureValue = "2.52606882168926";
        BrightnessValue = "2.661020629750272";
        ColorSpace = 1;
        ComponentsConfiguration =         (
            1,
            2,
            3,
            0
        );
        CustomRendered = 2;
        DateTimeDigitized = "2016:03:09 15:28:25";
        DateTimeOriginal = "2016:03:09 15:28:25";
        ExifVersion =         (
            2,
            2,
            1
        );
        ExposureBiasValue = 0;
        ExposureMode = 0;
        ExposureProgram = 2;
        ExposureTime = "0.0303030303030303";
        FNumber = "2.4";
        Flash = 32;
        FlashPixVersion =         (
            1,
            0
        );
        FocalLenIn35mmFilm = 31;
        FocalLength = "3.3";
        ISOSpeedRatings =         (
            160
        );
        LensMake = Apple;
        LensModel = "iPad Air 2 back camera 3.3mm f/2.4";
        LensSpecification =         (
            "3.3",
            "3.3",
            "2.4",
            "2.4"
        );
        MeteringMode = 5;
        PixelXDimension = 3264;
        PixelYDimension = 2448;
        SceneCaptureType = 0;
        SceneType = 1;
        SensingMethod = 2;
        ShutterSpeedValue = "5.058989898989899";
        SubjectArea =         (
            1631,
            1223,
            1795,
            1077
        );
        SubsecTimeDigitized = 996;
        SubsecTimeOriginal = 996;
        WhiteBalance = 0;
    };
    "{GPS}" =     {
        Altitude = "143.7687861271676";
        AltitudeRef = 0;
        DateStamp = "2016:03:09";
        DestBearing = "127.0993788819876";
        DestBearingRef = T;
        HPositioningError = 5;
        ImgDirection = "12x.xxx3788819876";
        ImgDirectionRef = T;
        Latitude = "36.588055";
        LatitudeRef = N;
        Longitude = "139.xxx3666666667";
        LongitudeRef = E;
        Speed = 0;
        SpeedRef = K;
        TimeStamp = "06:28:25";
    };
    "{MakerApple}" =     {
        1 = 4;
        10 = 2;
        14 = 1;
        2 = <37007700 73003500 34004c00 5c006c00 67007c00 75008500 a300b100 b000e400 6e006500 68003600 38005f00 62006c00 66007700 70007b00 8200b600 e200cd00 5800b900 9d009400 5f004300 52005e00 5c008100 8300b100 d000cf00 de000701 56006800 a8009700 7e005300 4c006600 7c008e00 9300ea00 1601f100 e900e300 53006100 83007c00 7e00c300 ca00b800 86008c00 9200f200 ff000101 d500b100 5e006200 6b00be00 4a016d01 7d018d01 5001cd00 a200c900 f800df00 e000af00 71009600 fd004d01 4b014401 4f014f01 6a01a201 44013a01 16012501 37019000 6f015801 0f01fc00 f800f000 f7002101 3f016a01 9001bb01 9b018101 4401f700 d500a200 e700d700 dc00e500 f500e800 18013b01 60019001 6a013001 12010b01 5f008000 9d00b200 9d009b00 b900c800 e7002a01 87017201 3401fa00 c800cb00 5c008000 91009400 85009900 9600af00 be005601 59012b01 e300d800 18012f01 6e008a00 7c007500 72008200 9100a500 e7006701 3a01c400 e9007601 8c018e01 41007100 63005c00 5f006c00 83009100 3a014f01 dc00fb00 9e019d01 aa01ad01 59005300 4f005500 52005d00 72000c01 53010b01 1701b001 a701a201 9b019c01 44005100 51005000 5200a400 0a014701 23010c01 af01b801 9f019c01 9701a601 36003800 48006700 d000f300 08014301 19018701 ad01b101 9c019901 93018a01>;
        20 = 3;
        3 =         {
            epoch = 0;
            flags = 1;
            timescale = 1000000000;
            value = 817736307596166;
        };
        4 = 1;
        5 = 214;
        6 = 220;
        7 = 1;
        8 =         (
            "-0.9269282",
            "0.0151294",
            "-0.3894437"
        );
    };
    "{TIFF}" =     {
        DateTime = "2016:03:09 15:28:25";
        Make = Apple;
        Model = "iPad Air 2";
        Orientation = 1;
        ResolutionUnit = 2;
        Software = "9.2.1";
        XResolution = 72;
        YResolution = 72;
    };

}

2016年8月7日日曜日

画像をメタデータ付きで読み書き

アプリ内にあるDocumentsディレクトリ内の画像を、メタデータ(EXIFやGPSの位置情報など)付きで読み書きする方法。

準備

プロジェクトにメタデータの付いた画像をインストールしておく。(いきなりDocumentディレクトリに画像入れられないから)
名前は一応gazo.jpgで。
最近はAssetCatalogにインストールすることが多いけど、そこの画像にアクセスする方法がよくわからないので、以前のようにXcodeのプロジェクトナビゲータの所に突っ込むこと。

今回は以下の手順を取る。
  1. リソースとしてインストールされた画像gazo.jpgとメタデータを読み込み
  2. Documentディレクトリにメタデータとともに書き込み
  3. 画像とメタデータを読み込んで表示する
また、ImageIO.frameworkをインポートしとくこと。
#import <ImageIO/ImageIO.h>

書き編

//リソースにある画像を読み、メタデータとともにアプリ内のDocumentディレクトリに保存
- (void)writeImageWithMetadata {
    //🌟準備としてリソース画像とmetadataを読む
    //リソースディレクトリへのパスを得る
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *resourcePath = [bundle resourcePath];
    NSString *imgPath = [resourcePath stringByAppendingPathComponent:@"gazo.jpg"];
    NSLog(@"リソースのpath %@",imgPath);
    //リソースの画像とメタデータを得る
    CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:imgPath]), nil);
    NSDictionary *metadata = (NSDictionary *) CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL));
    
    //🌟画像とmetadataをDocumentディレクトリに保存
    //Documentに保存するファイルのpathを作る
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"gazo.jpg"];
    
    //書き込むための画像データを入れるNSMutableData型変数を作る
    NSMutableData *imageData = [[NSMutableData alloc] init];
    //書き込むための情報を入れる変数destRefに、書き込む画像データの変数(imageData)の情報と、データタイプ(JPEGとか?)、画像ファイル中の画像枚数(複数画像を持つフォーマットもあるため)を入れる
    //sourceRefはデータタイプを得る関数で参照しているだけなので、ここでは画像そのものは加えられていない
    CGImageDestinationRef destRef = CGImageDestinationCreateWithData((CFMutableDataRef)imageData, CGImageSourceGetType(sourceRef), 1, nil);
    //引数1のdestRefに、引数2の画像と引数4のメタデータを追加する。引数3の0は複数画像がある場合の画像のインデックスらしい
    CGImageDestinationAddImageFromSource(destRef, sourceRef, 0,(CFDictionaryRef)metadata);
    //書き込むための情報destRefをファイナライズして完成
    CGImageDestinationFinalize(destRef);
    //画像データのimageDataの書き込みをすることで、destRefを参照してメタデータともども書き込んでくれるらしい
    [imageData writeToFile:filePath atomically:YES];
    //使用後は解放
    CFRelease(sourceRef);
    CFRelease(destRef);
}

読み編

//Documentディレクトリ内の画像を読み込んで表示&メタデータ取得して表示
- (void)readImageWithMetadata {
    //Documentディレクトリにあるファイルのpath作成
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"gazo.jpg"];

    //画像読み込み
    NSError *err = nil;
    NSData *imageData = [NSData dataWithContentsOfFile:filePath
                                              options:NSDataReadingMappedAlways
                                                error:&err];
    //エラー処理するならこの辺で
    NSLog(@"Error -> %@",err);
    //NSDataからCIImage経由でUIImageに変換
    CIImage *ciImage = [CIImage imageWithData:imageData];
    UIImage *image = [UIImage imageWithCIImage:ciImage];
    //こっから。UIImage作り直し開始
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    //ここまで。UIImage作り直し終了
    _myImageView.image = [UIImage imageWithData:imageData];

    //メタデータ取得
    //CGImageSourceCreateWithURLを使う場合
    //Documentディレクトリ内の画像とメタデータを得る
    CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:filePath]), nil);
    //sourceRefの情報からメタデータを取り出す
    NSDictionary *metadata = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, nil));

    //使用後は解放
    CFRelease(sourceRef);

    //確認のためmetadata表示
    NSLog(@"メタデータ %@",metadata);
    //EXIF情報取り出し
    NSDictionary *ExifDictionary = [metadata objectForKey:(NSString*)kCGImagePropertyExifDictionary];
    NSLog(@"EXIFデータ %@",ExifDictionary);
}

各メソッド、関数について

NSBundleはアプリにインストールされてるリソースにアクセスするためのクラス。
resourcePathメソッドでリソースディレクトリへのpathが得られるので、ファイル名を追加。

CGImage〜Ref

CGImageSourceRef、CGImageDestinationRefは画像ファイルに付加されたEXIFなどの情報にアクセスするためのクラスで、ImageIO Frameworkが必要。
  • 画像ファイル読み込み時 
    • CGImageSourceRef 
  • 画像ファイル書き込み時 
    • CGImageDestinationRef

メタデータの中身にアクセス

metadataの辞書からEXIF情報を、キーを使って取り出し表示している。
ちなみにEXIFは入れ子になっている辞書なので、
float ss = [metadata[(NSString *)kCGImagePropertyExifDictionary][(NSString *)kCGImagePropertyExifExposureTime] floatValue];
などとやることで下の階層の情報にアクセスする。

CGImageDestinationCreateWithData

CGImageDestinationRef CGImageDestinationCreateWithDataConsumer ( CGDataConsumerRef consumer,CFStringRef type, size_t count, CFDictionaryRef options );

引数
consumer:書き込みデータのコンシューマー(何じゃそりゃ?)だそうだ。初期化しただけのNSMutableDataを指定してる。多分ここで指定した変数に画像データのsourceを書き込み、さらに結果をdestに返り値を返してるんだと思う。
type:データのタイプ(たとえばJPEGとかPNGとか)らしいが、この場合直接指定せずにCGImageSourceGetType関数でソース画像のタイプを得ている。
count:TIFFなどのように複数の画像を含む画像の場合、何枚あるのかを指定するが、1枚だけなら1でいい。
options:将来のための予約とあるので、nilでよし。

CGImageDestinationAddImageFromSource

void CGImageDestinationAddImageFromSource ( CGImageDestinationRef idst, CGImageSourceRef isrc, size_t index, CFDictionaryRef properties );

引数
idst:画像のdestination
isrc:画像のソース
index:ソース画像で画像の位置を指定するインデックス(0オリジン)。多分複数画像を持つフォーマットの場合に、何枚目の画像かを指定するんじゃないかと思うが、よくわからん。とりあえず1枚だけのフォーマットなら0でいいかと。
properties:ソース画像のプロパティに上書きするか追加するか指定する辞書。プロパティのキーがkCFNullなら画像のdestinationは削除される。わかりづらいが、ここに辞書を指定してやればソース画像(source)+辞書をdestinationに追加してくれるらしい。

変数destは書き込む画像のデータ(imagaData)とメタデータ(metadata)の情報を参照していて、CGImageDestinationFinalizeでファイナライズした後にimagaDataの方をwriteToFileすると、メタデータ付きで書き込まれるのではないか?
最後にsourceとdestはreleaseしてやる必要があるようだ。

destに色々情報を追加しつつ、最後はimageDataを書き込んでるのがわかりづらい。imageDataを書き込むとdestに書かれた参照情報を見てmetadataも一緒に書き込まれるという理解でいいのか?

CGImageにすると向きの情報が消えることがある

読み書き時に止むを得ずCGImageに変換を要する場合があるが、UIImageでは持っている画像の向きの情報であるimageOrientationプロパティがないため、UIImageに復元しても縦長に撮った画像が90度横倒しに表示されてしまうことがあった。
その場合、
[UIImage imageWithCGImage: scale: orientation:]メソッドでorientationを指定しつつ復元してやるが、それでもうまくいかない場合は、
UIGraphicsBeginImageContext()
と
UIGraphicsEndImageContext()
に挟まれた間で、UIImageを作り直してやるといい。
なにやらめんどくさいがしょうがない。
ちなみに前述のコード中、直接CGImageから変換したのではうまくいかなかったため、一度CIImageを経由した。
なにやら本当にめんどくさい。

謎のメタデータ追加メソッド

メタデータを追加するメソッドとしては
CGImageDestinationAddImageAndMetadata(CGImageDestinationRef idst, CGImageRef image, CGImageMetadataRef metadata, CFDictionaryRef options)
というのもあるようなのだが、APIリファレンス見てもちゃんとした説明が書いてないし、よくわからない。
釈然としないけど、CGImageDestinationAddImageFromSourceでうまくいくんならそれでいんじゃね?

アセットカタログ内の画像にアクセスしてみる

結論から言うとうまくいきませんでした。

NSString *imgSuffix = @"-universal-1x";
NSBundle *bundle = [NSBundle mainBundle];
//Assets Catalogのディレクトリへのパスを作る

NSString *imgPath = [bundle pathForResource:[NSStringstringWithFormat:@"%@%@",@"gazo2",imgSuffix] ofType:@"jpg"];

アセットカタログのjsonファイル(Contents.json)を開くと、
{
  "images" : [
    {
      "idiom" : "universal",
      "filename" : "gazo2.jpg",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}
などとなっており、アクセスにはファイル名にidiomやscaleのサフィックスを付けるというような情報を見たのだけど、サフィックスが合ってないのか、そもそも情報自体が間違いなのか、imgPathはnilになってしまい、ダメだった。
メタデータを取るためにアクセスするようなことがなければ、単にUIImageのファイル名で楽にアクセスできるのにねぇ。

感想

CGImageSourceRefとCGImageDestinationRefがどういうものかよくわからず、ひたすら悩んで調べまくった。
今もよくわかってないけどね。
引数1に引数2以降の情報を追加するという仕様や、情報を追加した変数(たとえばdestRef)じゃなく画像本体の方を書き込むとdestRefを参照してメタデータも書き込まれるとかはわかりづらい。

今回はリソースとDocumentディレクトリの間だったけど、イメージライブラリとの間だとALAssetLibraryを使って(最近はPhotosFrameworkなの?)また別のやり方になるはず。
とにかくめんどっちー。
UIImageのプロパティとして簡単に扱えるようにならんもんか…。