需要のないページ

プログラミングや趣味や。

OpenCVでイメージウィンドウを閉じられるようにする

能書き

OpenCVでイメージを表示した際に、面倒な落とし穴がある。
クローズボタンを押してしまうと処理がハングアップする場合があることだ。

上記回答ではプロパティを確認する方法が紹介されている。
ただ、毎度似たような処理を書くのは面倒に思える。

記事の目的
  • ウィンドウの可視性プロパティを取得する際の挙動を追うこと
  • もっと簡単かつ安全にウィンドウを表示する方法がないか検討すること

ドキュメントにはどのように書いてある?

この手の問題は普通はドキュメントに明記してあるものだ。

12436288584_94d6bc46d2_b.jpg
Provides parameters of a window. The function getWindowProperty returns properties of a window.
Parameters ...
See also setWindowProperty

書いてねぇ。
setWindowPropertyにもWindowPropertyFlagsにも目的の情報はなかった。

仕方ないので実装を覗く

cv::getWindowProperty @highgui/src/window.cpp#L90
cvGetWindowPropertyに主だった処理を委託している。

cvGetWindowProperty @highgui/src/window.cpp#L236
関連した部分を引用する。


  /* return -1 if error */
  CV_IMPL double cvGetWindowProperty(const char* name, int prop_id)
  {
      if (!name)
          return -1;
  
      switch(prop_id)
      {
      ...
  
      case CV_WND_PROP_VISIBLE:
          #if defined (HAVE_QT)
              return cvGetPropVisible_QT(name);
          #else
              return -1;
          #endif
      break;
      default:
          return -1;
      }
  }
    

Qtに委託しているのか。そういうことをドキュメントに書いておけよ。

cvGetPropVisible_QT @highgui/src/window_QT.cpp#L141


  double cvGetPropVisible_QT(const char* name) {
      if (!guiMainThread)
          CV_Error( CV_StsNullPtr, "NULL guiReceiver (please create a window)" );
  
      double result = 0;
  
      QMetaObject::invokeMethod(guiMainThread,
          "getWindowVisible",
          autoBlockingConnection(),
          Q_RETURN_ARG(double, result),
          Q_ARG(QString, QString(name)));
  
      return result;
  }
    

まずGUIスレッドが起動していないといけないようだ。
こいつについても調査したのだが、話がそれるのでここでは省略する。*1

少なくとも画像を表示しているときはスレッドは起動している。
よって例外が生じたときは『画像は閉じられている』と認識しても良い。

次に調査すべきは getWindowVisible だ。*2

GuiReceiver::getWindowVisible @highgui/src/window_Qt.cpp#L924
このコードは随分シンプルだ。


  double GuiReceiver::getWindowVisible(QString name)
  {
      QPointer w = icvFindWindowByName(name);
  
      if (!w)
          return 0;
  
      return (double) w->isVisible();
  }
    

ここで isVisible の正体は QWidget::isVisible で、返り値は当然boolである。

可視性を調べたときの挙動まとめ

  • バックエンドがQtでなく、調査不能である際は -1 を返す。
  • guiMainThreadが起動されていないときは、CVError を投げる。
  • 該当する名前のウィンドウがないときは、0 を返す。
  • 該当する名前のウィンドウがある場合
    • 開いているときは 1 を返す。
    • 閉じているときは 0 を返す。

使いやすいラッパー関数を考える

単純に返り値や例外を制御するだけならば、次のようになるだろう。


  import cv2
  
  BackendError = type('BackendError', (Exception,), {})
  def is_visible(winname):
      try:
          ret = cv2.getWindowProperty(
              winname, cv2.WND_PROP_VISIBLE
          )
  
          if ret == -1:
              raise BackendError('Use Qt as backend to check whether window is visible or not.')
  
          return bool(ret)
  
      except cv2.error:
          return False
    

動作を確認するなかで、次の事実に気付いた。

  • 明示的にデストロイしないと、ウィンドウは『見える』扱い。
  • namedWindowを呼び出しただけでも『見える』扱い。

よって、is_visibleを単体で使うメリットは薄い。人間の直感に反する。

本当に使いやすいラッパー関数を考える

結局imshowをラップするのが分かりやすいのだろう。


  import cv2
  
  BackendError = type('BackendError', (Exception,), {})
  def _is_visible(winname):
      try:
          ret = cv2.getWindowProperty(
              winname, cv2.WND_PROP_VISIBLE
          )
  
          if ret == -1:
              raise BackendError('Use Qt as backend to check whether window is visible or not.')
  
          return bool(ret)
  
      except cv2.error:
          return False
  
  
  ORD_ESCAPE = 0x1b
  def closeable_imshow(winname, img, *, break_key=ORD_ESCAPE):
      while True:
          cv2.imshow(winname, img)
          key = cv2.waitKey(10)
  
          if key == break_key:
              break
          if not _is_visible(winname):
              break
      
      cv2.destroyWindow(winname)
    

良い感じ。

成果物

ブレークするための条件をもうちょっと詳細に設定できるようにしてみた。

  • closeable_imshow(winname, img, *, break_keycode=0x1b)
  • Parameters
    • winname: str
    • img: numpy.ndarray
    • break_keycode (optional keyword)

      ウィンドウを閉じるためのアスキーコードを指定する。コードの指定方法は整数値でも文字でも良いが、リストやタプルを用いて複数指定する場合はアスキーコードでなくてはならない。また'all'(全てのキー入力に反応)や'nothing'(全てのキータイプを無視)を指定することも出来る。

  • Returns
    • None
  • Raises
    • BackendError
      調査に充分なバックエンド(Qt)がないとき
    • ValueError
      break_keycodeを文字列で指定したが、その構成が誤っているとき
    • TypeError
      break_keycodeの指定が誤っているとき

ソースコードはGistに上げておいた。cv_util.py · GitHub

*1:後日記事に起こすかもしれないし、そうでないかもしれない。

*2:getWindowVisibleを励起する記法はQt独特だが、結局ユーザから見た動作は result = guiMainThread.getWindowVisible(name); と同じだ。

/* コードブロック */