前言

       公司老板展望未来时,做了一个任务规划的,就是要在我们的设备上做美颜实现。因为现在都爱美热爱生活,所以哪怕是学驾照,也希望有给美美的照片。况且我们的竞争对手有,我们要知道人无我有,人有我精。
       这个规划一直记着在,也是一直腾不出手。现在该上线的上线了,该交付的交付了,支付平台也已经实现客户管理、扫码收款了,稳稳的。所以有时间可以来攻一下,之前一直就是看好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!
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐