【去后厂村摆摊吧】基于Jeston Nano部署(可用PC代替)的实时预览美甲机

【创造营第三期项目】基于Jeston Nano(可用笔记本代替)部署的实时美甲预览机

友情链接 - 给女朋友做一次AI美甲

项目介绍

美甲预览机是一种可以实时预览美甲的机器,下文简称美甲机

美甲机的功能是:实时识别摄像头拍摄的画面,并将画面中的指甲部分渲染成涂过美甲的样子,无需用户真正做一次美甲,即可便捷地观察到自己做过美甲的样子。

美甲机的预期目标群体为

  1. 想要做美甲,但是对颜色和图案拿不定主意的人,可以通过美甲机预览涂抹颜色之后的样子,便于决策;
  2. 对美甲有兴趣,但不敢做美甲的人,可以通过美甲机预览涂抹颜色之后的样子,无需真正的在手指上涂抹甲油;
  3. 其他有使用美甲机需求的人。

本项目详细介绍了如何通过深度学习方法,构造上述美甲机。项目内容包含训练指甲识别模型、Jetson Nano配置(Jetson Nano可用笔记本代替)、美甲机组装、效果优化等。

样例展示

以下是部分样例图片

纯色渲染复杂图案渲染1复杂图案渲染2部署界面截图
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

【视频录制中,暂时空置,样例图片可以参考2.2节、3.3节、3.4节、4.1节等各个章节的图片/动图/视频】

【如果你觉得模型运行效果不好请不要失望,训练集也就52张图,我扩充了之后也就100张,最重要是!没钱!拍摄画面质量跟不动啊哈哈哈哈】

下面,让我们一起来构造这样的一个美甲机吧~

环境准备(复现提示)

复现本项目需要准备以下内容:

  • 必须:Aistudio在线运行环境、个人主机(CPU/GPU均可,如未安装paddlepaddle-gpu,无需特意安装,直接使用CPU版本即可)、摄像头
  • 可选:Jetson Nano、补光灯、双面胶、纸箱、线

使用补光灯可以提高摄像头画面质量,提升模型运行效果。

<Jetson Nano + (摄像头) + (显示屏)> 可以用电脑主机/NUC/笔记本/手机等任何具有计算能力的设备代替,只要替代之后方便携带,你都可以带上你的美甲机向小伙伴展示,如果不方便携带…你可能就没办法带着你的美甲机去百度大楼下面摆摊了。

目录

  1. 指甲识别模型

    1.1. 数据准备

    1.2. PaddleSeg环境准备

    1.3. PaddleSeg模型选择

    1.4. PaddleSeg配置文件

    1.5. 训练PPLite

    1.6. 模型导出

    1.7. 模型预测
  2. PyQt5简单界面封装


    2.1. 环境准备

    2.2. 用PyQt5创建界面
  3. 配置Jetson Nano

    3.1. Jetson Nano导览

    3.2. Jetson Nano环境搭建

    3.3. Jetson Nano运行美甲机代码

    3.4. Jetson Nano无法选色处理
  4. 组装美甲机

    4.1. 美甲机组装(贫民版)

    4.2. 美甲机组装(非贫民版)
  5. 效果优化之增加样本(另类蒸馏)

    5.0. EISeg(不推荐)

    5.1. 通过PyQt5实环境样本采集

    5.2. 数据标注之训练OCRNet

    5.3. 数据标注之OCRNet标注图片

    5.3. 数据标注之OCRNet标注图片

    5.4. 数据标注之PyQt5交互式修图

    5.5. 将修改后的数据添加到训练图片集合中

    5.6. 结果示例
  6. 效果优化之模型推理降级

    6.0. 牺牲精度的推理加速方案

    6.1. 计算加速FastDeploy
  7. 效果优化之渲染策略

    7.2. 本项目原有的渲染策略

    7.1. 色彩抖动模型

    7.2. 基于色彩抖动模型的PyQt5封装

    7.2. 基于色彩抖动模型的复杂图案美甲
  8. 成品展示
  9. 结语

1. 指甲识别模型

1.1. 数据准备

项目使用数据集由李长安大佬提供,这份数据共52张图片,标注格式为:0表示背景信息,1表示指甲部分。(255代表白色,人眼极难分辨0和1的区别,所以在Nails/labels中看起来是纯黑的画面)。

将数据集解压,并按照下述格式写入txt文档。

图片地址1<空格>标签地址1
图片地址2<空格>标签地址2
图片地址3<空格>标签地址3
图片地址4<空格>标签地址4
图片地址5<空格>标签地址5
%cd ~
! unzip /home/aistudio/data/data68764/Nails.zip
import os
with open('train.txt','w') as f:
    for item in os.listdir('Nails/images'):
        if 'ipynb_checkpoints' not in item:
            f.write('Nails/images/'+item+' '+'Nails/labels/'+item+'\n')

1.2. PaddleSeg环境准备

PaddleSeg是Paddle发布的语义分割模型套件,包含了丰富的分割任务模型。PaddleSeg支持配置化训练,也支持pip安装后调用API接口。

本项目使用的是配置化训练,PaddleSeg环境安装方式参考了PaddleSeg安装文档

# 拉取PaddleSeg,国内用gitee方便一些,国外可以直接使用github
! git clone https://gitee.com/PaddlePaddle/PaddleSeg.git
# 安装,这个部分每次进入项目都要运行一次
%cd PaddleSeg
! pip install -r requirements.txt
# 检验环境是否成功安装,如果下方代码运行正常则安装成功
# ! sh tests/run_check_install.sh

1.3. PaddleSeg模型选择

PaddleSeg是Paddle发布的语义分割模型套件,直接进入官网,下拉可以看到如下内容:、

点击轻量级语义分割模型展开列表:

选一个mIoU和FPS都很高的模型即可。综合考虑本项目选择PP-LiteSeg STDC2。(如果为了部署视频效果更流畅可以切换更轻量的网络,如MobileSeg)

1.4. PaddleSeg配置文件

在开始训练之前需要配置一个yml文件,本项目中将yml文件命名为myconfig.yml 并放置于根目录\home\aistudio下。

配置文件内容如下:

batch_size: 8 # 配置批大小和迭代次数
iters: 5000

train_dataset: # 设置训练集路径,图像增强方法仅包含随机裁剪/缩放/调整明暗度和归一
  type: Dataset
  dataset_root: /home/aistudio
  train_path: /home/aistudio/train.txt
  num_classes: 2
  mode: train
  transforms:
    - type: RandomPaddingCrop
      crop_size: [480, 360]
    - type: Resize
      target_size: [480, 360]
    - type: RandomHorizontalFlip
    - type: RandomDistort
      brightness_range: 0.5
      contrast_range: 0.5
      saturation_range: 0.5
    - type: Normalize

val_dataset: # 设置验证集
  type: Dataset
  dataset_root: /home/aistudio
  val_path: /home/aistudio/train.txt
  num_classes: 2
  mode: val
  transforms:
    - type: Normalize

optimizer: # 设置优化器
  type: sgd
  momentum: 0.9
  weight_decay: 4.0e-5

lr_scheduler: # 设置学习率
  type: PolynomialDecay
  learning_rate: 0.01
  end_lr: 0
  power: 0.9

loss: # 使用交叉熵损失
  types:
    - type: CrossEntropyLoss
  coef: [1, 1, 1]


model: # 选择模型为PPLiteSeg
  type: PPLiteSeg
  backbone:
    type: STDC2
    pretrained: https://bj.bcebos.com/paddleseg/dygraph/PP_STDCNet2.tar.gz
  # pretrained: ./output/best_model/model.pdparams # 想要从自己训练好的模型进行训练取消注释即可

在根目录\home\aistudio下创建一个名为myconfig.yml 的文件,并将上述内容复制粘贴进去即可。

1.5. 训练PPLite

%cd ~/PaddleSeg
! export CUDA_VISIBLE_DEVICES=0 # 设置1张可用的卡

# 如果训练中断可以通过resume恢复
# **windows下请执行以下命令**
# **set CUDA_VISIBLE_DEVICES=0**
! python train.py \
       --config ~/myconfig.yml \
       --save_interval 2500 \
       --do_eval \
       --use_vdl \
       --save_dir output # \
       # --resume ./output/iter_1000

1.6. 模型导出

将模型导出到根目录下名为infer_model的文件夹中

! python export.py \
       --config ~/myconfig.yml \
       --model_path output/best_model/model.pdparams \
       --save_dir ~/infer_model

1.7. 模型预测

本节展示了如何通过paddle读取保存后的模型并且进行预测。

%cd ~
import paddle
import cv2

# 读取模型
model = paddle.jit.load('./infer_model/model')
model.eval()

# 读取图片
ori_img = cv2.imread('Nails/images/1eecab90-1a92-43a7-b952-0204384e1fae.png')

# 对图片进行归一化,调整为符合模型输入要求的形式
img = ori_img/255
img = paddle.vision.transforms.normalize(img, mean = [0.5,0.5,0.5], std = [0.5,0.5,0.5], data_format='HWC')
img = img.transpose((2,0,1))
img = paddle.to_tensor(img,dtype='float32')
img = img.reshape([1]+img.shape)

# 模型预测
pre = model(img)

# 将预测内容转化为图片
import numpy as np
mask = pre[0]
mask = mask*255
mask = np.array(mask, np.uint8)
cv2.imwrite('mask.jpg',mask)

# 对指甲部分的像素进行替换
hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV)
hsv_img[mask > 0,0] = 45
bgr_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR)

# 保存美甲部分颜色
cv2.imwrite('result.jpg',bgr_img)

上述内容运行后会在根目录下生成两个文件,分别是mask.jpgresult.jpg,用于预测图片可以在Nails/images/1eecab90-1a92-43a7-b952-0204384e1fae.png找到。

几张图片的对比效果如下:

原图分割结果美甲
在这里插入图片描述在这里插入图片描述在这里插入图片描述

一眼看上去效果还是可以的,贴上了绿色的美甲,保留了指甲的光泽信息。

上面的模型没有完美的把所有指甲部分分割开来,如果需要进一步的提升效果可以尝试以下操作:

  1. 重新训练(可以调整训练参数,使用resume,接着之前训练好的模型进行训练)
  2. 补充信息(训练集、知识蒸馏等)

使用第一种方法直接修改yml文件和训练命令即可,使用第二种方法可以参考第五章 5. 效果优化之增加样本

2. PyQt5简单界面封装

本节只对代码内容进行简单介绍,无法在Aistudio上一键运行,请将代码复制到本地进行运行。

2.1. 环境准备

本节需要准备如下内容:

  1. 个人主机(包含CPU/GPU均可)
  2. USB摄像头(如果使用笔记本电脑,并自带摄像头,可以直接用笔记本摄像头代替,如果在已有笔记本摄像头的基础下使用USB摄像头,可能需要对代码参数进行调整,具体参考代码注释即可,仅需修改一个数值)
  3. 第一节中运行后导出的模型文件(infer_model文件夹)
  4. 主机配置Python环境,安装依赖PaddlePaddle,PyQt5,opencv-python(新手小白可参考百度搜索Pycharm安装Pycahrm安装PaddlePaddle等内容)

2.2. 用PyQt5创建界面

使用PyQt5的方式非常简单,继承类->实现类初始化(定义界面布局、定义按钮关联事件)->在主函数中调用这个类,使用PyQt5的基本流程如下所示:

# 导入需要的包
import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()
        
        # 初始化相关变量、定义界面布局、将相关按钮关联到事件(函数)上

    def event1(self):
        # 点击第一个按钮所执行的事件(函数) 

    def event1(self):
        # 点击第二个按钮所执行的事件(函数) 
        
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

在上面代码中填入相关的内容即可在桌面创建一个窗口,能够根据点击执行对应的操作。

下面展示了如何在桌面上开启一个简单美甲预览机。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
zetcode.com
"""

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_width, self.img_height = [120 * x for x in [4,3]]
        self.color = QColor(0, 0, 0)

        self.initCamera()
        self.initUI()
        self.initModel()

    # 初始化Camera相关信息
    def initCamera(self):
        # 开启视频通道
        self.camera_id = 0 # 为0时表示视频流来自笔记本内置摄像头,如果笔记本自带摄像头但又使用了USB摄像头,可以将数值改为1或2或3...
        self.camera = cv2.VideoCapture()  # 视频流
        self.camera.open(self.camera_id)

        # 通过定时器读取数据
        self.flush_clock = QTimer()  # 定义定时器,用于控制显示视频的帧率
        self.flush_clock.start(60)   # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
        self.flush_clock.timeout.connect(self.show_frame)  # 若定时器结束,show_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        self.Color_Frame = QFrame(self)
        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
        grid.addWidget(self.Color_Frame, 0, 0, 6, 1)

        ColorText = QLabel('颜色预览', self)
        grid.addWidget(ColorText, 6, 0, 1, 1)

        Choose_Color = QPushButton('选择颜色', self)
        grid.addWidget(Choose_Color, 7, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Exit_Exe = QPushButton('退出', self)
        grid.addWidget(Exit_Exe, 19, 0, 1, 1)
        Exit_Exe.clicked.connect(self.close)

        self.Camera_Box = QLabel()  # 定义显示视频的Label
        self.Camera_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Camera_Box, 0, 1, 20, 20)

        self.Mask_Box = QLabel()  # 定义显示视频的Label
        self.Mask_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Mask_Box, 0, 21, 20, 20)

        self.Pred_Box = QLabel()  # 定义显示视频的Label
        self.Pred_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Pred_Box, 0, 41, 20, 20)

        self.setWindowTitle('test')
        self.show()

    def initModel(self):
        # 读取模型
        model_dir = "../infer_model/model" # 模型路径根据需要进行调整,这里设置的infer_model文件夹和名为Pycode的文件夹在同一目录下,run.py文件放在Pycode文件夹下
        self.model = paddle.jit.load(model_dir)
        self.model.eval()

    def show_frame(self):
        _, img = self.camera.read()  # 从视频流中读取

        img = cv2.resize(img, (self.img_width, self.img_height))  # 把读到的帧的大小重新设置为 640x480
        mask, pred = self.Infer(img)

        # 往显示视频的Label里 显示QImage
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        showImage = QImage(img, img.shape[1], img.shape[0], QImage.Format_RGB888)
        self.Camera_Box.setPixmap(QPixmap.fromImage(showImage))
        showImage = QImage(mask, mask.shape[1], mask.shape[0], QImage.Format_Grayscale8)
        self.Mask_Box.setPixmap(QPixmap.fromImage(showImage))
        showImage = QImage(pred, pred.shape[1], pred.shape[0], QImage.Format_RGB888)
        self.Pred_Box.setPixmap(QPixmap.fromImage(showImage))

    def Infer(self, ori_img):
        h, w, c = ori_img.shape
        img = paddle.vision.resize(ori_img,[self.img_width, self.img_height])
        img = img / 255
        img = paddle.vision.transforms.normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC')
        img = img.transpose((2, 0, 1))
        img = paddle.to_tensor(img, dtype='float32')
        img = img.reshape([1] + img.shape)

        pre = self.model(img)

        mask = pre[0]
        mask = mask * 255
        mask = np.array(mask, np.uint8)
        mask = paddle.vision.resize(mask, [h,w])
        # cv2.imwrite('mask.jpg', mask)

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV)

        # print(hsv_img.shape)
        # hsv_img[mask > 0, 0] = 45
        hsv_img[mask > 0, 0] = self.color.getHsv()[0]/2 * 1.1
        hsv_img[mask > 0, 1] = self.color.getHsv()[1]

        bgr_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB)
        # bgr_img = paddle.vision.resize(bgr_img, [h,w])

        return mask, bgr_img

    def choose_color(self):
        col = QColorDialog.getColor()
        if col.isValid():
            self.color = col
            self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % col.name())

        # print(self.color.name)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

将上面代码写入到名为run.py的文件中,并在电脑配置以下路径:

|-infer_model
|-Pycode
   |-run.py

即infer_model文件夹和名为Pycode的文件夹在同一目录下,run.py文件放在Pycode文件夹下,在路径Pycode下运行run.py即可。

运行即可看到一张具有三个方格的图片,第一个方格展示原图,第二个方格展示分割结果,第三个方格展示修改指甲部分颜色图片。

将手放在摄像头面前,即可预览指甲添加颜色之后的样子

也可以通过左边按钮选择颜色挑选一个你喜欢的颜色

比如,选择蓝色再点击ok之后再把手放在摄像头面前,就可以实时预览做了蓝色美甲的样子啦。

在这里插入图片描述

至此,你已经完成了美甲模型的创建和电脑端部署的全流程啦!不在意外壳,完全可以把它当作一个电脑端的应用程序(.exe)来用~

如果你想在Jetson Nano上部署这个美甲机,可以继续查看第三章配置Jetson Nano;如果你没有Jetson Nano,但是拥有笔记本电脑/Aibox/其他边缘部署设备,并且知道如何将模型部署在这些设备中(本章节就相当于在笔记本电脑上部署美甲模型了),并且对组装一个能抱去夜市上摆摊的美甲机感兴趣(理论来说你能抱得动台式电脑主机的话…台式机也算边缘部署设备了),可以查看第四章组装美甲机;如果你觉得美甲机的创意还不错,但是效果实在有点不好,可以查看第五章效果优化之增加样本

当然,对于用户而言是不需要中间的调试信息(原图+分割图)的,展示这些信息只是方便我们开发人员进行调试。如果希望整个界面完全面向用户,使用下面这段代码即可。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
zetcode.com
"""


import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_width, self.img_height = [120 * x for x in [4,3]]
        self.color = QColor(0, 0, 0)

        self.initCamera()
        self.initUI()
        self.initModel()

    # 初始化Camera相关信息
    def initCamera(self):
        # 开启视频通道
        self.camera_id = 0 # 为0时表示视频流来自摄像头
        self.camera = cv2.VideoCapture()  # 视频流
        self.camera.open(self.camera_id)

        # 通过定时器读取数据
        self.flush_clock = QTimer()  # 定义定时器,用于控制显示视频的帧率
        self.flush_clock.start(60)   # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
        self.flush_clock.timeout.connect(self.show_frame)  # 若定时器结束,show_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        self.Color_Frame = QFrame(self)
        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
        grid.addWidget(self.Color_Frame, 0, 0, 6, 1)

        ColorText = QLabel('颜色预览', self)
        grid.addWidget(ColorText, 6, 0, 1, 1)

        Choose_Color = QPushButton('选择颜色', self)
        grid.addWidget(Choose_Color, 7, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Exit_Exe = QPushButton('退出', self)
        grid.addWidget(Exit_Exe, 19, 0, 1, 1)
        Exit_Exe.clicked.connect(self.close)

        self.Pred_Box = QLabel()  # 定义显示视频的Label
        self.Pred_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Pred_Box, 0, 1, 20, 20)

        self.setWindowTitle('test')
        self.show()


    def initModel(self):
        # 读取模型
        model_dir = "../infer_model/model"
        self.model = paddle.jit.load(model_dir)
        self.model.eval()

    def show_frame(self):
        _, img = self.camera.read()  # 从视频流中读取

        img = cv2.resize(img, (self.img_width, self.img_height))  # 把读到的帧的大小重新设置为 640x480
        mask, pred = self.Infer(img)

        # 往显示视频的Label里 显示QImage
        showImage = QImage(pred, pred.shape[1], pred.shape[0], QImage.Format_RGB888)
        self.Pred_Box.setPixmap(QPixmap.fromImage(showImage))

    def Infer(self, ori_img):
        h, w, c = ori_img.shape
        img = paddle.vision.resize(ori_img,[self.img_width, self.img_height])
        img = img / 255
        img = paddle.vision.transforms.normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC')
        img = img.transpose((2, 0, 1))
        img = paddle.to_tensor(img, dtype='float32')
        img = img.reshape([1] + img.shape)

        pre = self.model(img)

        mask = pre[0]
        mask = mask * 255
        mask = np.array(mask, np.uint8)
        mask = paddle.vision.resize(mask, [h,w])
        # cv2.imwrite('mask.jpg', mask)

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV)

        hsv_img[mask > 0, 0] = self.color.getHsv()[0]/2 * 1.1
        hsv_img[mask > 0, 1] = self.color.getHsv()[1]


        bgr_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB)

        return mask, bgr_img

    def choose_color(self):
        col = QColorDialog.getColor()
        if col.isValid():
            self.color = col
            self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % col.name())

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

上述代码的界面如下所示,用户将手伸到摄像头下方,即可看到做了美甲的样子。

在这里插入图片描述

3. 配置Jetson Nano

本节粗略记录了一下我拿到Jetson Nano的感受和配置流程。

在介绍前,强烈对以下项目表示感谢,如果没有他们我一定没办法跑通整个部署流程!

  1. 教你如何在三步内Jetson系列上安装PaddlePaddle
  2. 在Jetson Nano上基于python部署Paddle Inference

3.1. Jetson Nano导览

Jetson Nano可以看作是一个电脑主机,非常小巧,下图是我已经组装好之后的样子,就不拆开给大家看了~

左下角有5V 4A的字样,在这个字样下面的一排就是各种接口,包括常见的电源口,USB接口,HDMI接口。通俗来说,配置好之后,插上鼠标、键盘、网卡、显示屏,你就可以启动Jetson Nano进入电脑桌面,打开AIStudio,然后登陆账号,找到一个项目【去后厂村摆摊吧】基于Jeston Nano部署(可用PC代替)的实时预览美甲机,学习如何将美甲机部署到Jstson Nano上。

3.2. Jetson Nano环境搭建

Jetson Nano的基础环境配置(烧写两张卡,类似于向两个U盘里拷东西,只是需要用专门的软甲拷贝),可以参考厂商附带的说明文档或Jetson系列——Ubuntu18.04版本基础配置总结(换源、重要组件安装及配置、ROS、WiFi、远程桌面)

假设大家都可以根据对应厂商或者网上教程完成完成基础配置。给Jetson Nano连接风扇(风扇主要是扇热,如果你有其他散热手段可以不用风扇)、电源、键盘、鼠标、网卡、USB摄像头。

接下来打开终端Terminal。按照教你如何在三步内Jetson系列上安装PaddlePaddle执行到安装pip3完毕即可。

打开浏览器,在官方提供的 下载安装Linux预测库 中,找到一个名称中包含Jetson Nano的预购建Wheel包。

然后确认自己的Jetpack版本,如果是4.6就下载下面那个包,如果是4.4就下载上面那个包即可。

之后,在终端安装下载的包:

pip3 install paddlepaddle_gpu-2.2.2-cp36-cp36m-linux_aarch64.whl

即可。

如果没有成功安装…我也没办法了…

3.3. Jetson Nano运行美甲机代码

在Jetson Nano上运行代码有很多不方便的地方,比如

pip install pyqt5
pip install opencv-python

可能会报错。遇到这些错误也不要慌,也许你进入python环境后:

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

都没问题!

当然,如果你不能成功进行pip install,可能会导致本地代码版本和Jetson Nano上的环境有较大差距。比如说我的Jetson Nano中的PyQt5是不存在QImage.Format_BGR888的,仅存在QImage.Format_RGB888

完成了上面所有的配置内容,将infer_modelPycode/run.py通过U盘拷贝到Jetson Nano中,假设你的路径是桌面,则在命令行中执行以下代码即可:

sudo sh -c "echo 255 > /sys/devices/pwm-fan/target_pwm"
cd Desktop/Pycode
python3 run.py

运行结果如下:

3.4. Jetson Nano无法选色处理

如果之前的包安装有误,就无法畅通地运行一些功能,比如打开调色板选择颜色。可以考虑替代方案:指定一组颜色让用户选择。

对之前的代码进行细微调整即可:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
zetcode.com
"""

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_width, self.img_height = [120 * x for x in [4,3]]
        self.color = QColor(0, 0, 0)

        self.initCamera()
        self.initUI()
        self.initModel()

    # 初始化Camera相关信息
    def initCamera(self):
        # 开启视频通道
        self.camera_id = 0 # 为0时表示视频流来自摄像头
        self.camera = cv2.VideoCapture()  # 视频流
        self.camera.open(self.camera_id)

        # 通过定时器读取数据
        self.flush_clock = QTimer()  # 定义定时器,用于控制显示视频的帧率
        self.flush_clock.start(60)   # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
        self.flush_clock.timeout.connect(self.show_frame)  # 若定时器结束,show_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        self.Color_Frame = QFrame(self)
        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
        grid.addWidget(self.Color_Frame, 0, 0, 6, 1)

        ColorText = QLabel('颜色预览', self)
        grid.addWidget(ColorText, 6, 0, 1, 1)

        Choose_Color = QPushButton('黑色', self)
        grid.addWidget(Choose_Color, 7, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('紫色', self)
        grid.addWidget(Choose_Color, 8, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('天蓝', self)
        grid.addWidget(Choose_Color, 9, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('红色', self)
        grid.addWidget(Choose_Color, 10, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('黄色', self)
        grid.addWidget(Choose_Color, 11, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('粉色', self)
        grid.addWidget(Choose_Color, 12, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('绿色', self)
        grid.addWidget(Choose_Color, 13, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Exit_Exe = QPushButton('退出', self)
        grid.addWidget(Exit_Exe, 19, 0, 1, 1)
        Exit_Exe.clicked.connect(self.close)

        self.Pred_Box = QLabel()  # 定义显示视频的Label
        self.Pred_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Pred_Box, 0, 1, 20, 20)

        self.setWindowTitle('test')
        self.show()

    def initModel(self):
        # 读取模型
        model_dir = "../infer_model/model"
        self.model = paddle.jit.load(model_dir)
        self.model.eval()

    def show_frame(self):
        _, img = self.camera.read()  # 从视频流中读取

        img = cv2.resize(img, (self.img_width, self.img_height))  # 把读到的帧的大小重新设置为 640x480
        mask, pred = self.Infer(img)

        # 往显示视频的Label里 显示QImage
        showImage = QImage(pred, pred.shape[1], pred.shape[0], QImage.Format_RGB888)
        self.Pred_Box.setPixmap(QPixmap.fromImage(showImage))

    def Infer(self, ori_img):
        h, w, c = ori_img.shape
        img = paddle.vision.resize(ori_img,[self.img_width, self.img_height])
        img = img / 255
        img = paddle.vision.transforms.normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC')
        img = img.transpose((2, 0, 1))
        img = paddle.to_tensor(img, dtype='float32')
        img = img.reshape([1] + img.shape)

        pre = self.model(img)

        mask = pre[0]
        mask = mask * 255
        mask = np.array(mask, np.uint8)
        mask = paddle.vision.resize(mask, [h,w])
        # cv2.imwrite('mask.jpg', mask)

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV)

        # print(hsv_img.shape)
        # hsv_img[mask > 0, 0] = 45
        hsv_img[mask > 0, 0] = self.color.getHsv()[0]/2 * 1.1
        hsv_img[mask > 0, 1] = self.color.getHsv()[1]

        bgr_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB)

        return mask, bgr_img

    def choose_color(self):
        if self.sender().text() == '紫色':
            self.color.setHsv(240, 85, 255)
        elif self.sender().text() == '天蓝':
            self.color.setHsv(180, 85, 255)
        elif self.sender().text() == '红色':
            self.color.setHsv(0, 255, 255)
        elif self.sender().text() == '黄色':
            self.color.setHsv(60, 255, 255)
        elif self.sender().text() == '粉色':
            self.color.setHsv(300, 85, 255)
        elif self.sender().text() == '绿色':
            self.color.setHsv(99, 128, 255)
        else:
            self.color.setHsv(0, 0, 0)

        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

效果如下,虽然无法通过选色盘选择颜色,但是可以通过一些预定地颜色进行美甲,如果需要跟多颜色,可以手动地写入更多地按钮用来选择颜色。

在Jetson Nano上的运行效果如下
在这里插入图片描述

如果你希望全屏显示,可以在代码中设置图片缩放的尺寸/调整布局令画面居中/更改分辨率更低的显示屏等~

4. 组装美甲机

4.1. 美甲机组装(贫民版)

首先,展示一下贫民版美甲机成品

图片1图片2图片3
未工作在这里插入图片描述在这里插入图片描述在这里插入图片描述
工作状态在这里插入图片描述在这里插入图片描述在这里插入图片描述

如图所示,美甲机主要由一个带有Jetson Nano、补光灯、显示屏、USB摄像头的纸箱子组成,以下是具体的组成内容和组合方式:

  1. 需要准备一个箱子,比如!你可以买个电磁炉,还送个锅,100+/200+ 人民币就可以有一个大小尺寸都很合适的纸箱子!当然也可以用各种贴纸/硬卡纸,只要具有一定的承重能力均可。(当然从收垃圾的老大爷那里也能拿到一个合适的箱子)
  2. (可选)有一个补光灯和绑带,在纸箱侧边剪开一个区域,用于塞你的补光灯,如果不好固定就在纸箱子上开几个洞,把绳子传过去进行捆绑固定。
  3. 准备Jetson Nano(使用笔记本电脑的玩家跳过这个步骤)和双面胶,除非你的Jstson Nano是裸机壳,不然的话可以直接在外壳上贴上双面胶,固定在纸箱上,固定位置要考虑你的数据线是否方便连接。
  4. 屏幕,如果你使用笔记本电脑,可以参考上图中屏幕放置的区域,进行放置。
  5. 摄像头,摄像头应当保持镜头对着补光灯打光的位置,上图中的纸箱开口/屏幕位置也是为了方便用户伸手并在屏幕上看到自己的美甲效果而放置的。

下面,来看看组装后的美甲机如何使用吧(建议通过画中画模式,这样画面大一点,或者直接跳转bilibili:https://www.bilibili.com/video/BV15d4y1z72T/ ):

4.2. 美甲机组装(非贫民版)

非贫民版的美甲机需要有人赞助一组亚克力板~还在设计图纸中qwq欢迎有想法的小伙伴在评论区报名

5. 效果优化之增加样本(另类蒸馏)

在以上章节中,已经完成了从选择模型到界面编程到实机组装的全流程~但事实上,这个美甲机存在一系列不足之处,包括但不仅限于:分割结果不够准确,分割推理过于耗时不利于边缘部署,美甲色彩渲染不自然等问题。

本节介绍从数据采集到生成一个合适的数据集的全流程。

5.0. EISeg(不推荐)

在正式开始之前,首先,介绍一下EISeg

EISeg是一个非常方便的分割工具,根据官方教程,下载并安装EISeg,打开即用,非常方便。其使用方法是打开图片,导入官方配置好的模型(在Github上有链接可以下载),点击自己需要的分割区域中任意一点,即可自动拟合一整片区域,如果勾选了自己不需要的区域,只需要右键点击被勾选的区域即可自动拟合一块新的区域。

简单来说,只要你反复点点点,就能完成一张图片的分割数据集标注。非常推荐大家去体验一下。

**但是!**对于本项目来说,使用EISeg有以下不足之处:

  1. 每一次点击的背后,都是一次模型的推理,推理的速度相对于点击的速度来说比较慢;
  2. 分割结果的边缘不是特别的好,高质量的分割标签需要消耗大量的耐心反复点击,并且人眼的判断也是会出错的;
  3. 在我粗略的通过EISeg生成的数据集进行训练之后,发现模型效果反而降低了。

总之,根据我个人的使用经验,我不是很推荐通过EISeg生成本项目所需要的补充数据集。但是我非常推荐大家都去尝试一下这个交互式语义分割标注工具。有可能我训练不好只是因为我的数据标注比较粗糙,而非每个人使用EISeg效果都不好。

5.1. 通过PyQt5实环境样本采集

第4节已经搭建了一个美甲机环境,出于增加训练样本和实际场景的贴合度,本节直接通过美甲机摄像头采集对应图片。首先,还是将摄像头连接到电脑上,运行以下脚本

注1:请确保当前环境中,包含一个名为saved_img的文件夹,没有手动创建一个即可

注2:所有和摄像头,PyQt5相关的代码都不能在AIStudio上运行的,请拷贝到自己的电脑环境上运行

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
zetcode.com
"""

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_width, self.img_height = [120 * x for x in [4,3]]
        self.Index = 0
        self.initCamera()
        self.initUI()

    # 初始化Camera相关信息
    def initCamera(self):
        # 开启视频通道
        self.camera_id = 0 # 为0时表示视频流来自笔记本内置摄像头
        self.camera = cv2.VideoCapture()  # 视频流
        self.camera.open(self.camera_id)

        # 通过定时器读取数据
        self.flush_clock = QTimer()  # 定义定时器,用于控制显示视频的帧率
        self.flush_clock.start(60)   # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
        self.flush_clock.timeout.connect(self.show_frame)  # 若定时器结束,show_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        ColorText = QLabel('当前标号', self)
        grid.addWidget(ColorText, 0, 0, 1, 1)

        self.IndexText = QLabel(str(self.Index), self)
        grid.addWidget(ColorText, 1, 0, 1, 1)

        Save_Img = QPushButton('保存', self)
        grid.addWidget(Save_Img, 2, 0, 1, 1)
        Save_Img.clicked.connect(self.save)

        Exit_Exe = QPushButton('退出', self)
        grid.addWidget(Exit_Exe, 3, 0, 1, 1)
        Exit_Exe.clicked.connect(self.close)

        self.Camera_Box = QLabel()  # 定义显示视频的Label
        self.Camera_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Camera_Box, 0, 1, 20, 20)

        self.setWindowTitle('test')
        self.show()

    def show_frame(self):
        _, self.img = self.camera.read()  # 从视频流中读取

        img = cv2.resize(self.img, (self.img_width, self.img_height))

        # 往显示视频的Label里 显示QImage
        showImage = QImage(img, img.shape[1], img.shape[0], QImage.Format_BGR888)
        self.Camera_Box.setPixmap(QPixmap.fromImage(showImage))

    def save(self):
        # 一定要确保目录是存在的
        cv2.imwrite('./saved_img/'+str(self.Index)+'.jpg',self.img)
        self.Index = self.Index+1
        self.IndexText.setText(str(self.Index))

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

运行即可看到如下窗口。

将一只手伸到摄像头下方,另一只手点击左上角的保存按钮即可。不断改变手的角度和姿势,点击保存,即可在saved_img文件夹中生成一系列图片0.jpg 1.jpg 2.jpg …。

5.2. 数据标注之训练OCRNet

本节开头已经表明了使用EISeg是一种较为费劲的方式,为了更轻松的对数据进行标注,本项目先训练一个拟合能力更好的网络,然后通过这个网络预测刚刚采集到的图片,最后通过人工修图的方式,把网络预测错误的地方改正过来,从而得到一批新的数据集。这批新的数据还可以和之前的数据合并在一起再次对这个网络进行训练,然后再对一批新的样本进行模型标注+人工修改。反复几次之后,每一批样本中需要人工手动修改的地方就会越来越少,这批样本的质量也非常的高。

直接目标网络(PPLite)而不是OCRNet也是可行的,只是OCRNet的学习能力更好,能够更好的降低人工修改的时间成本。

训练OCRNet的方式和第一节中训练PPLite的方式是完全相同的,只是使用的yaml文件中内容需要更换为以下内容:

batch_size: 8
iters: 75000

train_dataset:
  type: Dataset
  dataset_root: /home/aistudio
  train_path: /home/aistudio/train.txt
  num_classes: 2
  mode: train
  transforms:
    # - type: ResizeStepScaling
    #   min_scale_factor: 0.5
    #   max_scale_factor: 2.0
    #   scale_step_size: 0.25
    - type: RandomPaddingCrop
      crop_size: [640, 480]
    - type: Resize
      target_size: [640, 480]
    - type: RandomHorizontalFlip
    - type: RandomDistort
      brightness_range: 0.5
      contrast_range: 0.5
      saturation_range: 0.5
    - type: Normalize

val_dataset:
  type: Dataset
  dataset_root: /home/aistudio
  val_path: /home/aistudio/train.txt
  num_classes: 2
  mode: val
  transforms:
    - type: Normalize

model:
  type: OCRNet
  backbone:
    type: HRNet_W18
    pretrained: https://bj.bcebos.com/paddleseg/dygraph/hrnet_w18_ssld.tar.gz
  backbone_indices: [0]
  # pretrained: /home/aistudio/OCRNetModel/model.pdparams

optimizer:
  type: sgd

lr_scheduler:
  type: PolynomialDecay
  learning_rate: 0.01
  power: 0.9


loss:
  types:
    - type: CrossEntropyLoss
    - type: CrossEntropyLoss
  coef: [1, 1]

训练时的调用代码需要注意save_interval 应当随着iters适当增大。

5.3. 数据标注之OCRNet标注图片

假设,已经训练好了一个OCRNet,并且训练好的模型所在路径为~/OCRNetModel/model.pdparams,训练yml文件路径为~/MyData/MyFingerLabel,待预测(采集的)样本路径为~/MyData/MyFinger,则可以通过以下命令预测对应的标签:

%cd ~/PaddleSeg
! python predict.py \
       --config ~/myconfig-OCRNet.yml \
       --model_path ~/OCRNetModel/model.pdparams \
       --image_path ~/MyData/MyFinger \
       --save_dir ~/MyData/MyFingerLabel

OCRNet的预测结果如下所示:

图片1图片2图片3
在这里插入图片描述在这里插入图片描述在这里插入图片描述

可以看到有的图片标注的非常完美,自己亲自标注可能也不一定能做到这么好。但是也有的图片会漏标或者多标。

5.4. 数据标注之PyQt5交互式修图

基本思想是,通过PyQt5读取原图和分割后的图片,并展示叠加后的图片,通过鼠标左键长按拖拽,将鼠标滑过区域标记为指甲部分,通过鼠标右键长按拖拽,将鼠标滑过的区域标记为非指甲部分。

运行下列代码时请创建MyFinger文件夹,并将0.jpg 1.jpg 2.jpg …放到这个文件夹中;创建MyFingerLabel文件夹,将上面预测得到的pseudo_color_prediction文件夹中的0.png 1.png 2.png …放进去。

注意:一定要保证序号从0开始

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
zetcode.com
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
https://blog.csdn.net/shangxiaqiusuo1/article/details/89205185
https://blog.csdn.net/qq_36780295/article/details/109034448
"""

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtGui import QColor, QImage, QPixmap
# from PyQt5 import QtWidgets,QtCore,QtGui,Qt

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_dir = './MyFinger/-1.jpg'
        self.label_dir = './MyFingerLabel/-1.png'
        self.index = -1
        self.img_x = 0
        self.img_y = 0
        self.hold_flag = 0
        self.draw_color = 255

        self.initUI()

        self.next_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        self.IndexText = QLabel(str(self.index), self)
        grid.addWidget(self.IndexText, 0, 0, 1, 1)

        self.SiteText = QLabel('', self)
        grid.addWidget(self.SiteText, 1, 0, 1, 1)

        Next_button = QPushButton('下一个', self)
        grid.addWidget(Next_button, 2, 0, 1, 1)
        Next_button.clicked.connect(self.next_frame)

        self.Img_Box = QLabel()  # 定义显示视频的Label
        self.Img_Box.setFixedSize(640, 480)
        grid.addWidget(self.Img_Box, 3, 0, 20, 20)
        self.Img_Box.setMouseTracking(True)

        self.Ori_Img_Box = QLabel()  # 定义显示视频的Label
        self.Ori_Img_Box.setFixedSize(640, 480)
        grid.addWidget(self.Ori_Img_Box, 3, 20, 20, 20)

        self.setWindowTitle('test')
        self.show()

    def next_frame(self):
        if self.index != -1:
            cv2.imwrite(self.label_dir,self.label_img/255*128)
        try:
            # read next img
            self.index = self.index + 1
            self.img_dir = self.img_dir.replace(str(self.index-1),str(self.index))
            self.label_dir = self.label_dir.replace(str(self.index - 1), str(self.index))

            self.img = cv2.imread(self.img_dir)

            showImage = QImage(self.img, self.img.shape[1], self.img.shape[0], QImage.Format_RGB888)
            self.Ori_Img_Box.setPixmap(QPixmap.fromImage(showImage))

            self.label_img = cv2.imread(self.label_dir)[:,:,1]/128*255
            self.img[self.label_img > 0,0] = (self.img[self.label_img > 0,0]+255)/2

            showImage = QImage(self.img, self.img.shape[1], self.img.shape[0], QImage.Format_RGB888)
            self.Img_Box.setPixmap(QPixmap.fromImage(showImage))

            # update index
            self.IndexText.setText(str(self.index))
        except:
            # 保持原有编号,其实应该设计提示框,这里直接不设计了
            self.index = self.index

    def mouseMoveEvent(self, event):

        s = event.windowPos()

        self.setMouseTracking(True)
        self.img_x = int(s.x()-self.Img_Box.x())
        self.img_y = int(s.y()-self.Img_Box.y())
        self.SiteText.setText('X:' + str(s.x()-self.Img_Box.x())+' Y:' + str(s.y()-self.Img_Box.y()))

        if self.hold_flag:
            self.label_img[self.img_y:self.img_y+3, self.img_x:self.img_x+3] = self.draw_color
            self.img = cv2.imread(self.img_dir)
            self.img[self.label_img > 0, 0] = (self.img[self.label_img > 0, 0] + 255) / 2
            showImage = QImage(self.img, self.img.shape[1], self.img.shape[0], QImage.Format_RGB888)
            self.Img_Box.setPixmap(QPixmap.fromImage(showImage))

    def mousePressEvent(self, event):
        self.hold_flag = 1
        if event.buttons() == Qt.LeftButton:
            self.draw_color = 255
        elif event.buttons() == Qt.RightButton:
            self.draw_color = 0

    def mouseReleaseEvent(self, event):
        self.hold_flag = 0


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

使用时的示例如下:

左键长按涂抹没有标注的区域,即可补全标注,右键涂抹标注错误的区域,即可取消标注。修改好后点击左上角下一个按钮即可自动保存。

在这里插入图片描述

5.5. 将修改后的数据添加到训练图片集合中

以Aistudio训练环境为例,将MyFinger文件夹、MyFingerLabel文件夹打包上传Aistudio,解压缩后更新train.txt即可。下面的代码展示了如何将打包后的文件配置在对应的路径中,并且更新train.txt。

由于标注后的图片中指甲区域像素为128,非指甲区域为0,因此需要通过img = img/128调整图片格式为PaddleSeg的训练格式。

%cd ~
! unzip MyFinger.zip -d Nails/MyAddData1
import os
import cv2
with open('train.txt','a') as f:
    for item in os.listdir('Nails/MyAddData1/MyFinger'):
        if 'ipynb_checkpoints' not in item:
            img_dir = 'Nails/MyAddData1/MyFinger/'+item
            label_dir = 'Nails/MyAddData1/MyFingerLabel/'+item.replace('.jpg','.png')
            img = cv2.imread(label_dir)[:,:,1]
            img = img/128
            cv2.imwrite(label_dir,img)
            f.write(img_dir+' '+label_dir+'\n')

重复以上步骤,你的训练数据集就可以蓬勃增长啦!用扩展过的数据训练美甲机模型(PPLite),就可以让的指甲部分分割结果也会更稳定啦~

5.6. 结果示例

下面是补充了50张图片,再通过PPLite训练后的结果,可以看到依旧卡顿,而且运动的时候会产生运动模糊,无法识别分割。除此之外,分割稳定性和准确性大幅度提高。

6. 效果优化之模型推理降级

本节主要介绍如何加快推理速度。加快推理速度,即提高视频流畅度的方法通常可以从以下方面入手:

  1. 数据预处理
  2. 模型优化
  3. 计算优化

6.0. 牺牲精度的推理加速方案

  1. 设置推理图片为原推理大小的1/4,例如原始推理图片的大小为[360, 480],将图片更改为[180, 240]进行更改。
  2. 使用更轻量的网络进行推理,如MobileSeg。

测试结果表明上述两种方法均能提高摄像头画面的流畅性。但降低图片大小会导致分割区域不稳定,识别区域会变大。使用仅扩张一次的训练集训练MobileSeg的鲁棒性较低,无法识别晃动手部时产生的模糊图片。

对于上述两种情况,可以尝试补充数据集以达到更好的推理效果。

6.1. 计算加速FastDeploy

FastDeploy是一款易用高效的推理部署开发套件。覆盖业界热门AI模型并提供开箱即用的部署体验,包括图像分类、目标检测、图像分割、人脸检测、人脸识别、人体关键点识别、文字识别、语义理解等多任务,满足开发者多场景,多硬件、多平台的产业部署需求。集成Paddle Inference、ONNX Runtime、TensorRT等推理引擎并提供统一的部署体验。

简单来说,如果你仅需要使用当前经典算法(Paddle套件中包含的网络结构)进行部署,使用FastDeploy进行部署可以提高模型推理时间。对于视频流实时推理来说,加快模型推理时间很重要。

注:能否在Jetson Nano上安装成功全靠个人运气,可以使用PC跟着本节进行FastDeploy部署

6.1.1. 安装FastDeploy

根据官方教程选择合适的包进行安装

CPU:pip install numpy opencv-python fastdeploy-python -f https://www.paddlepaddle.org.cn/whl/fastdeploy.html

GPU:pip install numpy opencv-python fastdeploy-gpu-python -f https://www.paddlepaddle.org.cn/whl/fastdeploy.html

注:两个包不能同时安装,例如,当前已经安装了CPU版的FastDeploy,如果需要安装GPU版的,需要先将CPU版的FastDeploy卸载

6.1.2. 使用FastDeploy

以美甲机为例,首先需要引入包

import fastdeploy

将美甲机代码中initModel函数替换为如下内容

    def initModel(self):
        # 读取模型
        model_dir = "../infer_model/"
        self.model = fastdeploy.vision.segmentation.PaddleSegModel(model_dir+'model.pdmodel',
                                                                   model_dir+'model.pdiparams',
                                                                   model_dir+'deploy.yaml')

将美甲机代码中Infer函数替换为如下内容

    def Infer(self, ori_img):
        h, w, c = ori_img.shape

        img = paddle.vision.resize(ori_img,[self.infer_img_height, self.infer_img_width])

        pre = self.model.predict(img)

        mask = np.reshape(pre.label_map, pre.shape) * 255
        mask = np.array(mask, np.uint8)
        mask = paddle.vision.resize(mask, [h,w])

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV)

        hsv_img[mask > 0, 0] = self.color.getHsv()[0]/2 * 1.1
        hsv_img[mask > 0, 1] = self.color.getHsv()[1]

        bgr_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB)

        return mask, bgr_img

相比于之前的Infer函数,新版的Infer函数不需要写入一大堆数据预处理的内容,直接调用FastDeploy封装好的predict即可预测。

需要注意的是,返回的pre的类型是struct,并不能直接作为mask,需要先转成list,再reshape一下才能和图片格式对齐。

6.1.3. 效果展示

等待手模中~大家知道运行起来更流畅就可以了

7. 效果优化之渲染策略

渲染策略会影响最后输出的画面中的真实性,比如,在RGB空间中,直接用目标美甲颜色代替整块区域,就会丢失指甲质感。本节介绍一种新的渲染策略,并且根据这种策略,将美甲机支持的渲染内容从纯色美甲扩展到复杂图片渲染。

7.0. 本项目原有的渲染策略

直接查看上面的代码可得

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV)

        hsv_img[mask > 0, 0] = self.color.getHsv()[0]/2 * 1.1
        hsv_img[mask > 0, 1] = self.color.getHsv()[1]

即本项目原有的渲染策略是:将图片转化到hsv空间,保留图片的明度(V)替换图片的色调(H)和饱和度(S)。

7.1. 色彩抖动模型

本项目的分割内容主要包含三个部分:指甲前缘、指甲盖和指甲弧。其中指甲前缘就是每次剪指甲会剪下的部分,指甲弧是指甲上靠近手指关节处的一块月缺形的区域,指甲盖就是指甲前缘和指甲弧之间的部分。

其中,指甲盖的颜色偏粉,而另外两个部分偏白。在不考虑指甲前缘和指甲弧的情况下,不妨假设指甲部分的颜色是统一且均匀的。则最后呈现在我们眼前的指甲色彩主要由两部分决定:指甲基色光线+景深+各种因素

对于某个特定的像素点有:

C o l o r = B a s e _ C o l o r + I n f l u e n c e Color=Base\_Color+Influence Color=Base_Color+Influence

我们做美甲,相当于对指甲基色,也就是 B a s e _ C o l o r Base\_Color Base_Color进行修改。因此,只需要将抖动项,也就是 I n f l u e n c e Influence Influence求出即可。

对于一张给定的指甲图片,其指甲盖部分的基色可以通过求均值、求中位数获得;而 I n f l u e n c e Influence Influence可以通过 C o l o r − B a s e _ C o l o r Color-Base\_Color ColorBase_Color获得。从而做了美甲的图片中各个像素点的值为:

T a r g e t _ C o l o r = T a r g e t _ B a s e _ C o l o r + I n f l u e n c e Target\_Color=Target\_Base\_Color+Influence Target_Color=Target_Base_Color+Influence

整理可得:

T a r g e t _ C o l o r = C o l o r + ( T a r g e t _ B a s e _ C o l o r − B a s e _ C o l o r ) Target\_Color=Color+(Target\_Base\_Color-Base\_Color) Target_Color=Color+(Target_Base_ColorBase_Color)

对于HSV模型,可以认为亮度(V)是不需要调整的。因此仅对色调(H)和饱和度(S)应用上述模型即可。

新模型和原有模型的效果对比如下:

原图色调饱和度替换模型(原始模型)色彩抖动模型(新模型)

7.2. 基于色彩抖动模型的PyQt5封装

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
zetcode.com
"""

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_width, self.img_height = [120 * x for x in [4,3]]
        self.infer_img_width, self.infer_img_height = [120 * x for x in [4, 3]]
        self.color = QColor()
        self.color.setHsv(300, 85, 255)

        self.initCamera()
        self.initUI()
        self.initModel()

    # 初始化Camera相关信息
    def initCamera(self):
        # 开启视频通道
        self.camera_id = 0 # 为0时表示视频流来自摄像头
        self.camera = cv2.VideoCapture()  # 视频流
        self.camera.open(self.camera_id)

        # 通过定时器读取数据
        self.flush_clock = QTimer()  # 定义定时器,用于控制显示视频的帧率
        self.flush_clock.start(60)   # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
        self.flush_clock.timeout.connect(self.show_frame)  # 若定时器结束,show_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        self.Color_Frame = QFrame(self)
        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
        grid.addWidget(self.Color_Frame, 0, 0, 6, 1)

        ColorText = QLabel('颜色预览', self)
        grid.addWidget(ColorText, 6, 0, 1, 1)

        Choose_Color = QPushButton('粉色', self)
        grid.addWidget(Choose_Color, 7, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('米黄', self)
        grid.addWidget(Choose_Color, 8, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('浅绿', self)
        grid.addWidget(Choose_Color, 9, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('青绿', self)
        grid.addWidget(Choose_Color, 10, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('玫红', self)
        grid.addWidget(Choose_Color, 11, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Exit_Exe = QPushButton('退出', self)
        grid.addWidget(Exit_Exe, 19, 0, 1, 1)
        Exit_Exe.clicked.connect(self.close)

        self.Pred_Box = QLabel()  # 定义显示视频的Label
        self.Pred_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Pred_Box, 0, 1, 20, 20)

        self.setWindowTitle('test')
        self.show()

    def initModel(self):
        # 读取模型
        model_dir = "../infer_model/model"
        self.model = paddle.jit.load(model_dir)
        self.model.eval()

    def show_frame(self):
        _, img = self.camera.read()  # 从视频流中读取

        img = cv2.resize(img, (self.img_width, self.img_height))  # 把读到的帧的大小重新设置为 640x480
        mask, pred = self.Infer(img)

        # 往显示视频的Label里 显示QImage
        showImage = QImage(pred, pred.shape[1], pred.shape[0], QImage.Format_RGB888)
        self.Pred_Box.setPixmap(QPixmap.fromImage(showImage))

    def Infer(self, ori_img):
        h, w, c = ori_img.shape
        img = paddle.vision.resize(ori_img,[self.infer_img_height, self.infer_img_width])
        img = img / 255
        img = paddle.vision.transforms.normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC')
        img = img.transpose((2, 0, 1))
        img = paddle.to_tensor(img, dtype='float32')
        img = img.reshape([1] + img.shape)

        pre = self.model(img)

        mask = pre[0]
        mask = mask * 255
        mask = np.array(mask).astype('float32')
        mask = paddle.vision.resize(mask, [h,w])
        # cv2.imwrite('mask.jpg', mask)

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV).astype('float32')

        hsv_img[mask > 0, 0] = hsv_img[mask > 0, 0] + self.color.getHsv()[0]/2 - np.median(hsv_img[mask > 0,0])
        hsv_img[mask > 0, 1] = hsv_img[mask > 0, 1] + self.color.getHsv()[1] - np.median(hsv_img[mask > 0,1])
        hsv_img[hsv_img[:,:,0] > 179, 0] = 179
        hsv_img[hsv_img[:,:,0] < 1, 0] = 1
        hsv_img[hsv_img[:, :, 1] > 255, 0] = 255
        hsv_img[hsv_img[:, :, 1] < 1, 0] = 1

        bgr_img = cv2.cvtColor(hsv_img.astype('uint8'), cv2.COLOR_HSV2RGB)

        return mask, bgr_img

    def choose_color(self):
        if self.sender().text() == '米黄': # 米黄 有点偏绿 过曝了
            self.color.setHsv(40, 30, 255)
        elif self.sender().text() == '浅绿': # 浅绿 还不错
            self.color.setHsv(100, 85, 255)
        elif self.sender().text() == '青绿': # 青绿 还不错
            self.color.setHsv(150, 85, 255)
        elif self.sender().text() == '玫红': # 肉红色 感觉没啥问题就是不太好看
            self.color.setHsv(350, 160, 255)
        else:
            self.color.setHsv(300, 85, 255) # 粉色 标准色贼拉好

        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

这种方式相对于之前的模型而言,有更好的纹理表现效果,但是因为是对像素值进行加减,所以经常会超出像素值应当处于的数值间隔中。简单来说会出现过曝。例如:

期望渲染结果错误渲染结果

当然,也有可能是因为我程序里写错了一些地方。总之,这种渲染方式产生的图片具有更好的纹理效果,但是鲁棒性没有第一种渲染方式好。上面的代码中我挑选了部分颜色,这些颜色在第二种渲染方式下的表现都比较好,大家可以放心使用~

另外就是,这种过曝一样的效果也许是因为分割部分的颜色分布导致的,如果光线等条件比较充足,比如分布比较集中(类似于服从正态分布),可能效果更好。

7.2. 基于色彩抖动模型的复杂图案美甲

有了色彩抖动模型之后,只要图片质量不滑坡。就几乎!

只有你想不到的美甲,没有美甲机做不到的美甲!

首先从原始数据集Nails里,找出一张标标准准的看上去就像是应该做美甲的手:

原图指甲部分色调分布(H)指甲部分饱和度分布(S)

看这分布是多么的集中!这样的手!随便找个图!怎么贴图都好看!

整体的计算逻辑和7.2节相反,先计算 I n f l u e n c e Influence Influence,再根据指甲分割结果在Target_Image分割出对应的区块,最后将这个区块叠加 I n f l u e n c e Influence Influence,再赋值到原始的指甲图片上即可。需要注意的是,这里仅需要叠加后图片的色调部分和饱和度部分,亮度(V)还是保持原图的数值。

先来一段可以在Aistudio上运行的代码:

import cv2
import numpy as np

# 读取图片
img_dir = 'Nails/images/1eecab90-1a92-43a7-b952-0204384e1fae.png'
mask_dir = 'Nails/labels/1eecab90-1a92-43a7-b952-0204384e1fae.png'
source_dir = 'source.jpg'

img = cv2.imread(img_dir)
mask = cv2.imread(mask_dir)[:,:,0]

cv2.imwrite('now_img.jpg',img)
cv2.imwrite('now_mask.jpg',mask*255)

mask = mask.astype('float32')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype('float32')

source = cv2.imread(source_dir)
source = cv2.resize(source, (hsv.shape[1], hsv.shape[0]))
hsv_source = cv2.cvtColor(source, cv2.COLOR_BGR2HSV).astype('float32')

# 计算HSV的中位数
h_mid = np.median(hsv[mask>0,0])
s_mid = np.median(hsv[mask>0,1])
v_mid = np.median(hsv[mask>0,2])
hsv_mid = np.array([h_mid, s_mid, v_mid]).astype('float32')

# 赋值:指甲部分数值 = 目标图片数值 + Influence
hsv[mask>0,0] = (hsv[mask>0,0] - hsv_mid[0] + hsv_source[mask>0,0])
hsv[mask>0,1] = (hsv[mask>0,1] - hsv_mid[1] + hsv_source[mask>0,1]) # *0.5 + hsv[mask>0,1]*0.5

# bound
hsv[hsv[:,:,0]>179,0] = 179
hsv[hsv[:,:,0]<0,0] = 0
hsv[hsv[:,:,1]>255,1] = 255
hsv[hsv[:,:,1]<0,1] = 0

# 写图片
result = cv2.cvtColor(hsv.astype('uint8'), cv2.COLOR_HSV2BGR)
cv2.imwrite('test.jpg',result)

来看看结果吧~

序号自定义图片美甲
1
2

OHHHHHHH!这美丽的光泽感!

光速进行部署!

下面是PyQt5代码~

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Thanks to 
https://blog.csdn.net/u014453898/article/details/88083173
https://github.com/maicss/PyQt-Chinese-tutorial
https://maicss.gitbooks.io/pyqt5/content/
zetcode.com
"""

import sys
import cv2
import numpy as np
import paddle
import math
import time
import collections
import os
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication, QPushButton, QFileDialog, QLabel, QTextEdit, \
    QGridLayout, QFrame, QColorDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QImage, QPixmap

class Example(QWidget):

    def __init__(self):
        super().__init__()

        self.img_width, self.img_height = [120 * x for x in [4,3]]
        self.infer_img_width, self.infer_img_height = [120 * x for x in [4, 3]]
        self.color = QColor()
        self.color.setHsv(300, 85, 255)
        self.ifpic = 0

        self.initCamera()
        self.initUI()
        self.initModel()

    # 初始化Camera相关信息
    def initCamera(self):
        # 开启视频通道
        self.camera_id = 0 # 为0时表示视频流来自摄像头
        self.camera = cv2.VideoCapture()  # 视频流
        self.camera.open(self.camera_id)

        # 通过定时器读取数据
        self.flush_clock = QTimer()  # 定义定时器,用于控制显示视频的帧率
        self.flush_clock.start(60)   # 定时器开始计时30ms,结果是每过30ms从摄像头中取一帧显示
        self.flush_clock.timeout.connect(self.show_frame)  # 若定时器结束,show_frame()

    def initUI(self):

        grid = QGridLayout()
        self.setLayout(grid)

        self.Color_Frame = QFrame(self)
        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
        grid.addWidget(self.Color_Frame, 0, 0, 6, 1)

        ColorText = QLabel('颜色预览', self)
        grid.addWidget(ColorText, 6, 0, 1, 1)

        Choose_Color = QPushButton('粉色', self)
        grid.addWidget(Choose_Color, 7, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('米黄', self)
        grid.addWidget(Choose_Color, 8, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('浅绿', self)
        grid.addWidget(Choose_Color, 9, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('青绿', self)
        grid.addWidget(Choose_Color, 10, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Color = QPushButton('玫红', self)
        grid.addWidget(Choose_Color, 11, 0, 1, 1)
        Choose_Color.clicked.connect(self.choose_color)

        Choose_Pic = QPushButton('自选图片', self)
        grid.addWidget(Choose_Pic, 13, 0, 1, 1)
        Choose_Pic.clicked.connect(self.showDialog)

        Exit_Exe = QPushButton('退出', self)
        grid.addWidget(Exit_Exe, 19, 0, 1, 1)
        Exit_Exe.clicked.connect(self.close)

        self.Pred_Box = QLabel()  # 定义显示视频的Label
        self.Pred_Box.setFixedSize(self.img_width, self.img_height)
        grid.addWidget(self.Pred_Box, 0, 1, 20, 20)

        self.setWindowTitle('test')
        self.show()

    def initModel(self):
        # 读取模型
        model_dir = "../infer_model/model"
        self.model = paddle.jit.load(model_dir)
        self.model.eval()

    def show_frame(self):
        _, img = self.camera.read()  # 从视频流中读取

        img = cv2.resize(img, (self.img_width, self.img_height))  # 把读到的帧的大小重新设置为 640x480
        mask, pred = self.Infer(img)

        # 往显示视频的Label里 显示QImage
        showImage = QImage(pred, pred.shape[1], pred.shape[0], QImage.Format_RGB888)
        self.Pred_Box.setPixmap(QPixmap.fromImage(showImage))

    def Infer(self, ori_img):
        h, w, c = ori_img.shape
        img = paddle.vision.resize(ori_img,[self.infer_img_height, self.infer_img_width])
        img = img / 255
        img = paddle.vision.transforms.normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC')
        img = img.transpose((2, 0, 1))
        img = paddle.to_tensor(img, dtype='float32')
        img = img.reshape([1] + img.shape)

        pre = self.model(img)

        mask = pre[0]
        mask = mask * 255
        mask = np.array(mask).astype('float32')
        mask = paddle.vision.resize(mask, [h,w])
        # cv2.imwrite('mask.jpg', mask)

        hsv_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2HSV).astype('float32')

        if self.ifpic == 1:
            hsv_img[mask > 0, 0] = (hsv_img[mask > 0, 0] - np.median(hsv_img[mask > 0,0]) + self.choosed_pic[mask > 0, 0])
            hsv_img[mask > 0, 1] = (hsv_img[mask > 0, 1] - np.median(hsv_img[mask > 0,1]) + self.choosed_pic[mask > 0, 1])
        else:
            hsv_img[mask > 0, 0] = hsv_img[mask > 0, 0] + self.color.getHsv()[0]/2 - np.median(hsv_img[mask > 0,0])
            hsv_img[mask > 0, 1] = hsv_img[mask > 0, 1] + self.color.getHsv()[1] - np.median(hsv_img[mask > 0,1])

        hsv_img[hsv_img[:,:,0] > 179, 0] = 179
        hsv_img[hsv_img[:,:,0] < 1, 0] = 1
        hsv_img[hsv_img[:, :, 1] > 255, 0] = 255
        hsv_img[hsv_img[:, :, 1] < 1, 0] = 1

        bgr_img = cv2.cvtColor(hsv_img.astype('uint8'), cv2.COLOR_HSV2RGB)
        # bgr_img = paddle.vision.resize(bgr_img, [h,w])
        # cv2.imwrite('result.jpg', bgr_img)

        return mask, bgr_img

    def choose_color(self):
        self.ifpic = 0 # 取消自选图片的状态
        if self.sender().text() == '米黄': # 米黄 有点偏绿
            self.color.setHsv(40, 30, 255)
        elif self.sender().text() == '浅绿': # 浅绿 还不错
            self.color.setHsv(100, 85, 255)
        elif self.sender().text() == '青绿': # 青绿 还不错
            self.color.setHsv(150, 85, 255)
        elif self.sender().text() == '玫红': # 肉红色 感觉没啥问题就是不太好看
            self.color.setHsv(350, 160, 255)
        else:
            self.color.setHsv(300, 85, 255) # 粉色 标准色贼拉好

        self.Color_Frame.setStyleSheet("QWidget { background-color: %s }" % self.color.name())

        # print(self.color.name)

    def showDialog(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', '/home')
        try:
            self.ifpic = 1
            self.choosed_pic = cv2.imread(fname[0])
            self.choosed_pic = cv2.resize(self.choosed_pic, (self.img_width, self.img_height))
            self.choosed_pic = cv2.cvtColor(self.choosed_pic, cv2.COLOR_BGR2HSV).astype('float32')
        except:
            self.ifpic = 0

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
            self.choosed_pic = cv2.imread(fname[0])
            self.choosed_pic = cv2.resize(self.choosed_pic, (self.img_width, self.img_height))
            self.choosed_pic = cv2.cvtColor(self.choosed_pic, cv2.COLOR_BGR2HSV).astype('float32')
        except:
            self.ifpic = 0

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

运行之后的窗口中增加一个选择图片的按钮:

点击之后可以选择文件夹中的一张图片

之后就可以快乐预览啦~

注意,如果你的光圈不够大,透光不够好,摄像头不够优秀,真的不能跟示例的图片一样,随便换什么图片都可以美甲,尽量选一些颜色贴合基础推荐色的图片吧

如果想要更稳定(鲁棒性强)的结果,可以自行把Infer中赋值语句修改为以下内容

        if self.ifpic == 1:
            hsv_img[mask > 0, 0] = (self.choosed_pic[mask > 0, 0])
            hsv_img[mask > 0, 1] = (self.choosed_pic[mask > 0, 1])
        

就是第一种渲染方案啦~

8. 成品展示

难道上面那些还不够让你对这个项目心动吗!?还需要我展示成品吗???

好吧qwq 等我找到了手模和亚力克板一定补上qwq

9. 结语

本项目简单的跑通了一个美甲预览机的构建过程,但是仍有很多地方可以优化,比如模型推理优化,除了使用Fastdeploy之外还可以考虑使用剪枝策略等。对于颜色渲染方面,结果/纹理效果还不够稳定,可以继续提高,并且对于纹理方面的抽取研究有助于开发更多的创意项目,比如AI染发~

此文章为搬运
原项目链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值