kivyを使ってGUIプログラミング ~その5 画像でボタンを作る~

#はじめに

前回は、いろいろなボタンについて紹介をしました。またボタン関係です。以前、ボタンに画像をはっつけようとしてうまくできなかったので、ちょっと調べてみました。備忘録的な感じになると思います。ついでに、使い方の例を示したいと思います(タイトルの内容がメインではないものが出来上がってしまいましたが。。。)

#間違えていた方法

単純に```Button```のウィジェットに```Image```を```add_widget```して使おうとしてました。こんな感じになると思います。画像の後ろにボタンががっつり写ってしまい不恰好な感じがしますね。（たぶんいろいろ書けば、見た目はなんとかなるかもしれませんが）それと、ボタンの位置をずらすと、画像がついてきてくれなかったりと訳が分からないです。そのため、以前は実装を諦めてしまいました。

やることこんな感じになります。

![sippai.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421817/329437a6-6434-6d5a-7573-e7f60ae9d937.gif)


#画像ボタンはこう作る！

画像ボタンを作るには```kivy.uix.behaviors```というモジュールを用います。```behaviors.ButtonBehavior```を用いることで、ラベルや画像などにボタンの機能を付与することができます。そのほかにも色々できるようです！
https://kivy.org/doc/stable/api-kivy.uix.behaviors.html

![seikou.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421817/54efdf33-5304-7e6a-3e88-4fec3dfa56c6.gif)

ソースコードは、下記の通りです。```behaviors.ButtonBehavior```とボタンにしたいもの、ここでは```Image```を継承した新しいクラスを作成します。そしてImageの画像を変更し、ボタンを押した時の画像、話した時の画像を指定してあげるとうまいこと動作します。

```python
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior


class MyButton(ButtonBehavior, Image):
    def __init__(self, **kwargs):
        super(MyButton, self).__init__(**kwargs)
        # 適当な画像
        self.source = 'data/val2017/000000000285.jpg'

    def on_press(self):
        # 押した時の画像
        self.source = 'data/val2017/000000000776.jpg'

    def on_release(self):
        # 元の画像に戻す
        self.source = 'data/val2017/000000000285.jpg'


class SampleApp(App):
    def build(self):
        return MyButton()


SampleApp().run()
```

トグルボタンを使いたい場合には、```ToggleButtonBehavior```を継承してください。

```python

from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.behaviors import ToggleButtonBehavior


class MyButton(ToggleButtonBehavior, Image):
    def __init__(self, **kwargs):
        super(MyButton, self).__init__(**kwargs)
        self.source = 'data/val2017/000000000285.jpg'

    def on_state(self, widget, value):
        if value == 'down':
            self.source = 'data/val2017/000000000776.jpg'
        else:
            self.source = 'data/val2017/000000000285.jpg'


class SampleApp(App):
    def build(self):
        return MyButton()


SampleApp().run()
```


#画像ボタンを使って作ったもの
画像ボタンの作り方を調べているうちに、いつの間にか画像ビュアーができていました。
イメージはこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421817/98c8c3b1-9527-5458-54bc-e1ce4515e368.png)

任意の画像フォルダーの画像をスクロールビューに全て表示して、クリックした画像ボタンの画像が上のImageに表示されるような感じです。

# ソース

できたものは下のような感じです。

![imageviewer.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421817/7b776a0e-6ab0-b4b7-f846-10382d59f9a0.gif)

```python

import os
import cv2
import numpy as np

from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.clock import Clock
from kivy.graphics.texture import Texture

#画像ボタンクラス
class MyButton(ToggleButtonBehavior, Image):
    def __init__(self, **kwargs):
        super(MyButton, self).__init__(**kwargs)
        #画像ボタンの画像名を格納
        self.source = kwargs["source"]
        #画像を編集できるようにテクスチャーとして扱う
        self.texture = self.button_texture(self.source)

    # トグルボタンの状態、状態によって画像が変化する
    def on_state(self, widget, value):
        if value == 'down':
            self.texture = self.button_texture(self.source, off=True)
        else:
            self.texture = self.button_texture(self.source)

    # 画像を変化させる、押した状態の時に矩形+色を暗く
    def button_texture(self, data, off=False):
        im = cv2.imread(data)
        im = self.square_image(im)
        if off:
            im = self.adjust(im, alpha=0.6, beta=0.0)
            im = cv2.rectangle(im, (2, 2), (im.shape[1]-2, im.shape[0]-2), (255, 255, 0), 10)

        # 上下反転
        buf = cv2.flip(im, 0)
        image_texture = Texture.create(size=(im.shape[1], im.shape[0]), colorfmt='bgr')
        image_texture.blit_buffer(buf.tostring(), colorfmt='bgr', bufferfmt='ubyte')
        return image_texture

    # 画像を正方形にする
    def square_image(self, img):
        h, w = img.shape[:2]
        if h > w:
            x = int((h-w)/2)
            img = img[x:x + w, :, :]
        elif h < w:
            x = int((w - h) / 2)
            img = img[:, x:x + h, :]

        return img

    # 画像の色を暗くする
    def adjust(self, img, alpha=1.0, beta=0.0):
        # 積和演算を行う。
        dst = alpha * img + beta
        # [0, 255] でクリップし、uint8 型にする。
        return np.clip(dst, 0, 255).astype(np.uint8)


class Test(BoxLayout):
    def __init__(self, **kwargs):
        super(Test, self).__init__(**kwargs)
        # 読み込むディレクトリ
        image_dir = "data/val2017"

        # 縦配置
        self.orientation = 'vertical'

        # 画像ファイルの名前を管理
        self.image_name = ""

        # 画像を表示するウィジェットの準備
        self.image = Image(size_hint=(1, 0.5))
        self.add_widget(self.image)

        # 画像ボタンを配置する、スクロールビューの定義
        sc_view = ScrollView(size_hint=(1, None), size=(self.width, self.height*4))

        # スクロールビューには１つのウィジェットしか配置できないため
        box = GridLayout(cols=5, spacing=10, size_hint_y=None)
        box.bind(minimum_height=box.setter('height'))

        # 画像ボタンの一括定義、グリッドレイアウトに配置
        box = self.image_load(image_dir, box)

        sc_view.add_widget(box)
        self.add_widget(sc_view)

    # 画像ボタンの読み込み
    def image_load(self, im_dir, grid):
        images = sorted(os.listdir(im_dir))

        for image in images:
            button = MyButton(size_hint_y=None,
                              height=300,
                              source=os.path.join(im_dir, image),
                              group="g1")
            button.bind(on_press=self.set_image)
            grid.add_widget(button)

        return grid

    # 画像をボタンを押した時、画像ウィジェットに画像を表示
    def set_image(self, btn):
        if btn.state=="down":
            self.image_name = btn.source
            #画面を更新
            Clock.schedule_once(self.update)

    # 画面更新
    def update(self, t):
        self.image.source = self.image_name


class SampleApp(App):
    def build(self):
        return Test()


SampleApp().run()
```

#少し解説


上の説明では、　ボタンの画像が切り替わるだけで、ボタンが押されているのかよく分からない状態だったと思います。そこで、画像を押した時に枠がついて画像が少し暗くなって押したことがわかるような処理を追加しました。また、グリッドレイアウトに綺麗に並べるために、画像の中心でトリミングする処理も加えております。

画像ボタンクラスには、ボタンとなる画像のファイル名を格納している変数```source```があり、それを用いてこの関数で画像名からopencvで画像の処理をします。また、処理した画像を使うには、textureを使う必要があるため、返り値にtextureを指定しております。

※```texture```については[その3](https://qiita.com/kurodae/items/f339f1c30ed05b3aade6)で解説したので今回は省略いたします。



```python
    # 画像を変化させる、押した状態の時に矩形+色を暗く
    def button_texture(self, data, off=False):
        im = cv2.imread(data)
        im = self.square_image(im)
        if off:
            im = self.adjust(im, alpha=0.6, beta=0.0)
            im = cv2.rectangle(im, (2, 2), (im.shape[1]-2, im.shape[0]-2), (255, 255, 0), 10)

        # 上下反転
        buf = cv2.flip(im, 0)
        image_texture = Texture.create(size=(im.shape[1], im.shape[0]), colorfmt='bgr')
        image_texture.blit_buffer(buf.tostring(), colorfmt='bgr', bufferfmt='ubyte')
        return image_texture
```

アプリ側のボタンを押した時の処理ですが、ボタンを押した時に、画面上部の```Image```の```source```を変更するだけでは、画像は変化しません。画像を変えるためには、画面を更新する必要が必要です。そのため、画像を押した時に、```Clock```を一度動かして画面を更新しております。

```python

    # 画像をボタンを押した時、画像ウィジェットに画像を表示
    def set_image(self, btn):
        if btn.state=="down":
            self.image_name = btn.source
            #画面を更新
            Clock.schedule_once(self.update)

    # 画面更新
    def update(self, t):
        self.image.source = self.image_name
```


#参考文献
・[OpenCV - 画像の明るさやコントラストを変更、ガンマ補正など - Pynote](https://www.pynote.info/entry/opencv-change-contrast-and-brightness)
　画像を暗くする処理のソースを参考にいたしました。

・[COCO Dataset](http://cocodataset.org/#download)
　今回使用させていただいた画像のリンクです。