PythonのtkinterとOpenCVを繋ごう
このところ、思い出したようにまたPythonからOpenCVを触っている。LL言語だとメモリの解放忘れなどを気にする必要がなく、至極お手軽だ。プログラミングしていても快適。
・・・だけど、ひとつ困ったことがある。普段使い慣れたTkinterでOpenCVで加工した画像を表示しようとすると、はてな?簡単にはできない。あれこれトライしてみたが、結局TkinterとOpenCVのPython拡張モジュールを少々手直しすることになった。途中経過も含めてメモしておく。
ステップ1 PILに手伝って貰う
OpenCVのクックブックによると、PIL(Python Imaging Library)となら画像の相互交換ができるようだ。そこで、次のようなコードを書いて試してみた。
import Tkinter as Tk import tkFileDialog as FileDlg from PIL import Image, ImageTk from cv import * class PhotoFrame(Tk.Label): def load(self, img_file): self.img = LoadImage(img_file, CV_LOAD_IMAGE_COLOR) pil_img = Image.fromstring("RGB", GetSize(self.img), self.img.tostring()) self.pho = ImageTk.PhotoImage(pil_img) self.configure(image=self.pho) def change(self, event): filename = FileDlg.askopenfilename() if filename: self.load(filename) def __init__(self, img_file, autoloop=True, master=None): Tk.Label.__init__(self, master) self.master.title('My Photo Frame') self.load(img_file) self.pack() self.bind_all('<1>', self.change) if autoloop: self.mainloop() app = PhotoFrame('apple.jpg')
うっ! 赤くて美味しそうだったリンゴが、真っ青で気味悪いリンゴになってしまった。
なるほど、OpenCVのIplImage.tostring()が出力するrawイメージはBGRの順になっているんだね。じゃあ、BGRをRGBに並べ替えれば良い筈だけど・・・ちょっと面倒くさいな。
def flip_color(self): for y in xrange(self.img.height): for x in xrange(self.img.width): (b, g, r, a) = Get2D(self.img, y, x) Set2D(self.img, y, x, (r, g, b, a))
ステップ2 OpenCVの拡張モジュールにIplImage.toppm()を追加する
Tkinterに画像を渡す度に色変換をする必要があるなら、一層のことOpenCV拡張モジュールに新たなメソッドを付け加えてしまっても良いような。ついでにPILを使わなくても良い方法は? ・・・ TkinterのPhotoImageはPPM形式のバイナリ文字列を受け取ることができるようだね。これを利用しよう!!!
/* cv.cpp - OpenCVのPython拡張モジュールのソースコード */ /* 画像をPPM形式のバイナリ文字列に変換し出力する */ static PyObject *iplimage_toppm(PyObject *self, PyObject *args) { const int MAX_HEADER = 25; iplimage_t *pc = (iplimage_t*)self; IplImage *i; if (!convert_to_IplImage(self, &i, "self")) return NULL; if (i == NULL) return NULL; if (i->depth != IPL_DEPTH_8U && i->depth == IPL_DEPTH_8S) { return (PyObject*)failmsg("Unrecognised depth %d", i->depth); } int l, h; char* s; if (i->nChannels == 1) { l = i->width * i->height; s = new char[l+MAX_HEADER]; h = sprintf(s, "P5\n%d %d\n255\n", i->width, i->height); char* src = i->imageData; char* dst = s + h; for (int row = 0; row < i->height; row++) { memcpy(dst, src, i->width); src += i->widthStep; dst += i->width; } l += h; } else if (i->nChannels == 3) { l = 3*i->width * i->height; s = new char[l+MAX_HEADER]; h = sprintf(s, "P6\n%d %d\n255\n", i->width, i->height); char* src = i->imageData; char* dst = s + h; for (int row = 0; row < i->height; row++) { for (int p = 0; p < 3*i->width; p += 3) { dst[p+0] = src[p+2]; dst[p+1] = src[p+1]; dst[p+2] = src[p+0]; } src += i->widthStep; dst += 3*i->width; } l += h; } else { return (PyObject*)failmsg("Unrecognised nChannels %d", i->nChannels); } PyObject *r = PyString_FromStringAndSize(s, l); delete s; return r; } static struct PyMethodDef iplimage_methods[] = { {"tostring", iplimage_tostring, METH_VARARGS}, {"toppm", iplimage_toppm, METH_VARARGS}, {NULL, NULL} };
toppm()を使ってPythonのコードを書き直すと、ちょっとはスッキリしたかな。
def load(self, img_file): self.img = LoadImage(img_file, CV_LOAD_IMAGE_COLOR) self.pho = Tk.PhotoImage(data=self.img.toppm()) self.configure(image=self.pho)
で、実行すると。え〜〜っ、エラーする。
あらまっ、TkinterがPPMバイナリ文字列をユニコードとして扱ってるみたいだ。仕方がないので、こちらもちょこっと修正する。
/* _tkinter.c - Python同梱の_tkinter.pydのソースコード */ static Tcl_Obj* AsObj(PyObject *value) { Tcl_Obj *result; if (PyString_Check(value)) /* ByteArrayに差し替え return Tcl_NewStringObj(PyString_AS_STRING(value), PyString_GET_SIZE(value)); */ return Tcl_NewByteArrayObj(PyString_AS_STRING(value), PyString_GET_SIZE(value)); --以下略--