利用阿里云函数FC实现的抠图服务

总结

总结写在前面为了省流。

最后确定实施采用的付费API方案。原因是现在的开源模型在各场景下的表现效果没达到商用标准。但是整个流程是没问题的,展望现在AI的发展速度类似图片分割这类的模型开源也只是时间问题,到时候可以掏出这套流程直接部署线上。

可以后期关注 https://www.modelscope.cn/models/damo/cv_unet_universal-matting/summary

背景

我所在公司任职的项目组想开发抠图功能给用户提供免费的服务,其他项目组应用的第三方抠图服务成本20-30W一年,核心问题就是在抠图效果可接受的前提下需要缩减成本。团队也没有机器视觉处理和训练方面的经验。

模型选择

目前市面上几家巨头都有自己的AI社群,社群里面有训练好的模型并且可以在线体验。
脸书的 huggingface
阿里的 魔塔
百度的 飞浆
分别抽取了这几个社区图像分割热度最高的模型做测试

经过比对最后选定了阿里的damo/cv_swinL_panoptic-segmentation_cocopan模型

技术选型思考

  1. 机器学习首选Python,团队技术栈 PHP/GO为主
  2. 每家社区都有自己的框架,依赖项较多安装繁琐
  3. 网站日活10w用户,峰值qps1K

结合上述3点,我想的是

  1. gRPC对Python逻辑进行封装只处理抠图逻辑,client端调用处理业务逻辑(比如付费限流业务状态熔断等问题)统一开发语言
  2. 抠图服务做成无状态方便动态扩缩容
  3. 动态扩缩容可以选择k8s编排
  4. 考虑到k8s的维护成本和它在公司的普及率,换为阿里云函数

函数计算调研

场景

函数计算可以处理一小段的代码逻辑,触发方式分为事件触发HTTP请求,用于周期性的定时消费任务或者访问量不大的博客类网站有一定的价格优势。gRPC是基于HTTP2.0由此函数计算也兼容了gRPC调用。环境采用镜像的方式部署,代码可以接入各大托管平台。可以根据并发利用率阈值进行动态扩缩容。

目前来看解决了下面的问题

  1. 价格
  2. 容器编排
  3. 通信

计费

资源包 大小 金额(元)
vCPU 50w vCPU*秒 50
内存 100WGB*秒 10
调用函数包 1亿 80
流量 100GB 40
GPU 50wGB*秒 200

计费是多维度的比较玄幻公式是

1
2
vCPU资源使用费用=函数实例vCPU(vCPU)×执行时长(秒)×单价。
内存资源使用费用=函数实例内存(GB)×执行时长(秒)×单价。

实际使用中发现每次调用机器会悬停5-10分钟做为强制预热,期间也是按秒计费的。阿里提供了2中模式。

  1. 最后一次调用悬停10分钟按秒计费(调用1次也是会计费10分钟)
  2. 打开闲置计费强制永久1台机器为闲置状态费用是按秒计费的 1/10

肉眼评估下来用GPU服务任务控制在1s内价格是比直接买服务器来得划算的,并且可负载qps可长可短很鲁棒

DEMO

ProtoBuff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax="proto3";
option go_package ="../pb";
service ImgHandle {
rpc partition (Request) returns (Reply) {}
}

message Request {
string Url =1;
}


message Reply {
string jsonData = 1;
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks
from concurrent import futures
import grpc
import json
import pb.partition_pb2 as Impb
import pb.partition_pb2_grpc as IMrpc
import numpy as np

class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
return json.JSONEncoder.default(self, obj)

class ImgHandle(IMrpc.ImgHandleServicer):

def partition(self, request, context):
print(request.Url)
result = segmentor(request.Url)
s = json.dumps(result, cls=NumpyEncoder, ensure_ascii=False)

return Impb.Reply(jsonData=s)

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

IMrpc.add_ImgHandleServicer_to_server(ImgHandle(), server)
server.add_insecure_port('0.0.0.0:50051')
print("成功监听")
server.start()
server.wait_for_termination()
if __name__ == '__main__':

segmentor = pipeline(Tasks.image_segmentation, model='damo/cv_swinL_panoptic-segmentation_cocopan')
serve()


client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package main

import (
"bufio"
g "client/grpc"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
_ "image/png"
"io"
"net/http"
"os"
"strconv"
"time"
)

const ADDR = "*****:8089"

type T struct {
Masks [][][]int `json:"masks"`
Labels []string `json:"labels"`
Scores []float64 `json:"scores"`
}



func main() {

cred := credentials.NewTLS(&tls.Config{
InsecureSkipVerify: false,
})
url := "https://pic.xxxx.com/01/92/25/82t888piCbj9.jpg"

conn, err := grpc.Dial(ADDR,
grpc.WithTransportCredentials(cred),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*1024*20)),
)

if err != nil {
fmt.Println(err)
}

client := g.NewImgHandleClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 50000*time.Second)
defer cancel()
fmt.Println("投递抠图任务")

reply, e := client.Partition(ctx, &g.Request{Url: url})

Down(url)

if e != nil {
fmt.Println(e)
}

var t = T{}
json.Unmarshal([]byte(reply.JsonData), &t)
fmt.Println("抠图处理完成,开始保存图片")
Img(t)

}

func Down(url string) {
client := http.DefaultClient
client.Timeout = 10 * time.Second
reps, err := client.Get(url)
if err != nil {
fmt.Println(err)
}
defer reps.Body.Close()
file, err := os.Create("./temp.png")
if err != nil {
fmt.Println(err)
}
defer file.Close()
w := bufio.NewWriter(file)
io.Copy(w, reps.Body)
w.Flush()
}

func Img(processing T) {

file, err := os.Open("./temp.png")
if err != nil {
panic(any(err))
}
defer file.Close()

atImg, _, e := image.Decode(file)

if e != nil {
fmt.Println(e)
}

wide := atImg.Bounds().Max.X
high := atImg.Bounds().Max.Y



for i, v := range processing.Labels {
resultImg := image.NewRGBA(image.Rect(0, 0, wide, high))
for w := 0; w < wide; w++ {
for h := 0; h < high; h++ {
isShow := processing.Masks[i][h][w]
if isShow == 1 {
resultImg.Set(w, h, atImg.At(w, h))
}
}
}
ImgSave(resultImg, v+"_"+strconv.Itoa(i))
}

}

func ImgSave(img *image.RGBA, name string) {
file, e := os.Create("./" + name + ".png")
if e != nil {
fmt.Println(e)
}
defer file.Close()
b := bufio.NewWriter(file)
defer b.Flush()
png.Encode(b, img)
}

Dockerfile

1
2
3
4
5
6
FROM registry.cn-hangzhou.aliyuncs.com/modelscope-repo/modelscope:ubuntu20.04-py37-torch1.11.0-tf1.15.5-1.2.0
EXPOSE 50051
RUN mkdir -p /usr/local/bin/service
COPY ./service /usr/local/bin/service
RUN python /usr/local/bin/service/docker_init.py #这里需要下载个800M的模型文件,提前打入image
CMD [ "python", "/usr/local/bin/service/main.py" ]

注意点

  1. 打包后的image放在阿里自家服务的同一大区构建会很快
    https://cr.console.aliyun.com/cn-shanghai/instances
  2. 云函数计算gRPC调用是固定端口8089,容器中的监听端口会自动映射上去
  3. 云函数URL触发没有鉴权机制,需要同步开通网关服务
  4. 建议使用相同区域的机器做下流服务通信走vpc,可以不做鉴权和节省流量成本
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~