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中文问题解决方法(总结了多位前人经验,初学者必看)
Mar 13 Python
python的类变量和成员变量用法实例教程
Aug 25 Python
python3使用SMTP发送简单文本邮件
Jun 19 Python
python一行sql太长折成多行并且有多个参数的方法
Jul 19 Python
pandas 透视表中文字段排序方法
Nov 16 Python
对python多线程SSH登录并发脚本详解
Feb 14 Python
Python高级特性 切片 迭代解析
Aug 23 Python
python代码如何实现余弦相似性计算
Feb 09 Python
如何实现在jupyter notebook中播放视频(不停地展示图片)
Apr 23 Python
解决在keras中使用model.save()函数保存模型失败的问题
May 21 Python
python使用bs4爬取boss直聘静态页面
Oct 10 Python
python实现图片批量压缩
Apr 24 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
广播爱好者需要了解的天线知识
2021/03/01 无线电
用PHP和MySQL保存和输出图片
2006/10/09 PHP
PHP简介
2006/10/09 PHP
使用php计算排列组合的方法
2013/11/13 PHP
php断点续传之如何分割合并文件
2014/03/22 PHP
php从数据库读取数据,并以json格式返回数据的方法
2018/08/21 PHP
JavaScript 组件之旅(一)分析和设计
2009/10/28 Javascript
js动态设置鼠标事件示例代码
2013/10/30 Javascript
javascript对话框使用方法(警告框 javascript确认框 提示框)
2014/01/07 Javascript
JavaScript中统计Textarea字数并提示还能输入的字符
2014/06/10 Javascript
node.js中使用socket.io制作命名空间
2014/12/15 Javascript
js模仿php中strtotime()与date()函数实现方法
2015/08/11 Javascript
JavaScript中对JSON对象的基本操作示例
2016/05/21 Javascript
js实现动态创建的元素绑定事件
2016/07/19 Javascript
jQuery树形插件jquery.simpleTree.js用法分析
2016/09/05 Javascript
DataTables+BootStrap组合使用Ajax来获取数据并且动态加载dom的方法(排序,过滤,分页等)
2016/11/09 Javascript
MUI顶部选项卡的用法(tab-top-webview-main)详解
2017/10/08 Javascript
javascript实现QQ空间相册展示源码
2017/12/12 Javascript
关于element-ui的隐藏组件el-scrollbar的使用
2019/05/29 Javascript
[02:17]快乐加倍!DOTA2食人魔魔法师至宝+迎霜节活动上线
2019/12/22 DOTA
实例解析Python设计模式编程之桥接模式的运用
2016/03/02 Python
Python科学计算之Pandas详解
2017/01/15 Python
Python 查看文件的读写权限方法
2018/01/23 Python
python自动化报告的输出用例详解
2018/05/30 Python
python ftp 按目录结构上传下载的实现代码
2018/09/12 Python
python图片剪裁代码(图片按四个点坐标剪裁)
2020/03/10 Python
Python 通过正则表达式快速获取电影的下载地址
2020/08/17 Python
图解CSS3制作圆环形进度条的实例教程
2016/05/26 HTML / CSS
KIKO MILANO俄罗斯官网:意大利领先的化妆品和护肤品品牌
2021/01/09 全球购物
应聘教师推荐信
2013/10/31 职场文书
甜品店的创业计划书范文
2014/01/02 职场文书
前厅收银主管岗位职责
2014/02/04 职场文书
优秀大学生职业生涯规划书
2014/02/27 职场文书
企业领导对照检查材料
2014/08/20 职场文书
您对思维方式了解多少?
2019/12/09 职场文书
SpringCloud超详细讲解Feign声明式服务调用
2022/06/21 Java/Android