numpy.ndarray.astype(numpy.uint8)の落とし穴【Python/Numpy】

float64int32ndarrayuint8に型変換した時に自動的に近い境界値に丸めてくれると思っていたのですがそうではないようです。

OpenCVで画像を読み込んでNumPyで弄った後に符号無し8ビット整数に変換して書き出そうとした時に躓いたのでメモ。

概要

元の型より精度が低い整数型への型変換numpy.ndarray.astype(dtype)は変換前にnumpy.ndarray.clip(min, max)をすべきです。

例えば、a.astype(np.uint8)する時は以下のようにa.clip(0, 255)で値を先に制限します。

>>> import numpy as np
>>> a = np.array([-1, 0, 255, 256])
# こうではなくて
>>> a.astype(np.uint8) 
array([255,   0, 255,   0], dtype=uint8)
# こうする
>>> a.clip(0, 255).astype(np.uint8) 
array([  0,   0, 255, 255], dtype=uint8)

以下は詳細です。

環境

対話環境で確認しました。

$ python
Python 3.7.7 (tags/v3.7.7:d7c567b08f, Mar 10 2020, 10:41:24) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> np.__version__
'1.18.3'

numpy.ndarray.astype(numpy.uint8)とは

読んで字の如く、型変換のメソッドです。

numpy.ndarrayに対して型名を引数にして呼ぶと、配列が指定した型に変換されて返ってきます。

>>> a = np.zeros((32, 32, 3), dtype=np.float16)
>>> a.dtype
dtype('float16')
>>> a.astype(np.uint8).dtype
dtype('uint8')

落とし穴

uint8では0~255の数値を表せますが、その範囲外の数値を変換した時に想像とは違う数値変換がされます。

範囲内の場合

0~255の範囲内の場合は想像した通りの変換、つまり何も変換されません。

>>> a = np.array([0, 1, 10, 64, 150, 255])
>>> a.astype(np.uint8)
array([  0,   1,  10,  64, 150, 255], dtype=uint8)

範囲外の場合

一方、範囲外では少なくとも想像とは違う結果になりました。変換は以下の通りです。

>>> b = np.array([256, 511, 512, 32767, 32768, -1, -128, -127, -32768, -32767])
>>> b.astype(np.uint8)
array([  0, 255,   0, 255,   0, 255, 128, 129,   0,   1], dtype=uint8)

浮動小数点数からの変換は以下の通り。

>>> c = np.array([256.1, 511.3, 512.2, 32767.8, 32768.1, -1.2, -128.0, -127.9, -32768.5, -32767.4])
>>> c.astype(np.uint8)
array([  0, 255,   0, 255,   0, 255, 128, 129,   0,   1], dtype=uint8)

想像では、256 -> 255-1 -> 0のように近い方の整数に丸めて欲しかったのですが、どうやら実際は少数部分を切り捨てた後に十分なビット数で符号付き整数に変換した後に下位8ビットを取る変換をしているようです。

なので、256 -> 0000 0001 0000 0000 -> 0000 0000 -> 0-1 -> 1111 1111 -> 255-128 -> 1000 0000 -> 128となっています。

解決法

意図しない変換がされる前に先に近い方の整数に丸める変換してしまいましょう。

近い方の整数に丸めるには、numpy.clip(a, a_min, a_max)numpy.ndarray.clip(min, max)を使います。前者と後者の違いは呼び出し方だけです。

>>> b = np.array([256, 511, 512, 32767, 32768, -1, -128, -127, -32768, -32767])
# numpyから呼び出して引数に指定する
>>> np.clip(b, 0, 255).astype(np.uint8)
array([255, 255, 255, 255, 255,   0,   0,   0,   0,   0], dtype=uint8)
# numpy.ndarrayから呼び出す
>>> b.clip(0, 255).astype(np.uint8)
array([255, 255, 255, 255, 255,   0,   0,   0,   0,   0], dtype=uint8)

浮動小数点数に対しても想像通りの変換がされるようになりました。

>>> c = np.array([256.1, 511.3, 512.2, 32767.8, 32768.1, -1.2, -128.0, -127.9, -32768.5, -32767.4])
>>> np.clip(c, 0, 255).astype(np.uint8) 
array([255, 255, 255, 255, 255,   0,   0,   0,   0,   0], dtype=uint8)
>>> c.clip(0, 255).astype(np.uint8) 
array([255, 255, 255, 255, 255,   0,   0,   0,   0,   0], dtype=uint8)

警告も無しに気軽に変換してくれるので、諸々の事は勝手にやってくれると思っていたのですが、実は結構気に掛けねばならない事もあるようです。

ともかくこれで、OpenCVで出力した画像が白飛びしたりしなくなりました。めでたしめでたし。

参考文献

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です