写点什么

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

  • 2022-11-10
    广东
  • 本文字数:9129 字

    阅读完需:约 30 分钟

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

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


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



0.实现思路


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


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


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


  • 文字和图片的匹配度。 

  • 图片的风格化效果。


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


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


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


1.准备工作

1.1 文字搜图


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


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


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


1.2 图像风格化


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


官网效果:


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


控制台调用查看:


SDK 调用:


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

go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/commongo get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ft
复制代码


使用接口调用服务:

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()
// 输入图URLreq.Url = common.StringPtr(imageUrl)
// 返回结果URLreq.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 封装

// 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 代码

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 能力, 融合图片。


协议:

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; // 原始包体}
复制代码


逻辑代码:

// 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 服务:

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 调用看下效果:

curl --location --request POST 'http://127.0.0.1:8080/SearchImage' --header 'Content-Type: application/json' --data '{"text":"艾薇儿"}' | jq  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                 Dload  Upload   Total   Spent    Left  Speed100  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

<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

// 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:8080/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 = "" for (let v in that.data.Resp.result_url_list) { let item = that.data.Resp.result_url_list[v] // console.log("item: ", 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('#input_canvas') // 在 WXML 中填入的 id .fields({ node: true, size: true }) .exec((r) => { // Canvas 对象 const canvas = r[0].node // 渲染上下文 const ctx = canvas.getContext('2d') // 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("radio:", 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: "error", title: '请输入你的创意 ', }) 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 的多种风格化,来实现更多样的效果。

发布于: 2022-11-10阅读数: 72
用户头像

还未添加个人签名 2020-11-02 加入

还未添加个人简介

评论

发布
暂无评论
最佳实践|用腾讯云AI图像能力实现AI作画_腾讯云_牵着蜗牛去散步_InfoQ写作社区