Qtで始める画像処理

#ご挨拶

Qt Advent Calendar 2018での3回目の投稿となりますKATO Kanryuです。
よろしくお願いします。

##まず宣伝

- 世界最速の画像ビューアー、[QuickViewer](https://github.com/kanryu/quickviewer)をQtで作りました。(本稿)
- [QActionManager](https://github.com/kanryu/qactionmanager)
  - アプリにQt Creatorと同等のキーボード／マウスショートカットのカスタマイズ機能を提供します
  - マウスカスタマイズ機能はオリジナル
- [QLanguageSelector](https://github.com/kanryu/qlanguageselector)
    - 言語切り替えUI（メニュー)を自動生成
    - Qtアプリを再ビルドなしに翻訳言語を増やせるようになります
    - Qtアプリをテキストエディタだけでリアルタイムに翻訳できるようにします
- [QFullscreenFrame](https://github.com/kanryu/qfullscreenframe)
  - アプリをフルスクリーン表示させたときに、メインメニューやツールバーをスライド表示したいときがあると思います。それを実現するやつです。イベントハンドラを細かく設定することになるのでC++11推奨
- [QNamedPipe](https://github.com/kanryu/qnamedpipe)
  - アプリケーションを複数起動防止しつつ、2つ目以降のプロセスの起動オプションを1つ目のプロセスに引き渡したりする処理って単純ながら実装が面倒なものです。この問題を各OSのNamedPipeを使ってシンプルに解決するライブラリです。
  - 本家のQNamedPipeと異なり、QNetworkなどのコンポーネントは不要です。

##Qtにおける画像の位置づけ

GUIアプリにおいて、画像を表示するという機能は必要不可欠です。QToolBarにはツールチップの画像を貼り付けたボタンが並んでいますし、そもそもアプリケーションのアイコンを設定しておかないと、ユーザーはどのプログラムを実行していいかわからなくなるでしょう。メッセージボックスを表示するときも、それが情報提供なのか、エラーメッセージなのか、表示するときに指定することができますね。

また、ホスト内のローカルファイルや、インターネットからダウンロードしてきた画像をアプリの中で表示したりすることもあるでしょう。私が開発しているQuickViewerなどは画像ビューアなので、画像を表示するということが機能の全てみたいなものですから、様々な扱い方をしています。ご興味があればソースコードをご覧ください。

##Qtにおける画像のオブジェクト

これは主に3種類存在します。

**QImage**

> デバイスから独立した画像データ(DIB、ビットマップとも言う)を取り扱います。実体はピクセルの並びを連続したメモリバッファで表した二次元のバッファです。QImage->bits()メソッドでメモリバッファの開始ポインタを取得でき、QImageの外部から自由に書き換えることができます。
> ビットマップ上のピクセル座標(x,y)の開始ポインタは以下の計算式で特定できます。 

```C++
QImage->bits() + x*QImage->depth()/8 + y*QImage->bytesPerLine()
```

**QPixmap**

> 描画デバイス上の画像データ(DDB、フレームバッファとも言う)を取り扱います。実体は、Windows XPまでの2Dレンダリングを行っていた頃はビデオカード上のフレームバッファ、Windows Vista以降の3Dレンダリングを行っている現在ではGPU上のフレームバッファまたはテクスチャバッファを指していることが想定されます。要するにメインメモリ上にデータの実体がある保証がありません。そのかわり、それぞれの動作OSに合わせて高速な描画が行える状態になっています。

**QIcon**

> Qtの各Widgetで小さな画像を取り扱うときによく使用されている画像オブジェクトです。実体はQPixmapとほぼ同じなのですが、同じ図案の画像をサイズ違いで複数もたせることができ、描画サイズに合わせて適切な大きさの画像が取り出され描画されるという違いがあります。Windowsにおけるicn形式、OS Xにおけるicns形式のように、アイコン用の専用の画像フォーマットが用意されているOSもあります。

## 使い分け方

3種類あると急に言われても、どう扱っていいかわからないよ、という方は多いのではないでしょうか。私も実際にアプリを開発しながら試行錯誤をして理解していきました。

アプリケーションがローカルの画像ファイルを読み込んだとき、それはQImageのインスタンスになります。QImageはcopyableなオブジェクトなので、QImage*にしなくともauto変数としてそのまま書くことができますし、copy処理はdeepcopyではなくshallowcopyなのでメソッドからQImageを返すときもポインタ返しなどを意識する必要なくそのまま返しても大丈夫です。Qtは素晴らしいですね！

各Widgetが画像を表示する場合、たいていQPixmapで受け付けるAPIになっていることが多いと思います。この段階でQImageからQPixmapに変換してください。通常QPixmap::fromImage()を呼び出せば十分です。

QToolBar等で固定の画像をたくさん登録するとき、通常アプリのリソースに登録することになるでしょう。リソースから画像データを読み込むときにQIconのインスタンスとして取り出すこともでき、必要に応じてQImageと使い分けることになります。リソースに登録された画像は、Qt Designer(現在はQt Creatorのデザイン画面)のuiファイル上で指定するとリソースから取り出してWidgetに登録するソースコードが自動生成されるので、意識することなく使っているかもしれません。

逆にアプリの画像キャプチャ、あるいはPCのデスクトップ自体の画像キャプチャを行いたい場合もあるでしょう。その場合、それぞれのWidgetからQPixmapのインスタンスとして取得できるので、ここを起点にQPixmap->toImage()などでQImageのインスタンスに変換してから画像ファイルに保存すればよいです。

## QImageの初期化方法（画像を読み込む方法)

これまでで、QPixmap、QIconは基本的にQImageから変換できるよ、QPixmapからQImageにも逆変換できるよ、と学びました。では画像ファイルからQImageとして読み込むにはどうすればいいでしょうか。

通常の用途では、画像のファイル名を引数としたコンストラクタを普通に呼び出せば十分だと思います。

```C++
QImage(const QString &fileName, const char *format = nullptr)
```

format引数は通常設定する必要はありません。このパラメータは画像読み込みプラグインを指定するときに使用します。指定しない場合は自動判定されます。

Qtアプリを通常の動的リンク方式(要するにQtCore.dllなどがバンドルされる状態)でビルドすると、各種画像形式用のプラグインがimageformatsディレクトリに入った状態でデプロイされるでしょう。Qt本体が組み込みでサポートしている形式も含めると、主にこれらの画像形式をサポートした状態です。

- bmp, JPEG, PNG, GIF, icn(icns), SVG, TGA, TIFF, wbmp, WebP

しかしあなたがもしこれらのサポート状況では物足りず、自力でプラグインを追加したり、更にはJPEGローダーのlibjpeg-turbo版を用意してQt本体のローダーと差し替えたりなどの奇特なことを始めたときは、QImageReaderの使用を検討してみてください。

QImageReaderクラスは上記のQImageのコンストラクタ内で内部的に使用されているクラスで、画像読み込み用プラグインをより細かく制御することができます。実際に画像を読み込む前に画像形式や画像の大きさ、アニメーションするかなどを判定することができるので、画像ビューアを開発するときにはそういう情報がありがたいところです。

QuickViewerにおける画像読み込み部分の実装はこちらです。内部クラスをゴリゴリ使っているのであまり参考にならないかもしれませんが。
https://github.com/kanryu/quickviewer/blob/master/QuickViewer/src/models/volumemanager.cpp

##内部ビットマップの形式

QImageのインスタンスは既に2次元のメモリバッファですので、元の画像ファイルの情報は消えています。しかしながら、メモリバッファで表現される画像ピクセルのフォーマットは非常にたくさん存在しており、現在のOSで正常に表示できない場合があります。

QImageの内部ピクセルフォーマットの一覧：
http://doc.qt.io/qt-5/qimage.html#Format-enum

従って、各OSが確実に扱えるであろうピクセル形式に予め変換してしまうことがおすすめです。

- QImage::Format_RGB888
- QImage::Format_ARGB32

のいずれかにQImage->convertToFormat()を呼び出して変換をかけてしまいましょう。

QImageがサポートしているピクセルフォーマットは他にもたくさんあり、それぞれ利点／欠点はありますが、簡単に考えたいならばこの２つに絞り込んでしまうのが簡単です。QImage::Format_ARGB32は透過率を制御するα値を持っており、QImage::Format_RGB888はそうではないという違いがあります。

QuickViewerにおけるピクセルフォーマットの変換例：
https://github.com/kanryu/quickviewer/blob/master/zimg/qzimg.cpp

##アニメーションの制御

画像形式の中にはGIFやPNGのように、アニメーションをサポートしている場合があります。QImageはアニメーションをサポートしていないわけですが、ではQtアプリで画像のアニメーション再生を行いたい場合はどうすればいいでしょうか？

その場合、QImageに代わりQMovieを使用します。残念ながらQMovieはcopyableではありませんので、ポインタ形式で扱ったほうが良さそうです。

QMovieは自前でタイムスケジュールを持っており、finishded()、frameChanged(int)などのSIGNALが自動的に発行されます。そこで、アニメーションを表示したいWidgetでそれらのイベントハンドラを書きましょう。そしてQMovie->start()を呼び出すとこの時点からアニメーションが開始されます。

frameChanged(int)が呼び出されると、次の画像フレームに差し替えるタイミングが来たことを意味しますので、QMovie->jumpToFrame(frameNumber)、QMovie->currentPixmap()を順に呼び出せば今表示すべきQPixmapが取得できます。frameNumberはイベントが発行されたときに渡されます。

finished()が呼び出されると、アニメーションが終了したことを意味します。通常ループアニメーションすることが多いので、QMovie->jumpToFrame(0)を呼び出してからQMovie->start()を呼び出して最初からアニメーションをやり直しましょう。

実装例：
https://github.com/kanryu/quickviewer/blob/master/QuickViewer/src/models/pagecontent.cpp

##まとめ

- 画像は読み込むとQImageになる。通常はコンストラクタで生成すればよい。
- 必要に応じてQPixmapやQIconに変換する
- アニメーション画像はQMovieで読み込む
- バグを避けたいならピクセルフォーマットの変換も忘れずに

終わり。