成品效果
输入: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 sysconst 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 utilsimport uuid "github.com/satori/go.uuid"/*
生成全局唯一 uuid
*/
func GetUUID() string {return uuid.NewV4().String()
}
saveImgUtil :保存图片返回访问路径
package utilsimport ("image""image/jpeg""os". "simple-web/sys"
)func SaveImg(src image.Image) string {// 写入新文件imgId := GetUUID() + ".jpg"path := OUT_PATH + imgIdf, _ := 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 utilsimport ("fmt""image""log". "simple-web/sys""gocv.io/x/gocv"
)func GetTransferResult(deviceID string) []string {var res []stringfor 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.NetBackendDefaulttarget := 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 Blobblob := 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.939g := probMat.GetFloatAt(0, i+dims)g += 116.779b := probMat.GetFloatAt(0, i+dims*2)b += 123.68out.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 controllerimport ("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 routerimport ("net/http". "simple-web/controller""github.com/gin-gonic/gin"
)/*
InitRouter 路由初始化
*/
func InitRouter() *gin.Engine {router := gin.Default()// 加载 templates 文件夹下所有的 tmplrouter.LoadHTMLGlob("templates/*")router.GET("/index", Index)// 文件上传router.POST("/upload", Upload)// ============== 静态资源/文件 ==============// 加载静态资源,例如网页的css、jsrouter.Static("/static", "./static")// 加载静态资源,一般是上传的资源,例如用户上传的图片router.StaticFS("/upload", http.Dir("upload"))// 加载单个静态文件// r.StaticFile("/favicon.ico", "./static/favicon.ico")return router
}
main 入口
package mainimport (. "simple-web/router"
)func main() {r := InitRouter()// Listen and Server in 0.0.0.0:8080r.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 文件夹无法识别。
网页测试:成功!