経緯
機械学習アルゴリズムを試すために,OpenCVで画像を読み込むプログラムを書きました。
画像サイズは$512\times 512$ですが,必要な$256\times 256$の領域のみを切り出し,その外側は使いません。
画像は約14万枚あり,このすべてに同じように読み込んでから切り出し処理を行います。
以下に状況を再現できる程度に簡単化したコードを示します。(大量のメモリを消費するので実行の際は注意してください)
import numpy as np
import cv2
images = []
for i in range(140000):
# 画像読み込み(のつもり)
# 本当はcv2.imread()
img = np.ones((512,512,3), dtype=np.uint8)
img = img[:256, :256] # 256x256の領域を切り出す
images.append(img)
cv2.imread()
で画像を読み込むと,符号なし8bit整数型のNumPy配列が返されます。
カラー画像なので,14万枚に対して切り出し処理まで行った場合,単純計算で
256\times 256\times 3\times 140000\;{\rm [B]} = 25.6\;{\rm [GB]}
の容量になります。
計算機のメモリは64GBなので十分余裕なはずですが・・・
足りませんでした
原因(?)
NumPyを分かっている人なら当然の話かもしれません。
NumPy配列(ndarray)に対してスライスで一部を切り出すと,新しい配列が生成されるわけではなく,viewと呼ばれる種類のndarrayが返されます。
viewはもとの配列とメモリ上の同じ領域を参照しているので,viewを変更するともとの配列の値も変更されます。
a = np.array([1,2,3,4,5])
b = a[1:4]
print(a) # => array([1,2,3,4,5])
print(b) # => array([2,3,4])
b[1] = 100
print(a) # => array([1,2,100,4,5])
さて,ここで最初のコードについて考えます。
画像から領域を切り出す際にスライスを使っていました。
つまり,リストimages
にはviewがappend
されていきます。
この時,切り出した外側の領域はどうなったのでしょうか?
通常,参照されなくなった領域はGCによりそのうち開放されますが,スライスを用いた場合,切り出されなかった領域もメモリに残り続けるのではないかと考えました。
そこで以下のようにしてviewをコピーし新たな配列を生成したところ,メモリは余裕で足りました。消費量も大体予想通りになりました。
import numpy as np
import cv2
images = []
for i in range(140000):
# 画像読み込み(のつもり)
# 本当はcv2.imread()
img = np.ones((512,512,3), dtype=np.uint8)
img = img[:256, :256] # 256x256の領域を切り出す
img = np.array(img) # コピーを生成する
images.append(img)
必要な領域のみをコピーしたことで,元の$512\times 512\times 3$の配列は完全にどこからも参照されなくなり,開放されているのだと思われます。
おわりに
NumPyのviewについての勉強になりました。
原因については,スライス後にコピーしたら解決したという事実から推測したものなので,誤っている可能性もあります。
何かありましたらご指摘いただけると幸いです。