【pyqt】自制的图片裁剪分割器

前半个月左右,因为需要,要对一些图片进行裁剪。其实主要是因为要对一些瓦片素材进行处理,例如一堆瓦片里我只想要那么几块瓦片,所以需要裁剪一下。然后网上下载了十几个裁剪器,不是垃圾就是收费,或者就是垃圾+收费,弄的我火气直接顶满




本来预想着这玩意儿应该一星期左右就能做好,结果两个多星期才完成,期间有消极怠工玩了几天,但也有能力问题,例如裁剪时用的数据结构不对(例如使用的XJ_Rect的效果并不好以及XJ_Pair成为摆设)弄的工作量+++效率- - -,例如布局时因为一些控件被挤到角落就花几个小时查这查那的,算了,反正就是碰到了很多问题,花的时间巨长无比。

py代码文件非常多(因为太多,所以在文末再附上),额,12个。。因为太多,就用了UML建模工具(Software Ideas Modeler)简单弄弄理清下关系。模块关系如下,箭头指向的是主调用方。
模块关系图
那些标注“可以直接使用”的模块是作为QT自定义控件存在的,也就是可以抽离出来投入到别的地方使用(前提是把那些依赖的模块也给带进来)
然后然后,也没啥要说的了,大概就是这样了。搞了那么久的一个玩意儿,现在也完全没啥精力瞎扯些啥了,代码量1k+,看着都觉得小有成就的呢,摸了,搞别的了。虽然可以做出个exe文件直接造福白嫖党,但我要怎么传呢?不喜欢毒盘,也没啥个人网盘,所以我就省事不搞那么多直接把py脚本代码全贴下去,想用的自己全烤过去(那个字故意打错的)然后自个儿跑跑就知道了。

虽然按理说应该po到gayhub(其实是github)上分享比较合适,而不应该把代码沾在这里,但,没怎么用过gayhub(画外音:大学生?就这?),之后再学学怎么用gayhub,先浪了。
葱葱葱



哦对了对了,先附上运行结果作为展示(因为图片都太大,所以我把main以外的图片的预览都弄的很小,查看大图需要点开):

【main.py】
主窗口,或者说主函数,不想深究那么多的就直接运行这个。
简单说明功能:
1、左侧是文件选择列表,粗体是文件,斜体是文件夹,返回上一路径的项在列表底部
2、左侧上方是目录路径选择,当要找的文件的路径过于复杂时用这个选择路径会更快一些
3、中间是裁剪器,左键拖拽裁剪区,中键移动图片,右键清除裁剪区,双击右键最大化裁剪区,双击中键最大化图片显示和初始化图片位置(当图片不知道被弄到哪里的时候使用)。
4、中间下方按钮导出裁剪结果裁剪的数值显示
5、右侧是参数控制,数值可以通过滚轮或者双击进行修改,颜色通过点击进行修改
宽高比”的其中一个值为0就为自由裁剪,
分割数”就是将裁剪区按几行几列分割出子图,
边界粗细”和“边界颜色”是在裁剪区的线不够显眼时进行设置的,
流畅裁剪”是当“宽高比”均不为零(即约束裁剪)时有效,当流畅裁剪没选中时,裁剪区的大小会严格控制在“宽高比”的整数比上,例如宽高比设置为16:10,那么裁剪区的宽高只会是16:10的整数倍,如果图片很小的话裁剪会有明显停顿不流畅,看个人需要。
马赛克背景”的“颜色1”和“颜色2”是马赛克背景的色块,当图片看上去并不理想的时候(有时候图片透明部分很多会造成混色看不清的问题)就对其进行调整
马赛克背景”的“格子大小”是马赛克背景的格子大小,当进行瓦片素材裁剪时,这个数值调到适当大小时会非常舒服(如16、32、48之类的)

【XJ_LineEdit.py】
非常水的一个控件,单纯贪方便而设,不咋好用,算是封装最烂的一个了,能跑就行。

【XJ_TreeView.py】
双击某一行将发出“doubleClicked”信号,具体的使用可以看代码。
功能比较简单,完成的是"增查改",“删”和“排序”功能我不想做,因为没啥需求,想搞的自己搞去。

【XJ_NumInput.py】
作为一个控件而存在,当数值发生变化时发送信号"valueChange"。
控件内的数值可以通过滚轮修改,也可以通过双击进行内容编辑。

【XJ_ColorChoose.py】
作为一个控件而存在,当点击时弹出颜色选择框,当颜色发生变化时发送信号“valueChange”。

【XJ_SampleCropper.py】
名字意思很简单,就是样例裁剪器,不能直接投入使用(因为其中的参数要修改的话非常麻烦。
左键拖拽裁剪区,右键清除裁剪区,中键拖拽图片。
双击右键最大化裁剪区,双击中键最大化图片显示和初始化图片位置(当图片不知道被弄到哪里的时候使用)。

【XJ_Cropper.py】
功能比较完善的裁剪器,其实就是main.py的阉割版。不能选择文件,不能导出结果,点击下方的按钮时只会发送btnClick_saveCrops信号






鸡汤来咯 代码来咯

#【main.py】
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QRect
from PyQt5.QtGui import QPainter,QPen,QColor,QImage,QFont
from PyQt5.QtWidgets import *
import cv2.cv2 as cv2

from XJ_Cropper import *
from XJ_TreeView import *
from XJ_LineEdit import *

class XJ_Main(QMainWindow):
    def __init__(self,parent=None):
        super(XJ_Main, self).__init__(parent)

        self.__canvas=XJ_Cropper()        
        self.__files=XJ_TreeView()
        self.__path=XJ_LineEdit(self,'当前路径:',os.getcwd().replace('\\','/')+'/','选择目录')#路径名的反斜杠全改为斜杠
        self.__path.SetEnable_Input(False)
        self.__filesType=['.png','.jpg','.bmp']#文件类型
        
        #设置布局
        vbox=QVBoxLayout()
        vbox.addWidget(self.__path)
        vbox.addWidget(self.__files)
        vbox.setStretchFactor(self.__files,1)
        widget=QWidget()
        widget.setLayout(vbox)
        spt=QSplitter(Qt.Horizontal)
        spt.addWidget(widget)
        spt.addWidget(self.__canvas)
        self.setCentralWidget(spt)
        
        #绑定响应函数
        self.__path.SetClicked_Button(self.__ClickPath)
        self.__files.doubleClicked.connect(self.__DoubleClickFiles)
        self.__canvas.btnClick_saveCrops.connect(self.__SaveCrops)

        #其他的初始化
        self.__LoadDir()#初始化self.__files的内容
        
    def __ClickPath(self):#选择目录
        path=QFileDialog.getExistingDirectory(self,"选择目录").replace('\\','/')#路径名的反斜杠全改为斜杠
        if(len(path)):
            path=path+'/'#加上一个斜杠
            self.__path.SetText_Input(path)
            self.__LoadDir()

    def __LoadPict(self,path):#加载路径下的图片
        cvImg = cv2.imdecode(np.fromfile(path,dtype=np.uint8),cv2.IMREAD_UNCHANGED)
        qtImg=GetQPixmap(cvImg).toImage()
        self.__canvas.Load_Img(qtImg)
        self.__canvas.update()
        
    def __DoubleClickFiles(self,abc):#双击文件列表,如果是文件则更新裁剪图,如果是目录则更新目录。
        file=self.__files.GetCurrIter().GetData()[0]
        path=os.path.join(self.__path.GetText_Input(),file).replace('\\','/')#路径名的反斜杠全改为斜杠

        if(os.path.isfile(path)):
            self.__LoadPict(path)
        else:
            if(file=='..'):#返回上一级目录
                path=self.__path.GetText_Input()
                path=path[:path[:-1].rfind('/')+1]
            if(os.path.exists(path)):#以防万一的
                self.__path.SetText_Input(path)
                self.__LoadDir()
        
    def __LoadDir(self):#加载path下的文件及目录到XJ_TreeView中
        self.__files.Clear()
        iter=self.__files.GetHead()
        
        path=self.__path.GetText_Input()
        files=[]
        folders=[]
        for f in os.listdir(path):
            if os.path.isdir(os.path.join(path,f)):
                folders.append(f)
            elif self.__filesType.count(f[-4:])!=0:
                files.append(f)

        font=QFont()
        font.setBold(True)
        font.setPixelSize(18)
        for f in files:
            iter.AppendRow([f]).SetFont(0,font)
        font.setBold(False)
        font.setItalic(True)
        font.setPixelSize(14)
        for f in folders:
            iter.AppendRow([f]).SetFont(0,font)
        if(path.count('/')>1):
            iter.AppendRow(['..']).SetFont(0,font)#返回上一级目录

    def __SaveCrops(self):#导出图片
        crops=self.__canvas.Get_CropImgs()
        if(crops):
            path=QFileDialog.getExistingDirectory(self,"选择目录")
            if(path):
                file=self.__files.GetCurrIter().GetData()
                file='空白图片.png' if file==None else file[0]
                file=file[:file.rfind('.')]
                path=os.path.join(path,file).replace('\\','/')

                path_copy=path
                num=1
                while(os.path.exists(path) and os.path.isdir(path)):
                    path=path_copy+'_'+str(num)
                    num=num+1   
                os.makedirs(path)
                
                for row in range(len(crops)):
                    for col in range(len(crops[row])):
                        file=os.path.join(path,'[{},{}].png'.format(row,col))
                        crops[row][col].save(file)
                QMessageBox.information(None,r'图片导出结束','文件夹路径为:\n{}'.format(path))
        else:
            QMessageBox.information(None,r'失败','截图不存在')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    
    win=XJ_Main()
    win.resize(1000,500)
    win.show()
    win.setWindowTitle("XJ图片裁剪器")
 
    sys.exit(app.exec())
#【XJ_TreeView.py】
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt,QModelIndex,QItemSelectionModel,pyqtSignal
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import *

class XJ_TreeView(QTreeView):
    class XJ_Iter:
        def __init__(self,iter):
            self.__iter=iter

        def AppendRow(self,data):#添加数据(一个列表
            lst=[]
            for i in data:
                lst.append(QStandardItem(str(i)))
                lst[-1].setEditable(False)
            self.__iter.appendRow(lst)
            return XJ_TreeView.XJ_Iter(lst[0])
        
        def Copy(self):
            return XJ_TreeView.XJ_Iter(self.__iter)
            
        def Back(self):#返回上一级(返回失败则返回false
            if(type(self.__iter)==QStandardItemModel):
                return False
            if(self.__iter.parent()==None):
                self.__iter=self.__iter.model()
            else:
                self.__iter=self.__iter.parent()
            return True
            
        def Next(self,i):#进入下一级(进入失败则返回false
            if(0<=i<self.__iter.rowCount()):
                if(type(self.__iter)!=QStandardItemModel):
                    self.__iter=self.__iter.child(i,0)
                else:
                    self.__iter=self.__iter.itemFromIndex(self.__iter.index(i,0))
                return True
            else:
                return False
                
        def GetData(self):#获取数据(一个列表
            if(type(self.__iter)!=QStandardItem):
                return None
            result=[]
            model=self.__iter.model()
            index=self.__iter.index().siblingAtColumn(0)
            i=1
            while(index.isValid()):
                result.append(model.itemFromIndex(index).text())
                index=index.siblingAtColumn(i)
                i+=1
            return result
            
        def SetData(self,i,data):#设置第i个单元格的内容(设置失败则返回false
            if(type(self.__iter)==QStandardItemModel):
                return False
            model=self.__iter.model()
            index=self.__iter.index().siblingAtColumn(i)
            if(index.isValid()==False):
                return False
            item=model.itemFromIndex(index)
            item.setText(str(data))
            return True
            
        def SetFont(self,i,font):#设置第i个单元格的字体样式(设置失败则返回false
            if(type(self.__iter)==QStandardItemModel):
                return False
            model=self.__iter.model()
            index=self.__iter.index().siblingAtColumn(i)
            item=model.itemFromIndex(index)
            item.setFont(font)
            return True
        
        def SetCheckable(self,flag):#设置是否显示复选框(设置失败则返回false),复选框为双态
            if(type(self.__iter)==QStandardItemModel):
                return False
            self.__iter.setCheckable(flag)
            if(flag==False):
                self.__iter.setCheckState(-1)
            return True
            
        def GetCheckable(self):#获取复选框状态(如果获取失败则返回false),返回结果为:【全选:Qt.Checked(2)、部分选:Qt.PartiallyChecked(1)、不选:Qt.Unchecked(0)】
            if(type(self.__iter)==QStandardItemModel):
                return None
            return self.__iter.checkState()
        
        def SetEditable(self,i,flag):#设置第i个单元格可以双击修改(设置失败则返回false
            if(type(self.__iter)==QStandardItemModel):
                return False
            model=self.__iter.model()
            index=self.__iter.index().siblingAtColumn(i)
            item=model.itemFromIndex(index)
            item.setEditable(flag)
            return True

    doubleClicked=pyqtSignal(XJ_Iter)#槽信号,当前行双击时发送信号(如果行未发生变化则不发送
    
    def __init__(self,parent=None):
        super(XJ_TreeView, self).__init__(parent)
        model=QStandardItemModel(self)
        self.setModel(model)
        self.headerLables=[]
        self.__currIndex=None#用于判定选中行是否发生变化
        
    def GetHead(self):#返回根部迭代器
        return XJ_TreeView.XJ_Iter(self.model())
    def Clear(self):
        width=[]
        for i in range(self.model().columnCount()):
            width.append(self.columnWidth(i))
        self.model().clear()
        self.model().setHorizontalHeaderLabels(self.headerLables)
        for i in range(len(width)):
            self.setColumnWidth(i,width[i])
    def SetHeaderLabels(self,labels):#设置列头
        self.headerLables=labels
        self.model().setHorizontalHeaderLabels(labels)
    
    def GetCurrIter(self):#获取当前行的迭代器
        return XJ_TreeView.XJ_Iter(self.model().itemFromIndex(self.currentIndex()))

    def mouseDoubleClickEvent(self,event):
        currIndex=self.currentIndex()
        self.setCurrentIndex(currIndex)
        
        if(self.__currIndex!=currIndex):
            self.__currIndex=currIndex
            self.doubleClicked.emit(self.GetCurrIter())
        event.accept()


if __name__ == '__main__':
    app = QApplication(sys.argv)

    tv=XJ_TreeView()
    tv.show()
    
    print(tv.GetCurrIter().GetData())


    iter=tv.GetHead()
    iter.AppendRow(['AAA','333']).AppendRow(['AAAAA',''])
    iter.AppendRow(['BBB','222'])
    iter.AppendRow(['CCC','111'])
    
    print(tv.GetCurrIter().GetData())
    
    tv.doubleClicked.connect(lambda line:print(line.GetData()))
    sys.exit(app.exec())

#【XJ_Cropper.py】
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QRect,QSize,pyqtSignal
from PyQt5.QtGui import QPainter,QPen,QColor,QImage,QFont
from PyQt5.QtWidgets import *

from XJ_SampleCropper import *
from XJ_CropperSetting import XJ_SettingForCropper,XJ_SettingForMosaicBg

class XJ_Cropper(QWidget):#图片裁剪器(装入了按钮控件便于参数的设置
    btnClick_saveCrops=pyqtSignal()#当保存文件的按钮按下时发送信号
    
    def __init__(self,width=500,height=500,parent=None):
        super(XJ_Cropper, self).__init__(parent)
        self.resize(width,height)
        self.setFocusPolicy(Qt.ClickFocus|Qt.WheelFocus)#让控件可以获取焦点

        self.__cropper=XJ_SampleCropper(self)
        self.__setting_cropper
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
可以使用QPixmap和QGraphicsScene来实现图片裁剪。 首先,需要在PyQt5中导入以下模块: ```python from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem, QFileDialog, QGraphicsRectItem, QGraphicsItem from PyQt5.QtGui import QPixmap, QCursor from PyQt5.QtCore import Qt, QRectF ``` 接着,创建一个QGraphicsScene对象并将其设置为QGraphicsView的场景: ```python scene = QGraphicsScene() view = QGraphicsView(scene) ``` 然后,使用QFileDialog打开一张图片,将其加载到QPixmap对象中,并将该对象设置为QGraphicsPixmapItem的Pixmap: ```python filename, _ = QFileDialog.getOpenFileName(None, "Open Image", "", "Image Files (*.png *.jpg *.bmp)") if filename: pixmap = QPixmap(filename) pixmap_item = QGraphicsPixmapItem(pixmap) scene.addItem(pixmap_item) ``` 接下来,使用QGraphicsRectItem创建一个裁剪框,并将其添加到场景中: ```python rect_item = QGraphicsRectItem() rect_item.setPen(Qt.red) rect_item.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) rect_item.setZValue(1) scene.addItem(rect_item) ``` 为了使裁剪框可以拖动和缩放,需要在鼠标按下事件和鼠标移动事件中添加代码: ```python def mousePressEvent(event): if event.button() == Qt.LeftButton: rect_item.setFlags(QGraphicsItem.ItemIsMovable) rect_item.setCursor(QCursor(Qt.ClosedHandCursor)) else: rect_item.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) rect_item.setCursor(QCursor(Qt.ArrowCursor)) def mouseMoveEvent(event): if rect_item.flags() & QGraphicsItem.ItemIsMovable: rect_item.setCursor(QCursor(Qt.ClosedHandCursor)) rect = QRectF(event.scenePos(), rect_item.rect().size()) if rect.intersects(pixmap_item.boundingRect()): rect_item.setRect(rect) ``` 最后,添加一个按钮,当用户点击该按钮时,将裁剪后的图片保存到本地: ```python def save_image(): rect = rect_item.rect() cropped_pixmap = pixmap.copy(rect.toRect()) cropped_pixmap.save("cropped_image.png", "PNG") button = QPushButton("Save Image") button.clicked.connect(save_image) ``` 完整代码如下所示: ```python from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem, QFileDialog, QGraphicsRectItem, QGraphicsItem, QPushButton from PyQt5.QtGui import QPixmap, QCursor from PyQt5.QtCore import Qt, QRectF app = QApplication([]) scene = QGraphicsScene() view = QGraphicsView(scene) filename, _ = QFileDialog.getOpenFileName(None, "Open Image", "", "Image Files (*.png *.jpg *.bmp)") if filename: pixmap = QPixmap(filename) pixmap_item = QGraphicsPixmapItem(pixmap) scene.addItem(pixmap_item) rect_item = QGraphicsRectItem() rect_item.setPen(Qt.red) rect_item.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) rect_item.setZValue(1) scene.addItem(rect_item) def mousePressEvent(event): if event.button() == Qt.LeftButton: rect_item.setFlags(QGraphicsItem.ItemIsMovable) rect_item.setCursor(QCursor(Qt.ClosedHandCursor)) else: rect_item.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) rect_item.setCursor(QCursor(Qt.ArrowCursor)) def mouseMoveEvent(event): if rect_item.flags() & QGraphicsItem.ItemIsMovable: rect_item.setCursor(QCursor(Qt.ClosedHandCursor)) rect = QRectF(event.scenePos(), rect_item.rect().size()) if rect.intersects(pixmap_item.boundingRect()): rect_item.setRect(rect) view.mousePressEvent = mousePressEvent view.mouseMoveEvent = mouseMoveEvent def save_image(): rect = rect_item.rect() cropped_pixmap = pixmap.copy(rect.toRect()) cropped_pixmap.save("cropped_image.png", "PNG") button = QPushButton("Save Image") button.clicked.connect(save_image) view.show() button.show() app.exec_() ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值