Pythonでオブジェクトを選択してクロップするツールを作ってみた

Pythonで、図の中のオブジェクトを選択してクロップする範囲を決める、クロップツールを作ってみました。GUIはtkinterです。

目次

  1. クロップする範囲内に消したいオブジェクトがある
  2. コード

クロップする範囲内に消したいオブジェクトがある

例えば、官報とかの一部を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()

長いですね。同じようなコードが繰り返されているので、もう少し短く出来るかもしれません。

動かしてみるとこんな感じです。

まだいろいろとダメなところがありますが、自分で使うだけならこれで十分かな。

公開日

広告