Home About Contact
PIL , Python , DeepLabV3+

DeeplabV3+ 再び、PILのパレットモードについて調べる

以前 DeepLab v3+ でパン領域の判定を試みていた。 これに再度取り組もうとしたらさっぱり忘れていたので復習する。

基本的にカスタムの判別器をつくりたければ、DeepLabV3+ が想定する自前のデータセット(トレーニング用)を用意して あとは実行するだけなのだが、そのデータセットのつくり方を 雰囲気でなんとなくやっていたので、もうすこし理解を深める。

トレーニング用画像をパレットモードで作成する

ここではパン領域(というかパンのピクセル)だけを判定することを想定しているので、 画像に対してパンピクセルは 1 それ以外は 0 を設定したいわゆるラベル画像をつくる。

このとき、ラベリング作業中は作業者が分かるように色付けをしたいので、パレットモードの画像を使う。

ここでは話を簡単にするために 5x5 の画像サイズで考える。

my_pixel_values = [
  0,0,0,0,0,
  0,0,1,1,0,
  0,1,1,1,0,
  0,1,1,0,0,
  0,0,0,0,0  
]

1 で指定した部分がパンのピクセル(ということにする)。 中央にパンが配置されている画像です。

これを PIL を使って、パレットモードを持った PNG 画像に変換します。

パレットの作成

パンを赤色にそのほか(背景)を白色として表現することにします。

my_palette = [
  255,255,255,
  255,0,0
]

最初の3つの値が パレット0 のRGB値(白色)になります。 次の3つが パレット1 のRGB値(赤色)です。

PILを使って パレットモードで画像を生成するには:

img = Image.new("P", (width, height))

モードとして P を指定します。(width, height はそれぞれ 5 です。)

次に使用する自前のパレットを設定:

img.putpalette(my_palette)

これだけです。

あとは、 img.putpixel( (x,y), パレットインデックス値 ) を使って、 ピクセルごとにどのパレットの値を使うか指定します。

ここではパレットは二色しかありません。パレットインデックス値として...

という具合いです。

画像を生成するコード全体はこれ:

width = 5
height = 5
img = Image.new("P", (width, height))
img.putpalette(my_palette)

for y in range(height):
  for x in range(width):
    pxIndex = y * height + x
    pxValue = my_pixel_values[pxIndex]
    img.putpixel( (x,y), pxValue )

img.save("1.png", "PNG")

生成された画像:

1.png

トレーニング用コードを確認

keras の DeepLabV3+ のトレーニング用コードはこれ https://keras.io/examples/vision/deeplabv3_plus/ です。

トレーニング用画像を読み込むコードは次にようになっています。

def read_image(image_path, mask=False):
    image = tf_io.read_file(image_path)
    if mask:
        image = tf_image.decode_png(image, channels=1)
        image.set_shape([None, None, 1])
        image = tf_image.resize(images=image, size=[IMAGE_SIZE, IMAGE_SIZE])
    else:
        image = tf_image.decode_png(image, channels=3)
        image.set_shape([None, None, 3])
        image = tf_image.resize(images=image, size=[IMAGE_SIZE, IMAGE_SIZE])
    return image

ピクセルごとに番号が入れてあるトレーニング用画像を読むときは mask 値が True だと思うので(要確認)、 つまり、次のコードで読み込みが行われている(はず)。

image = tf_io.read_file(image_path)
image = tf_image.decode_png(image, channels=1)
image.set_shape([None, None, 1])
image = tf_image.resize(images=image, size=[IMAGE_SIZE, IMAGE_SIZE])

それでは先ほど生成した 1.png 画像をこのコードで読み取ったらどうなるか調べます。

def read_image(image_path, width, height):
  image = tf_io.read_file(image_path)
  image = tf_image.decode_png(image, channels=1)
  image.set_shape([None, None, 1])
  return tf_image.resize(images=image, size=[width, height])

img = read_image(f, width, height)
print(img)

実行してみます:

tf.Tensor(
[[[255.]
  [255.]
  [255.]
  [255.]
  [255.]]

 [[255.]
  [255.]
  [ 76.]
  [ 76.]
  [255.]]

 [[255.]
  [ 76.]
  [ 76.]
  [ 76.]
  [255.]]

 [[255.]
  [ 76.]
  [ 76.]
  [255.]
  [255.]]

 [[255.]
  [255.]
  [255.]
  [255.]
  [255.]]], shape=(5, 5, 1), dtype=float32)

白色で塗ったピクセルは 255.0 赤色は 76.0 の値になりました。 (これは意図したデータ形式ではない。)

パレットを削除してグレースケール画像として保存

def remove_palette(image_path):
  raw_annotation = np.array(Image.open(image_path))
  return Image.fromarray(raw_annotation.astype(dtype=np.uint8))

パレットを削除・・・というか画像の配列を取り出して、その配列から画像を生成しているだけですが、 そうやってつくった画像を次のようにして普通に保存すれば結果的にパレットはなくなる。

remove_palette("1.png").save("2.png")

パレットがなくなった 2.png 画像を read_image 関数に渡して内容を見てみる:

img = read_image("2.png", 5, 5)
print(img)

img は次のようになる:

tf.Tensor(
[[[0.]
  [0.]
  [0.]
  [0.]
  [0.]]

 [[0.]
  [0.]
  [1.]
  [1.]
  [0.]]

 [[0.]
  [1.]
  [1.]
  [1.]
  [0.]]

 [[0.]
  [1.]
  [1.]
  [0.]
  [0.]]

 [[0.]
  [0.]
  [0.]
  [0.]
  [0.]]], shape=(5, 5, 1), dtype=float32)

パンピクセル以外(背景)が 0.0 で パンピクセルが 1.0 になっている。 (これがここでは意図したデータ形式)

ちなみにパレットを削除して保存した画像 2.png のモードを確認してみます。

img_mode = Image.open("2.png").mode
print(img_mode) # -> L

(パレットモードの P ではなく) L (グレースケールモード) になっています。

まとめ

トレーニング用画像データの作り方として、まずパレットモード画像を用意(というか、ラベリング用ツールの出力が画像形式がパレットモードの画像なのだと思う、おそらく)し、そこからパレットを削除してグレースケール画像にする。 そうやってつくったグレースケール画像をトレーニング用コードに渡す・・・ という手順を鵜呑みにして作業していたが、結局トレーニング用コードの欲しかったデータは、 ピクセルごとに 番号がセットされたテンソル(ここでは 0, 1 の番号)だったということ。

通常のワークフローではこれが make sense だと思うが、 わざわざ PNG 画像をつくる必要はないわけだ。

もちろん、入力データ形式を変えたらそれにあわせた読み込みコードに変更する必要が生じる。 だったら、トレーニング用コードが期待する入力データ形式に合わせた方が楽、ということはある。

最後に今回使ったコード全体を掲載する: (なお必要なライブラリは入っているものとする。)

from PIL import Image
import numpy as np
from tensorflow import image as tf_image
from tensorflow import io as tf_io

def create_image(pixel_values, palette, width, height):
  img = Image.new("P", (width, height))
  img.putpalette(palette)
  
  for y in range(height):
    for x in range(width):
      pxIndex = y * height + x
      pxValue = pixel_values[pxIndex]
      img.putpixel( (x,y), pxValue )

  return img

def read_image(image_path, width, height):
  image = tf_io.read_file(image_path)
  image = tf_image.decode_png(image, channels=1)
  image.set_shape([None, None, 1])
  return tf_image.resize(images=image, size=[width, height])

def remove_palette(image_path):
  raw_annotation = np.array(Image.open(image_path))
  return Image.fromarray(raw_annotation.astype(dtype=np.uint8))


my_pixel_values = [
  0,0,0,0,0,
  0,0,1,1,0,
  0,1,1,1,0,
  0,1,1,0,0,
  0,0,0,0,0  
]

my_palette = [
  255,255,255,
  255,0,0
]

width = 5
height = 5

file1 = "1.png"
file2 = "2.png"

create_image(my_pixel_values, my_palette, width, height).save(file1, "PNG")

img1 = read_image(file1, width, height)
print(img1)

remove_palette(file1).save(file2, "PNG")

img2 = read_image(file2, width, height)
print(img2)

使用したバージョン:

$ python --version
Python 3.9.6

以上です。