省型旧形国電の残影を求めて

戦前型旧形国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。他のサイトでは得られない、筆者独自開発の写真補正ツールや補正技法についても情報提供しています。写真補正技法への質問はコメント欄へどうぞ

GIMP3 / Python サンプルプログラム: UI 上で cairo を使ってグラフを描く

 このサンプルプログラムは UI 上で与えたパラメータに応じて、UI (ダイアログ) 上にグラフを書くものです。cairo という2次元のベクトル画像を描く Gtk のライブラリを使っています。

 サンプルプログラムのダウンロードはこちらから。

 このサンプルプログラムをインストールすると、以下のメニューから起動できます。

メニュー位置

 起動すると以下のダイアログが表示されます。グラフエリアが表示されていますが、最初は、何もグラフは表示されていません。

ダイアログ

 ガンマ値を入力してみましょう。

ダイアログに係数を入力したところ

 

 ご覧のようにダイアログ上にガンマ値を入力すると、そのガンマカーブをダイアログ上にグラフとして表示します。なお、このプログラムは UI サンプルですので、実際に画像にガンマ補正を適用するところまでは行っていません。

 

---------------------

 では、コードのポイントを説明していきます。

・ライブラリの読み込み

from gi.repository import Gtk, GLib, Gegl, GObject, Gdk, cairo

 cairo を使ってグラフを描画するのに、Gdk と cairo というライブラリを使いますので、それを追加でインポートします。直接使うのは、cairo というライブラリですが、cairo は Gdk の下位ライブラリなので両方インポートします。

 

・ダイアログ上のグラフ描画領域の定義

-----------

        #---add graph---
        graph_box = \
            Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        box.add(graph_box)
        ydata = [0] * 256
        xdata = [0] * 256
        self.graph = GraphArea(ydata, xdata) # graph on Dialog
        graph_box.add(self.graph)
        label = Gtk.Label\
            (label="Tone curve graph ")
        label.set_xalign(1)
        graph_box.pack_start(label, True, True, 0)
        #---add end----

-----------

 描画を描くボックスを設定するのは、他のウィジェットと同じです。その上に、GraphArea(ydata, xdata) ユーザ定義クラスを使って、self.graph というグラフ描画領域を作成し、ボックスの上に載せます。graph の初期値としては、x, y として 0 を 256個並べたデータを読み込ませていますが、これは最初は軸のみ描いて、グラフデータ自体はすべて 0 のみでなので、グラフは描かないということです。

 そして説明のラベルをつけています。

 

・入力ボックスにデータを入力したあとの動作

-------------------

    def on_entry_changed(self, entry, id):
        text = entry.get_text()
        if is_num(text) == True:
            self.text_org = text
            self.label3.set_label("Input Result: " + text)
            pointsX=[i/255.0 for i in range(256)]
            pointsY=[p ** (1/ eval(text)) for p in pointsX]
            self.graph.update_data(pointsY, pointsX, Gimp.HistogramChannel.VALUE)
        else:
            Gimp.message("not Alphapet and number")
            entry.set_text(self.text_org)

-------------------

 入力ボックスである entry の状態が変化したイベントが発生したときに呼び出すユーザ定義関数です。

 is_num というのは入力値が数値もしくは数式として解釈できるかを判断するユーザ定義関数です(ここでは引用していませんが元コードをご覧ください)。数値もしくは数式として解釈できるなら、その入力を UI 上のラベルに出力し、それに基づきガンマ補正を掛けた場合の Y 値を計算します。そして、X, Y の値を与えて、self.graph.update_data というユーザ定義関数を呼び出します。

 この関数は、GraphArea クラスを更に呼び出して、グラフを新しい値に基づいて書き換えます。

 

・GraphArea クラスの定義

------------------

class GraphArea(Gtk.DrawingArea):
    def __init__(self, ydata, xdata):
        super().__init__()
        self.ch = Gimp.HistogramChannel.VALUE #channel color
        self.data = ydata
        self.xdata = xdata
        self.set_size_request(400, 300)
        self.connect("draw", self.on_draw)

    def update_data(self, new_data, new_xdata, ch):
        self.data = new_data
        self.xdata = new_xdata
        self.ch = ch # channel name
        self.queue_draw()

    def on_draw(self, widget, cr):
        width = self.get_allocated_width()
        height = self.get_allocated_height()

        cr.set_source_rgb(0, 0, 0)  # background: black
        cr.paint()

        # draw axes
        cr.set_source_rgb(1, 1, 1)
        cr.set_line_width(2)
        cr.move_to(40, 10)
        cr.line_to(40, height - 20)
        cr.line_to(width - 10, height - 20)
        cr.stroke()

        if not self.data:
            return

        # draw data
        max_val = max(self.data)
        step_x = (width - 60) / (len(self.data) - 1)
        scale_y = (height - 40) / max_val

        if self.ch == Gimp.HistogramChannel.VALUE:
            cr.set_source_rgb(1.0, 1.0, 0.0)  # line color: Y
        elif self.ch == Gimp.HistogramChannel.RED:
            cr.set_source_rgb(1.0, 0.0, 0.0)  # line color: R
        elif self.ch == Gimp.HistogramChannel.GREEN:
            cr.set_source_rgb(0.0, 1.0, 0.0)  # line color: G
        elif self.ch == Gimp.HistogramChannel.BLUE:
            cr.set_source_rgb(0.0, 0.0, 1.0)  # line color: B
        cr.set_line_width(2)
        xfactor = (width - 10) - 40 # factor for drawing x axis
        for i in range(256):
            x = 40 + self.xdata[i] * xfactor
            y = height - 20 - self.data[i] * scale_y
            if i == 0:
                cr.move_to(x, y)
            else:
                cr.line_to(x, y)
        cr.stroke()

------------------

 

    def __init__(self, ydata, xdata):

で、このクラスは、グラフの y データと x データを引数として取ることを定義しています。

    super().__init__()

は親クラスのメソッド等の継承を指定しています。

その後描画領域のサイズを指定し、その後 draw イベントの発生で def on_draw  を呼び出します。基本的に GraphArea クラスが呼び出されることで、draw イベントが発生するものと思われます。

 

 def on_draw では、widget と cr という引数を取っています。しかし、上で on_draw を呼び出したときには引数が指定されていません。これらは Gtk.DrawingArea を呼び出した時に、自動的に用意されるようです。ちょうど、GIMPPython プログラムを書く時に、image (今参照している画像オブジェクト) や drawables (今選択されている drawables) が自動的に用意されるように。この内 widge は Gtk.DrawingArea 自体、つまり、self と同じ意味のようです。cr は cairo の描画コンテクストです。描画コンテクストとは、描画エリア+描画データが集まったオブジェクトのようです。c でプログラムを書くときは描画コンテクストを定義しないといけないようですが、 Python の場合は、自動的に用意してくれるようです。おそらく、super() を指定することで、そのまま引用できるようになっているものと思われます。on_draw で具体的な描画を行いますが、これらを引数として指定しないとちゃんと描画がされません。widgeton_draw の中で直接引用されませんが、引数として取らないと描画されません。cr が動作するには必須と思われます。いわば cr を操作するために座布団として必要なようです。box がないと widget が配置できないのと同様な関係と思われます。また、おそらく widget という名前で引用されることも必要なものと思われます。cr も同様かと。

 そして、描画エリアの高さと幅を取得し、さらに、set_source_rgb(0, 0, 0) で描画色 (黒) を指定した後、エリアをpaint で黒塗りにします。

 次に軸を描画しますが、set_source_rgb(1, 1, 1) で描画色を白に、線の幅を 2 ピクセルに指定し、軸を描き始めます。

 その後の move_to はいわば「カーソル」を動かすメソッドで x: 40 y: 10 に移動したら、そこからline_to で縦軸をまず書きます。原点は描画エリアの左上です。次に、その終点 (40, height - 20) から出発して、横軸を書きます。最後に、line_to で設定したカーソルの動きをなぞって、stroke() メソッドで実際に線を描きます。

 次に、グラフデータを描画します。

 最初に Y 軸の最大値を取得し、データの個数に応じて、x 軸に沿って描く刻み幅を設定するとともに、y の最大値に応じた y 方向の描画幅に合わせるスケール値を設定します。なお当然ながら x 軸データと y 軸データの個数は合わせなければなりません。

 その次にグラフを描くラインカラーを設定します。ここでは、self.ch に Gimp.HistogramChannel.VALUE を設定しているため、黄色で描きます。またライン幅も設定します。

 その後、x 軸を所定の幅に収めるための、factor 値を計算します。

 そして、ここではデータを 256 点分与えていますので、for ループを使って 256 個分の点を line_to でなぞっていき、最後に stroke() メソッドで、グラフ曲線を描画します。

 

・データの書き換え

 もし入力ボックスの値が書き換えられた場合は、

  self.graph.update_data

を使って、このクラスの中の、update_data 関数 (メソッド) が呼び出されます。この関数により、x, y および ch データが更新され、queue_draw() メソッドによって再描画がスケジュールされます。ちょっと分かりにくいかもしれませんが、これは、

self.graph = GraphArea(ydata, xdata)

の引数データを更新した上で、self.queue_draw() を実行すると、

self.graph = GraphArea(ydata, xdata) という命令文の更新引数データを使った描画の再実行を設定するというメソッドのようです。この、queue_draw() メソッドは Gtk.Widget に共通に備えられているメソッドですので、結構便利に使えるのではないかと思います。

 

-----------------

[参考サイト]

 cairo の c 用チュートリアルです。

cairographics.org

cairo の Pythonチュートリアルです。

www.tortall.net

Python 用 cairo の情報ページです。リファレンス、チュートリアルなどがあります。

pycairo.readthedocs.io

入門 Gtk+ (菅谷 保之)

https://iim.cs.tut.ac.jp/member/sugaya/GTK+/files/gtkbook-20210127.pdf

日本語の Gtk+ 入門ガイドです。