2016/11/26(土) [n年前の日記]
#1 [python] 塗りつぶし処理(floodfill)について
お絵かきアプリでよくある、閉じた範囲の中でクリックすると指定色で塗り潰す処理を、Python + PySide (+ PIL(Pillow)) を使って試したい。どうやら、その手の処理を、
_Flood fill
と呼ぶらしいけど。
Ruby + DXRuby を使った版なら、昔書いたことがあったようで。 _2015/12/24の日記 にメモしてあった。Flood fill の一つ、Scanline Seed Fill を試していた模様。
コレを Python + PySide に移植してもいいのだけど、もしかしてこの手の処理は既に用意されているんじゃないか、と思ってググっていたら、どうやら PIL (Pillow) に、そのものズバリの floodfill というメソッドがあるようで。ただ、文書化はされていない模様。
_python - How to flood-fill part of a bitmap enclosed by a black border with my chosen color? - Stack Overflow
_floodfill_test.py
以下の画像を渡してみる。
以下の結果が得られた。
塗りつぶしができてる。
3回塗り潰してみたけど、手元の環境で時間を測ってみたら 0.433000秒だった。そんなに遅くはない、かな…。
ググってみたら、以下のページが。
_python-imaging 1.1.7 - namespace_p_i_l_1_1_image_draw.html
この floodfill() は Python で書かれてる、ということかな…。上下左右を1ドットずつ見ていく、という比較的シンプルなアルゴリズムっぽい。
Ruby + DXRuby を使った版なら、昔書いたことがあったようで。 _2015/12/24の日記 にメモしてあった。Flood fill の一つ、Scanline Seed Fill を試していた模様。
コレを Python + PySide に移植してもいいのだけど、もしかしてこの手の処理は既に用意されているんじゃないか、と思ってググっていたら、どうやら PIL (Pillow) に、そのものズバリの floodfill というメソッドがあるようで。ただ、文書化はされていない模様。
_python - How to flood-fill part of a bitmap enclosed by a black border with my chosen color? - Stack Overflow
>>> from PIL import Image, ImageDraw
>>> help(ImageDraw.floodfill)
Help on function floodfill in module PIL.ImageDraw:
floodfill(image, xy, value, border=None)
(experimental) Fills a bounded region with a given color.
:param image: Target image.
:param xy: Seed position (a 2-item coordinate tuple).
:param value: Fill color.
:param border: Optional border value. If given, the region consists of
pixels with a color different from the border color. If not given,
the region consists of pixels having the same color as the seed
pixel.
たしかに入っているっぽい。だったら、コレを使わせてもらおうか…。_floodfill_test.py
u"""
PIL(Pillow)のfloodfillを試す.
PIL(Pillow)には、文書化されていないが floodfill という関数があって、
塗りつぶし処理ができるらしい。
動作確認環境 : Windows10 x64 + Python 2.7.12 + Pillow 3.4.2
"""
from PIL import Image
from PIL import ImageDraw
import time
im = Image.open("./tmp_image.png")
start = time.time()
ImageDraw.floodfill(im, (64, 64), (255, 0, 0, 255), border=None)
ImageDraw.floodfill(im, (200, 240), (0, 255, 0, 255), border=None)
ImageDraw.floodfill(im, (480, 400), (0, 0, 255, 255), border=None)
etime = time.time() - start
print("%f" % etime)
im.save("./tmp_image_result.png")
以下の画像を渡してみる。
以下の結果が得られた。
塗りつぶしができてる。
3回塗り潰してみたけど、手元の環境で時間を測ってみたら 0.433000秒だった。そんなに遅くはない、かな…。
ググってみたら、以下のページが。
_python-imaging 1.1.7 - namespace_p_i_l_1_1_image_draw.html
この floodfill() は Python で書かれてる、ということかな…。上下左右を1ドットずつ見ていく、という比較的シンプルなアルゴリズムっぽい。
◎ QImageからPIL Imageへの変換。 :
塗りつぶしは PIL の floodfill() を利用させてもらうとして。PySide上で使おうとするなら…。
PIL Image から QImage への変換は、PIL の ImageQt を使えばできるはず、ということになってるけど。逆はどうすれば…。
ググったら以下のページが。
_setting python, eclipse, opencv and QT in linux: Converting from qimage to pil image
_PIL.Image と PyQt4.QtGui.QImageの相互変換 - None is None is None
バッファとやらを使って、一旦画像ファイルにして記録してから、また画像ファイルとして読み込む、ということをすれば変換できなくもないらしい。
ただ、PIL の ImageQt にも似たような処理をしてる部分があるような…。
_Pillow/ImageQt.py at master - python-pillow/Pillow
fromqimage() てのがあるけど、やってることが似ている…。が、pil_im = ImageQt.fromqimage(qim) とかやってみたらエラーが。
アレコレ試してたら、そもそも PySide + Pillow の組み合わせで ImageQt を使うと Python が不正終了・crash することに気が付いた。PIL Image から QImage への変換すらできないのでは困る。コレは別項でメモしたほうがいいかな…。
- QImage から PIL Image への変換。
- PIL Image から QImage への変換。
PIL Image から QImage への変換は、PIL の ImageQt を使えばできるはず、ということになってるけど。逆はどうすれば…。
ググったら以下のページが。
_setting python, eclipse, opencv and QT in linux: Converting from qimage to pil image
_PIL.Image と PyQt4.QtGui.QImageの相互変換 - None is None is None
バッファとやらを使って、一旦画像ファイルにして記録してから、また画像ファイルとして読み込む、ということをすれば変換できなくもないらしい。
ただ、PIL の ImageQt にも似たような処理をしてる部分があるような…。
_Pillow/ImageQt.py at master - python-pillow/Pillow
fromqimage() てのがあるけど、やってることが似ている…。が、pil_im = ImageQt.fromqimage(qim) とかやってみたらエラーが。
アレコレ試してたら、そもそも PySide + Pillow の組み合わせで ImageQt を使うと Python が不正終了・crash することに気が付いた。PIL Image から QImage への変換すらできないのでは困る。コレは別項でメモしたほうがいいかな…。
[ ツッコむ ]
#2 [python] Windows + PySide + Pillow(PIL)の組み合わせでImageQtの動作が怪しい
Windows10 x64 + Python 2.7.12 + PySide 1.2.4 + Pillow(PIL) 3.4.2 の環境で、Pillow に入ってる ImageQt を使って PIL Image を PySide の QImage に変換しようとしていたのだけど、Python が強制終了・crashしまくることに気づいて悩んだり。
ちなみに、PySide 1.2.4 と PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe を両方インストールしてある環境。
例えば、以下のソースと画像を使って実験すると…。
_imageqt_test.py
_tmp_grayscale.png
_tmp_indexed.png
_tmp_rgb.png
_tmp_rgba.png
やってることは、
コレを実行すると、Python が死ぬ。
これがまたよく分からんのだけど、PySide を使わずに PyQt4 を使うと Python が死なずに動いちゃう。
最初のあたりの use_pyside = True を use_pyside = False にすると、PyQt4 を使って動かせる。
各行をコメントアウトして動作確認してみたけど、どうも QPixmap.fromImage() を呼んだ時点で Python が死んでる感じ。QPixmap.fromImage() をコメントアウトすると Python が死なないわけで。
すると ImageQt が QImage を返せていないのか、おかしな QImage を QPixmap.fromImage() に渡してしまっているのか。と思ったけど、 _Pillow/ImageQt.py at master - python-pillow/Pillow を眺めると、ImageQt は QImage を継承してるわけで…。github上の ImageQt.py と、手元の ImageQt.py は、どちらも似たようなソースだし…。
アレ? 待てよ? 微妙に違うな。
_Pillow 2.9 causes PySide application to crash (whereas 2.8.2 does not) - Issue #1370 - python-pillow/Pillow
上記のバグ報告ページで、self.__data = im_data['data'] を追加してソレを使って QImage を生成すると落ちない、という報告が。手元の ImageQt.py はその行が入ってない。
手元の ImageQt.py にも試しに反映させてみたら、落ちなくなった。
しかし困ったな…。これでは他の環境にスクリプトを持っていった際に ImageQt が使えないではないか…。一々、「ImageQt.py をエディタで開いて該当行を修正してください」とお願いするのもアレだし…。
ちなみに、PySide 1.2.4 と PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe を両方インストールしてある環境。
例えば、以下のソースと画像を使って実験すると…。
_imageqt_test.py
u"""
Pillow(PIL)のImageQtについて動作確認.
動作確認 : Windows10 x64 + Python 2.7.12 + PySide 1.2.4 + Pillow 3.4.2
"""
use_pyside = True
import sys
if use_pyside:
# use PySide
# import PySide
# sys.modules['PyQt4'] = PySide
# ここでPyQt4を使わないようにしているはず…なのだが…
from PySide import QtGui
from PySide import QtCore
sys.modules['PyQt4.QtGui'] = QtGui
sys.modules['PyQt4.QtCore'] = QtCore
from PySide.QtCore import * # NOQA
from PySide.QtGui import * # NOQA
else:
# use PyQt4
from PyQt4.QtCore import * # NOQA
from PyQt4.QtGui import * # NOQA
from PIL import Image
from PIL import ImageQt
class MyWidget(QWidget):
def __init__(self, *argv, **keywords):
super(MyWidget, self).__init__(*argv, **keywords)
layout = QGridLayout()
lst = [
"./tmp_grayscale.png",
"./tmp_indexed.png",
"./tmp_rgb.png",
"./tmp_rgba.png"
]
for i, fpath in enumerate(lst):
im = Image.open(fpath)
old_im_mode = im.mode
if im.mode != "RGBA":
im = im.convert("RGBA")
print("im.mode %s -> %s" % (old_im_mode, im.mode))
qim1 = ImageQt.ImageQt(im)
qim2 = QImage(qim1)
pm = QPixmap.fromImage(qim2)
lbl = QLabel(old_im_mode, self)
lbl.setPixmap(pm)
layout.addWidget(lbl, i / 2, i % 2)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())
_tmp_grayscale.png
_tmp_indexed.png
_tmp_rgb.png
_tmp_rgba.png
やってることは、
- グレースケール、インデックスカラー、RGB、RGBA、の4つ画像を、PIL の Image.open() で読み込んで、
- PIL の ImageQt を使って PySide の QImage に変換してから、
- 得られた QImage を QPixmap に変換して、ラベルウィジェットを使って表示。
コレを実行すると、Python が死ぬ。

これがまたよく分からんのだけど、PySide を使わずに PyQt4 を使うと Python が死なずに動いちゃう。
最初のあたりの use_pyside = True を use_pyside = False にすると、PyQt4 を使って動かせる。
各行をコメントアウトして動作確認してみたけど、どうも QPixmap.fromImage() を呼んだ時点で Python が死んでる感じ。QPixmap.fromImage() をコメントアウトすると Python が死なないわけで。
すると ImageQt が QImage を返せていないのか、おかしな QImage を QPixmap.fromImage() に渡してしまっているのか。と思ったけど、 _Pillow/ImageQt.py at master - python-pillow/Pillow を眺めると、ImageQt は QImage を継承してるわけで…。github上の ImageQt.py と、手元の ImageQt.py は、どちらも似たようなソースだし…。
アレ? 待てよ? 微妙に違うな。
_Pillow 2.9 causes PySide application to crash (whereas 2.8.2 does not) - Issue #1370 - python-pillow/Pillow
上記のバグ報告ページで、self.__data = im_data['data'] を追加してソレを使って QImage を生成すると落ちない、という報告が。手元の ImageQt.py はその行が入ってない。
手元の ImageQt.py にも試しに反映させてみたら、落ちなくなった。
しかし困ったな…。これでは他の環境にスクリプトを持っていった際に ImageQt が使えないではないか…。一々、「ImageQt.py をエディタで開いて該当行を修正してください」とお願いするのもアレだし…。
[ ツッコむ ]
以上、1 日分です。


