安装依赖
$ python -m pip install --upgrade pip --user $ python -m pip install Pillow --user $ python -m pip install Numpy --user $ python -m pip install OpenCV-Python --user
源代码:
#!/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, askdirectory else : import tkinter as tk # 导入 Tkinter 库 from tkinter.filedialog import askopenfilename, askdirectory from PIL import Image, ImageTk, ImageDraw from time import sleep import numpy as np import cv2 as cv import collections 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) #center window on desktop nScreenWid, nScreenHei = self.top.maxsize() winPos = '%sx%s+%s+%s' % (int(self.top.winfo_reqwidth()), int(self.top.winfo_reqheight()), int((nScreenWid - self.top.winfo_reqwidth())/2), int((nScreenHei - self.top.winfo_reqheight())/2)) self.top.geometry(winPos) # 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): if not self.sel: return 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 = 18 # 文本标签的高度 self.btnHeight = 30 # 按钮的高度 self.brightnessScale = tk.StringVar() # 亮度比 self.brightnessScale.set('1.0') self.defSavePath = '' # 默认保存路径 self.imageDispWidth = IMAGE_HEIGHT # 图片显示区域的最大高度,宽度 self.imageDispHeight = self.frameDispHeight / 2 - self.labelTextHeight * 2 self.liRect = collections.OrderedDict() # 选择区域 self.rawImageEditor = None self.currentListBoxSelIdx = 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 loadImageCfgFile(self, imf): (path, name) = os.path.split(imf) cfgname = imf + '.txt' if (not os.path.exists(cfgname)) or (not os.path.isfile(cfgname)) : cfgname = os.path.join(path, 'mask', name) + '.txt' if (not os.path.exists(cfgname)) or (not os.path.isfile(cfgname)) : cfgname = os.path.join(path, 'cfg', name) + '.txt' self.liRect.clear() if os.path.exists(cfgname) and os.path.isfile(cfgname) : with open(cfgname, "r") as f: lines = f.readlines() for line in lines: rs = line.split(',') r = (float(rs[0].strip()), float(rs[1].strip()), float(rs[2].strip()), float(rs[3].strip())) if len(rs) > 4 : self.liRect[r] = rs[4].strip() else : self.liRect[r] = '' # 打开图片时使用,传值(图)给展示函数 def openAndDisplayImage(self): imF = self.selectImageFile() if '' != imF : self.rawImagePath = imF self.loadImageCfgFile(self.rawImagePath) self.drawListBox() self.rawImage = Image.open(self.rawImagePath) self.rawImage = self.rawImage.convert('RGBA') self.drawRawImageDisp() self.image_l_trans.image = None self.transRawImage = None def drawListBox(self): self.l_box.delete(0, tk.END) for r in self.liRect.keys(): r = '{},{},{},{}'.format(round(r[0],1), round(r[1],1), round(r[2],1), round(r[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) rs = list(self.liRect.keys()) for i in range(len(rs)) : r = rs[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 and (None == self.rawImageEditor) : ro = collections.OrderedDict() rs = self.liRect.keys() for i in range(len(rs)) : if i not in idx : r = rs[i] ro[r] = self.liRect[r] self.liRect = ro 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) and (None == self.rawImageEditor) : self.rawImageEditor = RawImageEditor(self, self.rawImage, self.liRect.keys()) self.rawImageEditor.waitForWindow(self.image_l_raw) rs = self.rawImageEditor.selectedPositions() trs = collections.OrderedDict() for k in rs : if k in self.liRect : trs[k] = self.liRect[k] else: trs[k] = '' self.liRect = trs self.rawImageEditor = None self.drawListBox() self.drawRawImageDisp() def onRectListboxSelect(self, event): idx = self.l_box.curselection() if len(idx) > 0 and (self.currentListBoxSelIdx != idx) : self.currentListBoxSelIdx = idx ctx = '' rs = list(self.liRect.keys()) for i in range(len(self.currentListBoxSelIdx)) : v = self.currentListBoxSelIdx[i] k = rs[v] ctx = ctx + self.liRect[k] if i < len(self.currentListBoxSelIdx)-1 : ctx = ctx + ',' self.edtListBoxSel.delete(0, tk.END) self.edtListBoxSel.insert(0, ctx) self.drawRawImageDisp(idx) def onEdtListBoxComplete(self, event): idx = self.l_box.curselection() if (len(idx) > 0) and (self.currentListBoxSelIdx == idx) : ctx = self.edtListBoxSel.get().strip() ctx = ctx.split(',') while len(idx) > len(ctx) : ctx.append('') rs = list(self.liRect.keys()) n = 0 for i in idx : r = rs[i] self.liRect[r] = ctx[n] n = n + 1 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)) rs = self.liRect.keys() for r in rs : 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() scale = float(self.brightnessScale.get()) for j in range(im.height) : for i in range(im.width) : hv = v[j,i] if hv < avg * scale: #im.putpixel((i, j), 0) # 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 saveTransCfg(self, pth): (path,name) = os.path.split(self.rawImagePath) cfg = os.path.join(pth, 'cfg') if os.path.exists(cfg) and os.path.isdir(cfg) : cfgname = os.path.join(cfg, name) + '.txt' else : cfgname = os.path.join(pth, name) + '.txt' with open(cfgname, "w") as f: for r in self.liRect.keys() : v = self.liRect[r] line = '{},{},{},{},{}\n'.format(r[0], r[1], r[2], r[3], v) f.write(line) def saveCombinedImage(self, pth): im_A = cv.cvtColor(np.array(self.rawImage), cv.COLOR_RGB2BGR) im_B = cv.cvtColor(np.array(self.transRawImage), cv.COLOR_RGB2BGR) im_AB = np.concatenate([im_A, im_B], 1) ext = os.path.splitext(self.rawImagePath)[-1] ext = ext.lower() name = os.path.basename(self.rawImagePath) sp = name.split('.')[:-1] comb = os.path.join(pth, 'comb') if os.path.exists(comb) and os.path.isdir(comb) : name = sp[0] + ext pth = comb else : name = sp[0] + '_comb' + ext path_AB = os.path.join(pth, name) cv.imwrite(path_AB, im_AB) def saveTransImage(self, path): ext = os.path.splitext(self.rawImagePath)[-1] ext = ext.lower() (path,name) = os.path.split(self.rawImagePath) mask = os.path.join(path, 'mask') fn = name.split('.')[:-1] if os.path.exists(mask) and os.path.isdir(mask) : fn = fn[0] + ext else : fn = fn[0] + '_mask' + ext im = os.path.join(mask, fn) if not im.endswith(ext) : im = im + ext self.transRawImage.save(im) def saveTransInformation(self, path): if '' != path : self.saveTransImage(path) self.saveTransCfg(path) self.saveCombinedImage(path) def onSaveTransRawImageBtnClicked(self): if None != self.transRawImage : (path,name) = os.path.split(self.rawImagePath) dirname = askdirectory(initialdir = os.path.expanduser(path), title = '保存结果') if '' != dirname : self.defSavePath = dirname self.saveTransInformation(self.defSavePath) def onDefaultSaveTransRawImageBtnClicked(self) : if (None != self.transRawImage) and ('' != self.defSavePath) : self.saveTransInformation(self.defSavePath) 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=75, x=30, width=300, height=self.btnHeight) l_selRect = tk.Label(left_f, text = '鼠标选定区域') l_selRect.place(x=0, y=125, width=self.leftFrameWidth, height=self.labelTextHeight) '''列表''' self.l_box = tk.Listbox(left_f, exportselection=False) # 创建两个列表组件 self.l_box.place(x=1, y=125+self.labelTextHeight, width=self.leftFrameWidth-2, height=270) self.l_box.bind('<<ListboxSelect>>', self.onRectListboxSelect) self.drawListBox() # 选中列表注释 ledt = tk.Label(left_f, text = '选中区域内容注释(回车结束):', anchor = 'w') ledt.place(x=1, y=420, width=self.leftFrameWidth-2) self.edtListBoxSel = tk.Entry(left_f) self.edtListBoxSel.place(x=1, y=440, width=self.leftFrameWidth-2) self.edtListBoxSel.bind('<Return>', self.onEdtListBoxComplete) # 亮度比例调节 tkScale = tk.Scale(left_f, from_=0.2, to=1.8, orient=tk.HORIZONTAL, resolution=0.1, length=self.leftFrameWidth-2, variable=self.brightnessScale, label = '亮度比:') tkScale.place(x=1, y=480) # 删除选定项 btnDel = tk.Button(left_f, text='删除选定项', command=self.deleteSelectedItemFromListBox) btnDel.place(y=550, x=30, width=300, height=self.btnHeight) btnSave = tk.Button(left_f, text='保存结果', command=self.onSaveTransRawImageBtnClicked) btnSave.place(y=600, x=30, width=300, height=self.btnHeight) btnDefSave = tk.Button(left_f, text='默认或上次位置保存', command=self.onDefaultSaveTransRawImageBtnClicked) btnDefSave.place(y=650, 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,一个入门级极简单的图像处理工具(一)
- python,一个入门级极简单的图像处理工具(二)
- Python tkinter 实现本地打开图片进行标注
- Python tkinter Canvas画布完全攻略(超级详细)
- 用python实现选择截图区域
- Python GUI编程(Tkinter)
- Python OpenCV实现鼠标画框
- 用python实现选择截图区域
- OpenCV-Python 选择ROI
- python3.6+opencv3.4实现鼠标交互查看图片像素
- Tkinter Toplevel always in front
- TypeError: __str__ returned non-string (type instance)
- Python Canvas.create_image Examples
- Tkinter tkFileDialog module
- python 调用super()初始化报错“TypeError: super() takes at least 1 argument”
- python – 使用PIL中的Image.point()方法来操作像素数据
- Using the Image.point() method in PIL to manipulate pixel data
- Pixel to pixel edit using PIL and Image.point
- PixelAccess Class
- python tkinter界面居中显示的方法
- MAC 系统中,Tkinter 无法用 中文输入法 输入中文
- macOS下Python的tkinter库Entry输入框无法输入中文问题的原因及解决办法
- IDLE and tkinter with Tcl/Tk on macOS