Edited at

Python + Tkinterで連番画像ファイルを素早く切り抜くGUI画像トリミングツール


はじめに

機械学習をはじめました。

固定サイズの画像をたくさん用意する必要がありましたが、自分の用途にあったツールがなかったのでさくっと作りました。

Pythonはあまり書き慣れてないので多目に見ていただけると嬉しいです。


機能


  • 指定ディレクトリ内に入っている連番画像ファイルを読み込み

  • マウスで指定したフレームで画像をトリミングし

  • トリミングした画像を指定された固定サイズ(例32x64)にリサイズして保存

  • トリミングした座標の情報をCSVファイルに保存・起動時に読み込み

操作としては『マウスでトリミングする範囲と位置を指定して、スペースキー押下』を繰り返すだけで次々とトリミング画像が作成できます。便利!

固定サイズしか必要なかったので切り抜く大きさは固定にしています。

トリミングした座標を外部CSVファイルに保存しておき、起動時に読み込むことで、作業を中断した場合でも再開できます。


crop.csv

image_0003.png,71,307,199,563

image_0002.png,158,131,286,387
image_0001.png,132,124,260,380
image_0005.png,142,160,270,416



画面上部には表示中の画像ファイル名が表示されます。

既にトリミング済み情報がある場合は下部にalready croppedと表示されます。


ファイル構成(準備)

├── crop.py

├── doll
│   ├── input
├── image_0001.png
├── image_0002.png
├── ...
│   ├── output
├── crop.csv

画像のファイル名の末尾は連番で4桁の0埋めになっていることを前提としています。

ffmpegなどを利用して動画ファイルから連番画像を作成できます。

参考:Qiita:ffmpegで連番画像から動画生成 / 動画から連番画像を生成 ~コマ落ちを防ぐには

トリミングした画像はoutputディレクトリに入力画像と同名で保存されます。

注意:画像サイズは横_canvas_width・縦_canvas_height以下である必要があります

(画像を縮小して表示すると座標の計算がいろいろやこしかったので)

画像サイズが大きいときは事前にリサイズしておく必要があります。(以下参考)


resize.py

from PIL import Image

import os
import glob

max_width = 600.0
max_height = 600.0

file_list = glob.glob('./original/image_*.png')

for filepath in file_list:
filename = os.path.basename(filepath)
savefilepath = './input/'+ filename

img = Image.open(filepath)
w, h = img.size
if h <= max_height and w <= max_width:
#no need to crop, just copy file
img.save(savefilepath)
else:
ratio = min(max_width/w, max_height/h)
print(ratio)
cw = int(w * ratio)
ch = int(h * ratio)
img_resize = img.resize((cw, ch))
img_resize.save(savefilepath)

print(savefilepath)



キー操作

キー
操作


次の画像へ移動


前の画像へ移動

スペースキー
現在の画像をトリミング・リサイズして、次の画像へ移動

マウスホイール↑
トリミングするフレームのサイズを大きくする

マウスホイール↓
トリミングするフレームのサイズを小さくする


ソースコード

エラー処理とかはサボっています。

コメントの英語がエグいのであとで修正します。


crop.py


# -*- coding: utf-8 -*-
from Tkinter import *
from PIL import Image
import os
import glob

parentdir = 'doll'
inputdir = 'input'
outputdir = 'output'
imgname_prefix = 'image_'
csvlogfilename = 'crop.csv'

_canvas_width = 600
_canvas_height = 600
_crop_width = 32
_crop_height = 64

#----------------------------------------------------------------------
class MainWindow():

#----------------
def __init__(self, main):
# warning : image must be smaller than canvas size.
# create image canvas
self.canvas = Canvas(main, width=_canvas_width, height=_canvas_height)
self.canvas.grid(row=0, column=0, columnspan=2, rowspan=4)

# load image list
# image file name must be 'prefix + number(0000-9999) .png'
self.file_list = []
for i in range(10000):
filepath = './' + parentdir + '/' + inputdir + '/' + imgname_prefix + ('%04d' % i) + '.png'
outputpath = './' + parentdir + '/' + outputdir + '/' + imgname_prefix + ('%04d' % i) + '.png'
if os.path.exists(filepath):
self.file_list.append((i, filepath, outputpath))

# load cropping frame information if csv file exists
self.crop_frame_dic = {}
self.csvfilepath = './' + parentdir + '/' + csvlogfilename
if os.path.exists(self.csvfilepath):
with open(self.csvfilepath) as f:
for line in f:
elements = line.split(',')
#filename, sx, sy, ex, ey
self.crop_frame_dic[elements[0]] = (int(elements[1]), int(elements[2]), int(elements[3]), int(elements[4]))

# load first image
self.now_image_index = 0
self.nowimage = PhotoImage(file=self.file_list[0][1])
self.image_on_canvas = self.canvas.create_image(0, 0, anchor=NW, image=self.nowimage)

# set callback function for key and mouse events
self.canvas.bind('<Right>', self.right_key_pressed)
self.canvas.bind('<Left>', self.left_key_pressed)
self.canvas.bind('<space>', self.space_key_pressed)
self.canvas.bind("<B1-Motion>", self.mouse_rclick_moving)
# mouse event with Windows OS
root.bind("<MouseWheel>", self.mouse_wheel_moving)
# mouse event with Linux OS
root.bind("<Button-4>", self.mouse_wheel_moving)
root.bind("<Button-5>", self.mouse_wheel_moving)

self.canvas.focus_set() #need to recive key events

# set initial value of crop frame
self.cropframe_width = 128
self.cropframe_height = self.cropframe_width * _crop_height / _crop_width
self.cropframe_centerposx = _canvas_width/2
self.cropframe_centerposy = _canvas_height/2

# draw crop frame
sx, sy, ex, ey = self.__getCropFrameCoordinate()
self.crop_rectangle = self.canvas.create_rectangle(sx, sy, ex, ey, outline="blue", width=3)

# filename label
self.filenamelabel = Label(self.canvas, bg="black",fg="green",text=self.file_list[0][1],borderwidth=0,font=("",20))
self.filenamelabel.place(anchor=NW)

#already exist label
self.alreadyexistlabel = Label(self.canvas, bg="black",fg="red",text="",borderwidth=0,font=("",20))
self.alreadyexistlabel.place(relx=0.0, rely=1.0,anchor=SW)

self.__imageRefresh()
#----------------
def right_key_pressed(self, event):
# open next image
self.now_image_index += 1
if self.now_image_index >= len(self.file_list):
self.now_image_index = 0

self.__imageRefresh()

def left_key_pressed(self, event):
# open previous image
self.now_image_index -= 1
if self.now_image_index < 0:
self.now_image_index = len(self.file_list) - 1

self.__imageRefresh()

def space_key_pressed(self, event):
# save current crop image, output logfile, and open next image
self.__addCropDic()
self.__saveCropImage()
self.__saveCropInfoToCSV()
print("saved")

self.now_image_index += 1
if self.now_image_index >= len(self.file_list):
self.now_image_index = 0

self.__imageRefresh()

def mouse_rclick_moving(self, event):
self.cropframe_centerposx = event.x
self.cropframe_centerposy = event.y
self.__cropRectangleRefresh()

def mouse_wheel_moving(self, event):
delta = 2
delta_width = delta
delta_height = delta * _crop_height / _crop_width
# respond to Linux or Windows wheel event
if event.num == 5 or event.delta == -120:
# mouse wheel down
self.cropframe_width = max(0, self.cropframe_width - delta_width)
self.cropframe_height = max(0, self.cropframe_height - delta_height)
if event.num == 4 or event.delta == 120:
# mouse wheel up
self.cropframe_width += delta_width
self.cropframe_height += delta_height
self.__cropRectangleRefresh()

#----------------
def __imageRefresh(self):
#change image
self.nowimage = PhotoImage(file=self.file_list[self.now_image_index][1])
self.canvas.itemconfig(self.image_on_canvas,image=self.nowimage)

#check whether cropped image coordinate information already exists
#ignore wheter cropped image file exists
inputfilepath = self.file_list[self.now_image_index][1]
imagefilename = os.path.basename(inputfilepath)
if imagefilename in self.crop_frame_dic:
#if cropping coordinate information exists, draw rectangle at the position based on the information
sx, sy, ex, ey = self.crop_frame_dic[imagefilename]
self.cropframe_width = ex - sx
self.cropframe_height = ey - sy
self.cropframe_centerposx = (sx + ex) / 2
self.cropframe_centerposy = (sy + ey) / 2
self.__cropRectangleRefresh()

#update filename label
self.filenamelabel["text"] = inputfilepath
#update already cropped label
if imagefilename in self.crop_frame_dic:
self.alreadyexistlabel["text"] = "already cropped"
else:
self.alreadyexistlabel["text"] = ""
def __cropRectangleRefresh(self):
sx, sy, ex, ey = self.__getCropFrameCoordinate()
self.canvas.coords(self.crop_rectangle, sx, sy, ex, ey)

def __saveCropImage(self):
im = Image.open(self.file_list[self.now_image_index][1])
sx, sy, ex, ey = self.__getCropFrameCoordinate()
im_crop = im.crop((sx, sy, ex, ey))
im_crop_resized = im_crop.resize((_crop_width, _crop_height))
im_crop_resized.save(self.file_list[self.now_image_index][2], quality=95)

def __addCropDic(self):
originalfilename = os.path.basename(self.file_list[self.now_image_index][1])
sx, sy, ex, ey = self.__getCropFrameCoordinate()
self.crop_frame_dic[originalfilename] = (sx, sy, ex, ey)

def __saveCropInfoToCSV(self):
with open(self.csvfilepath, mode='w') as f:
for filename, coordinate in self.crop_frame_dic.items():
f.write(','.join([filename, str(coordinate[0]), str(coordinate[1]), str(coordinate[2]), str(coordinate[3])]) + "\n")
print("csv saved")

def __getCropFrameCoordinate(self):
sx = self.cropframe_centerposx - self.cropframe_width/2
sy = self.cropframe_centerposy - self.cropframe_height/2
ex = self.cropframe_centerposx + self.cropframe_width/2
ey = self.cropframe_centerposy + self.cropframe_height/2
return sx, sy, ex, ey

#----------------------------------------------------------------------

root = Tk()
MainWindow(root)
root.mainloop()



参考

・Python + Tkinter で作る、GUIな画像トリミングツール

https://qiita.com/MasahikoYasui/items/4bfdfab0ba27c7ca0620