#はじめに
好きな漫画のキャラの分類をしたくてデータを集める必要があったが、画像ごとに編集ソフトを開いて顔の部分を選択して保存先を選択して保存して閉じるをくりかえしていたらおそらく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の最後には/をつけて下さい。
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:終了
#保存先を増やす
分類先のディレクトリのボタンを増やすとこうなります。
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)に格納します。
試しにここで配布されているアイコンを動かしてみるとこんな感じ。
これだけで効率がかなり上がりました。(感動)
もっと技術力のある人はこれに付け加えてどんどん機能をふやしていくといい感じになると思います。
#最後に
いざ手動でデータセットをつくろうとしたときに思うようなツールが見つから無かったので自作してみました。
意外と簡単で楽しかったですし、効率がかなり上がったので良かったです。
#参考
Python + Tkinterで連番画像ファイルを素早く切り抜くGUI画像トリミングツール - Qiita
Python + Tkinter で作る、GUIな画像トリミングツール - Qiita
機械学習用の画像切り出しツールを作る - Qiita
矩形切り出しツール「Region Cropper」を公開しました - めがねをかけるんだ
http://www.dokidokivisual.com/doubiju-rt/