Python tkinter實(shí)現(xiàn)圖片標(biāo)注功能(完整代碼)
.tkinter
tkinter是Python下面向tk的圖形界面接口庫(kù),可以方便地進(jìn)行圖形界面設(shè)計(jì)和交互操作編程。tkinter的優(yōu)點(diǎn)是簡(jiǎn)單易用、與Python的結(jié)合度好。tkinter在Python 3.x下默認(rèn)集成,不需要額外的安裝操作;不足之處為缺少合適的可視化界面設(shè)計(jì)工具,需要通過(guò)代碼來(lái)完成窗口設(shè)計(jì)和元素布局。
Python tkinter實(shí)現(xiàn)圖片標(biāo)注代碼,代碼如下所述:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os
import sys
if sys.version_info < (3, 0):
import Tkinter as tk # 導(dǎo)入 Tkinter 庫(kù)
from tkFileDialog import askopenfilename, asksaveasfilename
else :
import tkinter as tk # 導(dǎo)入 Tkinter 庫(kù)
from tkinter.filedialog import askopenfilename, asksaveasfilename
from PIL import Image, ImageTk, ImageDraw
from time import sleep
import numpy as np
import cv2 as cv
DEF_WIDTH = 1080
DEF_HEIGHT = 720
IMAGE_HEIGHT = 720
FRAME_LEFT_WIDTH = 360
# 太小的選定區(qū)域我們需要丟棄,防止誤操作
MINI_RECT_AREA = 20
class RawImageEditor:
def __init__(self, win, img, rects):
#變量X和Y用來(lái)記錄鼠標(biāo)左鍵按下的位置
self.X = tk.IntVar(value=0)
self.Y = tk.IntVar(value=0)
self.sel = False
self.lastDraw = None
self.lastDraws = []
self.imageScale = 1.0
self.dispWidth = DEF_WIDTH # 圖片顯示區(qū)域的最大高度,寬度
self.dispHeight = DEF_HEIGHT
self.rawImage = img
self.calcImageScale(self.rawImage)
self.dispWidth = int(self.imageScale * self.rawImage.width)
self.dispHeight = int(self.imageScale * self.rawImage.height)
# 圖片縮放
self.dispImage = self.rawImage.resize((self.dispWidth, self.dispHeight))
# 選擇區(qū)域
self.selPositions = []
for r in rects :
self.selPositions.append((r[0] * self.imageScale, r[1] * self.imageScale, r[2] * self.imageScale, r[3] * self.imageScale))
#創(chuàng)建頂級(jí)組件容器
self.top = tk.Toplevel(win, width=self.dispWidth, height=self.dispHeight)
#不顯示最大化、最小化按鈕
self.top.overrideredirect(True)
# Make topLevelWindow remain on top until destroyed, or attribute changes.
self.top.attributes('-topmost', 'true')
self.canvas = tk.Canvas(self.top, bg='white', width=self.dispWidth, height=self.dispHeight)
self.tkImage = ImageTk.PhotoImage(self.dispImage)
self.canvas.create_image(self.dispWidth//2, self.dispHeight//2, image=self.tkImage)
for r in self.selPositions :
draw = self.canvas.create_rectangle(r[0], r[1], r[2], r[3], outline='green')
self.lastDraws.append(draw)
#鼠標(biāo)左鍵按下的位置
def onLeftButtonDown(event):
self.X.set(event.x)
self.Y.set(event.y)
#開(kāi)始截圖
self.sel = True
#重新繪制已經(jīng)選擇的區(qū)域
for draw in self.lastDraws :
self.canvas.delete(draw)
self.lastDraws = []
for r in self.selPositions :
draw = self.canvas.create_rectangle(r[0], r[1], r[2], r[3], outline='green')
self.lastDraws.append(draw)
self.canvas.bind('<Button-1>', onLeftButtonDown)
#鼠標(biāo)左鍵移動(dòng),顯示選取的區(qū)域
def onLeftButtonMove(event):
if not self.sel:
return
try:
#刪除剛畫(huà)完的圖形,要不然鼠標(biāo)移動(dòng)的時(shí)候是黑乎乎的一片矩形
self.canvas.delete(self.lastDraw)
except Exception as e:
pass
self.lastDraw = self.canvas.create_rectangle(self.X.get(), self.Y.get(), event.x, event.y, outline='green')
self.canvas.bind('<B1-Motion>', onLeftButtonMove)
#獲取鼠標(biāo)左鍵抬起的位置,保存區(qū)域截圖
def onLeftButtonUp(event):
self.sel = False
sleep(0.1)
#考慮鼠標(biāo)左鍵從右下方按下而從左上方抬起的截圖
left, right = sorted([self.X.get(), event.x])
top, bottom = sorted([self.Y.get(), event.y])
if (right - left) * (bottom - top) > MINI_RECT_AREA :
self.selPositions.append((left,top,right,bottom))
#self.top.destroy()
#鼠標(biāo)右鍵按下
def onRightButtonDown(event):
self.sel = False
self.top.destroy()
self.canvas.bind('<Button-2>', onRightButtonDown)
self.canvas.bind('<ButtonRelease-1>', onLeftButtonUp)
self.canvas.pack(fill=tk.BOTH, expand=tk.YES)
def calcImageScale(self, image) :
w = image.width
h = image.height
self.imageScale = 1.0
# 計(jì)算最小的縮放比例,保證原始寬高比
if w > self.dispWidth and h > self.dispHeight :
ws = self.dispWidth * 1.0 / w
hs = self.dispHeight * 1.0 / h
if ws < hs :
self.imageScale = ws
else :
self.imageScale = hs
elif w > self.dispWidth and h < self.dispHeight :
self.imageScale = self.dispWidth * 1.0 / w
elif w < self.dispWidth and h > self.dispHeight :
self.imageScale = self.dispHeight * 1.0 / h
def waitForWindow(self, win) :
win.wait_window(self.top)
def selectedPositions(self) :
# 轉(zhuǎn)換為原始像素位置
realPos = []
for r in self.selPositions :
realPos.append((r[0] / self.imageScale, r[1] / self.imageScale, r[2] / self.imageScale, r[3] / self.imageScale))
return realPos
class MainWin(tk.Tk):
def __init__(self):
if sys.version_info >= (3, 0):
super().__init__()
else :
tk.Tk.__init__(self)
self.title('圖像處理工具')
self.geometry('{}x{}'.format(DEF_WIDTH, DEF_HEIGHT))
self.rawImagePath = ''
self.rawImage = None # self.rawImage 原始圖像,未經(jīng)過(guò)縮放處理
self.transRawImage = None # self.transRawImage 經(jīng)過(guò)轉(zhuǎn)換處理之后的原始圖像,沒(méi)有經(jīng)過(guò)縮放處理
self.dispImage = None # self.dispImage 顯示圖像,可能經(jīng)過(guò)縮放處理
self.imageScale = 1.0 # 圖片縮放比例,根據(jù)縮放比例進(jìn)行顯示的時(shí)候的縮放處理,后期選擇區(qū)域的時(shí)候,需要進(jìn)行縮放還原
self.leftFrameWidth = FRAME_LEFT_WIDTH
self.frameDispHeight = DEF_HEIGHT # 整個(gè)窗口的高度
self.labelTextHeight = 20 # 文本標(biāo)簽的高度
self.btnHeight = 40 # 按鈕的高度
self.imageDispWidth = IMAGE_HEIGHT # 圖片顯示區(qū)域的最大高度,寬度
self.imageDispHeight = self.frameDispHeight / 2 - self.labelTextHeight * 2
# 選擇區(qū)域
self.liRect = []
self.rawImageEditor = None
self.setupUI()
def scaleDisplayImage(self, image) :
w = image.width
h = image.height
self.imageScale = 1.0
# 計(jì)算最小的縮放比例,保證原始寬高比
if w > self.imageDispWidth and h > self.imageDispHeight :
ws = self.imageDispWidth * 1.0 / w
hs = self.imageDispHeight * 1.0 / h
if ws < hs :
self.imageScale = ws
else :
self.imageScale = hs
elif w > self.imageDispWidth and h < self.imageDispHeight :
self.imageScale = self.imageDispWidth * 1.0 / w
elif w < self.imageDispWidth and h > self.imageDispHeight :
self.imageScale = self.imageDispHeight * 1.0 / h
# 圖片縮放
return image.resize((int(self.imageScale * w), int(self.imageScale * h)))
# 打開(kāi)圖片時(shí)使用,傳值(圖)給展示函數(shù)
def openAndDisplayImage(self):
self.rawImagePath = self.selectImageFile()
if '' != self.rawImagePath :
self.rawImage = Image.open(self.rawImagePath)
self.rawImage = self.rawImage.convert('RGBA')
self.drawRawImageDisp()
def drawListBox(self):
self.l_box.delete(0,tk.END)
for item in self.liRect:
r = '{},{},{},{}'.format(round(item[0],1), round(item[1],1), round(item[2],1), round(item[3],1))
self.l_box.insert(0, r)
def drawRawImageDisp(self, selItems=[]):
self.dispImage = self.scaleDisplayImage(self.rawImage)
self.dispImage = self.dispImage.convert('RGB')
draw = ImageDraw.Draw(self.dispImage)
for i in range(len(self.liRect)) :
r = self.liRect[i]
if i in selItems :
draw.rectangle((r[0] * self.imageScale, r[1] * self.imageScale, r[2] * self.imageScale, r[3] * self.imageScale), outline = "red")
else :
draw.rectangle((r[0] * self.imageScale, r[1] * self.imageScale, r[2] * self.imageScale, r[3] * self.imageScale), outline = "green")
img = ImageTk.PhotoImage(self.dispImage)
self.image_l_raw.config(image=img)
self.image_l_raw.image = img
def deleteSelectedItemFromListBox(self):
#print(self.l_box.get(self.l_box.curselection()))
idx = self.l_box.curselection()
if len(idx) > 0 :
kp = []
for v in range(len(self.liRect)) :
if v not in idx :
kp.append(self.liRect[v])
self.liRect = kp
self.drawListBox()
self.drawRawImageDisp()
# 打開(kāi)圖片時(shí)使用,獲得地址
def selectImageFile(self):
path = tk.StringVar()
file_entry = tk.Entry(self, state='readonly', text=path)
path_ = askopenfilename()
path.set(path_)
return file_entry.get()
def rawImageLabelClicked(self, event):
if None != self.rawImage :
if None == self.rawImageEditor :
self.rawImageEditor = RawImageEditor(self, self.rawImage, self.liRect)
self.rawImageEditor.waitForWindow(self.image_l_raw)
self.liRect = self.rawImageEditor.selectedPositions()
self.rawImageEditor = None
self.drawListBox()
self.drawRawImageDisp()
def onRectListboxSelect(self, event):
idx = self.l_box.curselection()
if len(idx) > 0 :
self.drawRawImageDisp(idx)
def drawTransImageDisp(self):
transImage = self.scaleDisplayImage(self.transRawImage)
transImage = transImage.convert('L')
img = ImageTk.PhotoImage(transImage)
self.image_l_trans.config(image=img)
self.image_l_trans.image = img
def doTransRawImage(self):
self.transRawImage = Image.new('L', (self.rawImage.width, self.rawImage.height))
for r in self.liRect :
im = self.rawImage.crop(r)
cv_im = cv.cvtColor(np.asarray(im), cv.COLOR_RGB2BGR)
hsv = cv.cvtColor(cv_im, cv.COLOR_BGR2HSV)
_, _, v = cv.split(hsv)
avg = np.average(v.flatten())
pixels = im.load()
for j in range(im.height) :
for i in range(im.width) :
hv = v[j,i]
if hv < avg * 1.2:
#im.putpixel((i, j), 0) # slow
pixels[i, j] = 0
'''else :
im.putpixel((i, j), (255, 255, 255, 255))'''
self.transRawImage.paste(im, (int(r[0]),int(r[1])), mask = None)
self.drawTransImageDisp()
def onTransRawImageBtnClicked(self):
if None != self.rawImage :
self.doTransRawImage()
def onSaveTransRawImageBtnClicked(self):
if None != self.transRawImage :
ext = os.path.splitext(self.rawImagePath)[-1]
(path,name) = os.path.split(self.rawImagePath)
filename = asksaveasfilename(title = '保存圖片', initialfile = name, filetypes = (("jpeg files","*{}".format(ext)), ("all files","*.*")))
if '' != filename :
self.transRawImage.save(filename)
def setupUI(self):
# 左邊菜單欄
left_f = tk.Frame(self, height=self.frameDispHeight, width=self.leftFrameWidth)
left_f.pack(side=tk.LEFT)
# 各種功能按鈕名稱及位置
btnOpen = tk.Button(left_f, text='打開(kāi)圖像', command=self.openAndDisplayImage)
btnOpen.place(y=25, x=30, width=300, height=self.btnHeight)
btnTrans = tk.Button(left_f, text='處理圖像', command=self.onTransRawImageBtnClicked)
btnTrans.place(y=85, x=30, width=300, height=self.btnHeight)
l_selRect = tk.Label(left_f, text = '鼠標(biāo)選定區(qū)域')
l_selRect.place(x=0, y=165, width=self.leftFrameWidth, height=self.labelTextHeight)
'''列表'''
self.l_box = tk.Listbox(left_f) # 創(chuàng)建兩個(gè)列表組件
self.l_box.place(x=0, y=165+self.labelTextHeight, width=self.leftFrameWidth, height=270)
self.l_box.bind('<<ListboxSelect>>', self.onRectListboxSelect)
self.drawListBox()
# 刪除選定項(xiàng)
btnDel = tk.Button(left_f, text='刪除選定項(xiàng)', command=self.deleteSelectedItemFromListBox)
btnDel.place(y=460, x=30, width=300, height=self.btnHeight)
btnSave = tk.Button(left_f, text='保存結(jié)果', command=self.onSaveTransRawImageBtnClicked)
btnSave.place(y=550, x=30, width=300, height=self.btnHeight)
# 右側(cè)圖像顯示欄
right_f = tk.Frame(self, height=self.frameDispHeight, width=self.imageDispWidth)
right_f.pack(side=tk.RIGHT)
l_rawT = tk.Label(right_f, text = '原始圖片')
l_rawT.place(x=0, y=0, width=self.imageDispWidth, height=self.labelTextHeight)
self.image_l_raw = tk.Label(right_f, relief='ridge')
self.image_l_raw.place(x=0, y=self.labelTextHeight, width=self.imageDispWidth, height=self.imageDispHeight)
self.image_l_raw.bind("<Button-1>",self.rawImageLabelClicked)
l_transT = tk.Label(right_f, text = '處理后圖片')
l_transT.place(x=0, y=self.labelTextHeight + self.imageDispHeight, width=self.imageDispWidth, height=self.labelTextHeight)
self.image_l_trans = tk.Label(right_f, relief='ridge')
self.image_l_trans.place(x=0, y=self.labelTextHeight + self.imageDispHeight + self.labelTextHeight, width=self.imageDispWidth, height=self.imageDispHeight)
if __name__ == '__main__' :
win = MainWin()
# 進(jìn)入消息循環(huán)
win.mainloop()
總結(jié)
以上所述是小編給大家介紹的Python tkinter實(shí)現(xiàn)圖片標(biāo)注功能,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)歡迎給我留言,小編會(huì)及時(shí)回復(fù)大家的!
- python實(shí)現(xiàn)簡(jiǎn)單圖片物體標(biāo)注工具
- vue如何使用js對(duì)圖片進(jìn)行點(diǎn)擊標(biāo)注圓點(diǎn)并記錄它的坐標(biāo)
- 一款基于jQuery的圖片場(chǎng)景標(biāo)注提示彈窗特效
- 一款簡(jiǎn)單的jQuery圖片標(biāo)注效果附源碼下載
- vue下如何利用canvas實(shí)現(xiàn)在線圖片標(biāo)注
- jquery.picsign圖片標(biāo)注組件實(shí)例詳解
- 在React中用canvas對(duì)圖片標(biāo)注的實(shí)現(xiàn)
- 微信小程序給圖片做動(dòng)態(tài)標(biāo)注的實(shí)例分享
相關(guān)文章
Django form表單與請(qǐng)求的生命周期步驟詳解
這篇文章主要介紹了Django-form表單與請(qǐng)求的生命周期,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
基于Numpy.convolve使用Python實(shí)現(xiàn)滑動(dòng)平均濾波的思路詳解
這篇文章主要介紹了Python極簡(jiǎn)實(shí)現(xiàn)滑動(dòng)平均濾波(基于Numpy.convolve)的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-05-05
Python第三方庫(kù)jieba庫(kù)與中文分詞全面詳解
jieba庫(kù)是一款優(yōu)秀的Python第三方中文分詞庫(kù),jieba支持三種分詞模式:精確模式、全模式和搜索引擎模式,下面這篇文章主要給大家介紹了關(guān)于Python第三方庫(kù)jieba庫(kù)與中文分詞的相關(guān)資料,需要的朋友可以參考下2022-07-07
Python實(shí)戰(zhàn)之手勢(shì)識(shí)別控制電腦音量
這篇文章主要為大家詳細(xì)介紹了一個(gè)Python OpenCV的實(shí)戰(zhàn)小項(xiàng)目——手勢(shì)識(shí)別控制電腦音量,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-05-05
手把手教你將Flask應(yīng)用封裝成Docker服務(wù)的實(shí)現(xiàn)
這篇文章主要介紹了手把手教你將Flask應(yīng)用封裝成Docker服務(wù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Django基于客戶端下載文件實(shí)現(xiàn)方法
這篇文章主要介紹了Django基于客戶端下載文件實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
Python+Jmeter實(shí)現(xiàn)自動(dòng)化性能壓測(cè)的流程步驟
性能測(cè)試是一個(gè)全棧工程師/架構(gòu)師必會(huì)的技能之一,只有學(xué)會(huì)性能測(cè)試,才能根據(jù)得到的測(cè)試報(bào)告進(jìn)行分析,找到系統(tǒng)性能的瓶頸所在,而這也是優(yōu)化架構(gòu)設(shè)計(jì)中重要的依據(jù),本文給大家介紹了Python+Jmeter實(shí)現(xiàn)自動(dòng)化性能壓測(cè)的流程步驟,需要的朋友可以參考下2024-05-05

