复刻PS瘦脸液化形变工具

液化工具介绍

PS中有个推脸形变的修图工具,借助液化工具可以实现瘦脸瘦身等功能。详细的液化功能adobe的官网有介绍: https://helpx.adobe.com/cn/photoshop/using/liquify-filter.html

1
2
3
4
5
液化”滤镜可用于推、拉、旋转、反射、折叠和膨胀图像的任意区域。您创建的扭曲可以是细微的或剧烈的,这就使“液化”命令成为修饰图像和创建艺术效果的强大工具。液化滤镜可以应用于 8 位/通道或 16 位/通道图像。

Photoshop 使用液化滤镜扭曲图像
使用液化滤镜扭曲图像
“液化”对话框中提供了液化滤镜的工具、选项和图像预览。要显示该对话框,请选取“滤镜”>“液化”。选择“高级模式”可访问更多选项。

本篇复刻的是,实现液化工具中的推、拉功能

液化原理

液化原理源于一片非常经典的论文交互式形变

  • local translation warps局部平移

local-translation-warps

其中推拉的实现部分原理为论文中的局部平移,局部平移的原理为

liquefy

如图所示,阴影圆环代表一个半径为rmax的圆形区域。其中c点为鼠标点击下单点坐标,也是圆心。鼠标从c到m,产生的作用就是湿的途中的点u变换到点x。所以实现的原理就是,找到上面这个变换的逆变换,即已知点x时,可以求解出对应变换前的坐标u,然后使用变换前的图像在点u附近的像素进行插值,求出变换后的点u的像素值。以上操作作用在圆形内的所有的像素点,就完成了一次的交互推拉形变效果。

代码实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
#include "opencv2/opencv.hpp"
#include <algorithm>

cv::Mat src_img;
cv::Mat lut_img;
int slider_intensity = 0;
int radius = 100;

bool leftdown = false;

cv::Point start = cv::Point(-1, -1);
cv::Point end = cv::Point(-1, -1);


cv::Point Liquefy(const cv::Point& c, const cv::Point& m, const cv::Point& x, const float r)
{
    auto DistancePow = [](const cv::Point2f& p1, const cv::Point2f& p2){
        return std::pow(p1.x - p2.x, 2) +  std::pow(p1.y - p2.y, 2);
    };
    cv::Point u = cv::Point(-1, -1);
    cv::Point2f direction = m - c;
    double factor = 0.2 * std::pow((r * r - DistancePow(x, c)) / (r * r - DistancePow(x, c) + DistancePow(m, c) ), 2);
    u = x - cv::Point(factor * direction.x, factor * direction.y);
    return u;
}

void Reshape(const cv::Mat& src, const cv::Point& start, const cv::Point& end, const float radius, cv::Mat& result)
{
    if(src.empty()){ return;}
    result = src.clone();
    int rows = src.rows;
    int cols = src.cols;

    auto Distance = [](const cv::Point2f& p1, const cv::Point2f& p2){
        return std::sqrt(std::pow(p1.x - p2.x, 2) +  std::pow(p1.y - p2.y, 2));
    };

    for(int r = 1; r < rows - 1; r++)
    {
        for(int c = 1; c < cols - 1; c++)
        {
            cv::Point2f p = cv::Point2f(c, r);
            cv::Point2f rp = p;
            if(Distance(p, start) < radius)
            {
                rp = Liquefy(start, end, p, radius);
            }
            result.at<cv::Vec3b>(p) = 0.4 * src.at<cv::Vec3b>(rp)
                    + 0.15 *  src.at<cv::Vec3b>(cv::Point2f(rp.x , rp.y -1) ) + 0.15 *  src.at<cv::Vec3b>(cv::Point2f(rp.x -1, rp.y))
                    + 0.15 *  src.at<cv::Vec3b>(cv::Point2f(rp.x + 1, rp.y) )+ 0.15 *  src.at<cv::Vec3b>(cv::Point2f(rp.x, rp.y + 1)) ;
        }
    }
}

void LutTrack(int, void*)
{
    //无需处理
    std::cout << "radius: "<< radius << std::endl;
}

void OnMouse(int event, int x, int y, int  flag, void* usrdata)
{
    if(event == cv::EVENT_LBUTTONDOWN)
    {
        start = cv::Point(x, y);
        std::cout << "start: "<< x << "," << y << std::endl;
        cv::Mat tmp = src_img.clone();
        cv::circle(tmp, start, 2, cv::Scalar(0, 255, 0));
        cv::circle(tmp, start, radius, cv::Scalar(0, 255, 0));
        leftdown = true;
        cv::imshow("liquefy", tmp);
    }

    if(flag == cv::EVENT_FLAG_LBUTTON)
    {
        cv::Point current = cv::Point(x,y);
        std::cout << "current: "<< x << "," << y << std::endl;
        cv::Mat tmp = src_img.clone();
        cv::circle(tmp, start, 2, cv::Scalar(0, 255, 0));
        cv::circle(tmp, start, radius, cv::Scalar(0, 255, 0));
        cv::line(tmp, start, current, cv::Scalar(0, 255, 0));
        cv::imshow("liquefy", tmp);
    }
    if(event == cv::EVENT_LBUTTONUP)
    {
        if(leftdown)
        {
            leftdown = false;
            cv::Point end = cv::Point(x,y);
            cv::Mat result;
            Reshape(src_img, start, end, radius, result);
            src_img = result;
            cv::imshow("liquefy", result);
        }
    }
}

int main(int argc, char** argv)
{
    src_img = cv::imread("/Volumes/HaoMingSSD/Learnning/effect/data/img/lut/1.jpg");
    if(src_img.empty()) {return -1;}
    cv::namedWindow("liquefy");
    cv::setMouseCallback("liquefy", OnMouse, nullptr);
    //cv::createTrackbar("radius", "liquefy", &radius, 100, LutTrack, nullptr);
    //LutTrack(0, nullptr);
    cv::imshow("liquefy", src_img);
    cv::waitKey(0);
    return 0;
}

执行效果

running


微信公众号