iMind Developers Blog

iMind開発者ブログ

Open Image Dataset v4を使ってみる

概要

Open Image Dataset v4から600クラスのデータを落としてきて中身を確認したり軽く使ってみたり、Pascal VOCの形式に変換したりする。

v5が出たこの時期にいまさらな記事。

ダウンロード

下記からダウンロードする

https://storage.googleapis.com/openimages/web/download.html

Imagesは容量が大きいので後回しにして、それ以外のBoxes, Image Labels, Image IDs, MetadataなどをWebからダウンロードする。

一番容量が大きいImagesについてはWebダウンロードでは何度かエラーで止まりresumeも失敗する事象に悩まされたので、Download from CVDFの方に切り替える。

ダウンロード方法は下記に乗っている。

https://github.com/cvdfoundation/open-images-dataset#download-images-with-bounding-boxes-annotations

Google Cloud SDKがいるのでそれをまずインストールする。

https://cloud.google.com/sdk/install

自分のアカウントで認証する。

$ gcloud init

同じアカウントでCVDFのサイトでも認証しておく。

http://www.cvdfoundation.org/datasets/open-images-dataset/signup.html

コマンドからダウンロード実行。

$ mkdir test
$ gsutil -m rsync -r gs://open-images-dataset/test test

上記で画像データ(125,436枚、36GB)を落とせる。

画像の確認

$ ls test | head -5

000026e7ee790996.jpg
000062a39995e348.jpg
0000c64e1253d68f.jpg
000132c20b84269b.jpg
0002ab0af02e4a77.jpg

上記のような16進数の名前の画像がたくさん入っている。

ファイル数の確認

$ ls test | wc -l

125436

これらの画像がどういった分類をされているのか。まずは下記ファイルに分類クラスの名前が載っている。

class-descriptions-boxable.csv

全601クラス。左がID、右がラベル名。動物とか車とか食べ物とかいろいろ載ってる。今回は600クラス版を落としたけどその下にはさらにデカイ19,995クラス版もいる。

犬の画像だけ集めてみる

一般家庭の環境ではこれらすべてを使って機械学習に使うのは厳しいので、一旦犬画像だけ取り出して犬判定モデルでも作ってみる。

Dogでclass-descriptions-boxable.csvを検索すると下記の行がある。

$ grep Dog class-descriptions-boxable.csv

/m/0bt9lr,Dog
/m/0h8n6f9,Dog bed

Dog bedなんてあるんだ、というのは置いておいてDogのIDは /m/0bt9lr になっていることがわかった。

続いて test-annotations-bbox.csv の中を確認。ImageIDに対するアノテーションの情報が入っている。

$ head -5 test-annotations-bbox.csv 

ImageID,Source,LabelName,Confidence,XMin,XMax,YMin,YMax,IsOccluded,IsTruncated,IsGroupOf,IsDepiction,IsInside
000026e7ee790996,freeform,/m/07j7r,1,0.071905,0.145346,0.206591,0.391306,0,1,1,0,0
000026e7ee790996,freeform,/m/07j7r,1,0.439756,0.572466,0.264153,0.435122,0,1,1,0,0
000026e7ee790996,freeform,/m/07j7r,1,0.668455,1.000000,0.000000,0.552825,0,1,1,0,0
000062a39995e348,freeform,/m/015p6,1,0.205719,0.849912,0.154144,1.000000,0,0,0,0,0

これをDogのID、 /m/0bt9lr でgrepすれば犬画像が取れる。

# ヘッダだけ事前に出力
$ head -1 test-annotations-bbox.csv > dog-test-annotations-bbox.csv

# Dogカテゴリのboxを出力
$ grep "/m/0bt9lr" test-annotations-bbox.csv >> dog-test-annotations-bbox.csv

これで犬のAnnotationファイルができた。

$ head -5 dog-test-annotations-bbox.csv 

ImageID,Source,LabelName,Confidence,XMin,XMax,YMin,YMax,IsOccluded,IsTruncated,IsGroupOf,IsDepiction,IsInside
000ccf1d00f7f1cd,freeform,/m/0bt9lr,1,0.000000,0.823583,0.134748,0.999894,0,1,0,0,0
004130acea29204f,freeform,/m/0bt9lr,1,0.221060,0.937226,0.000726,0.926652,0,1,0,0,0
004421e64e1be85b,freeform,/m/0bt9lr,1,0.282111,0.783627,0.123731,0.999788,0,1,0,0,0
0050fb46c84869b1,freeform,/m/0bt9lr,1,0.082796,0.799341,0.174982,0.865755,0,0,0,0,0

件数は4810枚で5818頭。かなりの数だけど犬は個体差が大きいし種類も豊富だから数はたくさん欲しい。

続いてこの4811画像だけをコピーして別のフォルダに入れる。画像の名称は画像IDに.jpgを付けるだけ。

import os. shutil

os.mkdir('dog')

dog_ids = set()
with open('dog-test-annotations-bbox.csv', 'rt') as f:
    next(f) # 1行目のheaderはスキップ
    for line in f:
        dog_ids.add(line.split(',')[0])

for dog_id in dog_ids:
    src = 'test/{}.jpg'.format(dog_id)
    dest = 'dog/{}.jpg'.format(dog_id)
    shutil.copyfile(src, dest)

これで犬ファイルとそれに対するAnnotationのデータが揃った。

このデータでSSDのモデルを作ってみる。ライブラリとして下記のTensorflowを使ったものを利用する。

https://github.com/ljanyst/ssd-tensorflow

このライブラリはPascal-VOCのデータを読む用になっているので(annotationはXML形式)形式を変換しないといけない。変換用のコードはそのへんに落ちてそうだったけど検証するより書いた方が早そうなので車輪の再発明をする。

最低限、これだけあれば処理は動く。

<annotation>
        <filename>xxxxx.jpg</filename>
        <object>
                <name>dog</name>
                <bndbox>
                        <xmin>167</xmin>
                        <ymin>1</ymin>
                        <xmax>375</xmax>
                        <ymax>128</ymax>
                </bndbox>
        </object>
</annotation>

見ての通り、objectのxmin, ymin, xmax, ymax等がピクセルサイズになっている。

これに対してOpen Image Dataset v4では割合で記録している。

000026e7ee790996,freeform,/m/07j7r,1,0.071905,0.145346,0.206591,0.391306,0,1,1,0,0

まずは画像のサイズを見てここを変換してあげないといけない。OpenCVで画像のサイズを取ってピクセルに変換してみる。ついでに可視化もしてちゃんと変換できてるか確認する。

まずはpandasでannotationファイルを開く。

import cv2, pandas as pd

df = pd.read_csv('dog-test-annotations-bbox.csv')
df.head(3)
index ImageID Source LabelName Confidence XMin XMax YMin YMax
0 000ccf1d00f7f1cd freeform /m/0bt9lr 1 0 0.823583 0.134748 0.999894
1 004130acea29204f freeform /m/0bt9lr 1 0.22106 0.937226 0.000726 0.926652
2 004421e64e1be85b freeform /m/0bt9lr 1 0.282111 0.783627 0.123731 0.999788

(スペースの都合で一部カラムを省略)

動作確認用の画像として複数ワンコが写っているファイルが良いので、ID 00924896424a6180を選ぶ。

annotations = df[df.ImageID == '00924896424a6180']
annotations
ImageID Source LabelName Confidence XMin XMax YMin YMax
9 00924896424a6180 freeform /m/0bt9lr 1 0 0.733391 0.183994 0.866967
10 00924896424a6180 freeform /m/0bt9lr 1 0.253301 0.854086 0 0.263123

指定画像を開く。

img = cv2.imread('data/dog/dog/00924896424a6180.jpg')
y, x = img.shape[0:2]
x, y

shapeによると画像サイズは1024×683らしい。

枠の幅を計算してrectangle書いてみる。

ri = lambda d: int(round(d))
for idx, annot in annotations.iterrows():
    pt1 = (ri(x * annot.XMin), ri(y * annot.YMin))
    pt2 = (ri(x * annot.XMax), ri(y * annot.YMax))
    print(pt1, pt2)
    cv2.rectangle(img, pt1, pt2, color=(255, 128, 0), thickness=7)

%matplotlib inline
from matplotlib import pylab as plt
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

f:id:mwsoft:20190429162733p:plain

大丈夫そう。この処理でXMLを生成する。

from lxml import etree

multi_ids = df.groupby('ImageID').count()
multi_ids = multi_ids[multi_ids.Source > 1]
df2 = df[df.ImageID.isin(multi_ids.index)].head(8)
ris = lambda d: str(int(round(d)))
for image_id, rows in df2.groupby('ImageID'):
    img = cv2.imread('data/dog/dog/{}.jpg'.format(image_id))
    y, x = img.shape[0:2]
    doc = etree.Element('annotation')
    fname_elem = etree.SubElement(doc, 'filename')
    fname_elem.text = '{}.jpg'.format(image_id)
    for idx, row in rows.iterrows():
        box = {'xmin': ris(x * row.XMin), 'xmax': ris(x * row.XMax), 
              'ymin': ris(y * row.YMin), 'ymax': ris(y * row.YMax)}
        obj_elem = etree.SubElement(doc, 'object')
        obj_name = etree.SubElement(obj_elem, 'name')
        obj_name.text = 'dog'
        bndbox_elem = etree.SubElement(obj_elem, 'bndbox')
        for p in ['xmin', 'xmax', 'ymin', 'ymax']:
            e = etree.SubElement(bndbox_elem, p)
            e.text = box[p]
    with open('doc.xml', 'wb') as w:
        w.write(etree.tostring(doc, pretty_print=True))

出力されたXML。

<annotation>
  <filename>017c5a5507ec70b8.jpg</filename>
  <object>
    <name>dog</name>
    <bndbox>
      <xmin>190</xmin>
      <xmax>551</xmax>
      <ymin>214</ymin>
      <ymax>521</ymax>
    </bndbox>
  </object>
  <object>
    <name>dog</name>
    <bndbox>
      <xmin>537</xmin>
      <xmax>772</xmax>
      <ymin>374</ymin>
      <ymax>627</ymax>
    </bndbox>
  </object>
</annotation>

合ってそう。

間違いがあるといけないのでXMLから枠を出力するコードを書いて、一応確認する。

from lxml import etree
xml_file = 'doc.xml'
with open(xml_file) as f:
    tree = etree.parse(f)

img_name = tree.xpath('/annotation/filename')[0].text
objects = tree.xpath('/annotation/object')

img = cv2.imread('data/dog/dog/{}'.format(img_name))
print( 'data/dog/dog/{}'.format(img_name) )
for obj in objects:
    bndbox = obj.find('bndbox')
    xmin = int(bndbox.find('xmin').text)
    ymin = int(bndbox.find('ymin').text)
    xmax = int(bndbox.find('xmax').text)
    ymax = int(bndbox.find('ymax').text)
    print(xmin, ymin, xmax, ymax)
    cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color=(255, 128, 0), thickness=7)

from matplotlib import pylab as plt
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

f:id:mwsoft:20190429162951p:plain

問題なし。

最後にここで書いたコードをまとめて、下記のような動作をするスクリプトを書く。

指定したラベル(複数指定可)の画像を、指定フォルダ配下にコピーする コピーした画像に対してPascal-VOCと同じ形式のXMLアノテーションファイルを生成する その他、条件

引数にはラベル名、データセットがいるフォルダ、出力先フォルダを取る データセットの直下に class-descriptions-boxable.csvがいて、[train|test|validation]-annotations-bbox.csv のいずれかと、それに対応する[train|test|validation]フォルダがいるものとする train, test, validationの3種類のデータを読み込み(なければスキップ)、出力先にもtrain, test, validationそれぞれのフォルダを作ってその配下に出力する ということで書いたコード。

import os, shutil, argparse, cv2, pandas as pd
from lxml import etree

def main(labels, input_dir, output_dir):
    if not os.path.exists(output_dir):
        os.mkdir(output_dir)

    # ラベルのIDを取得
    label_info = pd.read_csv(os.path.join(input_dir, 'class-descriptions-boxable.csv'), names=['label_id', 'label_name'])
    label_info = label_info[label_info.label_name.isin(labels)]
    label_ids = dict([(l.label_id, l.label_name.lower()) for idx, l in label_info.iterrows()])

    # train, test, validationに対してそれぞれ処理を実行
    for target in ['train', 'test', 'validation']:
        execute(label_ids, input_dir, output_dir, target)

def execute(label_ids, input_dir, output_dir, target):
    # ファイルチェック
    annot_path = os.path.join(input_dir, '%s-annotations-bbox.csv' % target)
    if not os.path.exists(annot_path):
        print('ファイルがおらんね : ' + annot_path)
        return

    # 出力ディレクトリ生成
    target_output_dir = os.path.join(output_dir, target)
    if not os.path.exists(target_output_dir):
        os.mkdir(target_output_dir)

    # 対象ラベルのannotationを抽出
    annot = pd.read_csv(annot_path)
    annot = annot[annot.LabelName.isin(label_ids.keys())]

    ris = lambda d: str(int(round(d)))
    for image_id, rows in annot.groupby('ImageID'):
        # 画像ファイルの読み込み
        src = os.path.join(input_dir, target, '%s.jpg' % image_id)
        img = cv2.imread(src)
        y, x = img.shape[0:2]

        # XMLの生成
        doc = etree.Element('annotation')
        fname_elem = etree.SubElement(doc, 'filename')
        fname_elem.text = '{}.jpg'.format(image_id)
        for idx, row in rows.iterrows():
            box = {'xmin': ris(x * row.XMin), 'xmax': ris(x * row.XMax), 
                  'ymin': ris(y * row.YMin), 'ymax': ris(y * row.YMax)}
            obj_elem = etree.SubElement(doc, 'object')
            obj_name = etree.SubElement(obj_elem, 'name')
            obj_name.text = label_ids[row.LabelName]
            bndbox_elem = etree.SubElement(obj_elem, 'bndbox')
            for p in ['xmin', 'xmax', 'ymin', 'ymax']:
                e = etree.SubElement(bndbox_elem, p)
                e.text = box[p]
        with open(os.path.join(output_dir, target, '%s.xml' % image_id), 'wb') as w:
            w.write(etree.tostring(doc, pretty_print=True))

        # 画像のコピー
        dest = os.path.join(output_dir, target, '%s.jpg' % image_id)
        shutil.copy(src, dest)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--labels', required=True, help='extract labels')
    parser.add_argument('--input-dir', required=True, help='input folder')
    parser.add_argument('--output-dir', required=True, help='output folder')
    args = parser.parse_args()

    labels = args.labels.split(',')
    main(labels, args.input_dir, args.output_dir)

改定履歴

Author: Masato Watanabe, Date: 2019-05-10, 記事投稿