基于pyqt5的美图系统

0、前言

这是本作者的图像处理的课程设计作业。

1、设计原理和方案

  1. 设计原理

用Qt Designer设计用户交互界面,用python编写方法。考虑到目标用户为女性居多,因此主色调选择了粉色,界面设计尽量简洁,方便用户使用。总共有14个美图功能,其中我主要负责磨皮,美白,瘦脸,放大眼睛和缩小鼻子这五个功能,还有打开图片,保存图片,删除图片以及一键复原等底层逻辑展示功能。其中磨皮功能用到的是导向滤波器,美白功能用的是直方图均衡化,瘦脸,放大眼睛和缩小鼻子这三个功能用到的都是使用了论文Interactive Image Warping的局部缩放算法。

2.设计方案

1.UI界面设计

(1)显示图片区域,界面最大的框主要用来显示图片,下面有一个按钮,让用户选择要美化的图片,系统读取到图片后,由于每张照片的不同,系统先将图片等比例缩放后,再显示出来。

(2)选择功能区域,设有14个主按钮,对应主要的14个美图功能,根据每个功能的不同,如果有需要用户输入参数的功能,则加入对应的小组件,以方便用户的使用。

2.磨皮方法

我们都知道,磨皮这个功能相当于是使脸部更加平滑,从而让人视觉上变得好看。那么图片上的雀斑或者是痘痘,可以看成是一些噪声,也就是说我们可以采用滤波器来过滤掉这些噪声。翻阅了网上的大量资料,通常采用的是双边滤波来过滤这些噪声,主要是双边滤波可以很好的保留图像中的边缘细节,使得图像在很好地过滤噪声的同时,不会过于失真。但是我在实验的时候发现,双边滤波的效果确实很好,但是它会同时对背景进行过滤,使得背景也变得模糊,如下图2所示。与下图1中的原图进行对比可以看到右边花瓣的纹路被磨平了。因此这里我没有用到双边滤波,而是采用了导向滤波,先使用椭圆肤色模型对皮肤部分进行提取,得到掩膜数组,然后利用位运算将滤波后的图像与原图结合,做到了对背景的保护,如下图3所示,背景中的花瓣的纹路得到了很好的保护,并没有被过滤。

图1 原图

图2 双边滤波器磨皮后的效果

图3 滤波后与原图结合的效果

3.美白方法

这里使用的是直方图均衡化,可以增加图像的亮度,以达到美白的效果。

4.放大眼睛方法

在美颜中,要想进行大眼,瘦脸等操作,需要首先提取出人脸的关键点才可以做坐标变换。这里使用了Google的mediapipe进行检测,获取到眼睛的关键点后,使用论文Interactive Image Warping的局部缩放算法对眼睛进行放大,通过双线性插值将得到放大后的图所对应像素值插入到原图中,使得眼睛局部放大。

5.瘦脸方法

和放大眼睛一样,首先用Google的mediapipe进行检测,但是mediapipe的face_detection只能侦测到脸部的六个点,分别是左眼,右眼,鼻尖,嘴,左耳和右耳。因为我是根据人脸部的三庭五眼来计算出面部的点,所以这个瘦脸的方法只能用于正脸,那么瘦脸,其实就将脸部往里面平移,按照这样的思路,继续使用局部缩放算法。

6.缩小鼻子方法

和瘦脸的方法类似,也是先用face_detection获取到脸部的六个点,然后根据人脸部的三庭五眼来计算出鼻翼的点,接着用局部缩放的方法。

2、设计步骤和结果

  1. UI界面设计

考虑到界面的美观大气,我选择使用label来进行页面的显示,把底下的label设置成粉色的底色,同时将label的角度设置成有弧度,显得更加美观。在这个基础上再放置两个白色的label,一个大的label用来显示图片,另外一个小的label用来显示工具栏,在添加一个按钮用来读取图片,这样一个简洁的页面就完成了。然后将窗口的边框隐藏,并且将窗体的背景设置成透明,代码见下图4,总的系统UI界面见下图5。

#界面设计
        self.setWindowFlag(QtCore.Qt.FramelessWindowHint)    #隐藏边框
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)   #主窗体背景透明

图4 部分代码

图5 系统UI界面

用一个widget装着14个功能按钮,并给按钮设置对应的icon和文字提示,当用户点击加号放入图片时,功能按钮才会显示出来,调用了widget.show()函数,如下图6所示。

图6 系统UI界面

根据一些美图功能的使用,可以给用户自行输入参数去进行调节的,加入了一些相对应的组件,调用widget.hide()函数,把总的工具栏隐藏,然后将组件所对应的widget显示出来,调用widget_x.show()函数(x为对应组件的窗口),如点击放大眼睛后系统的UI界面如下图7所示。

图7 点击放大眼睛按钮后系统的UI界面

  1. 磨皮方法

考虑先使用椭圆肤色模型对皮肤部分进行提取,得到皮肤的掩膜数组,代码见下图8。

    def YCrCb_ellipse_model(self,img):     #椭圆肤色模型
        skinCrCbHist = np.zeros((256, 256), dtype=np.uint8)
        cv2.ellipse(skinCrCbHist, (113, 155), (23, 25), 43, 0, 360, (255, 255, 255), -1)  # 绘制椭圆弧线
        YCrCb = cv2.cvtColor(img, cv2.COLOR_BGR2YCR_CB)  # 转换至YCrCb空间
        (Y, Cr, Cb) = cv2.split(YCrCb)  # 拆分出Y,Cr,Cb值
        skin = np.zeros(Cr.shape, dtype=np.uint8)  # 掩膜
        (x, y) = Cr.shape
        for i in range(0, x):
            for j in range(0, y):
                if skinCrCbHist[Cr[i][j], Cb[i][j]] > 0:  # 若不在椭圆区间中
                    skin[i][j] = 255
        res = cv2.bitwise_and(img, img, mask=skin)
        return skin, res

图8 椭圆肤色模型

使用导向滤波来对图片去噪,代码见下图9。

    def guided_filter(self,I, p, win_size, eps):   #导向滤波
        assert I.any() <= 1 and p.any() <= 1
        mean_I = cv2.blur(I, (win_size, win_size))
        mean_p = cv2.blur(p, (win_size, win_size))

        corr_I = cv2.blur(I * I, (win_size, win_size))
        corr_Ip = cv2.blur(I * p, (win_size, win_size))

        var_I = corr_I - mean_I * mean_I
        cov_Ip = corr_Ip - mean_I * mean_p

        a = cov_Ip / (var_I + eps)
        b = mean_p - a * mean_I

        mean_a = cv2.blur(a, (win_size, win_size))
        mean_b = cv2.blur(b, (win_size, win_size))

        q = mean_a * I + mean_b

        return q

图9 导向滤波

然后对掩膜数组进行一次开运算,消除细小区域,再利用位运算将滤波后的图像与背景相结合,这样就做到了对背景的保护,代码见下图10。

    def mopi(self):
        skin, _ = self.YCrCb_ellipse_model(self.img)  # 获得皮肤的掩膜数组
        # 进行一次开运算,消除细小区域
        kernel = np.ones((3, 3), dtype=np.uint8)
        skin = cv2.erode(skin, kernel=kernel)
        skin = cv2.dilate(skin, kernel=kernel)
        img1 = self.guided_filter(self.img / 255.0, self.img / 255.0, 10, eps=0.001) * 255
        img1 = np.array(img1, dtype=np.uint8)
        img1 = cv2.bitwise_and(img1, img1, mask=skin)  # 将皮肤与背景分离
        skin = cv2.bitwise_not(skin)
        img1 = cv2.add(img1, cv2.bitwise_and(self.img, self.img, mask=skin))  # 磨皮后的结果与背景叠加

        self.img = img1
        self.show_image()

图10 磨皮的方法

将最后的方法与按钮链接起来,代码见下图11,结果见图3。

self.skin.clicked.connect(lambda : self.mopi())    #磨皮

图11 按钮交互

  1. 美白方法

使用直方图均衡化,代码见下图12,按钮的链接同磨皮的按钮交互方法一样,原图见图13,美白后的结果见图14。

    def be_bright(self):  #美白
        b,g,r = cv2.split(self.img)
        b = cv2.equalizeHist(b)
        g = cv2.equalizeHist(g)
        r = cv2.equalizeHist(r)
        self.img = cv2.merge((b,g,r))
        self.show_image()

图12 美白方法

图13 原图

图14 美白后的结果

  1. 放大眼睛方法

先使用mediapipe进行检测人脸,然后提取出人脸的关键点,这里要注意mediapipe获取的关键点需要相对应乘图片的长和宽,才是关键点在原图中坐标的对应值,代码见图15、16。

import mediapipe as mp

mp_face_detection = mp.solutions.face_detection
mp_drawing = mp.solutions.drawing_utils

图15 引入的头文件,对应检测模型的调用

    def get_face_key_point(self,img):  #获取人脸关键点
        #创建FaceDetection类的对象,用来获得人脸关键点检测,模型选择为0或1,这里选1,全范围选项,稀疏模型用于提高推理速度。最小检测置信度[0.0,1.0],默认值为0.5
        with mp_face_detection.FaceDetection(model_selection=1, min_detection_confidence=0.5) as face_detection:
            results = face_detection.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) #用cv2转换图片
            if not results.detections:
                return None

            r, w, c = img.shape
            for detection in results.detections:
                left_eye = mp_face_detection.get_key_point(detection, mp_face_detection.FaceKeyPoint.LEFT_EYE)
                right_eye = mp_face_detection.get_key_point(detection, mp_face_detection.FaceKeyPoint.RIGHT_EYE)
                nose = mp_face_detection.get_key_point(detection,mp_face_detection.FaceKeyPoint.NOSE_TIP)
                mouth = mp_face_detection.get_key_point(detection,mp_face_detection.FaceKeyPoint.MOUTH_CENTER)
                right_ear = mp_face_detection.get_key_point(detection,mp_face_detection.FaceKeyPoint.RIGHT_EAR_TRAGION)
                left_ear = mp_face_detection.get_key_point(detection,mp_face_detection.FaceKeyPoint.LEFT_EAR_TRAGION)
                left_eye_pos = [int(left_eye.x * w), int(left_eye.y * r)]
                right_eye_pos = [int(right_eye.x * w), int(right_eye.y * r)]
                nose_pos = [int(nose.x * w) , int(nose.y * r)]
                mouth_pos = [int(mouth.x * w) , int(mouth.y * r)]
                right_ear_pos = [int(right_ear.x * w) , int(right_ear.y * r)]
                left_ear_pos = [int(left_ear.x * w) , int(left_ear.y * r)]

                return left_eye_pos, right_eye_pos , nose_pos , mouth_pos , right_ear_pos , left_ear_pos

图16 获取人脸关键点

根据局部缩放公式,编写局部放大函数,代码见图17。

    def Local_scaling_warps(self,img, cx, cy, r_max, a): #局部放大
        img1 = np.copy(img)
        for y in range(cy - r_max, cy + r_max + 1):
            d = int(math.sqrt(r_max ** 2 - (y - cy) ** 2))
            x0 = cx - d
            x1 = cx + d
            for x in range(x0, x1 + 1):
                r = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
                for c in range(3):
                    if r <= r_max:
                        vector_c = np.array([cx, cy])
                        vector_r = np.array([x, y]) - vector_c
                        f_s = (1 - ((r / r_max - 1) ** 2) * a)
                        vector_u = vector_c + f_s * vector_r  # 原坐标
                        img1[y][x][c] = self.bilinear_interpolation(img, vector_u, c)
        return img1

图17 局部放大函数

通过局部缩放的函数得到一个原图与输出图的像素坐标映射,通过双线性插值可以得到输出图的对应像素值,所以还要编写双线性插值函数,代码见图18。

    def bilinear_interpolation(self,img, vector_u, c):       #双线性插值
        ux, uy = vector_u
        x1, x2 = int(ux), int(ux + 1)
        y1, y2 = int(uy), int(uy + 1)

        f_x_y1 = (x2 - ux) / (x2 - x1) * img[y1][x1][c] + (ux - x1) / (x2 - x1) * img[y1][x2][c]
        f_x_y2 = (x2 - ux) / (x2 - x1) * img[y2][x1][c] + (ux - x1) / (x2 - x1) * img[y2][x2][c]

        f_x_y = (y2 - uy) / (y2 - y1) * f_x_y1 + (uy - y1) / (y2 - y1) * f_x_y2
        return int(f_x_y)

图18 双线性插值函数

由于局部放大函数一次只能对应一个点,因此需要调用两次具体代码见图19。其中还设置了两个参数,一个是缩放的范围r,一个是缩放的系数也就是程度a,这两个值可以提供给用户手动去进行一个调整,也就是前面界面设计提到的滑块,这里也是用connect函数去进行一个连接,具体代码见图20,原图见图13,放大眼睛的结果见图21。

    def big_eye(self, left_eye_pos=None, right_eye_pos=None):   #放大眼睛

        if left_eye_pos == None or right_eye_pos == None:
            left_eye_pos, right_eye_pos, nose_pos , mouth_pos , right_ear_pos , left_ear_pos= self.get_face_key_point(self.img)

        self.img = self.Local_scaling_warps(self.img, left_eye_pos[0], left_eye_pos[1], r_max=self.r, a=self.a / 100)
        self.img = self.Local_scaling_warps(self.img, right_eye_pos[0], right_eye_pos[1], r_max=self.r, a=self.a / 100)
        self.show_image()
        self.widget_4.show()
        self.widget_7.show()
        self.widget.hide()

图19 放大眼睛方法

    def change_r(self,value):   #放大眼睛范围
        self.r = value
        self.big_eye()

    def change_a(self,value):    #放大眼睛程度
        self.a = value
        self.big_eye()
        self.verticalSlider.valueChanged.connect(self.change_r)    #放大眼睛范围的滑块
        self.verticalSlider.setToolTip('范围')
        self.verticalSlider_2.valueChanged.connect(self.change_a)   #放大眼睛程度的滑块
        self.verticalSlider_2.setToolTip('程度')

图20 两个滑块的连接及方法

图21 放大眼睛后的效果

  1. 瘦脸方法

采用局部缩小的算法(具体代码见图22),假设当前点为(x,y),手动指定变形区域的中心点为C(cx,cy),变形区域半径为r,手动调整变形终点(从中心点到某个位置M)为M(mx,my),变形程度为strength,当前点对应变形后的目标位置为U。变形规律为:圆内所有像素均沿着变形向量的方向发生偏移;距离圆心越近,变形程度越大;距离圆周越近,变形程度越小,当像素点位于圆周时,该像素不变形;圆外像素不发生偏移。

    def localTranslationWarpFastWithStrength(self, startX, startY, endX, endY, radius, strength):  #局部缩小算法
        ddradius = float(radius * radius)
        copyImg = np.zeros(self.img.shape, np.uint8)
        copyImg = self.img.copy()
        np.seterr(divide='ignore', invalid='ignore')

        maskImg = np.zeros(self.img.shape[:2], np.uint8)

        cv2.circle(maskImg, (startX, startY), math.ceil(radius), (255, 255, 255), -1)

        K0 = 100 / strength

        # 计算公式中的|m-c|^2
        ddmc_x = (endX - startX) * (endX - startX)
        ddmc_y = (endY - startY) * (endY - startY)
        H, W, C = self.img.shape

        mapX = np.vstack([np.arange(W).astype(np.float32).reshape(1, -1)] * H)
        mapY = np.hstack([np.arange(H).astype(np.float32).reshape(-1, 1)] * W)

        distance_x = (mapX - startX) * (mapX - startX)
        distance_y = (mapY - startY) * (mapY - startY)
        distance = distance_x + distance_y
        K1 = np.sqrt(distance)
        ratio_x = (ddradius - distance_x) / (ddradius - distance_x + K0 * ddmc_x)
        ratio_y = (ddradius - distance_y) / (ddradius - distance_y + K0 * ddmc_y)
        ratio_x = ratio_x * ratio_x
        ratio_y = ratio_y * ratio_y

        UX = mapX - ratio_x * (endX - startX) * (1 - K1 / radius)
        UY = mapY - ratio_y * (endY - startY) * (1 - K1 / radius)

        np.copyto(UX, mapX, where=maskImg == 0)
        np.copyto(UY, mapY, where=maskImg == 0)
        UX = UX.astype(np.float32)
        UY = UY.astype(np.float32)
        copyImg = cv2.remap(self.img, UX, UY, interpolation=cv2.INTER_LINEAR)

        return copyImg

图22 局部缩小算法

根据人脸部的三庭五眼来计算出面部的点,首先先获得人脸的关键点,脸部的点由耳朵的x坐标和鼻子的y坐标来表示,平移后的点由鼻子的x坐标和鼻子的y坐标来表示,根据上面的变形规律,想要瘦脸的范围,应该是脸部五分之一的位置,变形程度为100,详细代码见图23,原图见图24,瘦脸的结果见图25。

    def small_face(self):     #瘦脸
        left_eye_pos, right_eye_pos, nose_pos, mouth_pos, right_ear_pos, left_ear_pos = \
            self.get_face_key_point(self.img)

        self.img = self.localTranslationWarpFastWithStrength((left_ear_pos[0] ),nose_pos[1],nose_pos[0],
                                                             nose_pos[1], (left_ear_pos[0] - right_ear_pos[0])/5,100)
        self.img = self.localTranslationWarpFastWithStrength((right_ear_pos[0] ), nose_pos[1],nose_pos[0] ,
                                                             nose_pos[1], (left_ear_pos[0] - right_ear_pos[0])/5 , 100)
        self.show_image()

图23 瘦脸方法

图24 原图

图25 瘦脸后的结果图

  1. 缩小鼻子方法

继续采用局部缩小的算法(具体代码见图22),先获取到人脸的关键点,然后根据人脸的三庭五眼,我们可以知道鼻翼的点位于眼睛和鼻尖位置的中间,然后设置两个值,一个left,一个right,用户可以通过滑块调整输入的值,来确定缩小的范围,还设置了一个布尔值让程序判断,当布尔值等于0时用户是调节左鼻翼,等于1时用户调节右鼻翼。缩小鼻子的详细代码见图26,滑块的连接代码见图27,原图见图13,缩小鼻子的结果见图28。

    def small_nose(self,nose_pos=None):    #缩小鼻子
        if nose_pos ==None:
            left_eye_pos, right_eye_pos, nose_pos, mouth_pos, right_ear_pos, left_ear_pos = \
                self.get_face_key_point(self.img)

        if self.l_r == None:
            self.img = self.localTranslationWarpFastWithStrength(int(nose_pos[0]+(left_eye_pos[0]-nose_pos[0])/2), nose_pos[1],
                                                             int(nose_pos[0] + (left_eye_pos[0]-nose_pos[0])/2 -
                                                                 self.left),nose_pos[1]-0.01  ,8, 100)

            self.img = self.localTranslationWarpFastWithStrength(int(nose_pos[0] - (nose_pos[0] - right_eye_pos[0])/2),
                                                                 nose_pos[1],
                                                                 int(nose_pos[0] - (nose_pos[0] - right_eye_pos[0])/2
                                                                     + self.right) ,
                                                             nose_pos[1] + 0.01 ,8 , 100)
        elif self.l_r == 0:
            self.img = self.localTranslationWarpFastWithStrength(int(nose_pos[0] + (left_eye_pos[0] - nose_pos[0]) / 2),
                                                                 nose_pos[1],
                                                                 int(nose_pos[0] + (left_eye_pos[0] - nose_pos[
                                                                     0]) / 2 - self.left), nose_pos[1] + 0.01, 10, 100)
        elif self.l_r ==1:
            self.img = self.localTranslationWarpFastWithStrength(
                int(nose_pos[0] - (nose_pos[0] - right_eye_pos[0]) / 2),
                nose_pos[1], int(nose_pos[0] - (nose_pos[0] - right_eye_pos[0]) / 2 + self.right),
                nose_pos[1] + 0.01, 10, 100)
        self.show_image()
        self.widget_6.show()
        self.widget_7.show()
        self.widget.hide()

图26 缩小鼻子的代码

    def change_left(self,value):  #左鼻子
        self.l_r = 0
        self.left = value
        self.small_nose()

    def change_right(self,value):  #右鼻子
        self.l_r = 1
        self.right = value
        self.small_nose()

图27 两个滑块连接方法

图28 缩小鼻子的结果图

3、课程设计总结

通过本次课程设计我熟练了Qt Designer的操作,从一开始不是特别会操作,只会基本的拖拽,但是发现做出来的页面非常的原始不好看,从而努力上网查阅资料学习如何设计一款简洁美观的界面。

基本的功能已经基本实现,但是还存在一些小毛病,比如说,瘦脸功能和缩小鼻翼的功能,只能正脸实现;还有因为用了label显示,且隐藏了边框导致了页面不能拖拽;以及button不能上图下字。但是由于时间有限,所以没来得及将这些地方进行更改,以后有空可以继续完善这个系统。

4、设计体会

在UI界面设计中,button的icon一开始先在网上找了很久,然后发现自己不会设置button的上图下字,然后把button的image设置为icon,想将文字设置为提示框显示,结果发现提示框的背景会带有icon,于是上网查阅资料,发现可以直接为button设置icon,这样提示框的背景就不会带有icon。

在编写方法时,一开始就被放大眼睛这个方法所难倒了,在网上查阅了大量资料后,终于找到了局部缩放这个方法,然后我调了一天,终于调出来了。然后第二个大难点是,使用Google的mediapipe进行人脸检测并获取关键点的时候,只有六个人脸关键点,不知道怎么定位到脸部和鼻翼的点,然后想了半天,又不想换别的方法,于是乎想到了三庭五眼,所以瘦脸和缩小鼻翼都有一定的限制,因为它获取的点我是把它当成正脸来计算的。

5、参考文献

  1. https://www.jb51.net/article/230280.htm

  1. https://blog.csdn.net/qtzbxjg/article/details/127311

  1. https://blog.csdn.net/weixin_43810267/article/details/107413009

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值