产品目标
开发一款智能化的人脸美妆系统,利用人工智能技术为用户提供个性化、精准的美妆效果。通过该系统,用户可以轻松实现虚拟试妆、美妆效果展示和个性化妆品推荐等功能,以满足用户对美妆的需求,提高用户体验。
产品效果
技术方案
AI方案:选择阿里云智能视觉开放平台>人脸人体>人脸美妆 在线SDK
产品形态:微信公众号H5
前端技术框架: uniapp
后端技术框架: java springboot
前端代码
<template>
<view class='demand' >
<u-subsection :list="list" activeColor="#f56c6c" mode="subsection" class="subsection"
:current="curNow"
@change="sectionChange"
></u-subsection>
<view v-if="curNow === 0">
<view class="yuantuView">
<image class="yuantuImage" :src="yuantuImage" mode="aspectFit" :style="canvasStyle"></image>
<button class="uButton" @click="immediateProduction" >
上传图片
</button>
</view>
</view>
<view v-else-if="curNow === 1">
<view class="effect">
<image class="effectImage" :src="effectImage" mode="aspectFit" :style="canvasStyle" ></image>
</view>
<view class="tipText">
长按3秒保存到相册
</view>
<view class="headTitle">
美妆强度(1-10)
</view>
<u-slider v-model="strength" min="1" max="10" step="1" inactiveColor="#c0c4cc" activeColor="#f56c6c" blockColor="#f56c6c" showValue></u-slider>
<view class="headTitle">
美妆风格
</view>
<view class="bottomView">
<view :class="['item', item.selected?'itemActivated':'']" v-for="(item,index) in resourceTypeList" :key="index" @click="itemClick(item)">
<view :class="['itemImage']">
<image class="image" :src="item.effectUrl" mode="scaleToFill"></image>
</view>
<view :class="['itemText']">
{{item.effectName}}
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import config from '@/common/config'
import {randomString, compressImage} from '@/util/common.js'
import {faceMakeup} from '@/common/api.js'
export default {
data() {
return {
list: ['原图', '效果图'],
curNow: 0,
yuantuImage: '../../static/images/faceMakeup/yuantu.png',
//效果图 url
effectImage: '../../static/images/faceMakeup/FaceMakeup0.png',
//美妆风格
resourceTypeList:[
{
animeStyle: '0',
effectName: '整装',
effectUrl: '../../static/images/faceMakeup/FaceMakeup0.png',
selected: true
},
{
animeStyle: '1',
effectName: '基础妆',
effectUrl: '../../static/images/faceMakeup/FaceMakeup1.png',
selected: false
},
{
animeStyle: '2',
effectName: '少女妆',
effectUrl: '../../static/images/faceMakeup/FaceMakeup2.png',
selected: false
},
{
animeStyle: '3',
effectName: '活力妆',
effectUrl: '../../static/images/faceMakeup/FaceMakeup3.png',
selected: false
},
{
animeStyle: '4',
effectName: '优雅妆',
effectUrl: '../../static/images/faceMakeup/FaceMakeup4.png',
selected: false
},
{
animeStyle: '5',
effectName: '魅惑妆',
effectUrl: '../../static/images/faceMakeup/FaceMakeup5.png',
selected: false
},
{
animeStyle: '6',
effectName: '梅子妆',
effectUrl: '../../static/images/faceMakeup/FaceMakeup6.png',
selected: false
},
],
strength: 5, //美妆强度
serveImagePath: '', //临时存储 上传图片 在服务器上存放目录
systemInfo: null,
canvasStyle: {},
}
},
onLoad() {
this.systemInfo = uni.getSystemInfoSync();
},
mounted(){
uni.getImageInfo({
src: this.yuantuImage,
success: image => {
this.canvasStyle = this.caclCanvasStyle(image.height, image.width);
},
fail: err => {
}
});
},
methods: {
sectionChange(index) {
this.curNow = index;
},
/**
* 计算高
* @param {Object} imgHeight
* @param {Object} imgWidth
*/
caclCanvasStyle(imgHeight, imgWidth) {
let width = 0, height = 0,
//缩放比
zoomRatio = 1.0;
if (imgWidth > imgHeight) {
if (this.systemInfo.windowWidth <= imgWidth) {
width = this.systemInfo.windowWidth * 0.9;
zoomRatio = width / imgWidth
height = zoomRatio * imgHeight;
} else {
width = imgWidth;
height = imgHeight;
}
} else {
if (this.systemInfo.windowHeight * 0.7 <= imgHeight) {
height = this.systemInfo.windowHeight * 0.7;
zoomRatio = this.systemInfo.windowHeight * 0.7 / imgHeight
width = zoomRatio * imgWidth;
if (this.systemInfo.windowWidth <= width) {
width = this.systemInfo.windowWidth * 0.8;
zoomRatio = this.systemInfo.windowWidth * 0.8 / width
height = zoomRatio * height;
}
} else {
width = imgWidth;
height = imgHeight;
}
}
return {
width: width.toFixed(0) + "px",
height: height.toFixed(0) + "px",
zoomRatio: zoomRatio
}
},
//效果切换
itemClick(item){
this.resourceTypeList.forEach(v=>{
v.selected = false;
})
item.selected = true
//点击了 上传图片 生成 动漫
if(this.serveImagePath != ''){
this.generateEffect(this.serveImagePath, item.animeStyle, this.strength)
}
else{
this.effectImage = item.effectUrl
}
},
//上传图片按钮
immediateProduction(){
let that = this
uni.chooseImage({
count: 1, //默认9
sizeType: ['compressed'], //可以指定是原图('original', )还是压缩图('compressed'),默认二者都有 'original',
sourceType: ['album', 'camera'], //从相册选择
success: function (res) {
//图片后缀 jpeg , png, gif
let imgType = res.tempFiles[0].type.substring(6)
// console.log('imgType:', imgType)
if(imgType !='jpeg' && imgType !='png'){
uni.showToast({
title: "请上传 jpg或 png格式图片",
icon:'none'
});
return
}
//此处先进行压缩函数处理 1. 处理尺寸 2.处理画面质量
compressImage(res.tempFiles[0]).then((res) =>{
that.uploadPictureCom(res)
})
}
});
},
//压缩上传 参数为file对象
uploadPictureCom(blobFile){
let that = this
//显示加载框
uni.showLoading({
title: '上传中'
});
//上传至后端
uni.uploadFile({
url: config.baseUrl + '/openApi/chatGPT/v1/uploadImage',
file: blobFile,
name: 'imageFile',
formData: {
modelName: 'faceMakeup'
},
success: (res) => {
//隐藏加载框
uni.hideLoading();
let result = res.data
let json = JSON.parse(result)
if(json.code != 200){
uni.$u.toast(json.message);
return
}
that.setPageValue(json.data.imagePath, json.data.urlPath)
}
});
},
//上传成功后 给页面赋值回显
setPageValue(imagePath ,urlPath){
let that = this
that.yuantuImage = urlPath
//服务器上的存放目录
that.serveImagePath = imagePath
that.resourceTypeList.forEach(v=>{
v.selected = false;
})
let item = that.resourceTypeList[0]
item.selected = true
//重新设置 尺寸大小
uni.getImageInfo({
src: urlPath,
success: image => {
that.canvasStyle = that.caclCanvasStyle(image.height, image.width);
}
});
//切换到效果图
that.sectionChange(1)
//生成第一种风格
that.generateEffect(imagePath, item.animeStyle, that.strength)
},
//生成效果
generateEffect(imagePath, resourceType, strength){
let formData = {
imagePath: imagePath,
resourceType: resourceType,
strength: strength/10
}
//显示加载框
uni.showLoading({
title: '生成中'
});
faceMakeup(formData).then(res =>{
//隐藏加载框
uni.hideLoading();
if(res.code != 200){
uni.showToast({
title: res.message,
icon:"none"
})
return
}
this.effectImage = res.data.imageUrl
})
}
}
}
</script>
<style scoped lang="less">
.demand{
box-sizing: border-box;
.subsection{
height: 90rpx;
line-height: 90rpx;
}
.yuantuView{
text-align: center;
margin: 40rpx auto;
.yuantuImage{
border-radius: 10rpx;
// width: 100%;
// height: 1200rpx;
}
.uButton{
margin: 40rpx auto;
width: 90%;
height: 90rpx;
line-height: 90rpx;
background: #f56c6c;
color: white;
border-radius: 10rpx;
opacity: 1;
font-size: 32rpx;
border-width: 0;
}
}
.effect{
margin: 40rpx auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.effectImage{
border-radius: 10rpx;
width: 100%;
height: 1200rpx;
}
}
.tipText{
text-align: center;
font-size: 32rpx;
font-weight: bold;
color: #4169E1;
}
.headTitle{
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin: 10rpx 40rpx;
}
.bottomView{
margin: 20rpx auto;
border: #4169E1 solid 0rpx;
background: #FFFFFF;
opacity: 1;
text-align: center;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-content: flex-start;
.item{
margin: 20rpx 20rpx;
border-radius: 10rpx;
.itemImage{
border-radius: 0;
.image{
width: 228rpx;
height: 98rpx;
}
}
.itemText{
height: 50rpx;
line-height: 50rpx;
text-align: center;
font-size: 30rpx;
background: #f56c6c;
color: white;
border-radius: 0;
}
}
.itemActivated{
text-align: center;
border: 5rpx solid #f56c6c;
border-radius: 10rpx;
}
.item:last-child {
margin-right: 290rpx;
}
}
}
</style>
后端代码
/**
* 人脸美妆
* @param jsonObject
* @return
*/
@RequestMapping("/v1/faceMakeup")
public ResponseData faceMakeup(@RequestBody JSONObject jsonObject) {
//网络地址
String imagePath = jsonObject.getString("imagePath");
//美妆风格
String resourceType = jsonObject.getString("resourceType");
//美妆强度
Float strength = jsonObject.getFloat("strength");
String key = imagePath + "_" + resourceType + "_" + strength;
String imageUrl = redisCache.getCacheObject(key);
if (StringUtils.isNotEmpty(imageUrl)){
JSONObject data = new JSONObject();
data.put("imageUrl", imageUrl);
return ResponseData.success(data);
}
try {
JSONObject data = GenerateHumanAnimeStyle.faceMakeup(imagePath, resourceType, strength);
if (data.getBooleanValue("success")){
redisCache.setCacheObject(key, data.getString("imageUrl"), 2, TimeUnit.HOURS);
return ResponseData.success(data);
}
else {
return ResponseData.error(data.getString("message"));
}
}catch (Throwable t){
}
return ResponseData.error("服务器异常");
}
调用阿里云代码部分
/**
* 人脸美妆
* @param imagePath
* @param resourceType
* @param strength
* @return
*/
public static JSONObject faceMakeup(String imagePath, String resourceType,Float strength) {
try {
if (client == null){
client = GenerateHumanAnimeStyle.createClient(accessKeyId, accessKeySecret);
System.out.println("初始化client");
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化client异常");
}
// 场景一,使用本地文件
InputStream inputStream = null;
try {
inputStream = new FileInputStream(new File(imagePath));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
com.aliyun.facebody20191230.models.FaceMakeupAdvanceRequest faceMakeupRequest = new com.aliyun.facebody20191230.models.FaceMakeupAdvanceRequest()
.setImageURLObject(inputStream)
.setMakeupType("whole")
.setResourceType(resourceType)
.setStrength(strength);
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
JSONObject resultData = new JSONObject();
try {
// 复制代码运行请自行打印 API 的返回值
FaceMakeupResponse response = client.faceMakeupAdvance(faceMakeupRequest, runtime);
// 获取整体结果
String result = com.aliyun.teautil.Common.toJSONString(TeaModel.buildMap(response));
System.out.println("result:" + result);
if (StringUtils.isEmpty(result)){
return null;
}
JSONObject jsonObject = JSONObject.parseObject(result);
if (jsonObject.getIntValue("statusCode") == 200){
JSONObject body = jsonObject.getJSONObject("body");
JSONObject data = body.getJSONObject("Data");
resultData.put("imageUrl",data.getString("ImageURL"));
resultData.put("success", true);
return resultData;
}
else {
resultData.put("message",jsonObject.getString("message"));
resultData.put("success", false);
return resultData;
}
} catch (TeaException error) {
// 获取整体报错信息
System.out.println();
// 获取单个字段
System.out.println(error.getCode());
String result = com.aliyun.teautil.Common.toJSONString(error);
JSONObject jsonObject = JSONObject.parseObject(result);
resultData.put("message",jsonObject.getString("message"));
resultData.put("success", false);
return resultData;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
代码踩坑经验分享
- 前端代码布局:布局设计和优化占用了我大量的时间,原因是我是后端工程师,对css样式不专业,哈哈,欢迎精通前端样式的小伙伴加入,我们后续会有大量的前端需求要实现,一起学习,共同进步,把创意落地到产品,技术就是生产力,
- 前端图片压缩:由于阿里云的接口参数对图片尺寸有严格要求(图片格式:PNG、JPG、JPEG、BMP、TIFF、WEBP等。 图片大小:小于3MB。 图片分辨率:小于2000×2000),因此需要前端先判断图片大小,苹果手机的拍摄的照片普遍都超过3M,和大于2000X2000分辨率,所以需要先进行compress压缩到小于3MB,并裁剪到小于2000X2000分辨率,在这里也坑了我好久,好在我能熬夜加班,最终找到了处理的方法,但是不算很完美,打算后面持续迭代,也欢迎做过压缩裁剪产品的小伙伴指导一下。
产品总结
通过开发这款AI人脸美妆系统,我们将为用户提供更加智能、个性化的美妆体验。通过准确的人脸分析和美妆效果叠加,用户可以轻松实现虚拟试妆和实时美妆的功能,并可以保存和分享美妆效果图像。同时,通过个性化的妆品推荐,我们也将帮助用户更好地选择适合自己的美妆产品。通过不断改进和迭代,我们将努力提升系统的性能和用户体验,满足用户对美妆的不同需求。