Practice of Programming

プログラム とか Linuxとかの話題

RustでExifのMakerNoteからPanasonicのカメラのレンズ情報を雑に取得する

Pnaasonicのカメラを使ってるのですが、Exifからレンズ情報が取れない...のですよね。

問題

通常、ExifのLensModelにレンズ名が入ってくるのだと思うのですが、Panasonicのカメラでは、入ってなさそうです(フルサイズ系(S系)は入っているかも)。少なくとも、僕の使ったことのあるカメラ、GM5, GX8, G9等では入ってなかったです。

ですが、ExifのMakerNoteの中に、データはあるようなので、それを使えばいけそうです。

ExifをParseするのに、rexifを使う

ExifをRustでParseするのには、rexifというものがありますので、これを使いました。

docs.rs

rexifでparseすると、entriesというメソッドで、各エントリーが取得できるので、MakerNoteの場合に、別の関数でentryの、ifd.ext_dataをparseします。下記のような感じですね。

            struct Exif {
               lens_model: String,
               // 他にも適当に
            }

            let mut data = Exif{lens_model = "".to_string() };
            let exif_data = rexif::parse_file(file_path);
            for e in exif_data.unwrap().entries {
                match e.tag {
                    rexif::ExifTag::MakerNote => {
                        let d = get_lens_from_maker_note(e.ifd.ext_data);
                        if d != "" {
                            data.LensModel = d;
                        }
                    },
                    // 他にも適当に
                }
            }

get_lens_from_maker_note でやっていること

この関数では、rexifから取ってきたe.ifd_ext_data をparseします。この型はVec<u8> のバイト列です。Panasonicのカメラの場合、先頭の12バイトは、80, 97, 110, 97, 115, 111, 110, 105, 99, 0, 0, 0 となりますが、これはPanasonic\0\0\0になります。

先頭がPanasonic\0\0\0の場合に、レンズ情報がどこにあるかですが、固定長というわけではなさそうだったので、先頭から適当にそれっぽい文字がでるまで探すことにしました。

確認できたところでは、LUMIX, LEICA, OLYMPUS, SIGMA 等の文字の後に、レンズ情報が来ていました。LEICA は、LEICA認証の通ったPanasonicのレンズ(例: LEICA DG 100-400/F4.0-6.3)の場合になります。

なので、下記のような感じで取るようにしました。

  1. 最初にPansonic\0\0\0があるか判定
  2. 制御文字以外をstd::char::from_u32で変換して連結していき、最後の10文字だけ取っておく
  3. レンズのプレフィックス用の正規表現にあたるかチェック
  4. そこから制御文字の手前までをレンズ情報とする

という、とても雑な感じです(正規表現にマッチするレンズ情報がない場合、最後まで読んでしまうので、効率が悪い)。

コードは以下のようになります。

// currently only for Panasonic camera
fn get_lens_from_maker_note(data: Vec<u8>) -> String {
    if data.len() < 9 {
        return "".to_string();
    }

    // Panasonic
    let panasonic: [u8; 12] = [80, 97, 110, 97, 115, 111, 110, 105, 99, 0, 0, 0];
    let first12chars = &data[0..12];

    // return when first 12 char is not "Panasonic\0\0\0"
    if first12chars != &panasonic {
        return "".to_string();
    }

    // Lens name prefix regex(I only confirmed LUMIX, LEICA, OLYNMUS, SIGMA)
    let re = regex::Regex::new("(?i)(LUMIX|LEICA|OLYMPUS|SIGMA|TAMRON|KOWA|COSINA|VOIGT|VENUS)$")
        .unwrap();

    let mut i = 12;
    let mut str = "         ".to_string(); // dummy 9 chars
    while i < data.len() {
        let d = data[i];
        if d < 32 || 126 < d {
            i += 1;
            continue;
        }
        // enough length for regex
        str = str[str.len() - 9..str.len()].to_string();
        str.push(std::char::from_u32(data[i].into()).unwrap());

        let captures = re.captures(&str);
        if captures.is_some() {
            let cap = captures.unwrap();
            let mut lens = cap[0].to_string();
            let mut i2 = i;
            while i2 < data.len() {
                i2 += 1;
                if data[i2] < 32 || 126 < data[i2] {
                    return lens.to_string();
                }
                lens.push(std::char::from_u32(data[i2].into()).unwrap());
            }
        }
        i += 1;
    }
    return "".to_string();
}

雑すぎやしないか?

もっと頑張ってMakerNoteを解析するという手段もありなのですが、MakerNoteの仕様は、各社バラバラです。まともに対応すると、PerlのExifToolsのコードを見るとかする必要がありそうなのですが、そこまで時間を取れないので、一旦これで手を打ちました。

※ExifToolsは下記です。 metacpan.org

ですが、12バイト目がエントリー数で、そこから、12バイト単位でエントリーが並んでおり、そこにはレンズ情報はないということはわかったので、せめて下記のような追加を行いました。

下記の用に12から始めているところを

   let mut i = 12;

コメント通りですが、(data[12] のエントリ数 + 1 ("Panasonic\0\0\0"(12byte)) ) x 12byte分をスキップすることにしました。

    // // safely skip 12byte x (data[12](num of entries) + "Panasonic\0\0\0")
    let mut i: usize = usize::from(data[12] + 1) * 12;

まぁ、大した効果はないとは思います。

後は、正規表現でやるより、スライスでチェックしたほうが速いかも...とか思いましたが、正直速度はなんの問題もないレベルなので、一旦これで良しとします。

おわり

というわけでかなり雑にではありますが、レンズ情報を取得できるようになりました。

めでたし。