Pythonでオブジェクトを選択してクロップするツールを作ってみた
Pythonで、図の中のオブジェクトを選択してクロップする範囲を決める、クロップツールを作ってみました。GUIはtkinterです。
目次
クロップする範囲内に消したいオブジェクトがある
例えば、官報とかの一部をPowerPointにコピペしたい、CADのスクリーンショットをコピペしたい、図面の一部をコピペしたい。そんなときに、コピペする四角形の範囲内に、不要なオブジェクト(図)があったりします。
こういうとき、ペイントの消しゴムモードでシコシコ消したりしますよね。しないですか?
そこで、必要なオブジェクトだけを選択してクロップする範囲を決めるツールを作ってみました。
手法としては、OpenCVの輪郭(コンター)を抽出する機能を使用します。輪郭毎に要不要を選択するわけですね。
コード
作成環境は、Windows10、Python 3.7です。OpenCV-python、Pillow、NumPy、PyWin32がインストールされている必要があります。
どんなエラーが出るかわかりませんので、使う方は自己責任でお願いします。
import tkinter as tk
import tkinter.ttk as ttk
import numpy as np
from PIL import Image, ImageTk, ImageGrab
import cv2
import win32clipboard
import io
class ScrollableFrame(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL)
vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=False)
hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL)
hscrollbar.pack(fill=tk.X, side=tk.BOTTOM, expand=False)
self.canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
vscrollbar.config(command=self.canvas.yview)
hscrollbar.config(command=self.canvas.xview)
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
self.canvas.config(scrollregion=(0, 0, canvas_width, canvas_height))
self.interior = tk.Frame(self.canvas)
self.canvas.create_window(0, 0, window=self.interior, anchor='nw')
def _configure_interior(event):
size = (max(self.interior.winfo_reqwidth(), canvas_width),
max(self.interior.winfo_reqheight(), canvas_height))
print(size)
self.canvas.config(scrollregion=(0, 0, size[0], size[1]))
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
self.canvas.config(width = self.interior.winfo_reqwidth())
if self.interior.winfo_reqheight() != self.canvas.winfo_height():
self.canvas.config(height = self.interior.winfo_reqheight())
self.interior.bind('<Configure>', _configure_interior)
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.master.title('Crop Objects')
self.pack(fill=tk.BOTH, expand=True)
self.create_widgets()
self.startup()
def create_widgets(self):
self.canvas_frame = ScrollableFrame(self)
self.canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.control_frame = tk.Frame(self)
self.control_frame.pack(side=tk.TOP, fill=tk.Y, expand=False)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=0)
self.paste_button = ttk.Button(self.control_frame, text='Paste', command=self.paste_image)
self.paste_button.pack(anchor=tk.W)
self.sep_0 = ttk.Separator(self.control_frame, orient='horizontal')
self.sep_0.pack(fill='x', pady=5)
self.label_1 = ttk.Label(self.control_frame, text='Choice Object')
self.label_1.pack(anchor=tk.W)
self.contour_button = ttk.Button(self.control_frame, text='Check All', command=self.analyze_contour)
self.contour_button.pack(anchor=tk.W)
self.check_state_0 = tk.IntVar()
self.contour_check_0 = ttk.Checkbutton(self.control_frame, text='green', variable=self.check_state_0, command=self.write_contour)
self.contour_check_0.pack(anchor=tk.W)
self.check_state_1 = tk.IntVar()
self.contour_check_1 = ttk.Checkbutton(self.control_frame, text='lt blue', variable=self.check_state_1, command=self.write_contour)
self.contour_check_1.pack(anchor=tk.W)
self.check_state_2 = tk.IntVar()
self.contour_check_2 = ttk.Checkbutton(self.control_frame, text='blue', variable=self.check_state_2, command=self.write_contour)
self.contour_check_2.pack(anchor=tk.W)
self.check_state_3 = tk.IntVar()
self.contour_check_3 = ttk.Checkbutton(self.control_frame, text='pink', variable=self.check_state_3, command=self.write_contour)
self.contour_check_3.pack(anchor=tk.W)
self.whiten_button = ttk.Button(self.control_frame, text='Clipping Object', command=self.whiten_unchecked_contour)
self.whiten_button.pack(anchor=tk.W)
self.sep_2 = ttk.Separator(self.control_frame, orient='horizontal')
self.sep_2.pack(fill='x', pady=5)
self.label_0 = ttk.Label(self.control_frame, text='Scope Select')
self.label_0.pack(anchor=tk.W)
self.contour_selected = ttk.Button(self.control_frame, text='Include selected', command=self.write_rectangle_selected)
self.contour_selected.pack(anchor=tk.W)
self.contour_all = ttk.Button(self.control_frame, text='Include all', command=self.write_rectangel_all)
self.contour_all.pack(anchor=tk.W)
self.sep_3 = ttk.Separator(self.control_frame, orient='horizontal')
self.sep_3.pack(fill='x', pady=5)
self.crop_button = ttk.Button(self.control_frame, text='Crop', command=self.crop_image)
self.crop_button.pack(anchor=tk.W)
self.sep_4 = ttk.Separator(self.control_frame, orient='horizontal')
self.sep_4.pack(fill='x', pady=5)
self.copy_button = ttk.Button(self.control_frame, text='Copy to Clipboard', command=self.copy_image)
self.copy_button.pack(anchor=tk.W)
self.sep_5 = ttk.Separator(self.control_frame, orient='horizontal')
self.sep_5.pack(fill='x', pady=5)
self.label_2 = ttk.Label(self.control_frame, text='Viewer scale')
self.label_2.pack(anchor=tk.W)
self.scale_val = tk.DoubleVar()
self.scale_selector = tk.Scale(self.control_frame, variable=self.scale_val, from_=0.5, to=1.0, resolution=0.5, command=self.resize_view, orient='horizontal')
self.scale_selector.pack(anchor=tk.W)
self.menu_crop = tk.Menu(self, tearoff=0)
self.menu_crop.add_command(label='crop')
self.canvas_frame.canvas.bind('<ButtonPress-1>', self.start_pickup)
self.canvas_frame.canvas.bind('<B1-Motion>', self.pickup_position)
self.canvas_frame.canvas.bind('<ButtonRelease-1>', self.end_pickup)
self.canvas_frame.canvas.bind('<ButtonPress-3>', self.show_popup)
self.master.bind('<Control-v>', self.paste_image)
self.master.bind('<Control-V>', self.paste_image)
self.master.bind('<Control-c>', self.copy_image)
self.master.bind('<Control-C>', self.copy_image)
im = ImageTk.PhotoImage(image=read_image)
self.canvas_frame.canvas.photo = im
self.canvas_image_id = self.canvas_frame.canvas.create_image(0, 0, anchor='nw', image=im)
def startup(self):
self.rect_start_x = None
self.rect_start_y = None
self.rect_end_x = None
self.rect_end_y = None
self.rect = None
self.scale_val.set(1.0)
self.reset_checkboxs()
def reset_checkboxs(self):
self.check_state_0.set(0)
self.check_state_1.set(0)
self.check_state_2.set(0)
self.check_state_3.set(0)
def show_popup(self, event):
self.menu_crop.post(event.x_root, event.y_root)
self.crop_image()
def start_pickup(self, event):
current_x = self.canvas_frame.canvas.canvasx(event.x)/self.scale_val.get()
current_y = self.canvas_frame.canvas.canvasy(event.y)/self.scale_val.get()
if 0 <= current_x <= canvas_width and 0 <= current_y <= canvas_height:
self.rect_start_x = current_x
self.rect_start_y = current_y
def pickup_position(self, event):
current_x = self.canvas_frame.canvas.canvasx(event.x)/self.scale_val.get()
current_y = self.canvas_frame.canvas.canvasy(event.y)/self.scale_val.get()
if 0 <= current_x <= canvas_width and 0 <= current_y <= canvas_height:
self.rect_end_x = current_x
self.rect_end_y = current_y
self.write_rectangle()
def end_pickup(self, event):
current_x = self.canvas_frame.canvas.canvasx(event.x)/self.scale_val.get()
current_y = self.canvas_frame.canvas.canvasy(event.y)/self.scale_val.get()
if 0 <= current_x <= canvas_width and 0 <= current_y <= canvas_height:
self.rect_end_x = current_x
self.rect_end_y = current_y
self.write_rectangle()
def write_rectangle(self):
if self.rect:
self.canvas_frame.canvas.coords(self.rect,
min(self.rect_start_x, self.rect_end_x)*self.scale_val.get(),
min(self.rect_start_y, self.rect_end_y)*self.scale_val.get(),
max(self.rect_start_x, self.rect_end_x)*self.scale_val.get(),
max(self.rect_start_y, self.rect_end_y)*self.scale_val.get())
else:
self.rect = self.canvas_frame.canvas.create_rectangle(
min(self.rect_start_x, self.rect_end_x)*self.scale_val.get(),
min(self.rect_start_y, self.rect_end_y)*self.scale_val.get(),
max(self.rect_start_x, self.rect_end_x)*self.scale_val.get(),
max(self.rect_start_y, self.rect_end_y)*self.scale_val.get(), outline='red')
def crop_image(self):
if self.rect:
xs = int(min(self.rect_start_x, self.rect_end_x))
ys = int(min(self.rect_start_y, self.rect_end_y))
xm = int(max(self.rect_start_x, self.rect_end_x))
ym = int(max(self.rect_start_y, self.rect_end_y))
global edited_canvas
global canvas_width
global canvas_height
edited_canvas = edited_canvas[ys:ym, xs:xm]
canvas_height, canvas_width, c_ = edited_canvas.shape[:3]
self.update_canvas()
self.canvas_frame.canvas.delete(self.rect)
self.rect=None
def update_canvas(self, event=None, draw_image=None):
self.canvas_frame.canvas.xview_moveto(0)
self.canvas_frame.canvas.yview_moveto(0)
self.canvas_frame.canvas.config(scrollregion=(0, 0, canvas_width*self.scale_val.get(), canvas_height*self.scale_val.get()))
if draw_image is not None:
im_out = Image.fromarray(cv2.cvtColor(draw_image, cv2.COLOR_BGR2RGB))
else:
im_out = Image.fromarray(cv2.cvtColor(edited_canvas, cv2.COLOR_BGR2RGB))
im = ImageTk.PhotoImage(image=im_out.resize(
(int(canvas_width*self.scale_val.get()),int(canvas_height*self.scale_val.get())),
Image.LANCZOS))
a = self.canvas_frame.canvas.photo = im
self.canvas_frame.canvas.itemconfig(self.canvas_image_id, image=a)
self.canvas_frame.canvas.config(scrollregion=(0, 0, canvas_width*self.scale_val.get(), canvas_height*self.scale_val.get()))
def resize_view(self, event=None):
self.update_canvas()
if self.rect:
self.write_rectangle()
def analyze_contour(self):
self.check_state_0.set(1)
self.check_state_1.set(1)
self.check_state_2.set(1)
self.check_state_3.set(1)
self.write_contour()
def write_rectangel_all(self):
im_gray = cv2.cvtColor(edited_canvas, cv2.COLOR_BGR2GRAY)
retval, im_bw = cv2.threshold(im_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, hierarchy = cv2.findContours(im_bw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
xs = 65535
ys = 65535
xe = 0
ye = 0
for i in contours:
x, y, w, h = cv2.boundingRect(i)
xs = min(xs, x)
ys = min(ys, y)
xe = max(xe, x+w)
ye = max(ye, y+h)
self.rect_start_x = xs
self.rect_start_y = ys
self.rect_end_x = xe
self.rect_end_y = ye
self.write_rectangle()
def write_rectangle_selected(self):
check_state = [0, 0, 0, 0]
check_state[0] = self.check_state_0.get()
check_state[1] = self.check_state_1.get()
check_state[2] = self.check_state_2.get()
check_state[3] = self.check_state_3.get()
if sum(check_state) == 0:
return
im_gray = cv2.cvtColor(edited_canvas, cv2.COLOR_BGR2GRAY)
retval, im_bw = cv2.threshold(im_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, hierarchy = cv2.findContours(im_bw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
im_con = edited_canvas.copy()
contour_size = []
for i in range(len(contours)):
contour_size.append(cv2.contourArea(contours[i]))
counter_size_index = np.argsort(contour_size)[::-1]
xs = []
xm = []
ys = []
ym = []
if check_state[0] and len(contours) > 0:
x, y, w, h = cv2.boundingRect(contours[counter_size_index[0]])
xs.append(x)
ys.append(y)
xm.append(x+w)
ym.append(y+h)
if check_state[1] and len(contours) > 1:
x, y, w, h = cv2.boundingRect(contours[counter_size_index[1]])
xs.append(x)
ys.append(y)
xm.append(x+w)
ym.append(y+h)
if check_state[2] and len(contours) > 2:
x, y, w, h = cv2.boundingRect(contours[counter_size_index[2]])
xs.append(x)
ys.append(y)
xm.append(x+w)
ym.append(y+h)
if check_state[3] and len(contours) > 3:
x, y, w, h = cv2.boundingRect(contours[counter_size_index[3]])
xs.append(x)
ys.append(y)
xm.append(x+w)
ym.append(y+h)
self.rect_start_x = min(xs)
self.rect_start_y = min(ys)
self.rect_end_x = max(xm)
self.rect_end_y = max(ym)
self.write_rectangle()
def write_contour(self, event=None):
check_state = [0, 0, 0, 0]
check_state[0] = self.check_state_0.get()
check_state[1] = self.check_state_1.get()
check_state[2] = self.check_state_2.get()
check_state[3] = self.check_state_3.get()
im_gray = cv2.cvtColor(edited_canvas, cv2.COLOR_BGR2GRAY)
im_bw = cv2.adaptiveThreshold(im_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)
contours, hierarchy = cv2.findContours(im_bw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
im_con = edited_canvas.copy()
contour_size = []
for i in range(len(contours)):
contour_size.append(cv2.contourArea(contours[i]))
counter_size_index = np.argsort(contour_size)[::-1]
if check_state[0] and len(contours) > 0:
cv2.drawContours(im_con, contours[counter_size_index[0]], -1, (0,255,0), 3)
if check_state[1] and len(contours) > 1:
cv2.drawContours(im_con, contours[counter_size_index[1]], -1, (255,255,0), 3)
if check_state[2] and len(contours) > 2:
cv2.drawContours(im_con, contours[counter_size_index[2]], -1, (255,0,0), 3)
if check_state[3] and len(contours) > 3:
cv2.drawContours(im_con, contours[counter_size_index[3]], -1, (255,0,255), 3)
self.update_canvas(draw_image=im_con)
def whiten_unchecked_contour(self):
global edited_canvas
check_state = [0, 0, 0, 0]
check_state[0] = self.check_state_0.get()
check_state[1] = self.check_state_1.get()
check_state[2] = self.check_state_2.get()
check_state[3] = self.check_state_3.get()
im_gray = cv2.cvtColor(edited_canvas, cv2.COLOR_BGR2GRAY)
im_bw = cv2.adaptiveThreshold(im_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)
contours, hierarchy = cv2.findContours(im_bw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
im_con = edited_canvas.copy()
contour_size = []
for i in range(len(contours)):
contour_size.append(cv2.contourArea(contours[i]))
counter_size_index = np.argsort(contour_size)[::-1]
mask = np.zeros_like(edited_canvas)
if check_state[0] and len(contours) > 0:
cv2.drawContours(mask, [contours[counter_size_index[0]]], 0, (255,255,255), -1)
if check_state[1] and len(contours) > 1:
cv2.drawContours(mask, [contours[counter_size_index[1]]], 0, (255,255,255), -1)
if check_state[2] and len(contours) > 2:
cv2.drawContours(mask, [contours[counter_size_index[2]]], 0, (255,255,255), -1)
if check_state[3] and len(contours) > 3:
cv2.drawContours(mask, [contours[counter_size_index[3]]], 0, (255,255,255), -1)
im_con = np.where(mask==255, edited_canvas, 255-mask)
edited_canvas = im_con.copy()
self.update_canvas()
self.reset_checkboxs()
def paste_image(self, event=None):
global read_image
global im_cv
global edited_canvas
global canvas_width
global canvas_height
im = ImageGrab.grabclipboard()
if isinstance(im, Image.Image):
read_image = im
im_cv = cv2.cvtColor(np.array(read_image), cv2.COLOR_RGB2BGR)
edited_canvas = im_cv.copy()
canvas_height, canvas_width, c_ = edited_canvas.shape[:3]
self.update_canvas()
def copy_image(self, event=None):
im = Image.fromarray(cv2.cvtColor(edited_canvas, cv2.COLOR_BGR2RGB))
output = io.BytesIO()
im.convert('RGB').save(output, 'BMP')
data = output.getvalue()[14:]
output.close()
send_to_clipboard(win32clipboard.CF_DIB, data)
def send_to_clipboard(clip_type, data):
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(clip_type, data)
win32clipboard.CloseClipboard()
read_image = Image.new('RGB', (10,10), (127,127,127))
canvas_width, canvas_height = read_image.size
minimal_canvas_size = (500, 400)
im_cv = cv2.cvtColor(np.array(read_image), cv2.COLOR_RGB2BGR)
edited_canvas = im_cv.copy()
root = tk.Tk()
app = Application(master=root)
app.mainloop()
長いですね。同じようなコードが繰り返されているので、もう少し短く出来るかもしれません。
動かしてみるとこんな感じです。
まだいろいろとダメなところがありますが、自分で使うだけならこれで十分かな。
公開日
広告
作ってみたカテゴリの投稿
- PythonでFizzBuzz問題をやってみた
- PythonでPDFファイルのサムネイル画像を作る
- Pythonでオブジェクトを選択してクロップするツールを作ってみた
- Pythonでデータロガーのログから瞬断を抽出してみる
- Pythonで写真の中の線を抽出してみた
- Pythonで動画から静止部分を抜き出してみた
- Pythonで測定データのピーク値を検出してみる
- Pythonで複数のCSVデータを1つのファイルにまとめてみた
- PythonとExcelでフォルダの使用量を調べてみた
- Sphinx(ablog)の後処理をする
- WordPressのブログを静的サイトに書き換えてみました
- シェルスクリプトでSphinxのビルドの前処理をする
- 数式を中置記法から後置記法(逆ポーランド記法)に変換してみた