代码达人带你十分钟手撸AI人脸美妆系统

产品目标

开发一款智能化的人脸美妆系统,利用人工智能技术为用户提供个性化、精准的美妆效果。通过该系统,用户可以轻松实现虚拟试妆、美妆效果展示和个性化妆品推荐等功能,以满足用户对美妆的需求,提高用户体验。

产品效果

技术方案

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;
    }

代码踩坑经验分享

  1. 前端代码布局:布局设计和优化占用了我大量的时间,原因是我是后端工程师,对css样式不专业,哈哈,欢迎精通前端样式的小伙伴加入,我们后续会有大量的前端需求要实现,一起学习,共同进步,把创意落地到产品,技术就是生产力,
  2. 前端图片压缩:由于阿里云的接口参数对图片尺寸有严格要求(图片格式:PNG、JPG、JPEG、BMP、TIFF、WEBP等。 图片大小:小于3MB。 图片分辨率:小于2000×2000),因此需要前端先判断图片大小,苹果手机的拍摄的照片普遍都超过3M,和大于2000X2000分辨率,所以需要先进行compress压缩到小于3MB,并裁剪到小于2000X2000分辨率,在这里也坑了我好久,好在我能熬夜加班,最终找到了处理的方法,但是不算很完美,打算后面持续迭代,也欢迎做过压缩裁剪产品的小伙伴指导一下。

产品总结

通过开发这款AI人脸美妆系统,我们将为用户提供更加智能、个性化的美妆体验。通过准确的人脸分析和美妆效果叠加,用户可以轻松实现虚拟试妆和实时美妆的功能,并可以保存和分享美妆效果图像。同时,通过个性化的妆品推荐,我们也将帮助用户更好地选择适合自己的美妆产品。通过不断改进和迭代,我们将努力提升系统的性能和用户体验,满足用户对美妆的不同需求。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值