PythonのtkinterとOpenCVを繋ごう

このところ、思い出したようにまたPythonからOpenCVを触っている。LL言語だとメモリの解放忘れなどを気にする必要がなく、至極お手軽だ。プログラミングしていても快適。

・・・だけど、ひとつ困ったことがある。普段使い慣れたTkinterでOpenCVで加工した画像を表示しようとすると、はてな?簡単にはできない。あれこれトライしてみたが、結局TkinterとOpenCVPython拡張モジュールを少々手直しすることになった。途中経過も含めてメモしておく。

ステップ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));
--以下略--

さてさて、これでやっと美味しそうな真っ赤なリンゴを表示することができた。めでたしめでたし。