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 相关文章推荐
详解Django缓存处理中Vary头部的使用
Jul 24 Python
python3使用PyMysql连接mysql数据库实例
Feb 07 Python
基于python实现名片管理系统
Nov 30 Python
Python 元组操作总结
Sep 18 Python
Pycharm 2019 破解激活方法图文详解
Oct 11 Python
Python selenium的基本使用方法分析
Dec 21 Python
详解Python Opencv和PIL读取图像文件的差别
Dec 27 Python
使用TensorFlow对图像进行随机旋转的实现示例
Jan 20 Python
python 写函数在一定条件下需要调用自身时的写法说明
Jun 01 Python
Python OpenCV读取中文路径图像的方法
Jul 02 Python
python按照list中字典的某key去重的示例代码
Oct 13 Python
Python Pygame实战之塔防游戏的实现
Mar 17 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程序中的常见漏洞进行攻击(下)
2006/10/09 PHP
php获取文件夹路径内的图片以及分页显示示例
2014/03/11 PHP
php parse_str() 函数的定义和用法
2016/05/23 PHP
php设计模式之装饰模式应用案例详解
2019/06/17 PHP
PHP基于timestamp和nonce实现的防止重放攻击方案分析
2019/07/26 PHP
用脚本调用样式的几种方法
2006/12/09 Javascript
枚举JavaScript对象的函数
2006/12/22 Javascript
jquery 指南/入门基础
2007/11/30 Javascript
JAVASCRIPT实现的WEB页面跳转以及页面间传值方法
2010/05/13 Javascript
jquery 单击li防止重复加载的实现代码
2010/12/24 Javascript
js中字符替换函数String.replace()使用技巧
2011/08/14 Javascript
javascript动态创建表格及添加数据实例详解
2015/05/13 Javascript
jQuery实现二级下拉菜单效果
2016/01/05 Javascript
jQuery UI Bootstrap是什么?
2016/06/17 Javascript
javascript与jquery动态创建html元素示例
2016/07/25 Javascript
js中遍历Map对象的方法
2016/07/27 Javascript
HTML5canvas 绘制一个圆环形的进度表示实例
2016/12/16 Javascript
详解AngularJs HTTP响应拦截器实现登陆、权限校验
2017/04/11 Javascript
利用require.js与angular搭建spa应用的方法实例
2017/07/19 Javascript
微信小程序实现指定显示行数多余文字去掉用省略号代替
2018/07/25 Javascript
Vue面试题及Vue知识点整理
2018/10/07 Javascript
JS实现利用闭包判断Dom元素和滚动条的方向示例
2019/08/26 Javascript
js实现数字跳动到指定数字
2020/08/25 Javascript
[58:37]Serenity vs Fnatic 2018国际邀请赛淘汰赛BO1 8.21
2018/08/22 DOTA
Python数据类型学习笔记
2016/01/13 Python
浅析Python中MySQLdb的事务处理功能
2016/09/21 Python
django认证系统实现自定义权限管理的方法
2018/07/16 Python
Python合并多个Excel数据的方法
2018/07/16 Python
基于django channel实现websocket的聊天室的方法示例
2019/04/11 Python
Python通用唯一标识符uuid模块使用案例
2020/09/10 Python
python基于pexpect库自动获取日志信息
2021/02/01 Python
纯HTML5+CSS3制作生日蛋糕(代码易懂)
2016/11/16 HTML / CSS
Pedro官网:新加坡时尚品牌
2019/08/27 全球购物
《翻越远方的大山》教学反思
2014/04/13 职场文书
2014个人反腐倡廉思想汇报
2014/09/15 职场文书
学雷锋倡议书
2015/01/19 职场文书