EdgeFace AI - 终端智能美颜引擎(让设备的每一次“芯”跳为你服务)
本文介绍了基于JavaCV和OpenCV实现的美颜功能方案。公司为提升产品竞争力,在设备本地部署精灵服务实现美颜处理,采用去中心化架构降低服务器负载。技术方案基于SpringBoot整合JavaCV平台,通过OpenCV实现人脸检测、磨皮、美白、瘦脸等美颜流程。文章展示了不同参数组合下的美颜效果对比,并提供了核心代码实现,包括JavaCV配置加载和控制器接口设计。该方案充分利用设备本地计算资源,避
前言
公司老板展望未来时,做了一个任务规划的,就是要在我们的设备上做美颜实现。因为现在都爱美热爱生活,所以哪怕是学驾照,也希望有给美美的照片。况且我们的竞争对手有,我们要知道人无我有,人有我精。
这个规划一直记着在,也是一直腾不出手。现在该上线的上线了,该交付的交付了,支付平台也已经实现客户管理、扫码收款了,稳稳的。所以有时间可以来攻一下,之前一直就是看好opencv,早前使用它实现了人脸识别、人脸识别剪裁。
今天就来分享下javacv基于最新版本的opencv实现相关美颜操作,其实昨天就开始研究了,目前先不强求效果,走通功能。
架构设计
-
基于JavaCV-platform,集成到SpringBoot/SpringCloud,对外暴露美颜接口服务。
-
JavaCV 的核心是作为 OpenCV 和 FFmpeg 等原生库在 Java 中的高效、易用的桥梁,通过 Frame 对象统一数据交换,并以 javacv-platform 的形式提供开箱即用的跨平台支持。
-
另外这块其实也是可以引入AI的,通过AI对人脸进行分析,综合调优,更智能。
-
图像处理:人脸检测、滤镜、对象识别(依赖OpenCV后端)
-
视频分析:运动检测、视频质量分析
-
多媒体操作:视频格式转换、音视频录制、RTMP直播推拉流(依赖FFmpeg后端)
-
相机交互:访问USB摄像头、IP相机,处理工业相机(如通过OpenCV或FlyCapture),在这里也可以通过JNA/JNI与设备交互的。
美颜流程
照片文件图像解码 -> 人脸识别模型加载 -> 磨皮 -> 美白 -> 红润 -> 饱和 -> 祛斑 -> 瘦脸 -> 大眼 -> 锐化 -> 输出返回数据
设计思路
之前觉得这种服务需要整合到公共服务里,如果这样处理可能就需要考虑带宽、流量、高并发问题。后来想到我之前设计架构考虑用Electron + VUE取代WPF,所以设计了一个在设备上运行的精灵服务。其实就算不用这个架构取代WPF,也是可以在每台设备上部署一个精灵服务的,在本地处理一些其他业务,同时为WPF提供本地接口服务。
这一套精灵服务方案肯定是可行的,而且能干的事情还很多,当前设备基本上都是性能过剩的,在本地处理一些业务就可以省去流量、带宽、中心服务器的成本,而且一台挂了不影响中心服务哦。这种去中心化服务是单兵、异动工作站的底层支撑。
美颜效果
磨皮

磨皮美白

磨皮美白锐化

磨皮美白红润锐化

磨皮美白红润饱和锐化

磨皮美白红润饱和祛斑瘦脸锐化

其他效果待续,接口已经设计支持自定义强调、是否美颜项目了。
核心代码
- pom.xml
<!-- javacv -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.12</version>
</dependency>
在上次的设备精灵服务上,增加引入这个依赖就ok了。
- 配置类JavaCVConfig
package com.hckj.deviceagent.config;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.opencv.opencv_java;
import org.springframework.context.annotation.Configuration;
/**
* javaCV配置类,负责加载本地OpenCV库
* @author zwmac
*/
@Slf4j
@Configuration
public class JavaCVConfig {
/**
* 初始化方法,加载OpenCV本地库
*/
@PostConstruct
public void init() {
try {
// JavaCV自动加载OpenCV本地库
log.info("Loading JavaCV/OpenCV native libraries...");
// 方式1:显式加载(推荐)
Loader.load(opencv_java.class);
// 方式2:检查是否加载成功
log.info("JavaCV/OpenCV loaded successfully!");
log.info("OpenCV version: {}", org.bytedeco.opencv.global.opencv_core.getVersionRevision());
// 打印更多信息
printLibraryInfo();
} catch (Throwable e) {
log.error("Failed to load JavaCV/OpenCV libraries", e);
throw new RuntimeException("JavaCV initialization failed", e);
}
}
/**
* 打印OpenCV库的详细信息
*/
private void printLibraryInfo() {
try {
// 检查关键组件
log.info("Checking OpenCV components...");
log.info("OpenCV build info: {}", org.bytedeco.opencv.global.opencv_core.getBuildInformation());
// 测试Mat类
org.bytedeco.opencv.opencv_core.Mat testMat = new org.bytedeco.opencv.opencv_core.Mat();
testMat.close();
log.info("OpenCV Mat class works properly");
} catch (Exception e) {
log.warn("Cannot get detailed OpenCV info", e);
}
}
}
- BeautyController
package com.hckj.deviceagent.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import com.hckj.deviceagent.dto.BeautyDto;
import com.hckj.deviceagent.service.BeautyService;
import com.hckj.deviceagent.vo.BeautyVo;
import com.rs.rsbase.entity.RestResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 美颜服务
*
* @author zwmac
*/
@Slf4j
@RestController
@RequestMapping("/api/beauty")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class BeautyController {
@Autowired
private BeautyService service;
/**
* 美颜处理
* @param file 图片文件
* @param smoothLevel 磨皮等级
* @param whitenLevel 美白等级
* @param sharpenLevel 锐化等级
* @param ruddyLevel 红润等级
* @param contrastLevel 对比度等级
* @param faceLiftStrength 瘦脸强度
* @param bigEyesStrength 大眼强度
* @param blemishLevel 祛斑等级
* @param faceLift 是否瘦脸
* @param bigEye 是否大眼
* @param removeBlemish 是否祛斑
* @param keepEdges 是否保留边缘细节
* @return 美颜处理结果
*/
@PostMapping("/dealBeauty")
public RestResponse<?> dealBeauty(@RequestParam("file") MultipartFile file,
@RequestParam(value = "smoothLevel") float smoothLevel,
@RequestParam(value = "whitenLevel") float whitenLevel,
@RequestParam(value = "sharpenLevel") float sharpenLevel,
@RequestParam(value = "ruddyLevel") float ruddyLevel,
@RequestParam(value = "contrastLevel") float contrastLevel,
@RequestParam(value = "faceLiftStrength") float faceLiftStrength,
@RequestParam(value = "bigEyesStrength") float bigEyesStrength,
@RequestParam(value = "blemishLevel") float blemishLevel,
@RequestParam(value = "faceLift", defaultValue = "false") Boolean faceLift,
@RequestParam(value = "bigEye", defaultValue = "false") Boolean bigEye,
@RequestParam(value = "removeBlemish", defaultValue = "false") Boolean removeBlemish,
@RequestParam(value = "keepEdges", defaultValue = "false") Boolean keepEdges) {
BeautyVo beautyVo = BeautyVo.builder()
.smoothLevel(smoothLevel)
.whitenLevel(whitenLevel)
.sharpenLevel(sharpenLevel)
.ruddyLevel(ruddyLevel)
.contrastLevel(contrastLevel)
.faceLiftStrength(faceLiftStrength)
.bigEyesStrength(bigEyesStrength)
.blemishLevel(blemishLevel)
.faceLift(faceLift)
.bigEye(bigEye)
.removeBlemish(removeBlemish)
.keepEdges(keepEdges)
.build();
BeautyDto data = service.dealBeauty(file, beautyVo);
return RestResponse.succ(data.getImageData());
}
}
- BeautyServiceImpl
package com.hckj.deviceagent.service.impl;
import com.hckj.deviceagent.beauty.JavaCVBeautyProcessor;
import com.hckj.deviceagent.dto.BeautyDto;
import com.hckj.deviceagent.service.BeautyService;
import com.hckj.deviceagent.vo.BeautyVo;
import com.rs.rsbase.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
/**
* @author zwmac
*/
@Slf4j
@Service
public class BeautyServiceImpl implements BeautyService {
@Autowired
private JavaCVBeautyProcessor beautyProcessor;
@Override
public BeautyDto dealBeauty(MultipartFile file, BeautyVo beautyVo) {
BeautyDto dto = new BeautyDto();
try {
byte[] result = beautyProcessor.applyBeauty(file.getBytes(), beautyVo);
dto.setImageData(result);
} catch (Exception e) {
log.error("美颜处理异常,原因:{}", e.getMessage(), e);
throw new ApiException("美颜处理失败,原因:" + e.getMessage());
}
return dto;
}
}
- JavaCVBeautyProcessor(核心)
package com.hckj.deviceagent.beauty;
import com.hckj.deviceagent.vo.BeautyVo;
import com.rs.rsbase.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.FloatPointer;
import org.bytedeco.javacpp.IntPointer;
import org.bytedeco.javacpp.indexer.UByteIndexer;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.URL;
import java.nio.file.Paths;
import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgcodecs.*;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
/**
* 修复并加固版 JavaCV 美颜处理器(bytedeco-only)
* @author zwmac
*/
@Slf4j
@Component
public class JavaCVBeautyProcessor {
// ==================== 美颜主方法 ====================
public byte[] applyBeauty(byte[] imageBytes, BeautyVo config) {
if (imageBytes == null || imageBytes.length == 0) {
throw new ApiException("输入图像为空");
}
Mat image = null;
Mat result = null;
BytePointer inputBp = null;
try {
// 1. 安全解码(bytedeco 用法:BytePointer + Mat(1, len, CV_8UC1, bp))
inputBp = new BytePointer(imageBytes);
Mat inputMat = new Mat(1, imageBytes.length, CV_8UC1, inputBp);
image = imdecode(inputMat, IMREAD_COLOR);
inputMat.release();
if (image == null || image.empty() || image.cols() <= 0 || image.rows() <= 0) {
log.error("图像解码失败或图像为空");
if (image != null) {
image.release();
}
return imageBytes;
}
log.info("图像解码完成,输入图像尺寸:{}x{}, 通道数:{}", image.cols(), image.rows(), image.channels());
// 2. 处理图像(入口加固)
result = processImage(image, config);
if (result == null || result.empty()) {
log.error("处理后图像为空");
image.release();
return imageBytes;
}
// 3. 编码(bytedeco 专用 imencode -> BytePointer)
IntPointer params = null;
BytePointer outBuf = null;
try {
params = new IntPointer(2);
params.put(0, IMWRITE_JPEG_QUALITY);
params.put(1, 90);
outBuf = new BytePointer();
boolean ok = imencode(".jpg", result, outBuf, params);
if (!ok || outBuf.limit() <= 0) {
log.error("编码失败");
throw new ApiException("图像编码失败");
}
byte[] output = new byte[(int) outBuf.limit()];
outBuf.get(output);
return output;
} catch (Exception e) {
log.error("结果图像编码失败,原因:{}", e.getMessage(), e);
throw new ApiException("结果图像编码失败,原因:" + e.getMessage());
} finally {
if (outBuf != null) {
outBuf.deallocate();
}
if (params != null) {
params.deallocate();
}
}
} catch (ApiException ae) {
throw ae;
} catch (Exception e) {
log.error("美颜处理失败,原因:{}", e.getMessage(), e);
throw new ApiException("美颜处理失败,原因:" + e.getMessage());
} finally {
if (image != null) {
image.release();
}
if (result != null) {
result.release();
}
if (inputBp != null) {
inputBp.deallocate();
}
}
}
private Mat processImage(Mat src, BeautyVo config) {
// 基本校验
if (src == null || src.empty() || src.cols() <= 0 || src.rows() <= 0) {
throw new ApiException("输入图像非法,无法进行美颜处理");
}
// 统一三通道(防止灰度图导致后续 cvtColor/HSV 操作崩溃)
Mat work = src;
boolean createdTemp = false;
if (src.channels() == 1) {
Mat tmp = new Mat();
cvtColor(src, tmp, COLOR_GRAY2BGR);
work = tmp;
createdTemp = true;
}
Mat result = work.clone();
try {
// 1. 磨皮
result = applySmooth(result, config.getSmoothLevel());
// 2. 美白
result = applyWhiten(result, config.getWhitenLevel());
// 3.红润
result = applyBlush(result, config.getRuddyLevel());
//4.饱和度
result = applySaturation(result, config.getContrastLevel());
//5.祛斑
if (config.isRemoveBlemish()) {
result = applyRemoveBlemish(result, config.getBlemishLevel());
}
// 5. 人脸检测和特效
if (config.isFaceLift() || config.isBigEye()) {
RectVector faces = detectFaces(result);
try {
if (faces != null && faces.size() > 0) {
//5.1瘦脸
if (config.isFaceLift()) {
for (long i = 0; i < faces.size(); i++) {
Rect face = faces.get(i);
// 对单个人脸调用 applySlimFace
Mat tmp = applySlimFace(result, face, config.getFaceLiftStrength());
result.release();
result = tmp;
}
}
//5.2大眼
if (config.isBigEye()) {
result = applyEyeLarge(result, faces, config.getBigEyesStrength());
}
}
} catch (Exception e) {
log.error("人脸特效处理失败,原因:{}", e.getMessage(), e);
throw new ApiException("人脸特效处理失败,原因:" + e.getMessage());
} finally {
if (faces != null) {
faces.close();
}
}
}
// 4. 锐化
if (config.getSharpenLevel() > 0) {
result = applySharpen(result, config.getSharpenLevel());
}
return result;
} finally {
if (createdTemp && work != null) {
work.release();
}
// note: do not release result here (caller will release)
}
}
// ==================== 祛斑 / 祛痘 ====================
private Mat applyRemoveBlemish(Mat src, float level) {
if (level <= 0) {
return src.clone();
}
// level 推荐:0.2 ~ 0.6
Mat result = new Mat();
Mat blur = new Mat();
Mat diff = new Mat();
Mat gray = new Mat();
Mat mask = new Mat();
try {
// 1. 双边滤波(保边磨皮)
int d = 9 + (int) (level * 10);
double sigma = 50 + level * 50;
bilateralFilter(src, blur, d, sigma, sigma);
// 2. 高频提取(原图 - 磨皮图)
absdiff(src, blur, diff);
// 3. 转灰度,用于检测斑点
cvtColor(diff, gray, COLOR_BGR2GRAY);
// 4. 自适应阈值提取瑕疵区域
threshold(gray, mask, 15, 255, THRESH_BINARY);
// 5. 形态学膨胀,覆盖完整斑点区域
Mat kernel = getStructuringElement(MORPH_ELLIPSE, new Size(3, 3));
dilate(mask, mask, kernel);
// 6. 用磨皮图替换原图中的斑点区域
Mat repaired = src.clone();
blur.copyTo(repaired, mask);
// 7. 与原图做权重融合,防止“塑料脸”
double mix = Math.min(level, 0.6);
addWeighted(src, 1 - mix, repaired, mix, 0, result);
kernel.release();
repaired.release();
log.info("完成祛斑处理,level={}", level);
return result;
} catch (Exception e) {
log.error("祛斑失败,原因:{}", e.getMessage(), e);
throw new ApiException("祛斑失败,原因:" + e.getMessage());
} finally {
blur.release();
diff.release();
gray.release();
mask.release();
}
}
// ==================== 基础美颜功能(修正API) ====================
private Mat applySmooth(Mat src, float level) {
if (level <= 0) {
return src.clone();
}
Mat dst = new Mat();
// 高斯模糊
int kernelSize = 5 + (int) (level * 8);
if (kernelSize % 2 == 0) {
kernelSize++;
}
GaussianBlur(src, dst, new Size(kernelSize, kernelSize), (double) (level * 2));
// 双边滤波(可选:更自然)
if (level > 0.5) {
Mat bilateral = new Mat();
int d = Math.max(1, (int) (15 * level));
double sigmaColor = 25 * level;
double sigmaSpace = 25 * level;
bilateralFilter(dst, bilateral, d, sigmaColor, sigmaSpace, BORDER_DEFAULT);
dst.release();
dst = bilateral;
}
Mat blended = new Mat();
addWeighted(src, 1 - level * 0.6, dst, level * 0.6, 0, blended);
dst.release();
log.info("完成磨皮处理,level={}", level);
return blended;
}
private Mat applyWhiten(Mat src, float level) {
if (level <= 0) {
return src.clone();
}
Mat dst = new Mat();
// 使用 convertTo 增加亮度/对比度(保证目标类型为 CV_8U)
src.convertTo(dst, CV_8U, 1.0 + 0.1 * level, 25.0 * level);
log.info("完成美白处理,level={}", level);
return dst;
}
private Mat applySharpen(Mat src, float level) {
if (level <= 0) {
return src.clone();
}
Mat kernel = new Mat(3, 3, CV_32F);
FloatPointer fp = new FloatPointer(new float[]{
0, -1 * level, 0,
-1 * level, 1 + 4 * level, -1 * level,
0, -1 * level, 0
});
kernel.data().put(fp);
Mat dst = new Mat();
filter2D(src, dst, src.depth(), kernel);
kernel.deallocate();
fp.deallocate();
log.info("完成锐化处理,level={}", level);
return dst;
}
// ==================== 不重影瘦脸(终极修复版)====================
private Mat applySlimFace(Mat src, Rect face, float level) {
if (src == null || src.empty() || face == null || level <= 0f) {
return src;
}
level = Math.min(Math.max(level, 0f), 1f);
Mat out = src.clone();
int imgW = src.cols();
int imgH = src.rows();
// ROI 扩展区域
int leftX = Math.max(0, face.x() - face.width() / 4);
int rightX = Math.min(imgW - 1, face.x() + face.width() + face.width() / 4);
int topY = Math.max(0, face.y());
int bottomY = Math.min(imgH - 1, face.y() + face.height() + face.height() / 3);
int w = rightX - leftX + 1;
int h = bottomY - topY + 1;
int cx = face.x() + face.width() / 2;
float halfFaceW = face.width() / 2f;
float maxPullPx = face.width() * 0.25f * level; // 最大收缩像素
UByteIndexer srcIdx = src.createIndexer();
UByteIndexer outIdx = out.createIndexer();
for (int y = 0; y < h; y++) {
int gy = topY + y;
float v = y / (float) h;
float jawWeight = (float) Math.pow(v, 1.3); // 下巴更明显
for (int x = 0; x < w; x++) {
int gx = leftX + x;
float dxNorm = (gx - cx) / halfFaceW; // -1 左脸, 1 右脸
float absX = Math.abs(dxNorm);
// 中央鼻子/嘴巴保护
if (absX < 0.25f) {
for (int c = 0; c < 3; c++) {
outIdx.put(gy, gx, c, srcIdx.get(gy, gx, c) & 0xFF);
}
continue;
}
// 脸颊拉动比例
float profile = smoothstep(0.25f, 0.9f, absX);
float pull = jawWeight * profile * maxPullPx;
int srcX;
if (dxNorm < 0) { // 左脸 → 向中线收缩
srcX = gx - Math.round(pull);
} else { // 右脸 → 向中线收缩
srcX = gx + Math.round(pull);
}
// 边界保护
if (srcX < 0) srcX = 0;
if (srcX >= imgW) srcX = imgW - 1;
for (int c = 0; c < 3; c++) {
outIdx.put(gy, gx, c, srcIdx.get(gy, srcX, c) & 0xFF);
}
}
}
srcIdx.release();
outIdx.release();
return out;
}
// smoothstep 辅助函数
private float smoothstep(float edge0, float edge1, float x) {
x = Math.max(0f, Math.min(1f, (x - edge0) / (edge1 - edge0)));
return x * x * (3 - 2 * x);
}
private Mat scaleCheek(Mat src, int x, int y, int width, int height, float scale) {
// 边界检查
if (x < 0) {
width += x;
x = 0;
}
if (y < 0) {
height += y;
y = 0;
}
if (x + width > src.cols()) {
width = src.cols() - x;
}
if (y + height > src.rows()) {
height = src.rows() - y;
}
if (width <= 10 || height <= 10) {
return src;
}
Rect cheekRect = new Rect(x, y, width, height);
Mat cheekRoi = new Mat(src, cheekRect);
// 计算新尺寸
int newWidth = Math.max(10, (int) (width * scale));
int newHeight = Math.max(10, (int) (height * scale));
Mat scaled = new Mat();
resize(cheekRoi, scaled, new Size(newWidth, newHeight), 0, 0, INTER_AREA);
// 创建渐变掩码(安全写法)
Mat mask = createGradientMask(width, height, scale);
Mat resizedMask = new Mat();
resize(mask, resizedMask, new Size(newWidth, newHeight), 0, 0, INTER_LINEAR);
// 计算放置位置
int offsetX = (width - newWidth) / 2;
int offsetY = (height - newHeight) / 2;
// 放置并混合(注意边界)
int tx = x + offsetX;
int ty = y + offsetY;
if (tx < 0 || ty < 0 || tx + newWidth > src.cols() || ty + newHeight > src.rows()) {
// 非常规情况,直接 skip
cheekRoi.release();
scaled.release();
mask.release();
resizedMask.release();
return src;
}
Rect targetRect = new Rect(tx, ty, newWidth, newHeight);
Mat targetRoi = new Mat(src, targetRect);
Mat tmpCopy = targetRoi.clone();
Mat scaledWithMask = new Mat();
scaled.copyTo(scaledWithMask, resizedMask);
addWeighted(tmpCopy, 0.4, scaledWithMask, 0.6, 0, targetRoi);
// 释放
cheekRoi.release();
scaled.release();
mask.release();
resizedMask.release();
tmpCopy.release();
scaledWithMask.release();
targetRoi.release();
return src;
}
// 安全实现:使用 UByteIndexer 操作(避免 createBuffer 的行不连续问题)
private Mat createGradientMask(int width, int height, float scale) {
Mat mask = new Mat(height, width, CV_8UC1, new Scalar(0));
UByteIndexer indexer = mask.createIndexer();
try {
int border = (int) (width * (1 - scale) / 2.0);
if (border <= 0) {
border = 1;
}
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
int distToBorder = Math.min(Math.min(j, width - 1 - j), Math.min(i, height - 1 - i));
int value;
if (distToBorder < border) {
double alpha = (double) distToBorder / border;
value = (int) (alpha * 255);
} else {
value = 255;
}
indexer.put(i, j, value);
}
}
} finally {
indexer.release();
}
return mask;
}
// ==================== 大眼功能(简化实现) ====================
private Mat applyEyeLarge(Mat src, RectVector faces, float strength) {
Mat dst = src.clone();
for (long i = 0; i < faces.size(); i++) {
Rect face = faces.get(i);
int eyeSize = Math.max(10, face.width() / 8);
int leftEyeX = face.x() + face.width() * 2 / 5;
int leftEyeY = face.y() + face.height() / 3;
int rightEyeX = face.x() + face.width() * 3 / 5;
int rightEyeY = leftEyeY;
dst = enlargeEye(dst, leftEyeX, leftEyeY, eyeSize, strength);
dst = enlargeEye(dst, rightEyeX, rightEyeY, eyeSize, strength);
}
log.info("完成大眼处理,strength={}", strength);
return dst;
}
private Mat enlargeEye(Mat src, int centerX, int centerY, int size, float strength) {
int radius = size / 2;
int x = Math.max(0, centerX - radius);
int y = Math.max(0, centerY - radius);
int maxWidth = src.cols() - x;
int maxHeight = src.rows() - y;
int eyeSize = Math.min(radius * 2, Math.min(maxWidth, maxHeight));
if (eyeSize <= 10) {
return src;
}
radius = eyeSize / 2;
Rect eyeRect = new Rect(x, y, eyeSize, eyeSize);
Mat eyeRoi = new Mat(src, eyeRect);
try {
int enlargedSize = calculateEnlargedSize(eyeSize, strength);
Mat enlarged = new Mat();
resize(eyeRoi, enlarged, new Size(enlargedSize, enlargedSize), 0, 0, INTER_LINEAR);
Mat circleMask = createCircleMask(eyeSize);
Mat resizedMask = new Mat();
resize(circleMask, resizedMask, new Size(enlargedSize, enlargedSize), 0, 0, INTER_LINEAR);
int offsetX = (eyeSize - enlargedSize) / 2;
int offsetY = (eyeSize - enlargedSize) / 2;
int tx = x + offsetX;
int ty = y + offsetY;
if (tx >= 0 && ty >= 0 && tx + enlargedSize <= src.cols() && ty + enlargedSize <= src.rows()) {
Rect targetRect = new Rect(tx, ty, enlargedSize, enlargedSize);
applyEyeEnlargement(src, targetRect, enlarged, resizedMask);
}
enlarged.release();
circleMask.release();
resizedMask.release();
} finally {
eyeRoi.release();
}
return src;
}
private int calculateEnlargedSize(int originalSize, float strength) {
float scale = 1.0f + strength * 0.8f;
int enlargedSize = (int) (originalSize * scale);
enlargedSize = Math.max(10, enlargedSize);
enlargedSize = Math.min(enlargedSize, originalSize * 2);
return enlargedSize;
}
private Mat createCircleMask(int size) {
if (size <= 0) {
return new Mat();
}
Mat mask = new Mat(size, size, CV_8UC1, new Scalar(0));
UByteIndexer indexer = mask.createIndexer();
int center = size / 2;
int radius = size / 2;
try {
for (int yy = 0; yy < size; yy++) {
for (int xx = 0; xx < size; xx++) {
double dx = xx - center;
double dy = yy - center;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= radius) {
double alpha = calculateAlpha(distance, radius);
int val = (int) (alpha * 255);
indexer.put(yy, xx, val);
}
}
}
} finally {
indexer.release();
}
return mask;
}
private double calculateAlpha(double distance, double radius) {
if (distance <= radius * 0.7) {
return 1.0;
}
double fadeStart = radius * 0.7;
double fadeEnd = radius;
double fadeRange = fadeEnd - fadeStart;
if (fadeRange <= 0) {
return 1.0;
}
double progress = (distance - fadeStart) / fadeRange;
return 1.0 - progress;
}
private boolean isValidRegion(Mat image, Rect region) {
return region.x() >= 0 &&
region.y() >= 0 &&
region.x() + region.width() <= image.cols() &&
region.y() + region.height() <= image.rows();
}
private void applyEyeEnlargement(Mat src, Rect targetRect, Mat enlarged, Mat mask) {
Mat targetRoi = new Mat(src, targetRect);
try {
Mat tmpCopy = targetRoi.clone();
Mat enlargedWithMask = new Mat();
enlarged.copyTo(enlargedWithMask, mask);
addWeighted(tmpCopy, 0.4, enlargedWithMask, 0.6, 0, targetRoi);
enlargedWithMask.release();
tmpCopy.release();
} finally {
targetRoi.release();
}
}
// ==================== 人脸检测 ====================
private RectVector detectFaces(Mat image) {
RectVector faces = new RectVector();
Mat gray = new Mat();
try {
if (image == null || image.empty()) {
return faces;
}
cvtColor(image, gray, COLOR_BGR2GRAY);
equalizeHist(gray, gray);
// 尝试加载 LBP 或 Haar 模型(放在 resources/haarcascades 下)
URL url = this.getClass().getClassLoader().getResource("haarcascades/haarcascade_frontalface_default.xml");
if (url == null) {
url = this.getClass().getClassLoader().getResource("haarcascades/lbpcascade_frontalface.xml");
}
if (url == null) {
log.warn("未找到人脸检测模型文件,返回空结果");
return faces;
}
String cascadePath;
try {
URI uri = url.toURI();
cascadePath = Paths.get(uri).toAbsolutePath().toString();
} catch (Exception e) {
cascadePath = url.getPath();
}
CascadeClassifier faceDetector = new CascadeClassifier();
boolean loaded = faceDetector.load(cascadePath);
if (!loaded) {
log.error("人脸检测模型加载失败: {}", cascadePath);
faceDetector.close();
return faces;
}
// 使用灰度图进行检测(非常重要)
//faceDetector.detectMultiScale(gray, faces);
// --- 核心优化参数 ---
double scaleFactor = 1.1; // 保持默认或略高(如1.2),减少计算量
int minNeighbors = 5; // 提高到 5 或 6,减少误检(最重要)
int flags = 0; // 默认值
Size minSize = new Size(50, 50); // 设置最小人脸尺寸(防止小尺寸误检)
Size maxSize = new Size(); // 默认值(不限制最大尺寸)
// 使用优化后的参数进行检测
faceDetector.detectMultiScale(
gray,
faces,
scaleFactor,
minNeighbors,
flags,
minSize,
maxSize
);
log.info("检测到人脸数量: {}", faces.size());
faceDetector.close();
return faces;
} catch (Exception e) {
log.error("人脸检测失败: {}", e.getMessage(), e);
return faces;
} finally {
gray.release();
}
}
// ==================== 红润(气色)功能 ====================
private Mat applyBlush(Mat src, float level) {
if (level <= 0) {
return src.clone();
}
Mat hsv = new Mat();
Mat result = new Mat();
try {
// BGR -> HSV
cvtColor(src, hsv, COLOR_BGR2HSV);
// 分离通道
MatVector channels = new MatVector();
split(hsv, channels);
Mat h = channels.get(0);
Mat s = channels.get(1);
Mat v = channels.get(2);
// 1. 提高饱和度(红润核心)
Mat sBoosted = new Mat();
s.convertTo(sBoosted, CV_8U, 1.0 + 0.8 * level, 10 * level);
// 2. 轻微提高亮度(模拟气色)
Mat vBoosted = new Mat();
v.convertTo(vBoosted, CV_8U, 1.0 + 0.2 * level, 5 * level);
// 替换通道
sBoosted.copyTo(channels.get(1));
vBoosted.copyTo(channels.get(2));
// 合并通道
merge(channels, hsv);
// HSV -> BGR
Mat blushBgr = new Mat();
cvtColor(hsv, blushBgr, COLOR_HSV2BGR);
// 与原图按权重混合,防止过红
addWeighted(src, 1 - level * 0.6, blushBgr, level * 0.6, 0, result);
// 释放
for (int i = 0; i < 3; i++) {
channels.get(i).release();
}
channels.close();
sBoosted.release();
vBoosted.release();
blushBgr.release();
log.info("完成红润处理,level={}", level);
return result;
} catch (Exception e) {
log.error("调整红润失败,原因:{}", e.getMessage(), e);
throw new ApiException("调整红润失败,原因:" + e.getMessage());
} finally {
hsv.release();
}
}
// ==================== 饱和度调整 ====================
private Mat applySaturation(Mat src, float level) {
if (level == 0) {
return src.clone();
}
// level 取值建议:-0.5 ~ +0.8
Mat hsv = new Mat();
Mat result = new Mat();
try {
// BGR -> HSV
cvtColor(src, hsv, COLOR_BGR2HSV);
// 分离通道
MatVector channels = new MatVector();
split(hsv, channels);
Mat s = channels.get(1);
// 饱和度缩放因子
double alpha = 1.0 + level; // >1 增强,<1 减弱
double beta = 5 * level; // 微调偏移,防止暗部灰掉
Mat sAdjusted = new Mat();
s.convertTo(sAdjusted, CV_8U, alpha, beta);
// 回写 S 通道
sAdjusted.copyTo(channels.get(1));
// 合并通道
merge(channels, hsv);
// HSV -> BGR
Mat saturated = new Mat();
cvtColor(hsv, saturated, COLOR_HSV2BGR);
// 与原图混合,防止颜色过炸
double mix = Math.min(Math.abs(level), 0.6);
addWeighted(src, 1 - mix, saturated, mix, 0, result);
// 释放
channels.get(0).release();
channels.get(1).release();
channels.get(2).release();
channels.close();
sAdjusted.release();
saturated.release();
log.info("完成饱和度调整,level={}", level);
return result;
} catch (Exception e) {
log.error("调整饱和度失败,原因:{}", e.getMessage(), e);
throw new ApiException("调整饱和度失败,原因:" + e.getMessage());
} finally {
hsv.release();
}
}
// ==================== 终极简化版(无API问题) ====================
public byte[] simpleBeauty(byte[] imageBytes, float smooth, float whiten) {
try {
BytePointer bp = new BytePointer(imageBytes);
Mat input = new Mat(1, imageBytes.length, CV_8UC1, bp);
Mat image = imdecode(input, IMREAD_COLOR);
input.release();
bp.deallocate();
if (image == null || image.empty()) {
return imageBytes;
}
Mat result = image.clone();
if (smooth > 0) {
int kernelSize = 3 + (int) (smooth * 4);
if (kernelSize % 2 == 0) {
kernelSize++;
}
Mat blurred = new Mat();
GaussianBlur(image, blurred, new Size(kernelSize, kernelSize), 1.5);
addWeighted(image, 1 - smooth, blurred, smooth, 0, result);
blurred.release();
}
if (whiten > 0) {
Mat whitened = new Mat();
result.convertTo(whitened, CV_8U, 1.0, whiten * 20);
result.release();
result = whitened;
}
BytePointer outBuf = new BytePointer();
imencode(".jpg", result, outBuf);
byte[] output = new byte[(int) outBuf.limit()];
outBuf.get(output);
outBuf.deallocate();
image.release();
result.release();
return output;
} catch (Exception e) {
log.error("简单美颜失败", e);
return imageBytes;
}
}
// ==================== 快速美颜(预设模式) ====================
public byte[] quickBeauty(byte[] imageBytes, String mode) {
BeautyVo config = new BeautyVo();
switch (mode) {
case "natural":
config.setSmoothLevel(0.3f);
config.setWhitenLevel(0.2f);
config.setFaceLift(true);
config.setFaceLiftStrength(0.2f);
break;
case "porcelain":
config.setSmoothLevel(0.7f);
config.setWhitenLevel(0.5f);
config.setFaceLift(true);
config.setFaceLiftStrength(0.3f);
break;
case "cute":
config.setSmoothLevel(0.4f);
config.setWhitenLevel(0.3f);
config.setBigEye(true);
config.setBigEyesStrength(0.4f);
break;
case "auto":
default:
config.setSmoothLevel(0.5f);
config.setWhitenLevel(0.3f);
config.setFaceLift(true);
config.setFaceLiftStrength(0.25f);
config.setBigEye(true);
config.setBigEyesStrength(0.4f);
}
return applyBeauty(imageBytes, config);
}
}
总结
- opencv依旧那么能打,可惜4.9之后模型不开源了
- 设备精灵处理这些业务似乎更合理
- 千人千面,如果美颜开关、强度开放给客户自己设置,是按项目收费合适,还是按你在设备上操作的时长收费合适?
- 引入AI将会效果飞跃
好,就写到这里,日拱一卒,希望能帮到大家,uping!
更多推荐
所有评论(0)