python - Tkinter:如何从缩放图像中获得正确的边界框?
问题描述
我正在用 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()
我认为解决方案是 indef MouseClick
和 in show_image
。我的想法是使用与从原始图像中提取图块(或 bbox)相同的方法show_image
。
我试图这样做,但我没有得到任何结果。我不明白我该怎么做,所以我在这里寻求帮助。
解决方案
我用这段代码做到了,其中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)
推荐阅读
- vagrant - Windows主机上的VMware同步文件夹速度?
- node.js - 招摇“响应验证失败:值应为数组/对象但不是”
- sql - 将输入时间戳与间隔进行比较,得到操作员错误
- applescript - 如何在不使用终端的情况下双击打开这个 .sh 文件?
- serenity-js - 需要一个如何在 serenity-js 上单击按钮的示例
- python - RPi.GPIO.wait_for_edge(4, GPIO.FALLING) 检测按钮的按下和释放
- d3.js - d3/cola:类 UML 图表的布局配置
- javascript - 试图确定一个对象是否在数组中
- java - Java Mission Control - 哪些方法在套接字 I/O 上花费时间?
- apache-spark - Spark 2.4.0 to_avro / from_avro 反序列化不适用于 Seq().toDF()