ディレクトリ単位で画像を読み込み、切り抜き・分類・保存をするツール

#はじめに
好きな漫画のキャラの分類をしたくてデータを集める必要があったが、画像ごとに編集ソフトを開いて顔の部分を選択して保存先を選択して保存して閉じるをくりかえしていたらおそらく30枚もしないで発狂するのでツールを作ることにした。
アニメキャラの顔の分類器があるのでそれを使ってもいいが今回はデータが少ないのでせめて正確に自分でやろうと思った。手動でデータを集めることや単にデータの分類をすることも多いのできっと誰かの役に立つはず。

#環境
```
# python3 --version                                                                                                               
Python 3.7.2+
# pip3 show pillow                                                                                                         
Name: Pillow
Version: 5.4.1
# pip3 python -m tkinter
This is Tcl/Tk version 8.6
```
#実装する機能
目標とする流れは
1.分類したい画像を一つのディレクトリに入れる
2.それを一つのウィンドウでビューワーのように開いてファイル間を自由に行き来して確認する
3.マウスで保存したい領域を矩形選択する
4.保存先のディレクトリごとにボタンを用意して保存と分類を一気に行う
5.次の画像に移る
という感じです。

実装する機能はおおまかに
・画像のパスを読み込みウィンドウに画像を表示する
・画像を回転させる
・マウスで領域を選択し、選択領域を四角で囲む
・選択領域を指定のディレクトリに保存する
・次の画像に移ったり戻ったりする
・10個ずつ次にいったり戻ったりする
・ウィンドウを閉じる

という感じです。

#実装
tkinterを使います。
python2はTkinterですが、python3はtkinterです。気をつけて下さい(二敗)
画面のサイズとかウィジェットの座標は各自調整してください。
image_dirに分類したい画像が入ったディレクトリを、save_dirに保存先を指定してください。
ただしpathの最後には/をつけて下さい。

```python
import sys
import os
import tkinter as tk
from tkinter import *
from PIL import Image,ImageTk

save_dir="/home/example/Picture/save/"
image_dir="/home/example/Picture/img/" 

name_list=[f for f in os.listdir(image_dir) if os.path.isfile(image_dir+f)]
name_list.sort()

class Window(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
    
        self.index=0
        self.count=1
        self.max_width=1400
        self.max_height=800
        self.start_x=0
        self.start_y=0
        self.end_x=0
        self.end_y=0
        
        self.canvas = Canvas(master, width=1400, height=800)
        self.canvas.grid(row=0, column=0, columnspan=2, rowspan=4)
        self.load_img() 
        self.create_widgets()
        self.grid()
        
        self.canvas.bind("<Button-1>", self.clicked)
        self.canvas.bind("<ButtonRelease-1>", self.release)
        self.canvas.bind("<B1-Motion>", self.move)

    def scale_to_height(self,img):
        img_width,img_height=img.size
        if img_width>self.max_width and img_height>self.max_height:
            if img_width>img_height and img_height/img_width<self.max_height/self.max_width:
                rate=img_height/img_width
                img=img.resize((self.max_width,int(self.max_width*rate)))
            else:
                rate=img_width/img_height
                img=img.resize((int(self.max_height*rate),self.max_height))
        else:
            scale = self.max_height / img_height
            img=img.resize((int(img_width*scale),int(img_height*scale)))
        return img
    
    def load_img(self):
        path=image_dir+name_list[self.index]
        
        img=Image.open(path)
        
        self.resize_image = self.scale_to_height(img)

        self.img=ImageTk.PhotoImage(self.resize_image)
        self.image_on_canvas = self.canvas.create_image(0, 0, anchor=NW, image=self.img)
    
    def create_widgets(self):
        self.button_back_skip=Button(self)
        self.button_back_skip["text"]="<<"
        self.button_back_skip["command"]=self.back_skip
        self.button_back_skip.grid(row=1, column=0)
        
        self.button_back=Button(self)
        self.button_back["text"]="back"
        self.button_back["command"]=self.back_image
        self.button_back.grid(row=1, column=1)
        
        self.button_next=Button(self)
        self.button_next["text"]="next"
        self.button_next["command"]=self.next_image
        self.button_next.grid(row=1, column=2)
        
        self.button_next_skip=Button(self)
        self.button_next_skip["text"]=">>"
        self.button_next_skip["command"]=self.next_skip
        self.button_next_skip.grid(row=1, column=3)
        

        self.button_save=Button(self)
        self.button_save["text"]="rotate"
        self.button_save["command"]=self.rotate_image
        self.button_save.grid(row=2, column=0)
        
        self.button_save=Button(self)
        self.button_save["text"]="save"
        self.button_save["command"]=self.save_image
        self.button_save.grid(row=2, column=1)

        self.quit = tk.Button(self, text="QUIT", fg="red",
        command=self.master.destroy)
        self.quit.grid(row=2, column=2)

#------- button press --------------------------------------------------------------
    def next_image(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index+=1
        if self.index==len(name_list):
            self.index=0
        self.load_img()

    def back_image(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index-=1
        if self.index==-1:
            self.index=len(name_list)-1
        self.load_img()

    def next_skip(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index=(self.index+10)%len(name_list)
        self.load_img()

    def back_skip(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index=(len(name_list)+self.index-10)%len(name_list)
        self.load_img()
    
    def save_image(self):
        cropped_img=self.resize_image.crop((self.start_x,self.start_y,self.end_x,self.end_y))
        cropped_img.save(save_dir+name_list[self.index],quality=95)
        
    def rotate_image(self):
        rotated_image=self.resize_image.rotate(90*self.count,expand=True)
        self.img=ImageTk.PhotoImage(rotated_image)
        self.image_on_canvas = self.canvas.create_image(0, 0, anchor=NW, image=self.img)
        self.count+=1

#------- key press --------------------------------------------------------------
    def clicked(self,event):
        self.canvas.delete("rect")
        self.start_x=event.x
        self.start_y=event.y

    def move(self,event):
        self.canvas.delete("tmp")
        self.canvas.create_rectangle(self.start_x,self.start_y,event.x,event.y,outline="blue",width=2,tag="tmp")

    def release(self,event):
        self.canvas.create_rectangle(self.start_x,self.start_y,event.x,event.y,outline="blue",width=2,tag="rect")
        self.end_x=event.x
        self.end_y=event.y


root = tk.Tk()
root.title("cropper")
win=Window(master=root)
win.mainloop()
```
これが基本形です。
##ボタンの機能
next:次の画像へ
back:前の画像へ
\>>:10個進む
<<:10個戻る
rotate:画像を回転させる
save:画像を保存する
QUIT:終了

#保存先を増やす
分類先のディレクトリのボタンを増やすとこうなります。

```python
import cv2
import sys
import os
import tkinter as tk
from tkinter import *
from PIL import Image,ImageTk

save_dir=""
image_dir="" 

characters=["momone","kinako","aoi","shion","suigyoku"]

name_list=os.listdir(image_dir)
name_list.sort()

class Window(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
    
        self.index=0
        self.local_num=0
        self.max_width=1400
        self.max_height=800
        self.count=1
        self.start_x=0
        self.start_y=0
        self.end_x=0
        self.end_y=0
        
        self.canvas = Canvas(master, width=1400, height=800)
        self.canvas.grid(row=0, column=0, columnspan=2, rowspan=4)
        self.load_img() 
        self.create_widgets()
        self.grid()
        
        self.canvas.bind("<Button-1>", self.clicked)
        self.canvas.bind("<ButtonRelease-1>", self.release)
        self.canvas.bind("<B1-Motion>", self.move)

    def scale_to_height(self,img):
        img_width,img_height=img.size
        if img_width>self.max_width and img_height>self.max_height:
            if img_width>img_height and img_height/img_width<self.max_height/self.max_width:
                rate=img_height/img_width
                img=img.resize((self.max_width,int(self.max_width*rate)))
            else:
                rate=img_width/img_height
                img=img.resize((int(self.max_height*rate),self.max_height))
        else:
            scale = self.max_height / img_height
            img=img.resize((int(img_width*scale),int(img_height*scale)))
        return img
    
    
    def load_img(self):
        path=image_dir+name_list[self.index]
        
        img=Image.open(path)
        
        self.resize_image = self.scale_to_height(img)

        self.img=ImageTk.PhotoImage(self.resize_image)
        self.image_on_canvas = self.canvas.create_image(0, 0, anchor=NW, image=self.img)
    
    def create_widgets(self):
        self.button_back_skip=Button(self)
        self.button_back_skip["text"]="<<"
        self.button_back_skip["command"]=self.back_skip
        self.button_back_skip.grid(row=1, column=0)
        
        self.button_back=Button(self)
        self.button_back["text"]="back"
        self.button_back["command"]=self.back_image
        self.button_back.grid(row=1, column=1)
        
        self.button_next=Button(self)
        self.button_next["text"]="next"
        self.button_next["command"]=self.next_image
        self.button_next.grid(row=1, column=2)
        
        self.button_next_skip=Button(self)
        self.button_next_skip["text"]=">>"
        self.button_next_skip["command"]=self.next_skip
        self.button_next_skip.grid(row=1, column=3)
        

        self.button_save=Button(self)
        self.button_save["text"]="rotate"
        self.button_save["command"]=self.rotate_image
        self.button_save.grid(row=2, column=0)
        
        for col,character in enumerate(characters):
            self.button_save=Button(self)
            self.button_save["text"]=character
            self.button_save["command"]= lambda: self.save_image(character)
            self.button_save.grid(row=2, column=col+1)

        self.button_save=Button(self)
        self.button_save["text"]="save"
        self.button_save["command"]=self.save_image_default
        self.button_save.grid(row=2, column=len(characters)+1)

        self.quit = tk.Button(self, text="QUIT", fg="red",
        command=self.master.destroy)
        self.quit.grid(row=2, column=len(characters)+2)


#------- button press --------------------------------------------------------------
    def next_image(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index+=1
        if self.index==len(name_list):
            self.index=0
        self.load_img()

    def back_image(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index-=1
        if self.index==-1:
            self.index=len(name_list)-1
        self.load_img()

    def next_skip(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index=(self.index+10)%len(name_list)
        self.load_img()

    def back_skip(self):
        self.canvas.delete("tmp")
        self.canvas.delete("rect")
        self.index=(len(name_list)+self.index-10)%len(name_list)
        self.load_img()
    
    def save_image(self,chara):
        cropped_img=self.resize_image.crop((self.start_x,self.start_y,self.end_x,self.end_y))
        cropped_img.save(save_dir+chara+"/"+name_list[self.index],quality=95)

    def save_image_default(self):
        cropped_img=self.resize_image.crop((self.start_x,self.start_y,self.end_x,self.end_y))
        cropped_img.save(save_dir+str(self.local_num)+name_list[self.index],quality=95)
        self.local_num+=1

    def rotate_image(self):
        rotated_image=self.resize_image.rotate(90*self.count,expand=True)
        self.img=ImageTk.PhotoImage(rotated_image)
        self.image_on_canvas = self.canvas.create_image(0, 0, anchor=NW, image=self.img)
        self.count+=1

#------- key press --------------------------------------------------------------
    def clicked(self,event):
        self.canvas.delete("rect")
        self.start_x=event.x
        self.start_y=event.y

    def move(self,event):
        self.canvas.delete("tmp")
        self.canvas.create_rectangle(self.start_x,self.start_y,event.x,event.y,outline="blue",width=2,tag="tmp")

    def release(self,event):
        self.canvas.create_rectangle(self.start_x,self.start_y,event.x,event.y,outline="blue",width=2,tag="rect")
        self.end_x=event.x
        self.end_y=event.y


root = tk.Tk()
root.title("cropper")
win=Window(master=root)
win.mainloop()
```
このとき、あらかじめ分類したいものの名前をつけたディレクトリをsave_dirの中に作成し、それぞれの名前を配列に(今回ならcharacters)に格納します。

試しに[ここ](http://www.dokidokivisual.com/doubiju-rt/doubiju_icon.zip)で配布されているアイコンを動かしてみるとこんな感じ。
![Screenshot from 2019-04-07 15-37-13.png](https://qiita-image-store.s3.amazonaws.com/0/332794/d34da7f0-0bb5-1bb1-d536-f6eac4abb284.png)
これだけで効率がかなり上がりました。(感動)
もっと技術力のある人はこれに付け加えてどんどん機能をふやしていくといい感じになると思います。
#最後に
いざ手動でデータセットをつくろうとしたときに思うようなツールが見つから無かったので自作してみました。
意外と簡単で楽しかったですし、効率がかなり上がったので良かったです。
#参考
[Python + Tkinterで連番画像ファイルを素早く切り抜くGUI画像トリミングツール - Qiita](https://qiita.com/lp6m/items/e6be545eb05a50b574bd)
[Python + Tkinter で作る、GUIな画像トリミングツール - Qiita](https://qiita.com/MasahikoYasui/items/4bfdfab0ba27c7ca0620)
[機械学習用の画像切り出しツールを作る - Qiita](https://qiita.com/t-ae/items/7bbe070907b310bd6407)
[矩形切り出しツール「Region Cropper」を公開しました - めがねをかけるんだ](https://blog.keiji.io/2016/09/about_region_cropper.html)
[http://www.dokidokivisual.com/doubiju-rt/](http://www.dokidokivisual.com/doubiju-rt/)