Python tkinter实现图片标注功能(完整代码)


Posted in Python onDecember 08, 2019

.tkinter

tkinter是Python下面向tk的图形界面接口库,可以方便地进行图形界面设计和交互操作编程。tkinter的优点是简单易用、与Python的结合度好。tkinter在Python 3.x下默认集成,不需要额外的安装操作;不足之处为缺少合适的可视化界面设计工具,需要通过代码来完成窗口设计和元素布局。

Python tkinter实现图片标注代码,代码如下所述:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os
import sys
if sys.version_info < (3, 0): 
  import Tkinter as tk # 导入 Tkinter 库
  from tkFileDialog import askopenfilename, asksaveasfilename
else :
  import tkinter as tk # 导入 Tkinter 库
  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
# 太小的选定区域我们需要丢弃,防止误操作
MINI_RECT_AREA = 20 
class RawImageEditor:
  def __init__(self, win, img, rects):
    #变量X和Y用来记录鼠标左键按下的位置
    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 # 图片显示区域的最大高度,宽度
    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))
    # 选择区域
    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))
    #创建顶级组件容器
    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)
    #鼠标左键按下的位置
    def onLeftButtonDown(event):
      self.X.set(event.x)
      self.Y.set(event.y)
      #开始截图
      self.sel = True
      #重新绘制已经选择的区域
      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)
    #鼠标左键移动,显示选取的区域
    def onLeftButtonMove(event):
      if not self.sel:
        return
      try:
        #删除刚画完的图形,要不然鼠标移动的时候是黑乎乎的一片矩形
        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)
    #获取鼠标左键抬起的位置,保存区域截图
    def onLeftButtonUp(event):
      self.sel = False
      sleep(0.1)
      #考虑鼠标左键从右下方按下而从左上方抬起的截图
      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()
    #鼠标右键按下
    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
    # 计算最小的缩放比例,保证原始宽高比
    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) : 
    # 转换为原始像素位置
    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 原始图像,未经过缩放处理
    self.transRawImage = None # self.transRawImage 经过转换处理之后的原始图像,没有经过缩放处理
    self.dispImage = None # self.dispImage 显示图像,可能经过缩放处理
    self.imageScale = 1.0 # 图片缩放比例,根据缩放比例进行显示的时候的缩放处理,后期选择区域的时候,需要进行缩放还原
    self.leftFrameWidth = FRAME_LEFT_WIDTH
    self.frameDispHeight = DEF_HEIGHT # 整个窗口的高度
    self.labelTextHeight = 20 # 文本标签的高度
    self.btnHeight = 40 # 按钮的高度
    self.imageDispWidth = IMAGE_HEIGHT # 图片显示区域的最大高度,宽度
    self.imageDispHeight = self.frameDispHeight / 2 - self.labelTextHeight * 2
    # 选择区域
    self.liRect = []
    self.rawImageEditor = None
    self.setupUI()
  def scaleDisplayImage(self, image) :
    w = image.width
    h = image.height
    self.imageScale = 1.0
    # 计算最小的缩放比例,保证原始宽高比
    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)))
 
  # 打开图片时使用,传值(图)给展示函数
  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() 
  # 打开图片时使用,获得地址
  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='打开图像', 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 = '鼠标选定区域')
    l_selRect.place(x=0, y=165, width=self.leftFrameWidth, height=self.labelTextHeight)
    '''列表'''
    self.l_box = tk.Listbox(left_f) # 创建两个列表组件
    self.l_box.place(x=0, y=165+self.labelTextHeight, width=self.leftFrameWidth, height=270)
    self.l_box.bind('<<ListboxSelect>>', self.onRectListboxSelect)
    self.drawListBox()
    # 删除选定项
    btnDel = tk.Button(left_f, text='删除选定项', command=self.deleteSelectedItemFromListBox)
    btnDel.place(y=460, x=30, width=300, height=self.btnHeight)
    btnSave = tk.Button(left_f, text='保存结果', command=self.onSaveTransRawImageBtnClicked)
    btnSave.place(y=550, x=30, width=300, height=self.btnHeight)
    # 右侧图像显示栏
    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()
  # 进入消息循环
  win.mainloop()

总结

以上所述是小编给大家介绍的Python tkinter实现图片标注功能,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!

Python 相关文章推荐
Python修改Excel数据的实例代码
Nov 01 Python
Python单例模式实例分析
Jan 14 Python
python将unicode转为str的方法
Jun 21 Python
Python环境搭建之OpenCV的步骤方法
Oct 20 Python
python3实现基于用户的协同过滤
May 31 Python
Python实现的txt文件去重功能示例
Jul 07 Python
200行python代码实现2048游戏
Jul 17 Python
python list多级排序知识点总结
Oct 23 Python
浅谈Keras中shuffle和validation_split的顺序
Jun 19 Python
Python远程linux执行命令实现
Nov 11 Python
python中yield的用法详解
Jan 13 Python
刚学完怎么用Python实现定时任务,转头就跑去撩妹!
Jun 05 Python
Python中six模块基础用法
Dec 08 #Python
python实现布隆过滤器及原理解析
Dec 08 #Python
python实现图片二值化及灰度处理方式
Dec 07 #Python
matplotlib实现显示伪彩色图像及色度条
Dec 07 #Python
python中利用matplotlib读取灰度图的例子
Dec 07 #Python
matplotlib.pyplot画图并导出保存的实例
Dec 07 #Python
python 实现turtle画图并导出图片格式的文件
Dec 07 #Python
You might like
PHP随机数生成代码与使用实例分析
2011/04/08 PHP
那些年一起学习的PHP(一)
2012/03/21 PHP
PHP屏蔽蜘蛛访问代码及常用搜索引擎的HTTP_USER_AGENT
2013/03/06 PHP
php根据用户语言跳转相应网页
2015/11/04 PHP
PHP对象链式操作实现原理分析
2016/10/09 PHP
php连接mysql数据库最简单的实现方法
2019/09/24 PHP
JS控制文本框textarea输入字数限制的方法
2013/06/17 Javascript
jquery中get和post的简单实例
2014/02/04 Javascript
js网页实时倒计时精确到秒级
2014/02/10 Javascript
三种检测iPhone/iPad设备方向的方法
2014/04/23 Javascript
手写的一个兼容各种浏览器的javascript getStyle函数(获取元素的样式)
2014/06/06 Javascript
jQuery实现长按按钮触发事件的方法
2015/02/02 Javascript
基于jQuery实现的旋转彩圈实例
2015/06/26 Javascript
JS组件Bootstrap Select2使用方法解析
2016/05/30 Javascript
jquery中用jsonp实现搜索框功能
2016/10/18 Javascript
Nodejs下DNS缓存问题浅析
2016/11/16 NodeJs
彻底学会Angular.js中的transclusion
2017/03/12 Javascript
Vue制作Todo List网页
2017/04/26 Javascript
angularjs中判断ng-repeat是否迭代完的实例
2018/09/12 Javascript
layer关闭当前窗口页面以及确认取消按钮的方法
2019/09/09 Javascript
jQuery 实现DOM元素拖拽交换位置的实例代码
2020/07/14 jQuery
[57:59]EG vs Secret 2018国际邀请赛淘汰赛BO3 第一场 8.22
2018/08/23 DOTA
Python3实现Web网页图片下载
2016/01/28 Python
Python cookbook(数据结构与算法)将序列分解为单独变量的方法
2018/02/13 Python
python简单商城购物车实例代码
2018/03/15 Python
python实现简单图片物体标注工具
2019/03/18 Python
几个适合python初学者的简单小程序,看完受益匪浅!(推荐)
2019/04/16 Python
python issubclass 和 isinstance函数
2019/07/25 Python
基于Python的OCR实现示例
2020/04/03 Python
Python分析微信好友性别比例和省份城市分布比例的方法示例【基于itchat模块】
2020/05/29 Python
Pytorch 卷积中的 Input Shape用法
2020/06/29 Python
html5 touch事件实现页面上下滑动效果【附代码】
2016/03/10 HTML / CSS
FC-Moto丹麦:欧洲最大的摩托车服装和头盔商店之一
2019/08/20 全球购物
LACOSTE波兰官网:Polo衫、服装和鞋类
2020/09/29 全球购物
优秀毕业生找工作自荐信
2014/06/23 职场文书
老公婚前保证书
2015/02/28 职场文书