最佳实践|用腾讯云AI图像能力实现AI作画

最近看到一篇有趣的文章,一副名为《太空歌剧院》(如下图)的艺术品在某美术比赛上,获得了第一名的成绩, 有意思的是这件作品是通过AI来实现的画作, 顿时觉得非常神奇。 结合近期科技媒体频频报道的AI作画爆火现象,深入了解了下,发现市面上有一些AI作画的小程序, 是通过输入一段文字给AI, 然后输出一副和文字意思相近的图片。 这个感觉非常有意思,某种程度上会给绘画行业带来新的发展契机。

那如果自己想实现一个类似的小程序,该如何做呢?下面详细分享下我的思考实践过程。

0.实现思路

目前看到的AI画画的基本流程如下:

输入文本-〉选择风格化(油画/素描/动漫等等)-〉生成图片。

根据实际体验, 很多小程序其实是在现有的实景图片基础上,做了一层风格化的后置处理,效果主要体现在以下两点:

  • 文字和图片的匹配度。
  • 图片的风格化效果。

根据这两点来思考, 如果需要实现一个类似的功能, 我们需要维护一个图库,并通过AI提取图片标签,映射图片和标签的关系,如下图:

上述的图库模块,主要是图片和文字的映射,可以通过腾讯云的图像标签来提取入库, 这个过程有点类似于搜索引擎的图片搜索,通过文字匹配图片。常用的搜索引擎(搜狗,百度,谷歌)都有类似的功能,只不过都是网图,不过也没关系, 我们可以通过现有的搜索引擎的能力快速验证下效果,简化一下流程如下:

基本方案确定, 下面详细描述下实现过程。

1.准备工作

1.1 文字搜图

通过文字生成意思相近的图片,发现搜狗有现成的能力,可以通过输入文字或图片,获取匹配度比较高的网图:

通过输入关键字,分析下接口调用:

直接调用下接口, 就可以拿到对应的图片url:

1.2 图像风格化

好了, 现在有数据源了, 我们先主要针对人物进行风格化处理, 调研一番,发现腾讯云官网有针对人像动漫画的处理,看下描述可以满足需求:

官网效果:

开通服务后,会赠送1000次的资源包:

控制台调用查看:

SDK调用:

我们使用golang来开发, 获取下依赖库:

代码语言:javascript
复制
go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common
go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ft

使用接口调用服务:

代码语言:javascript
复制
credential := common.NewCredential(
	"***",
	"***",
)
cpf := profile.NewClientProfile()
client, err := ft.NewClient(credential, "ap-guangzhou", cpf)
if err != nil {
	log.Errorf("NewClient, err=%+v", err)
	return nil, err
}
req := ft.NewFaceCartoonPicRequest()

// 输入图URL
req.Url = common.StringPtr(imageUrl)

// 返回结果URL
req.RspImgType = common.StringPtr("url")

resp, err := client.FaceCartoonPic(req)
if err != nil {
log.Errorf("FaceCartoonPic, err=%+v", err)
return nil, err
}

2. 小程序上实现AI画画

2.1 服务端-搜狗API封装

代码语言:javascript
复制
// Response 搜狗API返回结构
type Response struct {
Status int json:"status"
Info string json:"info"
Data struct {
AdPic []struct {
DocID string json:"docId"
Index int json:"index"
Mfid string json:"mfid"
ThumbHeight int json:"thumbHeight"
ThumbWidth int json:"thumbWidth"
} json:"adPic"
BlackLevel int json:"blackLevel"
CacheDocNum int json:"cacheDocNum"
HasPicsetRes int json:"hasPicsetRes"
HintWords []string json:"hintWords"
IsQcResult string json:"isQcResult"
IsStrongStyle int json:"is_strong_style"
Items []struct {
Anchor2 []string json:"anchor2"
Author string json:"author"
AuthorName string json:"author_name"
AuthorPageurl string json:"author_pageurl"
AuthorPicurl string json:"author_picurl"
AuthorThumbURL string json:"author_thumbUrl"
AuthorThumbMfid string json:"author_thumb_mfid"
Biaoqing int json:"biaoqing"
ChSiteName string json:"ch_site_name"
CutBoardInputSkin string json:"cutBoardInputSkin"
DocID string json:"docId"
Docidx int json:"docidx"
Gifpic int json:"gifpic"
Grouppic int json:"grouppic"
Height int json:"height"
HTTPSConvert int json:"https_convert"
Index int json:"index"
LastModified string json:"lastModified"
LikeNum string json:"like_num"
Link string json:"link"
LocImageLink string json:"locImageLink"
MfID string json:"mf_id"
Mood string json:"mood"
Name string json:"name"
OriPicURL string json:"oriPicUrl"
PainterYear string json:"painter_year"
PicURL string json:"picUrl"
Publishmodified string json:"publishmodified"
Size int json:"size"
Summarytype string json:"summarytype"
ThumbHeight int json:"thumbHeight"
ThumbURL string json:"thumbUrl"
ThumbWidth int json:"thumbWidth"
Title string json:"title"
Type string json:"type,omitempty"
URL string json:"url"
WapLink string json:"wapLink"
Width int json:"width"
Scale float64 json:"scale"
Did int json:"did"
ImgTag string json:"imgTag"
BgColor string json:"bgColor,omitempty"
ImgDefaultURL string json:"imgDefaultUrl"
} json:"items"
MaxEnd int json:"maxEnd"
NextPage string json:"next-page"
PainterDocCount int json:"painter_doc_count"
Parity string json:"parity"
PoliticFilterNum int json:"politicFilterNum"
PoliticLevel int json:"politicLevel"
QoInfo string json:"qo_info"
QueryCorrection string json:"queryCorrection"
ShopQuery string json:"shopQuery"
Tag [][]string json:"tag"
TagWords []string json:"tagWords"
TagWordsFeed []string json:"tagWords_feed"
TagFeed [][]string json:"tag_feed"
TotalItems int json:"totalItems"
TotalNum int json:"totalNum"
UUID string json:"uuid"
ColorList []struct {
Class string json:"class"
Name string json:"name"
Mood int json:"mood"
Stype string json:"stype"
} json:"colorList"
Query string json:"query"
HintList []struct {
LinkURL string json:"linkUrl"
Text string json:"text"
} json:"hintList"
TagList []struct {
Key string json:"key"
Value string json:"value"
Active bool json:"active"
} json:"tagList"
} json:"data"
}

type Option struct {
Tags []string json:"tags"
}

// Search ...
func Search(ctx context.Context, keywords, option string) (*Response, error) {
// https://pic.sogou.com/pics
// 关键词搜索
// https://pic.sogou.com/napi/pc/searchList?mode=1&start=48&xml_len=48&query=%E7%BE%8E%E5%A5%B3
// tag过滤搜索
// https://pic.sogou.com/napi/pc/searchList?mode=1&tagQSign=壁纸,d24f3a88|杨幂,645d0d1a&start=0&xml_len=48&query=迪丽热巴
params := url.Values{}
params.Set("mode", "1")
params.Set("start", "0")
params.Set("xml_len", "48")
params.Set("query", keywords)

if len(option) != 0 {
	opt := &Option{}
	err := json.Unmarshal([]byte(option), &opt)
	if err == nil {
		tags := ""
		for i := 0; i < len(opt.Tags)-1; i += 2 {
			tags += opt.Tags[i] + "," + opt.Tags[i+1]
			if i == len(opt.Tags)-2 {
				tags += "|"
			}
		}
		params.Set("tagQSign", tags)
	}
}
uri := "https://pic.sogou.com/napi/pc/searchList"
address, err := url.Parse(uri)
if err != nil {
	return nil, err
}
address.RawQuery = params.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, address.String(), nil)
if err != nil {
	log.Errorf("NewRequestWithContext error, %+v", err)
	return nil, err
}
resp, err := http.DefaultClient.Do(request)
if err != nil {
	log.Errorf"http do error, %+v", err)
	return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
	log.Errorf("Query, request read body failed, %+v", err)
	return nil, err
}
rsp := &Response{}
err = json.Unmarshal(body, &rsp)
if err != nil {
	panic(err)
}
return rsp, nil

}

2.2 服务端-动漫画接口

参考上述sdk代码

代码语言:javascript
复制
func FaceCartoonPicPro(ctx context.Context, imageUrl string, tp int) ([]byte, error) {
credential := common.NewCredential(
"",
"
",
)
cpf := profile.NewClientProfile()
client, err := ft.NewClient(credential, "ap-guangzhou", cpf)
if err != nil {
log.Errorf("NewClient, err=%+v", err)
return nil, err
}
req := ft.NewFaceCartoonPicRequest()
req.Url = common.StringPtr(imageUrl)
req.RspImgType = common.StringPtr("url")

resp, err := client.FaceCartoonPic(req)
if err != nil {
	log.Errorf(""FaceCartoonPic, err=%+v", err)
	return nil, err
}
return []byte(*resp.Response.ResultUrl), nil

}

2.3 服务端-小程序请求接口封装

小程序使用http协议访问, 这里提供一个http服务, 逻辑上分为两步:

一、search,通过文字匹配图片。

二、风格化,通过腾讯云AI能力, 融合图片。

协议:

代码语言:javascript
复制
message SearchImageReq {
string text = 1; // 关键字
string option_json = 2; // tag信息, 搜狗API使用
}

message Result {
string ori_url = 1; // 原始图
string res_url = 2; // 风格化后的图
}

message SearchImageRsp {
int64 error_code = 1;
string error_msg = 2;
repeated Result result_url_list = 3;
string raw_body = 4; // 原始包体
}

逻辑代码:

代码语言:javascript
复制
// SearchImage ...
func SearchImage(ctx context.Context, req *pb.SearchImageReq, rsp *pb.SearchImageRsp) (err error) {
rsp.ErrorCode = 1
if len(strings.TrimSpace(req.Text)) == 0 {
rsp.ErrorCode = -1
return nil
}
resp, err := Search(ctx, req.Text, req.OptionJson)
if err != nil {
rsp.ErrorCode = -2
log.Errorf("Search Error : %+v", err)
return nil
}

ret := make([]string, 0)
for _, v := range resp.Data.Items {
	ret = append(ret, v.OriPicURL)
}

raw, _ := json.Marshal(resp)
rsp.RawBody = string(raw)

// 只要成功了就直接返回
success := false
for _, v := range ret {
	var changeUrl string
	if !success {
		resUrl, err := FaceCartoonPicPro(ctx, v)
		if err == nil {
			success = true
		}
		changeUrl = string(resUrl)
	}
	rsp.ResultUrlList = append(rsp.ResultUrlList, &pb.Result{
		OriUrl: v,
		ResUrl: changeUrl,
	})
}
return nil

}

启动http服务:

代码语言:javascript
复制
http.HandleFunc("/SearchImage", func(writer http.ResponseWriter, r *http.Request) {
data, _ := ioutil.ReadAll(r.Body)
req := &pb.SearchImageReq{}
_ = json.Unmarshal(data, &req)

rsp := &pb.SearchImageRsp{}
_ = SearchImage(context.Background(), req, rsp)
body, _ := json.Marshal(rsp)
writer.Write(body)

})
http.ListenAndServe("127.0.0.1:8080", nil)

使用curl调用看下效果:

代码语言:javascript
复制
curl --location --request POST 'http://127.0.0.1/SearchImage' --header 'Content-Type: application/json' --data '{"text":"艾薇儿"}' | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 115k 0 115k 100 38 14765 4 0:00:09 0:00:08 0:00:01 31189
{
"error_code": "1",
"error_msg": "",
"result_url_list": [
{
"ori_url": "http://a0.att.hudong.com/67/37/01300000569933126015378092926.jpg",
"res_url": ""
},
{
"ori_url": "http://i2.hdslb.com/bfs/archive/24c06671653c74e9de14e0bab4bf2107bd97e5f1.png",
"res_url": "https://faceeffect-1254418846.cos.ap-guangzhou.myqcloud.com/ft/FaceCartoonPic/1253534368/ed046d5d-fb87-4c38-bcb3-6cbb4595e3cf"
},
{
"ori_url": "http://b-ssl.duitang.com/uploads/blog/201404/04/20140404200234_3xXzr.jpeg",
"res_url": ""
},
{
"ori_url": "http://img0.pclady.com.cn/pclady/1607/14/1544487_1535933_1216188_TUNGSTAR4871543.jpg",
"res_url": ""
}
]
}

效果还不错:

2.4 小程序-demo

下载微信开发者工具, 创建项目:

index.wxml

代码语言:javascript
复制
<view class="container" >
<div class="form-item" style="width: 673rpx; height: 70rpx; display: block; box-sizing: border-box">
<input placeholder="写下你的创意" class="input" bindinput="handlerInput" />
<button class="button" loading="{{buttonStatus}}" bindtap="handlerSearch" size="mini" style="width: 158rpx; height: 64rpx; display: block; box-sizing: border-box; left: 0rpx; top: 0rpx; position: relative"> 立即生成 </button>
</div>
<view class="text_box">
<text class="text_line">关键词</text>
</view>
<view class="view_line">
<view class="hot_txt" wx:for="{{tags}}" wx:key="histxt">
<view bindtap="clickItem" data-bean="{{item}}">
<view>{{item[0]}}</view>
</view>
</view>
</view>

<view class="output_line" style="position: relative; left: 0rpx; top: 50rpx; width: 714rpx; height: 58rpx; display: flex; box-sizing: border-box">
<text class="text_line" style="width: 99rpx; height: 30rpx; display: block; box-sizing: border-box; position: relative; left: 9rpx; top: -9rpx">作品图</text>
<view style="position: relative; left: -15rpx; top: 2rpx; width: 571rpx; height: 0rpx; display: block; box-sizing: border-box"></view>
</view>
<canvas type="2d" id="input_canvas" style="background: rgb(228, 228, 225); width: 673rpx; height: 700rpx; position: relative; left: 2rpx; top: 80rpx; display: block; box-sizing: border-box">
</canvas>
</view>

index.js

代码语言:javascript
复制
// index.js
// 获取应用实例
const app = getApp()

Page({
data: {
inputValue: "",
tags: [],
option: [],
buttonStatus: false,
index: 0,
motto: 'Hello World',
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo'),
canIUseGetUserProfile: false,
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName') // 如需尝试获取用户信息可改为false
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs'
})
},
onLoad() {
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
},

getUserProfile(e) {
// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log(res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
getUserInfo(e) {
// 不推荐使用getUserInfo获取用户信息,预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息
console.log(e)
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
})
},

imageDraw() {
var that = this
var opt = {}
if (that.data.option && that.data.option.length > 0) {
opt = {
"tags": that.data.option
}
}
console.log("option:", opt)
wx.request({
url: 'http://127.0.0.1/SearchImage',
data: {
"text": that.data.inputValue,
"option_json": JSON.stringify(opt)
},
method: "POST",
header: {
'Content-Type': "application/json"
},
success (res) {
if (res.data == null) {
wx.showToast({
icon: "error",
title: '请求失败',
})
return
}
console.log(res.data)
that.setData({
Resp: res.data,
})
let raw = JSON.parse(res.data.raw_body)
console.log("raw: ", raw)
console.log("tagWords: ", raw.data.tagWords)
let tags = []
for (let v in raw.data.tagWords) {
if (v >= 9) {
break
}
tags.push({
value: raw.data.tagWords[v]
})
}
that.setData({
tags: raw.data.tag,
tagWords: tags
})
that.drawInputImage()
},
fail(res) {
wx.showToast({
icon: "error",
title: '请求失败',
})
}
})
},

drawInputImage: function() {
var that = this;
console.log("resp: ", that.data.Resp)

let resUrl = &#34;&#34;
for (let v in that.data.Resp.result_url_list) {
  let item = that.data.Resp.result_url_list[v]
  // console.log(&#34;item: &#34;, v, item)
  if (item.res_url.length !== 0) {
    console.log(item.res_url)
    resUrl = item.res_url
    break
  }
}

wx.downloadFile({
  url: resUrl,
  success: function(res) {
    var imagePath = res.tempFilePath
    wx.getImageInfo({
      src: imagePath,
      success: function(res) {
        wx.createSelectorQuery()
        .select(&#39;#input_canvas&#39;) // 在 WXML 中填入的 id
        .fields({ node: true, size: true })
        .exec((r) =&gt; {
          // Canvas 对象
          const canvas = r[0].node
          // 渲染上下文
          const ctx = canvas.getContext(&#39;2d&#39;)
          // Canvas 画布的实际绘制宽高 
          const width = r[0].width
          const height = r[0].height
          // 初始化画布大小
          const dpr = wx.getWindowInfo().pixelRatio
          canvas.width = width * dpr
          canvas.height = height * dpr
          ctx.scale(dpr, dpr)
          ctx.clearRect(0, 0, width, height)

          let radio = height / res.height
          console.log(&#34;radio:&#34;, radio)
          const img = canvas.createImage()
          var x = width / 2 - (res.width * radio / 2)

          img.src = imagePath
          img.onload = function() {
            ctx.drawImage(img, x, 0, res.width * radio, res.height * radio)
          }
        })
      }
    })
  }
})

},

handlerInput(e) {
this.setData({
inputValue: e.detail.value
})
},

handlerSearch(e) {
console.log("input: ", this.data.inputValue)

if (this.data.inputValue.length == 0) {
  wx.showToast({
    icon: &#34;error&#34;,
    title: &#39;请输入你的创意 &#39;,
  })
  return
}
this.imageDraw()

},
handlerInputPos(e) {
console.log(e)
this.setData({
inputValue: e.detail.value
})
},
handlerInputImage(e) {
console.log(e)
},
clickItem(e) {
let $bean = e.currentTarget.dataset
console.log(e)
console.log("value: ", $bean.bean)
this.setData({
option: $bean.bean
})
this.imageDraw()
}
})

运行:

检索下关键字:

关键词过滤, 点击标签可以二次搜索:

至此,就实现了一个简单的AI画画的demo, 后面可以自行构造质量更高的图库,通过打标签的方式来管理,然后通过输入的关键字,搭配腾讯云AI的多种风格化,来实现更多样的效果。