成品效果
输入:jpg/png 格式图片
输出:风格迁移后的图片
目录结构
# tree
├─controller // 控制器
├─models // 风格迁移模型
│ ├─eccv16
│ └─instance_norm
├─router // 路由转发
├─static // 静态资源
├─sys // 系统配置
├─templates // html 模板
├─upload // 上传或者生成的图片
│ └─images
│ ├─input
│ └─output
└─utils // 封装的工具
简单构建 Web 网页
使用 Element 的上传组件和图片组件:
index.tmpl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- import axios -->
<!-- <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> -->
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
<el-upload class="upload-demo" ref="upload" action="http://127.0.0.1:8080/upload" :on-success="handleSuccess" :on-preview="handlePreview" :on-remove="handleRemove" :limit="1" :file-list="fileList" list-type="picture" :auto-upload="false">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload" v-loading.fullscreen.lock="fullscreenLoading" element-loading-text="拼命加载中">转换图片</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过8Mbit</div>
</el-upload>
<div class="demo-image__lazy" style="margin-top:30px;">
<el-image v-for="url in urls" :key="url" :src="url" lazy></el-image>
</div>
</div>
</body>
<script>
new Vue({
el: '#app',
data() {
return {
fileList: [],
urls: [],
fullscreenLoading: false
};
},
methods: {
// 提交表单
submitUpload() {
this.$refs.upload.submit();
this.fullscreenLoading = true;
},
// 移除图片
handleRemove(file, fileList) {
console.log(file, fileList);
},
// 预览略缩图
handlePreview(file) {
console.log(file);
},
// 超出限制
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
},
// 加载成功
handleSuccess(response, file, fileList){
// console.log(response.data);
this.urls = response.urls;
this.fullscreenLoading = false;
}
}
})
</script>
</html>
封装系统变量
package sys
const OUT_PATH string = "D:\\GoFiles\\simple-gocv-web\\upload\\images\\output\\"
const IN_PATH string = "D:\\GoFiles\\simple-gocv-web\\upload\\images\\input\\"
const IMG_URL string = "http://127.0.0.1:8080/upload/images/output/"
var MODELS_NAME = []string{
"D:\\GoFiles\\simple-gocv-web\\models\\eccv16\\composition_vii.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\eccv16\\la_muse.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\eccv16\\starry_night.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\eccv16\\the_wave.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\instance_norm\\candy.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\instance_norm\\feathers.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\instance_norm\\la_muse.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\instance_norm\\mosaic.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\instance_norm\\the_scream.t7",
"D:\\GoFiles\\simple-gocv-web\\models\\instance_norm\\udnie.t7",
}
工具类
uuidUtil :保证图片文件名不重复。
package utils
import uuid "github.com/satori/go.uuid"
/*
生成全局唯一 uuid
*/
func GetUUID() string {
return uuid.NewV4().String()
}
saveImgUtil :保存图片返回访问路径
package utils
import (
"image"
"image/jpeg"
"os"
. "simple-web/sys"
)
func SaveImg(src image.Image) string {
// 写入新文件
imgId := GetUUID() + ".jpg"
path := OUT_PATH + imgId
f, _ := os.OpenFile(path, os.O_RDWR|os.O_CREATE, os.ModePerm)
defer f.Close()
jpeg.Encode(f, src, nil)
// 返回图片路径
return IMG_URL + imgId
}
dnnUtil :进行图片风格迁移
package utils
import (
"fmt"
"image"
"log"
. "simple-web/sys"
"gocv.io/x/gocv"
)
func GetTransferResult(deviceID string) []string {
var res []string
for i := 0; i < len(MODELS_NAME); i++ {
res = append(res, DNNStyleTransfer(deviceID, MODELS_NAME[i]))
}
return res
}
func DNNStyleTransfer(deviceID string, model string) string {
if len(deviceID) == 0 || len(model) == 0 {
log.Fatal("How to run:\ndnn-style-transfer [videosource] [modelfile] ([backend] [device])")
}
// 使用默认参数
backend := gocv.NetBackendDefault
target := gocv.NetTargetCPU
// 读取的图片位置
img := gocv.IMRead(deviceID, gocv.IMReadColor)
defer img.Close()
// 打开下载好的 DNN 风格迁移模型
net := gocv.ReadNet(model, "")
if net.Empty() {
log.Fatalf("Error reading network model from : %v\n", model)
}
defer net.Close()
net.SetPreferableBackend(gocv.NetBackendType(backend))
net.SetPreferableTarget(gocv.NetTargetType(target))
fmt.Printf("Start reading device: %v\n", deviceID)
// 将 image Mat 转换为 DNN 风格迁移模型可以分析的 640x480 Blob
blob := gocv.BlobFromImage(img, 1.0, image.Pt(640, 480), gocv.NewScalar(103.939, 116.779, 123.68, 0), false, false)
// 把转换成的 Blob 放进转换器
net.SetInput(blob, "")
// 通过网络运行一个向前传递
probMat := net.Forward("")
sz := probMat.Size()
dims := sz[2] * sz[3]
out := gocv.NewMatWithSize(480, 640, gocv.MatTypeCV8UC3)
// 取 blob 并从中获取可显示的 image Mat 图像
for i := 0; i < dims; i++ {
r := probMat.GetFloatAt(0, i)
r += 103.939
g := probMat.GetFloatAt(0, i+dims)
g += 116.779
b := probMat.GetFloatAt(0, i+dims*2)
b += 123.68
out.SetUCharAt(0, i*3, uint8(r))
out.SetUCharAt(0, i*3+1, uint8(g))
out.SetUCharAt(0, i*3+2, uint8(b))
}
// 保存图片,得到返回路径
i, err := out.ToImage()
if err != nil {
log.Fatal(err)
}
path := SaveImg(i)
probMat.Close()
blob.Close()
out.Close()
return path
}
控制器
控制文件上传和返回结果。
package controller
import (
"log"
"net/http"
. "simple-web/sys"
. "simple-web/utils"
"github.com/gin-gonic/gin"
)
// 首页
func Index(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", nil)
}
/*
上传图片并实现风迁移,渲染到页面
*/
func Upload(c *gin.Context) {
// 单文件
file, _ := c.FormFile("file")
log.Println(file.Filename)
// 上传文件至指定目录
dst := IN_PATH + GetUUID()
err := c.SaveUploadedFile(file, dst)
if err != nil {
log.Println(err)
}
outDst := GetTransferResult(dst)
log.Println(outDst)
// 渲染
/*c.HTML(http.StatusOK, "index.tmpl", gin.H{
".bodyText": outDst,
})*/
// 返回结果
c.JSON(http.StatusOK, gin.H{
"status": "success",
"urls": outDst,
})
}
定义路由
package router
import (
"net/http"
. "simple-web/controller"
"github.com/gin-gonic/gin"
)
/*
InitRouter 路由初始化
*/
func InitRouter() *gin.Engine {
router := gin.Default()
// 加载 templates 文件夹下所有的 tmpl
router.LoadHTMLGlob("templates/*")
router.GET("/index", Index)
// 文件上传
router.POST("/upload", Upload)
// ============== 静态资源/文件 ==============
// 加载静态资源,例如网页的css、js
router.Static("/static", "./static")
// 加载静态资源,一般是上传的资源,例如用户上传的图片
router.StaticFS("/upload", http.Dir("upload"))
// 加载单个静态文件
// r.StaticFile("/favicon.ico", "./static/favicon.ico")
return router
}
main 入口
package main
import (
. "simple-web/router"
)
func main() {
r := InitRouter()
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}
完整项目
效果测试
Docker 部署项目
其实该镜像有问题,golang 版本低,dnn 函数出错。
自制 gocv 环境镜像
# 拉取 centos7 镜像
docker pull centos:7
# 启动镜像
docker run --name="centos7" -it -d --rm -p 8082:8082 centos:7
# 进入容器
docker exec -it centos7 bash
# 安装基础命令
yum install -y wget gcc make
# 安装 go 和 安装 cmake 在网上有很多教程
# 安装 gocv
go get -u -d gocv.io/x/gocv && cd $GOPATH/pkg/mod/gocv.io/x/gocv@v0.28.0 && make install
// 如果报错找不到 opencv4.pc ,不要紧,设置变量
echo 'export PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig' >>/etc/profile
source /etc/profile
// 如果报错找不到 XXX.so 文件,不要紧,就在 /usr/local/lib64 目录下
vim /etc/ld.so.conf.d/opencv.conf
添加一行 /usr/local/lib
:wq保存退出后执行 ldconfig 重载配置
完成环境安装之后验证环境:
[root@494d90036f52 ~]# source /etc/profile
[root@494d90036f52 ~]# go version
go version go1.16 linux/amd64
[root@494d90036f52 ~]# cd $GOPATH
[root@494d90036f52 gowork]# cd pkg/mod/gocv.io/x/gocv\@v0.28.0/
[root@494d90036f52 gocv@v0.28.0]# go run ./cmd/version/main.go
gocv version: 0.28.0
opencv lib version: 4.5.3
[root@494d90036f52 gocv@v0.28.0]#
ok,exit 退出准备 docker commit 打包镜像,其实应该用 Dockerfile 文件构建镜像的,但是有很多不可控的因素和一些奇奇怪怪的报错,因此我为简便起见使用 docker commit 指令。
# 查看当前运行的容器
docker ps
# 打包镜像
docker commit centos7 gocv:latest
# 查看打包后的镜像
docker images
现在已经有 go 和 gocv 环境的镜像了,现在构建项目的 Dockerfile :
FROM gocv:latest
WORKDIR $GOPATH/src/simple-gocv-web
ADD . ./
ENV TZ=Asia/Shanghai
ENV GO111MODULE=on
ENV GOPROXY="https://goproxy.io"
RUN source /etc/profile && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
go build -o simple-gocv-web .
EXPOSE 8083
ENTRYPOINT ["./simple-gocv-web"]
打包项目镜像
docker build -t demo .
运行项目
docker run -d -it --rm -p 8083:8083 demo
运行报错:
terminate called after throwing an instance of 'cv::Exception'
what(): OpenCV(4.5.3) /tmp/opencv/opencv-4.5.3/modules/dnn/src/torch/THDiskFile.cpp:496: error: (-2:Unspecified error) cannot open <models\eccv16\composition_vii.t7> in mode r in function 'THDiskFile_new'
无法加载模型,可能是没有识别到相对路径,尝试修改为绝对路径:
重新打包运行:
postman 测试:成功!
可能是反斜杠 “/” 与双斜杠 ”\\“ 不兼容的问题,upload 文件夹采用相对路径时却能正常识别,而 models 文件夹无法识别。
网页测试:成功!