【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=XJ_SettingForCropper(self)
        self.__setting_bg=XJ_SettingForMosaicBg(self)
        self.__text1=QLabel('0x0',self)
        self.__text2=QLabel('',self)
        self.__button=QPushButton('输出裁剪结果',self)
        
        #设置布局
        vbox1=QVBoxLayout()#装进裁剪器下方文本(__text1和__text2)
        vbox1.addWidget(self.__text1)
        vbox1.addWidget(self.__text2)
        hbox1=QHBoxLayout()#装进裁剪器下方控件(按钮self.__button和文本vbox1)
        hbox1.addWidget(self.__button)
        hbox1.addStretch(1)
        hbox1.addLayout(vbox1)
        vbox1=QVBoxLayout()#装进裁剪器以及裁剪器下方控件(裁剪器self.__cropper和下方控件hbox1)
        vbox1.addWidget(self.__cropper)
        vbox1.addLayout(hbox1)
        vbox2=QVBoxLayout()#装进裁剪器右侧的裁剪设置(__setting_cropper和__setting_bg)
        vbox2.addWidget(self.__setting_cropper)
        vbox2.addWidget(self.__setting_bg)
        vbox2.addStretch(1)
        frame1=QFrame()
        frame1.setLayout(vbox1)
        frame2=QFrame()
        frame2.setLayout(vbox2)
        box=QHBoxLayout()
        box.addWidget(frame1)
        box.addWidget(frame2)
        self.setLayout(box)
        
        self.frame1=frame1
        
        #控制控件大小
        box.setStretchFactor(frame1,1)
        vbox1.setStretchFactor(self.__cropper,1)
        
        #设置一些样式
        font=QFont()
        font.setBold(True)
        font.setPixelSize(24)
        self.__text1.setFont(font)
        self.__text1.setStyleSheet("QLabel{color:rgb(192,32,128);}")#设置颜色
        self.__text1.setAlignment(Qt.AlignVCenter|Qt.AlignRight)#设置居中靠右
        font.setPixelSize(20)
        self.__text2.setFont(font)
        self.__text2.setStyleSheet("QLabel{color:rgb(192,32,128);}")#设置颜色
        self.__text2.setAlignment(Qt.AlignVCenter|Qt.AlignRight)#设置居中靠右
        self.__button.setFont(font)
        self.__button.setStyleSheet("QPushButton{border-radius:5px;border:2px solid rgb(192,32,128);color:rgb(192,32,128);} QPushButton:hover{border-color: green}")#设置颜色
        
        frame1.setObjectName('frame1')
        frame2.setObjectName('frame2')
        frame1.setStyleSheet(".QFrame#frame1{border-radius:10px;border:3px solid rgb(96,192,255)}")
        frame2.setStyleSheet(".QFrame#frame2{border-radius:10px;border:3px solid rgb(96,192,255)}")
        frame2.setFrameShape(QFrame.Box)#设置外边框
        
        #绑定信号
        self.__setting_cropper.valueChange.connect(self.__SettingChange_Cropper)
        self.__setting_bg.valueChange.connect(self.__SettingChange_Bg)
        self.__cropper.valueChange.connect(self.__ValueChange_Cropper)
        self.__button.clicked.connect((lambda self:lambda :self.btnClick_saveCrops.emit())(self))
        
        #更新设置
        self.__UpdateSetting()
        
    def Load_Img(self,Img):#设置图片
        self.__cropper.SetImg(Img)

    def Load_Setting(self,path):#加载配置
        pass
        
    def Save_Setting(self,path):#保存配置
        pass
    
    def Get_CropImgs(self):#获取裁剪结果
        return self.__cropper.Get_Crops()
        
    def __SettingChange_Cropper(self,val):#当关于裁剪器的设置发生变更时调用该函数
        cpr=self.__cropper
        setting=cpr.Get_Setting().cropper
        if(val[0]=='宽' or val[0]=='高'):
            val=self.__setting_cropper.Get_AspectRatio()
            cpr.Set_AspectRatio((val['宽'],val['高']))
        elif(val[0]=='行'):
            setting.rowCnt=val[1]
        elif(val[0]=='列'):
            setting.colCnt=val[1]
        elif(val[0]=='外线粗细'):
            setting.thickness_Border=val[1]
        elif(val[0]=='内线粗细'):
            setting.thickness_Inner=val[1]
        elif(val[0]=='外线颜色'):
            setting.color_Border=val[1]
        elif(val[0]=='内线颜色'):
            setting.color_Inner=val[1]
        elif(val[0]=='流畅裁剪'):
            cpr.Set_SmoothCrop(val[1])
        cpr.update()
                
    def __SettingChange_Bg(self,val):#当关于马赛克背景的设置发生变更时调用该函数
        cpr=self.__cropper
        setting=cpr.Get_Setting().bg
        if(val[0]=='颜色1'):
            setting.colors[0]=val[1]
        elif(val[0]=='颜色2'):
            setting.colors[1]=val[1]
        elif(val[0]=='格子大小'):
            setting.size=val[1]
        cpr.SetMosaicBg()

    def __ValueChange_Cropper(self):#当裁剪区发生变化时改变文本内容
        area=self.__cropper.Get_CropArea()
        if(area):
            area.Neaten()
            self.__text1.setText('{}x{}'.format(area.width,area.height))
            self.__text2.setText('{},{}'.format((area.left,area.top),(area.right-1,area.bottom-1)))
        else:
            self.__text1.setText('0x0')
            self.__text2.setText('')
                    
    def __UpdateSetting(self):#更新裁剪器的设置
        cpr=self.__cropper
        cpr_setting=cpr.Get_Setting().cropper
        bg_setting=cpr.Get_Setting().bg        
        setting_cpr=self.__setting_cropper
        setting_bg=self.__setting_bg
        
        cpr.Set_AspectRatio((setting_cpr.Get_AspectRatio()['宽'],setting_cpr.Get_AspectRatio()['高']))#裁剪的宽高比
        cpr.Set_SmoothCrop(setting_cpr.Get_SmoothCrop())#流畅裁剪
        cpr_setting.rowCnt=setting_cpr.Get_CntRowCol()['行']#行数
        cpr_setting.colCnt=setting_cpr.Get_CntRowCol()['列']#列数
        cpr_setting.thickness_Border=setting_cpr.Get_Thickness()['外线粗细']#外线粗细
        cpr_setting.thickness_Inner=setting_cpr.Get_Thickness()['内线粗细']#内线粗细
        cpr_setting.color_Border=setting_cpr.Get_BorderColor()['外线颜色']#外线颜色
        cpr_setting.color_Inner=setting_cpr.Get_BorderColor()['内线颜色']#内线颜色
        
        bg_setting.colors[0]=setting_bg.Get_Color1()
        bg_setting.colors[1]=setting_bg.Get_Color2()
        bg_setting.size=setting_bg.Get_Size()

        cpr.SetMosaicBg()

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

    cpr=XJ_Cropper(1200,600)
    cpr.Load_Img(QImage('C:/Users/Administrator/Desktop/2.png'))
    cpr.btnClick_saveCrops.connect(lambda:print(cpr.Get_CropImgs()))
    cpr.show()
    
    sys.exit(app.exec())
#【XJ_SampleCropper.py】
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QRect,pyqtSignal
from PyQt5.QtGui import QPainter,QPen,QColor,QImage
from PyQt5.QtWidgets import *

from XJ_Tool import *
from XJ_Rect import *
from XJ_AbstractCropper import *

class XJ_SampleCropper(QWidget):#样例裁剪器,难以对其中的参数进行设置。投入使用还需进一步的封装
    valueChange=pyqtSignal()#槽信号,裁剪区发生变化时发送信号。不发XJ_Rect是因为裁剪结果有可能是None而导致发不出去

    class Setting:#各种可控设置(因为设置太多了,所以单独列出来存着
        class Setting_Cropper:#裁剪器的设置
            def __init__(self):
                self.rowCnt=3#分割的行数
                self.colCnt=3#分割的列数

                self.color_Border=(255,0,0)#边界颜色
                self.color_Inner=(0,0,255)#内线颜色
                self.thickness_Border=3#边界粗细
                self.thickness_Inner=1#内线粗细

        class Setting_MosaicBg:#马赛克背景的设置
            def __init__(self):
                self.colors=[(255,255,255,255),(190,210,210)]#马赛克颜色
                self.size=1#马赛克大小

        def __init__(self):
            self.cropper=XJ_SampleCropper.Setting.Setting_Cropper()
            self.bg=XJ_SampleCropper.Setting.Setting_MosaicBg()

    def __init__(self,parent=None,width=500,height=500):
        super(XJ_SampleCropper, self).__init__(parent)
        self.resize(width,height)
        self.setMouseTracking(True)#时刻捕捉鼠标移动

        self.__cropper=XJ_AbstractCropper(0,0,width,height)
        self.__setting=XJ_SampleCropper.Setting()
        self.__fg=QImage(width,height,QImage.Format_ARGB32)
        self.__fg.fill(QColor(0,0,0,0))
        self.SetMosaicBg()
        self.__currArea=None#用于判定裁剪区是否发生变化的,以便发送信号
        
    def SetImg(self,qtImg):#设置图片(如果图片不存在则设置失败,返回False
        size=qtImg.size()
        if(size.isNull()):
            return False
            
        scale1=self.size().width()/size.width()
        scale2=self.size().height()/size.height()
        scale=min(scale1,scale2)
        self.__cropper=XJ_AbstractCropper(0,0,size.width(),size.height(),scale)
        self.__fg=qtImg
        self.SetMosaicBg()
        self.update()

        self.valueChange.emit()#裁剪区发生变化
        return True

    def Set_SmoothCrop(self,flag):#设置流畅裁剪
        self.__cropper.Set_SmoothCrop(flag)#设置流畅裁剪
        self.__UpdateRecord()#更新记录

    def Set_AspectRatio(self,ratio):#设置裁剪的宽高比
        self.__cropper.Set_AspectRatio(ratio)#设置裁剪的宽高比
        self.__UpdateRecord()#更新记录

    def Get_Setting(self):
        return self.__setting

    def Get_Crops(self,split=True):#获取截图,如果分割split为真则以二维列表(行列)存放,不分割就返回整图。裁剪区不存在则返回None
        pixel=self.__cropper.Get_PixelArea_Crop()
        if(pixel==None):
            return None            

        set_cpr=self.__setting.cropper
        width=pixel.width/set_cpr.colCnt
        height=pixel.height/set_cpr.rowCnt
        left=pixel.left
        top=pixel.top

        if(split==False):
            return self.__fg.copy(pixel.left,pixel.top,pixel.width,pixel.height)

        lst=[]
        for i in range(set_cpr.rowCnt):
            row=[]
            for j in range(set_cpr.colCnt):
                row.append(self.__fg.copy(left+width*j,top+height*i,width,height))
            lst.append(row)
        return lst

    def Get_CropArea(self):
        return self.__cropper.Get_PixelArea_Crop()

    def Get_Setting_Cropper(self):
        return self.__setting.cropper

    def Get_Setting_MosaicBg(self):
        return self.__setting.bg

    def SetMosaicBg(self):#根据self.__fg的大小以及self.__setting.bg的参数设置马赛克背景图self.__bg
        set_bg=self.__setting.bg
        size=self.__fg.size()
        self.__bg=GetQPixmap(GetMosaicImg((size.width(),size.height()),set_bg.colors,(set_bg.size,set_bg.size))).toImage()
        self.update()
        
    def MaximizePict(self):#将图片最大化显示
        cpr=self.__cropper

        #设置缩放
        pictSize=self.__fg.size()
        winSize=self.size()        
        scale1=winSize.width()/pictSize.width()
        scale2=winSize.height()/pictSize.height()
        scale=min(scale1,scale2)
        cpr.ScalePict(0,0,scale)
        
        #设置图片位置
        area=cpr.Get_Area_Pict()
        cpr.ClickPict(area.left,area.top)
        cpr.MovePict(0,0)
        self.update()

    def MaximizeCrop(self):#将裁剪区最大化
        cpr=self.__cropper
        size=cpr.Get_Area_Pict()
        cpr.ClickPict(size.left,size.top)
        cpr.DragCrop(size.right+1,size.bottom+1)
        cpr.ReleaseCrop()
        self.__UpdateRecord()

    def __UpdateRecord(self):
        currArea=self.__cropper.Get_PixelArea_Crop()#现在的裁剪结果
        if(self.__currArea!=currArea):#裁剪区发生变化
            self.__currArea=currArea
            self.valueChange.emit()


    def paintEvent(self,event):
        painter=QPainter(self)
        cpr=self.__cropper
        set_cpr=self.__setting.cropper
        pict=cpr.Get_Area_Pict()

        #绘制图片
        qRect=QRect(pict.left,pict.top,pict.width,pict.height)
        painter.drawImage(qRect,self.__bg)
        painter.drawImage(qRect,self.__fg)

        rect=cpr.Get_Area_Crop()
        #绘制裁剪区
        if(rect):
            L=rect.left
            R=rect.right
            T=rect.top
            B=rect.bottom
            W=R-L
            H=B-T

            #画内线
            painter.setPen(QPen(QColor(*set_cpr.color_Inner),set_cpr.thickness_Inner))
            perW=W/set_cpr.colCnt
            perH=H/set_cpr.rowCnt
            for Y in [int(T+n*perH) for n in range(1,set_cpr.rowCnt)]:#画横线
                painter.drawLine(L,Y,R,Y)
            for X in [int(L+n*perW) for n in range(1,set_cpr.colCnt)]:#画纵线
                painter.drawLine(X,T,X,B)

            #画四条边界
            painter.setPen(QPen(QColor(*set_cpr.color_Border),set_cpr.thickness_Border))
            painter.drawLine(L,T,R,T)#上边界
            painter.drawLine(L,B,R,B)#下边界
            painter.drawLine(L,T,L,B)#左边界
            painter.drawLine(R,T,R,B)#右边界

    def mouseMoveEvent(self, event):
        x=event.pos().x()
        y=event.pos().y()
        cpr=self.__cropper
        rect=cpr.Get_Area_Crop()
        pict=cpr.Get_Area_Pict()

        if event.buttons() & Qt.MidButton:#按下中键进行拖拽
            cpr.MovePict(x,y)
        elif (event.buttons() & Qt.LeftButton):#按下左键进行拖拽
            cpr.DragCrop(x,y)
        elif(pict.IsInside(x,y)):#修改鼠标光标(鼠标位置要在图片范围内
            if(rect==None):#裁剪区不存在
                self.setCursor(Qt.ArrowCursor)#默认光标
            else:
                lines=rect.GetNearestLines(x,y,5)
                if(lines==None):
                    if(rect.IsInside(x,y)):
                        self.setCursor(Qt.SizeAllCursor)#十字方向箭头
                    else:
                        self.setCursor(Qt.ArrowCursor)#默认光标
                else:
                    if(len(lines)==1):
                        if(lines.count('L') or lines.count('R')):
                            self.setCursor(Qt.SizeHorCursor)#左右箭头
                        else:
                            self.setCursor(Qt.SizeVerCursor)#上下箭头
                    else:
                        if(lines.count('L')):
                            if(lines.count('T')):
                                self.setCursor(Qt.SizeFDiagCursor)#左上箭头
                            else:
                                self.setCursor(Qt.SizeBDiagCursor)#左下箭头
                        else:
                            if(lines.count('T')):
                                self.setCursor(Qt.SizeBDiagCursor)#右上箭头
                            else:
                                self.setCursor(Qt.SizeFDiagCursor)#右下箭头
        else:#鼠标在图片范围外,设置默认光标
            self.setCursor(Qt.ArrowCursor)#默认光标
        self.update()
        event.accept()
        self.__UpdateRecord()#更新记录

    def mousePressEvent(self, event):
        x=event.pos().x()
        y=event.pos().y()
        cpr=self.__cropper
        pict=cpr.Get_Area_Pict()

        if event.button()==Qt.LeftButton:#左键按下瞬间
            cpr.ClickPict(x,y)
        if event.button()==Qt.RightButton:#右键按下瞬间
            cpr.ClearCrop()#清除裁剪区
            self.update()
        if event.button()==Qt.MidButton:#中键按下瞬间
            crop=cpr.Get_Area_Crop()
            cpr.ClickPict(x,y)
            if(crop==None):
                cpr.ClearCrop()
        event.accept()

        self.__UpdateRecord()#更新记录

    def mouseReleaseEvent(self, event):
        if event.button()==Qt.LeftButton:#左键释放
            self.__cropper.ReleaseCrop()
        event.accept()
    
    def mouseDoubleClickEvent(self,event):
        cpr=self.__cropper
        if event.button()==Qt.MidButton:#中键双击将图片最大化
            crop=cpr.Get_Area_Crop()
            self.MaximizePict()
            cpr.ClickPict(event.pos().x(),event.pos().y())
            if(crop==None):
                cpr.ClearCrop()
        elif event.button()==Qt.RightButton:#右键双击将裁剪区最大化
            self.MaximizeCrop()
            
    def wheelEvent(self,event):
        x=event.pos().x()
        y=event.pos().y()
        cpr=self.__cropper
        if(event.angleDelta().y()>0):
            cpr.ScalePict(x,y,cpr.Get_ScaleRatio()+0.5)
        else:
            cpr.ScalePict(x,y,cpr.Get_ScaleRatio()-0.5)
        self.update()
        event.accept()

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

    cp=XJ_SampleCropper()
    cp.resize(700,700)
    cp.show()
    cp.SetImg(QImage('C:/Users/Administrator/Desktop/2.png'))
    cp.valueChange.connect(lambda :print(cp.Get_CropArea()))
    
    sys.exit(app.exec())
#【XJ_AbstractCropper.py】
from XJ_Rect import *
from XJ_Pair import *
from XJ_Tool import *

class XJ_AbstractCropper:#抽象裁剪器
    def __init__(self,L=0,T=0,Width=0,Height=0,scale=1):#Height和Width为原图宽高,scale为缩放,控制实际显示的图的大小
        self.__area_pict=XJ_Rect(L,T,L+int(Width*scale),T+int(Height*scale))#显示的图片边界
        self.__area_crop=XJ_Rect()#显示的裁剪边界
        self.__pixelArea_crop=XJ_Rect()#实际的裁剪边界
        self.__pixelArea_crop_copy=XJ_Rect()#鼠标按下时__pixelArea_crop的复制

        self.__pictSize=XJ_Pair(Width,Height)#实际的图片大小(固定值)
        self.__scaleRatio=scale#图片缩放比(原图*scale=显示的图
        self.__aspectRatio=XJ_Pair(0,6)#裁剪的宽高比(有一个为0就为自由裁剪

        self.__pos_click=XJ_Pair(0,0)#鼠标按下时的坐标
        self.__activeLine=''#当前活跃的裁剪区的边
        self.__show=False#显示裁剪区
        self.__smoothCrop=False#当其值为真时,将会流畅裁剪,否则则会严格根据像素进行裁剪

        self.__cropChangable=False#裁剪区可修改

    def ClickPict(self,x,y):#点击图片区(准备裁剪或拖拽
        self.__pos_click=XJ_Pair(x,y)
        self.__cropChangable=True
        if(self.__area_pict.IsInside(x,y)):#在图片区内部
            if(self.__show==False):#如果裁剪区不存在
                self.__show=True
                pos=self.__GetPixelPos(x,y)
                self.__activeLine='RB'#设置活跃边
                self.__pixelArea_crop=XJ_Rect(pos.x,pos.y,pos.x,pos.y)#设置裁剪区
                self.__pixelArea_crop_copy=self.__pixelArea_crop.copy()#拷贝
                self.__LimitPixelArea()
                self.__SetAreaCrop()#设置裁剪区
            else:#裁剪区存在
                self.__activeLine=self.__area_crop.GetNearestLines(x,y,5)#设置活跃边
                if(self.__activeLine):#在裁剪区边界上
                    self.__pixelArea_crop_copy=self.__pixelArea_crop.copy()#复制裁剪区
                elif(self.__area_crop.IsInside(x,y)==True):#在裁剪区内部
                    self.__pixelArea_crop_copy=self.__pixelArea_crop.copy()#复制裁剪区
                else:#在裁剪区外面的无效
                    self.__cropChangable=False
        else:#在图片区外面的无效
            self.__cropChangable=False

    def DragCrop(self,x,y):#左键拖拽裁剪区
        self.__pixelArea_crop=self.__pixelArea_crop_copy.copy()
        pixel=self.__pixelArea_crop
        lines=self.__activeLine
        scale=self.__scaleRatio

        if(self.__cropChangable):#如果点击坐标有效
            if(self.__activeLine):#如果活跃边有效,拖拽边
                pos=self.__GetPixelPos(x,y)
                if(lines.find('L')!=-1):
                    pixel.left=pos.x
                elif(lines.find('R')!=-1):
                    pixel.right=pos.x
                if(lines.find('T')!=-1):
                    pixel.top=pos.y
                elif(lines.find('B')!=-1):
                    pixel.bottom=pos.y
            else:#活跃边无效,拖拽裁剪区
                offsetX=int((x-self.__pos_click.x)/scale)
                offsetY=int((y-self.__pos_click.y)/scale)
                pixel.Move(offsetX,offsetY)#移动裁剪区
            self.__LimitPixelArea()#约束裁剪区
            self.__SetAreaCrop()#更新显示的裁剪区

    def ReleaseCrop(self):#左键释放裁剪区
        self.__pixelArea_crop.Neaten()
        self.__area_crop.Neaten()
        self.__pixelArea_crop_copy=self.__pixelArea_crop.copy()#拷贝

    def ClearCrop(self):#清除裁剪区
        self.__show=False

    def ScalePict(self,x,y,newScale):#缩放图片,以坐标(x,y)进行缩放
        if(newScale<=0):
            return

        scale=self.__scaleRatio
        pict=self.__area_pict
        pos=self.__GetPixelPos(x,y)

        size=self.__pictSize
        newPict=XJ_Rect(0,0,int(size.width*newScale),int(size.height*newScale))
        newPict.Move(pict.left,pict.top)
        
        self.__scaleRatio=newScale
        self.__area_pict=newPict
        self.__SetAreaCrop()

        offsetX=int(newScale*pos.x)-(x-pict.left)+1
        offsetY=int(newScale*pos.y)-(y-pict.top)+1
        self.__MovePict(-offsetX,-offsetY)
        
    def MovePict(self,x,y):#移动图片(要先调用ClickPict确定按下点
        self.__MovePict(x-self.__pos_click.x,y-self.__pos_click.y)
        self.__pos_click=XJ_Pair(x,y)

    def __MovePict(self,offsetX,offsetY):#移动图片
        self.__area_pict.Move(offsetX,offsetY)
        self.__area_crop.Move(offsetX,offsetY)

    def Get_Area_Crop(self):#获取实际裁剪区的边界(裁剪区不存在则返回None
        if(self.__show):
            return self.__area_crop
        return None

    def Get_PixelArea_Crop(self):#获取显示裁剪区的边界(裁剪区不存在则返回None
        if(self.__show):
            pixel=self.__pixelArea_crop.copy()
            pixel.Neaten()
            if(pixel.width==0):
                pixel.width=1
            if(pixel.height==0):
                pixel.height=1
            return pixel
        return None

    def Get_Area_Pict(self):#获取显示图片的边界
        return self.__area_pict

    def Get_ScaleRatio(self):#获取缩放比
        return self.__scaleRatio

    def Set_SmoothCrop(self,flag):#设置流畅裁剪
        self.__smoothCrop=flag
        if(flag==False):
            self.__LimitPixelArea()#约束裁剪区
            self.__SetAreaCrop()#更新显示的裁剪区
    
    def Set_AspectRatio(self,ratio:tuple):#设置裁剪的宽高比
        self.__aspectRatio=XJ_Pair(ratio[0],ratio[1])
        preLine=self.__activeLine
        self.__activeLine='RB'
        self.__LimitPixelArea()#约束裁剪区
        self.__SetAreaCrop()#更新显示的裁剪区
        self.__activeLine=preLine
    
    def __GetPixelPos(self,posX,posY):#获取实际的像素位置
        x=int((posX-self.__area_pict.left)/self.__scaleRatio)
        y=int((posY-self.__area_pict.top)/self.__scaleRatio)
        return XJ_Pair(x,y)

    def __LimitPixelArea(self):#执行约束,控制实际裁剪区pixelArea_crop不超出范围+裁剪区长宽比受限
        pixel=self.__pixelArea_crop
        size=self.__pictSize
        aspectRatio=self.__aspectRatio
        lines=self.__activeLine

        if(lines==None):#移动裁剪区
            pass
        else:#更改裁剪区大小
            #记录一下方向,当约束后裁剪区大小为0时恢复为1像素大小
            dictX=pixel.right-pixel.left
            dictY=pixel.bottom-pixel.top
            if lines.count('L'):
                dictX=-dictX
            if lines.count('T'):
                dictY=-dictY
            dictX=1 if dictX>0 else -1
            dictY=1 if dictY>0 else -1

            #约束在图片范围内
            pixel.left=LimitValue(pixel.left,(0,size.width))
            pixel.right=LimitValue(pixel.right,(0,size.width))
            pixel.top=LimitValue(pixel.top,(0,size.height))
            pixel.bottom=LimitValue(pixel.bottom,(0,size.height))
            #如果有长宽约束就进一步处理
            if(aspectRatio.width and aspectRatio.height):
                if(len(lines)==1):
                    if(lines=='L'):
                        lines='LB'
                        pixel.bottom=size.height
                    if(lines=='R'):
                        lines='RB'
                        pixel.bottom=size.height
                    if(lines=='T'):
                        lines='TR'
                        pixel.right=size.width
                    if(lines=='B'):
                        lines='BR'
                        pixel.right=size.width

                W=pixel.right-pixel.left
                H=pixel.bottom-pixel.top
                maxW=size.width-(pixel.left if lines.count('R') else pixel.right)
                maxH=size.height-(pixel.top if lines.count('B') else pixel.bottom)
                rateW=abs(W/aspectRatio.width)
                rateH=abs(H/aspectRatio.height)
                rateMaxW=abs(maxW/aspectRatio.width)
                rateMaxH=abs(maxH/aspectRatio.height)
                
                if(self.__smoothCrop==False):
                    rateW=int(rateW)
                    rateH=int(rateH)
                    rateMaxW=int(rateMaxW)
                    rateMaxH=int(rateMaxH)
                    
                rate=min(max(rateW,rateH),min(rateMaxW,rateMaxH))#从中选取最大的裁取比(约束在图片范围内
                rate=min(rateW,rateH)
                W=int(aspectRatio.width*rate *(1 if W>0 else -1))
                H=int(aspectRatio.height*rate *(1 if H>0 else -1))

                if(lines.find('L')!=-1):
                    pixel.left=pixel.right-W
                else:
                    pixel.right=pixel.left+W
                if(lines.find('T')!=-1):
                    pixel.top=pixel.bottom-H
                else:
                    pixel.bottom=pixel.top+H
            
            if(pixel.width==0):#如果长度为0,那么就需要弄成1像素大小
                pixel.width=dictX
            if(pixel.height==0):
                pixel.height=dictY
                

        #约束在图片范围内
        pixel.Neaten()
        if(pixel.left<0):
            pixel.Move(-pixel.left,0)
        if(pixel.right>size.width):
            pixel.Move(size.width-pixel.right,0)
        if(pixel.top<0):
            pixel.Move(0,-pixel.top)
        if(pixel.bottom>size.height):
            pixel.Move(0,size.height-pixel.bottom)
            
    def __SetAreaCrop(self):#设置self.__area_crop
        pixel=self.__pixelArea_crop.copy()
        pixel.Neaten()
        pict=self.__area_pict
        scale=self.__scaleRatio
        
        L=int(scale*pixel.left)+pict.left
        T=int(scale*pixel.top)+pict.top
        R=int(scale*pixel.right)+pict.left
        B=int(scale*pixel.bottom)+pict.top
        self.__area_crop=XJ_Rect(L,T,R,B)

if __name__=='__main__':
    pass
#【XJ_CropperSetting.py】
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QRect
from PyQt5.QtGui import QColor,QFont
from PyQt5.QtWidgets import *

from XJ_NumInput import *
from XJ_ColorChoose import *

class XJ_ColorWithHint(QWidget):#带有Hint的XJ_ColorChoose
    def __init__(self,hint,hintFont,color,horizontal=True,parent=None):
        super(XJ_ColorWithHint, self).__init__(parent)
        self.hint=QLabel(hint,self)
        self.color=XJ_ColorChoose(color,10,self)

        self.hint.setFont(hintFont)#设置字体
        self.hint.setAlignment(Qt.AlignCenter)#设置居中
        box=QHBoxLayout() if horizontal else QVBoxLayout()
        box.addWidget(self.hint)
        box.addWidget(self.color)
        self.setLayout(box)

class XJ_NumWithHint(QWidget):#带有Hint的XJ_NumInput
    def __init__(self,hint,hintFont,valMin,valMax,horizontal=True,parent=None):
        super(XJ_NumWithHint, self).__init__(parent)
        self.hint=QLabel(hint,self)
        self.num=XJ_NumInput(valMin,valMax,self)
        self.hint.setFont(hintFont)#设置字体
        self.hint.setAlignment(Qt.AlignCenter)#设置居中

        box=QHBoxLayout() if horizontal else QVBoxLayout()
        box.addWidget(self.hint)
        box.addWidget(self.num)
        self.setLayout(box)


class XJ_SettingForCropper(QWidget):#与图片裁剪相关的设置
    valueChange=pyqtSignal(tuple)#值发生改变时发射信号,值为二元组
    #信号内容取值如下:
    #('宽':int)
    #('高':int)
    #('行':int)
    #('列':int)
    #('外线粗细':int)
    #('内线粗细':int)
    #('外线颜色':(int,int,int))
    #('内线颜色':(int,int,int))
    #('流畅裁剪':bool)
    def __init__(self,parent=None):
        super(XJ_SettingForCropper, self).__init__(parent)
        self.setFocusPolicy(Qt.ClickFocus|Qt.WheelFocus)#让控件可以获取焦点


        #生成控件
        font=QFont()
        font.setBold(True)
        font.setPixelSize(16)
        self.__aspectRatio=[XJ_NumWithHint('宽',font,0,100,True),XJ_NumWithHint('高',font,0,100,True)]#长宽比
        self.__cntRowCol=[XJ_NumWithHint('行数',font,1,100,True),XJ_NumWithHint('列数',font,1,100,True)]#分割数
        self.__thickness=[XJ_NumWithHint('外线',font,1,5,True),XJ_NumWithHint('内线',font,1,5,True)]#边界粗细
        self.__borderColor=[XJ_ColorWithHint('外线',font,(255,50,50),self),XJ_ColorWithHint('内线',font,(64,0,255),self)]#边界颜色
        self.__smoothCrop=[QCheckBox(self)]#流畅裁剪


        #设置布局
        box = QVBoxLayout()
        lst=[
            [QLabel('宽高比',self),self.__aspectRatio],
            [QLabel('分割数',self),self.__cntRowCol],
            [QLabel('边界粗细',self),self.__thickness],
            [QLabel('边界颜色',self),self.__borderColor],
            [QLabel('流畅裁剪',self),self.__smoothCrop]
        ]
        for pst in range(len(lst)):
            hint=lst[pst][0]
            hint.setAlignment(Qt.AlignVCenter|Qt.AlignRight)#设置居中靠右
            hint.setFont(font)
            hint.setStyleSheet("QLabel{color:rgb(192,64,64);}")#设置颜色

            box1=QVBoxLayout()#竖直盒子
            for wid in lst[pst][1]:
                box1.addWidget(wid)
            box2=QHBoxLayout()#水平盒子
            box2.addWidget(hint)
            box2.addStretch(1)
            box2.addLayout(box1)
            frame=QFrame()
            frame.setLayout(box2)
            frame.setFrameShape(QFrame.StyledPanel)#设置外边框
            box.addWidget(frame)
        self.setLayout(box)


        #绑定信号
        def SetFunc(self,key):#整个闭包,省的翻车
            def Func(val):
                self.valueChange.emit((key,val))
            return Func
        self.__aspectRatio[0].num.valueChange.connect(SetFunc(self,'宽'))
        self.__aspectRatio[1].num.valueChange.connect(SetFunc(self,'高'))
        self.__cntRowCol[0].num.valueChange.connect(SetFunc(self,'行'))
        self.__cntRowCol[1].num.valueChange.connect(SetFunc(self,'列'))
        self.__thickness[0].num.valueChange.connect(SetFunc(self,'外线粗细'))
        self.__thickness[1].num.valueChange.connect(SetFunc(self,'内线粗细'))
        self.__borderColor[0].color.valueChange.connect(SetFunc(self,'外线颜色'))
        self.__borderColor[1].color.valueChange.connect(SetFunc(self,'内线颜色'))
        def SetFunc(self):#整个闭包,省的翻车
            def Func():
                self.valueChange.emit(('流畅裁剪',self.__smoothCrop[0].isChecked()))
            return Func
        self.__smoothCrop[0].clicked.connect(SetFunc(self))
                
        self.__thickness[0].num.Set_Value(4)
        self.__thickness[1].num.Set_Value(2)
        self.__cntRowCol[0].num.Set_Value(3)
        self.__cntRowCol[1].num.Set_Value(3)

    def Get_AspectRatio(self):
        nums=self.__aspectRatio
        return {'宽':nums[0].num.Get_Value(),'高':nums[1].num.Get_Value()}
    def Get_CntRowCol(self):
        nums=self.__cntRowCol
        return {'行':nums[0].num.Get_Value(),'列':nums[1].num.Get_Value()}
    def Get_Thickness(self):
        nums=self.__thickness
        return {'外线粗细':nums[0].num.Get_Value(),'内线粗细':nums[1].num.Get_Value()}
    def Get_BorderColor(self):
        cols=self.__borderColor
        return {'外线颜色':cols[0].color.Get_Color(),'内线颜色':cols[1].color.Get_Color()}
    def Get_SmoothCrop(self):
        return self.__smoothCrop[0].isChecked()
    
class XJ_SettingForMosaicBg(QWidget):#与马赛克背景相关的设置
    valueChange=pyqtSignal(tuple)#值发生改变时发射信号,值为二元组
    #信号内容取值如下:
    #('颜色1',(int,int,int))
    #('颜色2',(int,int,int))
    #('格子大小',int)
    def __init__(self,parent=None):
        super(XJ_SettingForMosaicBg, self).__init__(parent)
        self.setFocusPolicy(Qt.ClickFocus|Qt.WheelFocus)#让控件可以获取焦点

        #生成控件
        font=QFont()
        font.setBold(True)
        font.setPixelSize(16)
        self.__color1=XJ_ColorWithHint('颜色1',font,(255,255,255))
        self.__color2=XJ_ColorWithHint('颜色2',font,(192,228,228))
        self.__size=XJ_NumWithHint('格子大小',font,1,1024)
        hint=QLabel('马赛克背景',self)
        hint.setAlignment(Qt.AlignVCenter|Qt.AlignRight)#设置居中靠右
        hint.setFont(font)
        hint.setStyleSheet("QLabel{color:rgb(192,64,64);}")#设置颜色

        #设置布局
        box1=QVBoxLayout()
        box1.addWidget(self.__color1)
        box1.addWidget(self.__color2)
        box1.addWidget(self.__size)
        box2=QHBoxLayout()
        box2.addWidget(hint)
        box2.addStretch(1)
        box2.addLayout(box1)
        box=QVBoxLayout()
        frame=QFrame()
        frame.setLayout(box2)
        frame.setFrameShape(QFrame.StyledPanel)#设置外边框
        box.addWidget(frame)
        self.setLayout(box)
        
        self.__size.num.Set_Value(16)
        
        #绑定信号
        def SetFunc(self,key):#整个闭包,省的翻车
            def Func(val):
                self.valueChange.emit((key,val))
            return Func
        self.__color1.color.valueChange.connect(SetFunc(self,'颜色1'))
        self.__color2.color.valueChange.connect(SetFunc(self,'颜色2'))
        self.__size.num.valueChange.connect(SetFunc(self,'格子大小'))

    def Get_Color1(self):
        return self.__color1.color.Get_Color()
    def Get_Color2(self):
        return self.__color2.color.Get_Color()
    def Get_Size(self):
        return self.__size.num.Get_Value()

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

    set1=XJ_SettingForCropper()
    set1.show()

    set2=XJ_SettingForMosaicBg()
    set2.show()

    set1.valueChange.connect(lambda val:print(val))
    set2.valueChange.connect(lambda val:print(val))

    sys.exit(app.exec())
    
#【XJ_NumInput.py】
import sys
from PyQt5.QtCore import Qt,pyqtSignal
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import *

class XJ_NumInput(QLineEdit):
    valueChange=pyqtSignal(int)#槽信号,值修改时发送信号
    def __init__(self,valMin=0,valMax=100,parent=None):
        super(XJ_NumInput, self).__init__(str(valMin),parent)
        self.__curr=valMin#__curr是用来判断当前值有无发生修改的

        font=QFont()
        font.setBold(True)
        font.setPixelSize(20)

        self.setMouseTracking(True)#时刻捕捉鼠标移动
        self.setReadOnly(True)#设置只读
        self.setFont(font)#设置字体
        self.setAlignment(Qt.AlignCenter)#设置居中
        self.setMaximumWidth(80)
        
        self.Set_ValueRange(valMin,valMax)

    def focusOutEvent(self,event):#脱离焦点
        self.__LimitValue()
        self.setReadOnly(True)
        event.accept()

    def mouseMoveEvent(self,event):
        self.setCursor(Qt.PointingHandCursor)#手型光标
        event.accept()

    def mouseDoubleClickEvent(self,event):
        self.setReadOnly(False)
        self.setFocus()
        event.accept()

    def wheelEvent(self,event):
        delta=event.angleDelta()
        curr=int(self.text())
        if(delta.y()>0):#滚轮向上滚动,增加
            if(curr<self.__val_max):
                curr=curr+1
        elif(delta.y()<0):#向下滚动,减少
            if(curr>self.__val_min):
                curr=curr-1
        self.setText(str(curr))
        self.update()
        event.accept()

        if(curr!=self.__curr):
            self.__curr=curr
            self.valueChange.emit(curr)

    def Set_ValueRange(self,valMin,valMax):
        self.__val_min=valMin
        self.__val_max=valMax
        if(self.__val_max<self.__val_min):
            self.__val_max,self.__val_min=self.__val_min,self.__val_max
        self.__LimitValue()

    def Get_ValueRange(self):#返回取值范围
        return (self.__val_min,self.__val_max)

    def Set_Value(self,val):
        self.setText(str(val))
        self.__LimitValue()

    def Get_Value(self):
        return int(self.text())

    def __LimitValue(self):
        curr=''.join(list(filter(lambda c:c.isdigit() or c=='+' or c=='-',self.text()))).lstrip('0')
        curr=int(eval(curr)) if len(curr) else 0
        if(curr<self.__val_min):
            curr=self.__val_min
        if(curr>self.__val_max):
            curr=self.__val_max
        self.setText(str(curr))

        if(curr!=self.__curr):
            self.__curr=curr
            self.valueChange.emit(curr)


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

    tmp=XJ_NumInput()
    tmp.show()

    tmp.valueChange.connect(lambda i:print(i))

    sys.exit(app.exec())
    
#【XJ_ColorChoose.py】
import sys
from PyQt5.QtCore import Qt,pyqtSignal
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import *

class XJ_ColorChoose(QLabel):#小控件,点击弹出换色窗口。QWidget子类化时样式表不生效,np
    valueChange=pyqtSignal(tuple)#槽信号,值修改时发送信号
    def __init__(self,rgb=(255,50,50),width=10,parent=None):
        super(XJ_ColorChoose, self).__init__(parent)
        self.setMouseTracking(True)#时刻捕捉鼠标移动
        self.__color=QColor(*rgb)
        self.__SetColor()
        self.SetWidth(width)

    def __SetColor(self):
        self.setStyleSheet("QLabel{{background-color:rgb{0};}};".format(self.Get_Color()))#设置颜色
        self.update()
        
    def mouseMoveEvent(self,event):
        self.setCursor(Qt.PointingHandCursor)#手型光标

    def mousePressEvent(self,event):#设置点击事件
        if event.button()==Qt.LeftButton:#左键点击
            col=QColorDialog.getColor()
            if(col.isValid()):
                self.__color=col
                self.__SetColor()
                self.valueChange.emit(self.Get_Color())#值修改时发送信号
                
    def SetWidth(self,width):
        s=' '*width
        self.setText(s)

    def Get_Color(self):
        col=self.__color
        return (col.red(),col.green(),col.blue())

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

    test=XJ_ColorChoose()
    test.show()
    
    test.valueChange.connect(lambda t:print(t))
    
    sys.exit(app.exec())
    
#【XJ_LineEdit.py】
import sys
from PyQt5.QtWidgets import *

class XJ_LineEdit(QWidget):
    def __init__(self,parent=None,hint='提示文本:',input='内容文本',button='按钮文本'):
        super(XJ_LineEdit, self).__init__(parent)
        self.__hint=QLabel(hint,self)
        self.__input=QLineEdit(input,self)
        self.__button=QPushButton(button,self)
        hbox=QHBoxLayout()
        hbox.addWidget(self.__hint)
        hbox.addWidget(self.__input)
        hbox.addWidget(self.__button)
        self.setLayout(hbox)

    def SetText_Hint(self,tx):
        self.__hint.setText(tx)
    def SetText_Button(self,tx):
        self.__button.setText(tx)
    def GetText_Input(self):
        return self.__input.text()
    def SetText_Input(self,tx):
        self.__input.setText(tx)
    def SetClicked_Button(self,func):
        self.__button.clicked.connect(func)
    def SetEnable_Input(self,flag):
        self.__input.setReadOnly(not flag)
    def SetEnable_Button(self,flag):
        self.__button.setVisible(flag)
    def GetWidget_Hint(self):
        return self.__hint
    def GetWidget_Input(self):
        return self.__input
    def GetWidget_Button(self):
        return self.__button
        
if __name__ == '__main__':
    app = QApplication(sys.argv)

    le=XJ_LineEdit()
    le.show()

    sys.exit(app.exec())

#【XJ_Pair.py】
class XJ_Pair:
    def __init__(self,x,y):
        self.x=x
        self.y=y
        
    @property
    def width(self):
        return self.x
    @property
    def height(self):
        return self.y
    @width.setter
    def width(self,w):
        self.x=w
    @height.setter
    def height(self,h):
        self.y=h

    def __str__(self):
        return "XJ_Pair"+str((self.x,self.y))
    def copy(self):
        return XJ_Pair(self.x,self.y)
        
#【XJ_Rect.py】
class XJ_Rect:
    def __init__(self,L=0,T=0,R=0,B=0):
        self.__left=L
        self.__right=R
        self.__top=T
        self.__bottom=B

    @property
    def left(self):
        return self.__left
    @property
    def right(self):
        return self.__right
    @property
    def top(self):
        return self.__top
    @property
    def bottom(self):
        return self.__bottom
    @left.setter
    def left(self,L):
        self.__left=L
    @right.setter
    def right(self,R):
        self.__right=R
    @top.setter
    def top(self,T):
        self.__top=T
    @bottom.setter
    def bottom(self,B):
        self.__bottom=B

    @property
    def width(self):
        return self.__right-self.__left
    @property
    def height(self):
        return self.__bottom-self.__top
    @width.setter
    def width(self,W):
        self.__right=self.__left+W
    @height.setter
    def height(self,H):
        self.__bottom=self.__top+H

    def copy(self):
        return XJ_Rect(self.__left,self.__top,self.__right,self.__bottom)
    def __str__(self):
        return 'XJ_Rect'+str((self.__left,self.__top,self.__right,self.__bottom))

    def Neaten(self):
        if(self.__left>self.__right):
            self.__left,self.__right=self.__right,self.__left
        if(self.__top>self.__bottom):
            self.__top,self.__bottom=self.__bottom,self.__top
    def Move(self,x,y):
        self.__left=self.__left+x
        self.__right=self.__right+x
        self.__top=self.__top+y
        self.__bottom=self.__bottom+y
    def IsInside(self,x,y):#判断点是否在矩形内
        return self.__left<=x<=self.__right and self.__top<=y<=self.__bottom
    def GetNearestLines(self,x,y,dist=5):#返回距离点最近的边所对应的首字母,超过距离dist的将不视为“接近”
        '''
            如果边有效则返回对应的首字母,边无效返回None。
            如果位置靠近左边界则会返回'L',其他边同理。
            如果位置靠近左上顶点则返回'LT',其他顶点同理。
        '''

        L=abs(self.left-x)
        R=abs(self.right-x)
        T=abs(self.top-y)
        B=abs(self.bottom-y)

        rst=''#查询结果
        if(L<R and L<=dist):
            rst=rst+'L'
        elif(R<L and R<=dist):
            rst=rst+'R'
        if(T<B and T<=dist):
            rst=rst+'T'
        elif(B<T and B<=dist):
            rst=rst+'B'

        if(len(rst)==1):
            if(rst=='L' or rst=='R'):
                T=self.top-y
                B=self.bottom-y
                if(T^B>=0):
                    rst=''
            else:
                L=self.left-x
                R=self.right-x
                if(L^R>=0):
                    rst=''
        if(len(rst)==0):
            return None
        return rst

    def __eq__(self,rect):
        if(type(rect)!=XJ_Rect):
            return False
        return (
            self.left==rect.left and 
            self.right==rect.right and 
            self.top==rect.top and 
            self.bottom==rect.bottom 
        )

if __name__=='__main__':
    r=XJ_Rect(0,0,500,500)
    r.left=300
    r.left=600
    r.width=400
    print(r.copy())

#【XJ_Tool.py】
import cv2.cv2 as cv2
import numpy as np
from PIL import Image

def GetMosaicImg(shape,colors,interval):#返回马赛克图片(np二维数组
    '''
        shape为二元组,(图片宽,图片高)
        colors为二元组,(方块颜色1,方块颜色2)
        interval为二元组,(单方块宽,单方块高)
    '''
    #对color的值进行处理
    clr0=list(colors[0])
    clr1=list(colors[1])
    len0=len(clr0)
    len1=len(clr1)
    if(len0<len1):
        tmp=list(clr0)
        for i in range(len0,len1):
           tmp.append(colors[1][i])
        clr0=list(tmp)
    if(len0>len1):
        tmp=list(clr1)
        for i in range(len1,len0):
           tmp.append(colors[0][i])
        clr1=list(tmp)
    #因为cv2发神经,喜欢弄成bgr,平时都是rgb的,所以得调换一下
    if(len(clr0)>=3):
        clr0[0],clr0[2]=clr0[2],clr0[0]
        clr1[0],clr1[2]=clr1[2],clr1[0]
    
    #生成马赛克图案
    h=interval[1]
    w=interval[0]
    mosaic=np.zeros((h*2,w*2,len(clr0)),dtype=np.uint8)
    cv2.rectangle(mosaic,(0,0),(w,h),clr0,-1)
    cv2.rectangle(mosaic,(w,0),(w*2,h),clr1,-1)
    cv2.rectangle(mosaic,(0,h),(w,h*2),clr1,-1)
    cv2.rectangle(mosaic,(w,h),(w*2,h*2),clr0,-1)
    while(w<shape[0]):#从行开始,生成一行的马赛克
        mosaic=np.concatenate((mosaic,mosaic), axis=1)
        w=w*2
    while(h<shape[1]):#利用一行马赛克生成一片马赛克
        mosaic=np.concatenate((mosaic,mosaic), axis=0)
        h=h*2
    
    return mosaic[0:shape[1],0:shape[0]]#把这破事忘了。图片需要裁剪才行要不然大小不对
    #【注释掉的是旧算法,利用颜色填充逐个格子填色,效率非常低】
#    img=np.zeros((shape[1],shape[0],len(clr0)),dtype=np.uint8)
#    for i in range(int(shape[0]/interval[0])+1):
#        for j in range(int(shape[1]/interval[1])+1):
#            left=i*interval[0]
#            top=j*interval[1]
#            right=left+interval[0]
#            bottom=top+interval[1]
#            color=clr0 if (i+j)&1==0 else clr1
#            cv2.rectangle(img,(left,top),(right,bottom),color,-1)
#    return img
    

def FixImgs(cvFg,cvBg):#以cvFg的透明度作为蒙版将cvBg融合进去
    Bg_b,Bg_g,Bg_r=cv2.split(cvBg)
    Fg_b,Fg_g,Fg_r,Fg_a=cv2.split(cvFg)#alpha的值越大越不透明,越小才越透明
    Bg_Mask=np.invert(Fg_a)
    bg=cv2.merge((
        np.bitwise_and(Bg_b,Bg_Mask),
        np.bitwise_and(Bg_g,Bg_Mask),
        np.bitwise_and(Bg_r,Bg_Mask)))
    fg=cv2.merge((
        np.bitwise_and(Fg_b,Fg_a),
        np.bitwise_and(Fg_g,Fg_a),
        np.bitwise_and(Fg_r,Fg_a)))
    pict=cv2.add(fg,bg)
    return pict

def GetQPixmap(cvImg):#从cv的图片类型转成QT的QPixmap类型
    cvImg=cv2.cvtColor(cvImg, cv2.COLOR_BGRA2RGBA)
    im = Image.fromarray(cvImg)
    return im.toqpixmap()

def LimitValue(val,section):#将val的值控制在区间section内(包含边界)
    if(section[0]>section[1]):
        section=(section[1],section[0])
    return section[0] if val<section[0] else section[1] if val>section[1] else val

def InTheSection(val,section):#判断val是否在区间section内(包含边界)
    if(section[0]>section[1]):
        section=(section[1],section[0])
    return section[0]<=val<=section[1]

def GetMemberFromObject(obj,keyWord=''):
    return [m for m in dir(obj) if(m.lower().count(keyWord.lower()))]




if __name__=='__main__':
    img=GetMosaicImg((500,500),((255,255,128),(128,128,255)),(110,110))
    cv2.imshow('',img)
    cv2.waitKey()
    






最后说一句,不许转载,复制代码复制博客然后发在别的地方的行为就算被被别人下咒也不奇怪(虽然我不觉得会有哪些憨皮会这么做,但还是留个心眼)。代码可以用于个人使用,但像是直接生成个eXe文件然后就挂到某些地方搞收费的话小心被别人喷死。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
可以使用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_() ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值