Tkinter: Как получить правильную ограничивающую рамку из увеличенного изображения? - PullRequest
0 голосов
/ 10 октября 2019

Я пишу программу на python tkinter, которая позволяет вам выбирать определенные области на изображении. Это изображение можно увеличить и перетащить внутрь Canvas. Но проблема в том, что я не могу получить правильные координаты выборок и извлечь миниатюры из исходных изображений, используя маленькое.

Вот мой код и снимок экрана:

from tkinter import *
from tkinter import ttk
from tkinter import messagebox as mb
import glob
import os

from PIL import Image, ImageTk
import cv2
from src.lsh_utils import get_lsh, query_lsh
from src.io_utils import get_config


COLORS = ['red', 'blue', 'olive', 'teal', 'cyan', 'green', 'black']
CONFIG = get_config('conf.json')


class AutoScrollbar(ttk.Scrollbar):
    ''' A scrollbar that hides itself if it not needed.
        Works only if you use the grid geometry manager '''
    def set(self, lo, hi):
        if float(lo) <= 0.0 and float(hi) >= 1.0:
            self.grid_remove()
        else:
            self.grid()
            ttk.Scrollbar.set(self, lo, hi)

    def pack(self, **kw):
        raise TclError('Cannot use pack with this widget')

    def place(self, **kw):
        raise TclError('Cannot use place with this widget')


class Zoom_Advanced(ttk.Frame):
    ''' Advanced zoom of the image '''
    def __init__(self, mainframe, path):
        ''' Initialize the main Frame '''
        ttk.Frame.__init__(self, master=mainframe)
        self.master.title('Zoom with mouse wheel')
        # Vertical and horizontal scrollbars for canvas
        vbar = AutoScrollbar(self.master, orient='vertical')
        hbar = AutoScrollbar(self.master, orient='horizontal')
        vbar.grid(row=0, column=1, sticky='ns', rowspan=4)
        hbar.grid(row=1, column=0, sticky='we')
        # Create canvas and put image on it
        self.canvas = Canvas(self.master, highlightthickness=0,
                                xscrollcommand=hbar.set, yscrollcommand=vbar.set)
        self.canvas.grid(row=0, column=0, sticky='nswe')
        self.canvas.update()  # wait till canvas is created
        vbar.configure(command=self.scroll_y)  # bind scrollbars to the canvas
        hbar.configure(command=self.scroll_x)
        # Make the canvas expandable
        self.master.rowconfigure(0, weight=1)
        self.master.columnconfigure(0, weight=1)
        # Bind events to the Canvas
        self.canvas.bind('<Configure>', self.show_image)  # canvas is resized
        self.canvas.bind('<Button-3>', self.move_from)
        self.canvas.bind("<Button-1>", self.mouseClick)
        self.canvas.bind('<B3-Motion>',     self.move_to)
        self.canvas.bind("<Motion>", self.mouseMove)
        self.canvas.bind('<MouseWheel>', self.wheel)  # with Windows and MacOS, but not Linux
        self.canvas.bind('<Button-5>',   self.wheel)  # only with Linux, wheel scroll down
        self.canvas.bind('<Button-4>',   self.wheel)  # only with Linux, wheel scroll up
        self.master.bind("<Escape>", self.cancelBBox)  # press <Espace> to cancel current bbox
        self.master.bind("s", self.cancelBBox)
        self.master.bind("a", self.prevImage)  # press 'a' to go backforward
        self.master.bind("d", self.nextImage)  # press 'd' to go forward

        self.side_container = Frame(self.master)
        self.side_container.grid_rowconfigure(0, weight=1)
        self.side_container.grid_columnconfigure(0, weight=1)
        self.side_container.grid(row=0, column=2, sticky='nse')

        self.imscale = 1.0  # scale for the canvaas image
        self.delta = 1.3  # zoom magnitude

        self.lb1 = Label(self.side_container, text='Bounding boxes:')
        self.lb1.grid(row=0, column=0, sticky='nwe')
        self.listbox = Listbox(self.side_container)
        self.listbox.grid(row=1, column=0, sticky='nwe')
        self.btnDel = Button(self.side_container, text='Delete', command=self.delBBox)
        self.btnDel.grid(row=2, column=0, sticky='nwe')
        self.btnClear = Button(self.side_container, text='ClearAll', command=self.clearBBox)
        self.btnClear.grid(row=3, column=0, sticky='nwe')

        # control panel for image navigation
        self.ctrPanel = Frame(self.side_container)
        self.ctrPanel.grid(row=4, column=0, columnspan=2, sticky='we')
        self.prevBtn = Button(self.ctrPanel, text='<< Prev', width=10, command=self.prevImage)
        self.prevBtn.pack(side=LEFT, padx=5, pady=3)
        self.nextBtn = Button(self.ctrPanel, text='Next >>', width=10, command=self.nextImage)
        self.nextBtn.pack(side=LEFT, padx=5, pady=3)
        self.progLabel = Label(self.ctrPanel, text="Progress:     /    ")
        self.progLabel.pack(side=LEFT, padx=5)
        self.tmpLabel = Label(self.ctrPanel, text="Go to Image No.")
        self.tmpLabel.pack(side=LEFT, padx=5)
        self.idxEntry = Entry(self.ctrPanel, width=5)
        self.idxEntry.pack(side=LEFT)
        self.goBtn = Button(self.ctrPanel, text='Go', command=self.gotoImage)
        self.goBtn.pack(side=LEFT)
        self.runBtn = Button(self.ctrPanel, text='Run', command=self.run)
        self.runBtn.pack(side=LEFT, padx=5, pady=3)

        self.imageDir = ''
        self.imageList = []
        self.egDir = ''
        self.egList = []
        self.outDir = ''
        self.cur = 0
        self.total = 0
        self.category = 0
        self.imagename = ''
        self.labelfilename = ''
        self.tkimg = None
        self.cla_can_temp = []
        self.detection_images_path = CONFIG.get('DETECTIONS_PATH', 'data\\detections')
        self.orig_images_path = CONFIG.get('IMAGE_DIR')
        self.viewer = None
        self._new_window = None

        # initialize mouse state
        self.STATE = {}
        self.STATE['click'] = 0
        self.STATE['x'], self.STATE['y'] = 0, 0

        # reference to bbox
        self.bboxIdList = []
        self.bboxId = None
        self.bboxList = []
        self.hl = None
        self.vl = None

        self.disp = Label(self.ctrPanel, text='')
        self.disp.pack(side=RIGHT)

        self.loadDir()
        # self.show_image()

    def scroll_y(self, *args, **kwargs):
        ''' Scroll canvas vertically and redraw the image '''
        self.canvas.yview(*args, **kwargs)  # scroll vertically
        self.show_image()  # redraw the image

    def scroll_x(self, *args, **kwargs):
        ''' Scroll canvas horizontally and redraw the image '''
        self.canvas.xview(*args, **kwargs)  # scroll horizontally
        self.show_image()  # redraw the image

    def move_from(self, event):
        ''' Remember previous coordinates for scrolling with the mouse '''
        self.canvas.scan_mark(event.x, event.y)

    def move_to(self, event):
        ''' Drag (move) canvas to the new position '''
        self.canvas.scan_dragto(event.x, event.y, gain=1)
        self.show_image()  # redraw the image

    def wheel(self, event):
        ''' Zoom with mouse wheel '''
        x = self.canvas.canvasx(event.x)
        y = self.canvas.canvasy(event.y)
        bbox = self.canvas.bbox(self.container)  # get image area
        if bbox[0] < x < bbox[2] and bbox[1] < y < bbox[3]: pass  # Ok! Inside the image
        else: return  # zoom only inside image area
        scale = 1.0
        # Respond to Linux (event.num) or Windows (event.delta) wheel event
        if event.num == 5 or event.delta == -120:  # scroll down
            i = min(self.width, self.height)
            if int(i * self.imscale) < 30: return  # image is less than 30 pixels
            self.imscale /= self.delta
            scale        /= self.delta
        if event.num == 4 or event.delta == 120:  # scroll up
            i = min(self.canvas.winfo_width(), self.canvas.winfo_height())
            if i < self.imscale: return  # 1 pixel is bigger than the visible area
            self.imscale *= self.delta
            scale        *= self.delta
        self.canvas.scale('all', x, y, scale, scale)  # rescale all canvas objects
        self.show_image()

    def show_image(self, event=None):
        ''' Show image on the Canvas '''
        bbox1 = self.canvas.bbox(self.container)  # get image area
        # Remove 1 pixel shift at the sides of the bbox1
        bbox1 = (bbox1[0] + 1, bbox1[1] + 1, bbox1[2] - 1, bbox1[3] - 1)
        bbox2 = (self.canvas.canvasx(0),  # get visible area of the canvas
                 self.canvas.canvasy(0),
                 self.canvas.canvasx(self.canvas.winfo_width()),
                 self.canvas.canvasy(self.canvas.winfo_height()))
        bbox = [min(bbox1[0], bbox2[0]), min(bbox1[1], bbox2[1]),  # get scroll region box
                max(bbox1[2], bbox2[2]), max(bbox1[3], bbox2[3])]

        print(bbox)

        if bbox[0] == bbox2[0] and bbox[2] == bbox2[2]:  # whole image in the visible area
            bbox[0] = bbox1[0]
            bbox[2] = bbox1[2]
        if bbox[1] == bbox2[1] and bbox[3] == bbox2[3]:  # whole image in the visible area
            bbox[1] = bbox1[1]
            bbox[3] = bbox1[3]

        print(bbox2, bbox1)

        self.canvas.configure(scrollregion=bbox)  # set scroll region
        x1 = max(bbox2[0] - bbox1[0], 0)  # get coordinates (x1,y1,x2,y2) of the image tile
        y1 = max(bbox2[1] - bbox1[1], 0)
        x2 = min(bbox2[2], bbox1[2]) - bbox1[0]
        y2 = min(bbox2[3], bbox1[3]) - bbox1[1]

        print(x1, y1, x2, y2)

        if int(x2 - x1) > 0 and int(y2 - y1) > 0:  # show image if it in the visible area
            x = min(int(x2 / self.imscale), self.width)   # sometimes it is larger on 1 pixel...
            y = min(int(y2 / self.imscale), self.height)  # ...and sometimes not

            print(x, y)

            image = self.image.crop((int(x1 / self.imscale), int(y1 / self.imscale), x, y))
            self.imagetk = ImageTk.PhotoImage(image.resize((int(x2 - x1), int(y2 - y1))))
            imageid = self.canvas.create_image(max(bbox2[0], bbox1[0]), max(bbox2[1], bbox1[1]),
                                               anchor='nw', image=self.imagetk)
            self.canvas.lower(imageid)  # set image into background
            self.canvas.imagetk = self.imagetk  # keep an extra reference to prevent garbage-collection

    def run(self):
        labels = self._compare_labels()
        lsh = get_lsh(CONFIG.get('LSH_DATASET'))
        if not labels:
            return

        for file, bboxes in labels.items():
            im = cv2.imread(file)
            h, w, _ = im.shape
            print(w, h)
            rois = []

            for box in bboxes:
                x1 = int(int(box[0])/600*w)
                y1 = int(int(box[1])/800*h)
                x2 = int(int(box[2])/600*w)
                y2 = int(int(box[3])/800*h)

                roi = im[y1:y2, x1:x2].copy()


                print(file, [n[0][1] for n in query_lsh(lsh, roi)])


                # cv2.imshow(str(box), roi)

        # cv2.waitKey()

    # def _update_labels_boxes(self, w, h):
    #     for i, bbox in enumerate(self.listbox):
    #         x1 = int(int(bbox[0]) / 768 * w)
    #         y1 = int(int(bbox[1]) / 768 * h)
    #         x2 = int(int(bbox[2]) / 768 * w)
    #         y2 = int(int(bbox[3]) / 768 * h)
    #

    def _compare_labels(self):
        label_dict = dict()
        if not self.imageDir:
            return None
        for label_file in glob.glob(os.path.join(self.imageDir, '*.txt')):
            with open(label_file, 'r') as f:
                filename = ''.join([os.path.splitext(label_file)[0], '.jpg'])
                label_dict[filename] = [tuple(l.split()) for l in f.readlines()]

        return label_dict

    def loadDir(self, dbg=False):
        # get image list
        self.imageDir = self.orig_images_path
        # print self.imageDir
        # print self.category
        self.imageList = glob.glob(os.path.join(self.imageDir, '*.JPG'))
        print(self.imageList)
        if len(self.imageList) == 0:
            print('No .JPG images found in the specified dir!')
            return

        # default to the 1st image in the collection
        self.cur = 1
        self.total = len(self.imageList)

        # set up output dir
        self.outDir = self.imageDir
        if not os.path.exists(self.outDir):
            os.mkdir(self.outDir)

        self.loadImage()
        print('%d images loaded from %s' % (self.total, self.orig_images_path))

    def new_window(self, path):
        self.newWindow = Toplevel(self.master)
        # frame = Frame(self.newWindow)
        self.new_panel = Canvas(self.newWindow, cursor='tcross')

        self.new_img = ImageTk.PhotoImage(Image.open(path).resize((600, 800)))
        self.new_panel.pack()
        self.new_panel.config(width=max(self.new_img.width(), 400), height=max(self.new_img.height(), 400))
        self.new_panel.create_image(0, 0, image=self.new_img, anchor=NW)

        return self.new_panel

    def loadImage(self):
        # load image
        imagepath = self.imageList[self.cur - 1]
        self.img = Image.open(imagepath).resize((600, 800))

        detect_image_path = os.path.join(self.detection_images_path, 'detect_'+os.path.basename(imagepath))

        if not os.path.exists(detect_image_path):
            mb.showerror(f'image {detect_image_path} doesn\' exists')
            self.nextImage(save=False)

        self.new_window(detect_image_path)

        # cv2.imshow(f'{detect_image_path}', detect_image_path)
        self.image = Image.open(path)  # open image
        self.width, self.height = self.image.size

        # Put image into container rectangle and use it to set proper coordinates to the image
        self.container = self.canvas.create_rectangle(0, 0, self.width, self.height, width=0)

        self.show_image()

        # load labels
        self.clearBBox()
        self.imagename = os.path.split(imagepath)[-1].split('.')[0]
        labelname = self.imagename + '.txt'
        self.labelfilename = os.path.join(self.outDir, labelname)
        if os.path.exists(self.labelfilename):
            with open(self.labelfilename) as f:
                for (i, line) in enumerate(f):
                    # tmp = [int(t.strip()) for t in line.split()]
                    tmp = line.split()
                    self.bboxList.append(tuple(tmp))
                    tmpId = self.canvas.create_rectangle(int(tmp[0]), int(tmp[1]),
                                                         int(tmp[2]), int(tmp[3]),
                                                         width=2,
                                                         outline=COLORS[(len(self.bboxList) - 1) % len(COLORS)])
                    # print tmpId
                    self.bboxIdList.append(tmpId)
                    self.listbox.insert(END, '(%d, %d) -> (%d, %d)' % (int(tmp[0]), int(tmp[1]),
                                                                       int(tmp[2]), int(tmp[3])))
                    self.listbox.itemconfig(len(self.bboxIdList) - 1,
                                            fg=COLORS[(len(self.bboxIdList) - 1) % len(COLORS)])

    def saveImage(self):
        if self.newWindow:
            self.newWindow.destroy()

        with open(self.labelfilename, 'w') as f:
            for bbox in self.bboxList:
                f.write(' '.join(map(str, bbox)) + '\n')
        print('Image No. %d saved' % (self.cur))

    def mouseClick(self, event):
        new_x, new_y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        if self.STATE['click'] == 0:
            self.STATE['x'], self.STATE['y'] = new_x, new_y
        else:
            x1, x2 = min(self.STATE['x'], new_x), max(self.STATE['x'], new_x)
            y1, y2 = min(self.STATE['y'], new_y), max(self.STATE['y'], new_y)

            x, y = int(x2 / self.imscale), int(y2 / self.imscale)

            print(self.imscale, (int(x1 / self.imscale), int(y1 / self.imscale), x, y))
            im = self.image.crop((int(x1 / self.imscale), int(y1 / self.imscale), x, y))
            im.show()

            self.bboxList.append((int(x1 / self.imscale), int(y1 / self.imscale), x, y))
            self.bboxIdList.append(self.bboxId)
            self.bboxId = None
            self.listbox.insert(END, '(%d, %d) -> (%d, %d)' % (x1, y1, x2, y2))
            self.listbox.itemconfig(len(self.bboxIdList) - 1, fg=COLORS[(len(self.bboxIdList) - 1) % len(COLORS)])
        self.STATE['click'] = 1 - self.STATE['click']

    def mouseMove(self, event):
        new_x, new_y = int(self.canvas.canvasx(event.x)), int(self.canvas.canvasy(event.y))
        self.disp.config(text='x: %d, y: %d' % (new_x, new_y))
        if self.canvas:
            if self.hl:
                self.canvas.delete(self.hl)
            self.hl = self.canvas.create_line(0, new_y, self.canvas['width'], new_y, width=2)
            if self.vl:
                self.canvas.delete(self.vl)
            self.vl = self.canvas.create_line(new_x, 0, new_x, self.canvas['height'], width=2)
        if 1 == self.STATE['click']:
            if self.bboxId:
                self.canvas.delete(self.bboxId)
            self.bboxId = self.canvas.create_rectangle(self.STATE['x'], self.STATE['y'],
                                                          new_x, new_y,
                                                          width=2,
                                                          outline=COLORS[len(self.bboxList) % len(COLORS)])

    def cancelBBox(self, event):
        if 1 == self.STATE['click']:
            if self.bboxId:
                self.canvas.delete(self.bboxId)
                self.bboxId = None
                self.STATE['click'] = 0

    def delBBox(self):
        sel = self.listbox.curselection()
        if len(sel) != 1:
            return
        idx = int(sel[0])
        self.canvas.delete(self.bboxIdList[idx])
        self.bboxIdList.pop(idx)
        self.bboxList.pop(idx)
        self.listbox.delete(idx)

    def clearBBox(self):
        for idx in range(len(self.bboxIdList)):
            self.canvas.delete(self.bboxIdList[idx])
        self.listbox.delete(0, len(self.bboxList))
        self.bboxIdList = []
        self.bboxList = []

    def prevImage(self, event=None):
        self.saveImage()
        if self.cur > 1:
            self.cur -= 1
            self.loadImage()

    def nextImage(self, event=None, save=True):
        if save:
            self.saveImage()
        if self.cur < self.total:
            self.cur += 1
            self.loadImage()

    def gotoImage(self):
        idx = int(self.idxEntry.get())
        if 1 <= idx <= self.total:
            self.saveImage()
            self.cur = idx
            self.loadImage()

path = 'data\\detections\\detect_1.jpg'  # place path to your image here
root = Tk()
root.geometry('1280x720')
app = Zoom_Advanced(root, path=path)
root.mainloop()

GUI

Я думаю, что решение в def MouseClick и в show_image. Я думаю, использовать те же методы извлечения плитки (или bbox) из исходного изображения, что и в show_image.

Я пытался это сделать, но я не получил никаких результатов. Я не понимаю, как я могу это сделать, поэтому я ищу здесь помощь.

1 Ответ

0 голосов
/ 17 октября 2019

Я сделал это с помощью этого кода, где self.container - это прямоугольник вокруг изображения с его шириной и высотой, а self.imscale - это масштаб изображения.

    ...
    new_x, new_y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
    if self.STATE['click'] == 0:
        self.STATE['x'], self.STATE['y'] = new_x, new_y
    else:
        x1, x2 = min(self.STATE['x'], new_x), max(self.STATE['x'], new_x)
        y1, y2 = min(self.STATE['y'], new_y), max(self.STATE['y'], new_y)

        self.canvas.create_text((x1 + x2) // 2, (y1 + y2) // 2, text=self.bbox_num)

        bbox = self.canvas.bbox(self.container)

        x1 = int((x1 - bbox[0]) / self.imscale)
        y1 = int((y1 - bbox[1]) / self.imscale)
        x2 = int((x2 - bbox[0]) / self.imscale)
        y2 = int((y2 - bbox[1]) / self.imscale)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...