计算机视觉与人工智能在医美人脸皮肤诊断方面的应用

news2024/11/26 4:45:40

一、人脸皮肤诊断方法

近年来,随着计算机技术和人工智能的不断发展,中医领域开始逐渐探索利用这些先进技术来辅助面诊和诊断。在皮肤望诊方面,也出现了一些现代研究,尝试通过图像分析技术和人工智能算法来客观化地获取皮肤相关的色形参数,从而辅助中医面诊。
一些研究将计算机视觉和图像处理技术应用于皮肤望诊,旨在提取皮肤颜色、纹理、斑点等特征,然后通过模式识别算法来进行分析和诊断。这些研究通常需要大量的医学图像数据作为基础,使用机器学习和深度学习技术,如卷积神经网络(CNN),来训练模型以识别不同的皮肤状况和问题。
在中医面诊中,色形参数是评估患者身体状况的重要指标之一。这些参数通常包括皮肤颜色的变化、皮肤纹理的变化、斑点的出现等。通过对这些参数进行量化分析,可以帮助中医医生更客观地了解患者的身体状况,并作出相应的诊断和治疗建议。
然而,需要注意的是,虽然现代技术可以在一定程度上辅助中医面诊,但中医诊断是一门复杂的艺术和科学,它涉及到诸多因素,包括患者的整体状况、舌诊脉诊等。因此,尽管现代技术可以提供一些有用的信息,但在中医面诊中仍需要结合传统的临床经验和知识进行综合判断。

1.颜色

现代色度学研究认为,颜色的基本要素包括色调、饱和度和亮度。在医学领域,特别是在中医色诊客观化研究中,选择适当的颜色模型以准确地描述望诊五色特征是一个具有挑战性的问题。虽然多种颜色模型可供选择,如RGB、YCrCb、Lab、YUV、HLS、Ohta和Hue模型等,但目前还没有统一的标准确定哪种模型最适合中医色诊的研究,能够更准确地表达中医的望诊五色特征。
尽管各种颜色模型都具有优势,但确切地确定哪种模型在中医色诊客观化研究中更适用仍然是一个待解决的问题。RGB模型在应用中较为广泛,但它可能并不完全符合人类视觉感知的特性。
在这里插入图片描述

Lab模型是一个更符合人类视觉感知的颜色空间,可能在中医色诊的客观化研究中具有优势,但在实际应用中仍需深入研究。
在这里插入图片描述
另一方面,肤色在YCbCr空间可能具有一定的聚类特性和稳定性,这为中医望诊提供了一种可能性,但也需要进一步的实证研究来验证其在中医面色望诊中的可行性和准确性。
在这里插入图片描述

2.纹理和皱纹

人体皮肤表面的纹理和皱纹是生理和年龄变化的结果。皮肤纹理是由微小的皮丘和皮沟组成的,而随着时间的推移和外部环境的影响,皮肤纹理可能会发生变化,形成皱纹。这些变化可能是由于皮肤的自然衰老、过度暴晒太阳、重复的肌肉运动等因素引起的。在现代研究中,关于皮肤纹理和皱纹的特征提取已经逐渐完善。
在面部纹理特征的提取方面,研究者已经提出了多种方法。一些方法包括使用灰度信息,如基于Gabor滤波的方法,用于人脸和掌纹的识别。此外,灰度共生矩阵(GLCM)和局部二值模式(LBP)等统计学方法也被用来提取纹理特征。这些方法可以帮助描绘纹理的细节特征,对皮肤纹理进行精细评价。
特别是,采用灰度共生矩阵法提取皮肤纹理特征被认为是合理有效的方法。灰度共生矩阵通过分析像素之间的灰度关系来捕捉纹理信息,这种方法能够较好地刻画出皮肤纹理的细节特征。
在研究皱纹时,常见的皱纹部位包括额纹、眼角纹、川字纹、法令纹、嘴角纹等。研究者们采用不同的方法来评价和提取皱纹特征,包括主观评分、图像处理软件(如Photoshop)的评价以及一些特定设备的评价,如Visioscan。这些方法在不同的环境下,从不同的角度提取皱纹特征,可以帮助更好地理解和评估皮肤纹理的变化。
总体而言,现代研究通过多种方法来研究皮肤纹理和皱纹的特征提取,这些方法在皮肤疾病诊断、美容医学等领域具有潜在的应用前景。但要注意,纹理和皱纹的变化是复杂的生理过程,综合考虑多种特征和方法可能有助于更准确地评价皮肤的状态和健康状况。

3.毛孔

根据皮肤学研究,毛孔是皮肤表面的微小凹孔,其尺寸范围通常在50到500微米之间。对毛孔的评价方法包括等级评分法、标准照片评分法、皮肤镜检测、Visia皮肤检测仪以及算法识别等多种方式。
针对毛孔的研究,研究者们提出了不同的方法来评估和描述毛孔的特征。其中,一些研究基于数字化手段,提出了一些精细的参数来描述毛孔的情况。例如,研究者提出了“皮肤毛孔整体粗糙度”这一参数,通过数字化的方式更准确地评估面部毛孔的粗糙程度。另外,一些研究利用改进的算法来分割毛孔,从而获得毛孔的色调、形状和尺寸等特征。这些方法都旨在通过数字化分析来获得关于毛孔的更精确的信息。
此外,皮肤镜检测也被应用于毛孔的研究。皮肤镜可以识别计算毛孔的平均面积,并通过比较毛孔内部颜色与周围区域的颜色差异来表示毛孔的特征。这些方法都帮助了毛孔特征的客观化分析。
对于色斑的研究,研究者们也提出了多种方法。颜色直方图中的HSV空间模型被应用于描述色斑的颜色信息。同时,通过摄像机获取图像并对色斑进行量化分析,可以得出色斑的几何信息,如面积、周长、最大直径、几何形状和对称性等。
总的来说,图像分析法在皮肤学研究中具有许多优点,如客观性、操作简便、重复性好等。通过这些方法,可以定量地获取皮肤特征的信息,用于医学诊断、皮肤评价以及化妆品和皮肤病治疗前后的比较。不过,这些方法的应用还需要进一步的研究和验证,以确保其准确性和可靠性。

二、人脸皮肤区域获取

1.人脸皮肤分割

在做皮肤检测前提前条件是先把人脸分割出来,人脸皮肤分割是指将人脸图像中的皮肤部分从其他背景或物体中分离出来的过程。常用的方向有以下几种:

  • 基于颜色阈值的方法: 人脸皮肤通常具有特定的颜色范围,比如在RGB颜色空间中,皮肤可能落在一定的红色、绿色和蓝色通道值范围内。通过设置适当的颜色阈值,可以将皮肤像素从其他像素中分离出来。然而,这种方法容易受到光照变化和肤色多样性的影响,导致分割效果不稳定。
  • 基于机器学习的方法: 使用机器学习算法,如支持向量机(SVM)、随机森林、卷积神经网络(CNN)等,可以训练一个分类器,将皮肤像素与非皮肤像素分开。这需要大量的标注数据进行训练,但结果通常更准确。
  • 基于深度学习的方法: 使用深度学习技术,特别是卷积神经网络(CNN),可以更精确地进行皮肤分割。可以设计一个CNN架构,输入人脸图像,输出一个相应大小的二值分割掩码,其中皮肤区域被标记为1,非皮肤区域被标记为0。
  • 基于图像分割算法的方法: 图像分割算法,如基于区域的分割(如区域增长、分水岭算法)、基于边缘的分割(如Canny边缘检测)等,也可以应用于人脸皮肤分割。这些方法通过分析像素之间的相似性或差异性来确定皮肤区域。

这里使用的基于深度学习的face-parsing 。训练出模型之后,转成onnx,然后使用onnxruntime进行推理:

#include "face_parsing_bisenet.h"
#include "../core/ort_utils.h"

using ortcv::FaceParsingBiSeNet;

Ort::Value FaceParsingBiSeNet::transform(const cv::Mat &mat)
{
  cv::Mat canvas;
  cv::resize(mat, canvas, cv::Size(input_node_dims.at(3), input_node_dims.at(2)));
  cv::cvtColor(canvas, canvas, cv::COLOR_BGR2RGB);
  // e.g (1,3,512,512)
  ortcv::utils::transform::normalize_inplace(canvas, mean_vals, scale_vals);
  return ortcv::utils::transform::create_tensor(
      canvas, input_node_dims, memory_info_handler,
      input_values_handler, ortcv::utils::transform::CHW); // deepcopy inside
}

void FaceParsingBiSeNet::detect(const cv::Mat &mat, types::FaceParsingContent &content,
    std::vector<cv::Mat>& cv_features,bool minimum_post_process)
{
  if (mat.empty()) return;

  // 1. make input tensor
  Ort::Value input_tensor = this->transform(mat);
  // 2. inference
  auto output_tensors = ort_session->Run(
      Ort::RunOptions{nullptr}, input_node_names.data(),
      &input_tensor, 1, output_node_names.data(), num_outputs
  );
  // 3. generate mask
  this->generate_mask(output_tensors, mat, content,cv_features, minimum_post_process);
}

static inline uchar argmax(float *mutable_ptr, const unsigned int &step)
{
  std::vector<float> logits(19, 0.f);
  for (unsigned int i = 0; i < 19; ++i)
    logits[i] = *(mutable_ptr + i * step);
  uchar label = 0;
  float max_logit = logits[0];
  for (unsigned int i = 1; i < 19; ++i)
  {
    if (logits[i] > max_logit)
    {
      max_logit = logits[i];
      label = (uchar) i;
    }
  }
  return label;
}


static const uchar part_colors[20][3] = {
    {0, 0,   0},
    {0, 0,  255},//脸
    {255, 170, 0},//右眉毛
    {255, 0,   85},//左眉毛
    {0, 0,   0},
    {0,   0, 0},
    {0,  0, 0},
    {0, 0, 0},
    {0,   0, 0},//耳朵
    {0,   0, 0},
    {0,   170,   255},//鼻子
    {0,  0,   0},
    {0, 125,   255},//上嘴唇
    {0,   255,  0},//下嘴唇
    {0,   0, 0},
    {0, 0, 0},
    {0, 0, 0},
    {0, 0, 0},//头发
    {0, 0,   0},
    {0, 0,  0}
};



void FaceParsingBiSeNet::generate_mask(std::vector<Ort::Value> &output_tensors, const cv::Mat &mat,
                                       types::FaceParsingContent &content, std::vector<cv::Mat>& cv_features,
                                       bool minimum_post_process)
{
  cv_features.clear();
  Ort::Value &output = output_tensors.at(0); // (1,19,h,w)
  const unsigned int h = mat.rows;
  const unsigned int w = mat.cols;

  auto output_dims = output.GetTypeInfo().GetTensorTypeAndShapeInfo().GetShape();
  const unsigned int out_h = output_dims.at(2);
  const unsigned int out_w = output_dims.at(3);
  const unsigned int channel_step = out_h * out_w;

  float *output_ptr = output.GetTensorMutableData<float>();
  std::vector<uchar> elements(channel_step, 0); // allocate
  for (unsigned int i = 0; i < channel_step; ++i)
  {
      elements[i] = argmax(output_ptr + i, channel_step);
  }

  cv::Mat label(out_h, out_w, CV_8UC1, elements.data());

  cv::Mat cv_EB(out_h, out_w, CV_8UC1, cv::Scalar(0));
  cv::Mat cv_face = cv_EB.clone();
  cv::Mat cv_nose = cv_EB.clone();
  cv::Mat cv_ulip = cv_EB.clone();
  cv::Mat cv_dlip = cv_EB.clone();
  cv::Mat cv_all = cv_EB.clone();

  if (!minimum_post_process)
  {
    const uchar *label_ptr = label.data;
    cv::Mat color_mat(out_h, out_w, CV_8UC3, cv::Scalar(0, 0, 0));
    
    for (unsigned int i = 0; i < cv_EB.rows; ++i)
    {
      cv::Vec3b* p = color_mat.ptr<cv::Vec3b>(i);
      uchar* EP = cv_EB.ptr<uchar>(i);
      uchar* face = cv_face.ptr<uchar>(i);
      uchar* nose = cv_nose.ptr<uchar>(i);
      uchar* ulip = cv_ulip.ptr<uchar>(i);
      uchar* dlip = cv_dlip.ptr<uchar>(i);
      uchar* all = cv_all.ptr<uchar>(i);

      for (unsigned int j = 0; j < cv_EB.cols; ++j)
      {
        if (label_ptr[i * out_w + j] == 0) continue;
        p[j][0] = part_colors[label_ptr[i * out_w + j]][0];
        p[j][1] = part_colors[label_ptr[i * out_w + j]][1];
        p[j][2] = part_colors[label_ptr[i * out_w + j]][2];
        switch (label_ptr[i * out_w + j])
        {
        case 1://脸
            all[j] = 255;
            face[j] = 255;
            break;
        case 2://眉毛
            all[j] = 255;
            EP[j] = 255;
            break;
        case 3:
            all[j] = 255;
            EP[j] = 255;
            break;
        case 10://鼻子
            all[j] = 255;
            nose[j] = 255;
            break;
        case 12:
            all[j] = 255;
            ulip[j] = 255;
            break;
        case 13:
            all[j] = 255;
            dlip[j] = 255;
            break;
        default:
            break;
        }
      }
    }

   /* cv::Mat cv_ulipm, cv_dlipm;
    
    morph(cv_dlip, cv_ulipm);*/

    cv_features.push_back(cv_face);
    cv_features.push_back(cv_nose);
    cv_features.push_back(cv_ulip);
    cv_features.push_back(cv_dlip);
    cv_features.push_back(cv_EB);
    cv_features.push_back(cv_all);

    for (int i = 0; i < cv_features.size(); i++)
    {
        cv::resize(cv_features[i], cv_features[i], cv::Size(w, h));
    }

    //cv::resize(color_mat, color_mat, mat.size());
    //cv::addWeighted(mat, 0.8, color_mat, 0.2, 3, content.merge);
    //cv::namedWindow("src", 0);
    //cv::imshow("src", mat);
    //cv::namedWindow("seg", 0);
    //cv::imshow("seg", content.merge);
  }
  // already allocated a new continuous memory after resize.
  if (out_h != h || out_w != w) cv::resize(label, label, cv::Size(w, h),cv::INTER_LANCZOS4);
  // need clone to allocate a new continuous memory if not performed resize.
  // The memory elements point to will release after return.
  else label = label.clone();
  content.label = label; // auto handle the memory inside ocv with smart ref.
  content.flag = true;
}

在这里插入图片描述

2.区域获取

分割之后就要获取要诊断的皮肤区域,一般要获取额头、左颊、右颊、下巴、鼻头、嘴唇这六个区域的皮肤颜色值,然后对这些区域的皮肤的颜色值进行分析。
在这里插入图片描述
在面部皮肤分析方面,黄指数(Y)、白指数(W)、青指数(C)、红指数(R)和黑指数(B)以及面色指数用于量化面部不同颜色成分的强度。光泽指数则描述了皮肤表面的光泽程度,从有光泽、少光泽到无光泽。还要提到了多种颜色空间(RGB、Lab、YCbCr值),这些颜色空间能够更精确地表示不同颜色的特征。
对于唇部,想要通过H、S、I值以及Lab值来描述颜色特征。这些参数可以帮助描绘唇部色调、饱和度、亮度和在颜色空间中的位置,从而实现更精确的分析和处理。
另外,可能还要够分割上下嘴唇以及检测唇纹,这需要使用图像分割和边缘检测等技术来实现。通过对唇部不同区域进行更详细的分析,可以获得更精准的颜色特征。
上面的代码已经分割出五官的大体位置,要根据五官的位置获取到算法需要用到的6个区域。

#include "features_seg.h"

static bool sortArea(const std::vector<cv::Point>& v1, const std::vector<cv::Point>& v2)
{
    double v1Area = fabs(contourArea(cv::Mat(v1)));
    double v2Area = fabs(contourArea(cv::Mat(v2)));
    return v1Area > v2Area;
}

cv::Rect findContoursArea(cv::Mat& cv_src)
{
    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarcy;

    cv::Mat  cv_canny_e, cv_canny_d;
    cv::Mat element_d = getStructuringElement(cv::MORPH_RECT, cv::Size(7, 7));
    cv::dilate(cv_src, cv_canny_d, element_d);

    cv::Mat element_e = getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
    cv::erode(cv_canny_d, cv_canny_e, element_e);
    cv::findContours(cv_canny_e, contours, hierarcy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);

    std::sort(contours.begin(), contours.end(), sortArea);

    return boundingRect(cv::Mat(contours[0]));
}

cv::Point getCenterPoint(cv::Rect rect)
{
    cv::Point cpt;
    cpt.x = rect.x + cvRound(rect.width / 2.0);
    cpt.y = rect.y + cvRound(rect.height / 2.0);
    return cpt;
}

void seg_featurs(std::vector<cv::Mat>& cv_featurs)
{
    cv::Mat cv_nose;
    cv::resize(cv_featurs[2], cv_nose, cv::Size(512, 512));
    cv::Rect nose_rect = findContoursArea(cv_nose);

    cv::Point c_p = getCenterPoint(nose_rect);

    std::vector<Line> lines;

    lines.push_back(Line{ cv::Point(c_p.x, 0),cv::Point(c_p.x,cv_nose.rows) });
    
    cv::Mat cv_eb;
    cv::resize(cv_featurs[4], cv_eb, cv::Size(512, 512));
    cv::Mat cv_leb(cv_eb.size(), CV_8UC1, cv::Scalar(0));
    cv::Mat cv_reb(cv_eb.size(), CV_8UC1, cv::Scalar(0));

    cv::Rect l_rect = cv::Rect(cv::Point(0, 0), lines[0]._p2);
    cv::Rect r_rect = cv::Rect(lines[0]._p1, 
        cv::Point(cv_eb.cols, cv_eb.rows));

    cv::Mat cv_lc = cv_eb(l_rect);
    cv::Mat cv_rc = cv_eb(r_rect);

    cv::Mat cv_roi1 = cv_leb(l_rect);
    cv_lc.copyTo(cv_roi1);

    cv::Mat cv_roi2 = cv_reb(r_rect);
    cv_rc.copyTo(cv_roi2);

    cv::resize(cv_leb, cv_leb, cv_featurs[4].size());
    cv::resize(cv_reb, cv_reb, cv_featurs[4].size());
    cv_featurs.push_back(cv_leb);
    cv_featurs.push_back(cv_reb);
}

int drawpoly(cv::Mat& cv_src, cv::Mat& cv_dst, cv::Size size)
{
    cv::Mat cv_dilate;
    cv::Mat element_d = getStructuringElement(cv::MORPH_RECT, 
        size, cv::Point(-1, -1));
    cv::dilate(cv_src, cv_dilate, element_d);
    cv::Mat element_e = getStructuringElement(cv::MORPH_RECT, 
        size, cv::Point(-1, -1));
    cv::erode(cv_dilate, cv_dilate, element_e);

    std::vector<std::vector<cv::Point> > contours;
    std::vector<std::vector<cv::Point> > f_contours;
    std::vector<cv::Point> approx2;
    //注意第5个参数为CV_RETR_EXTERNAL,只检索外框
    findContours(cv_dilate, f_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); //找轮廓
    //求出面积最大的轮廓
    int max_area = 0;
    int index = 0;
    for (int i = 0; i < f_contours.size(); i++)
    {
        double tmparea = fabs(contourArea(f_contours[i]));
        if (tmparea > max_area)
        {
            index = i;
            max_area = tmparea;
        }
    }

    contours.push_back(f_contours[index]);

    std::vector<cv::Point> tmp = contours[0];

    cv_dst = cv::Mat(cv_src.size(), CV_8UC1, cv::Scalar(0));

    drawContours(cv_dst, contours, 0, cv::Scalar(255), 4, cv::LINE_AA); //注意线的厚度,不要选择太细的

    return 0;
}


//两条线的交点
static cv::Point2f computeIntersect(Line& l1, Line& l2)
{
    int x1 = l1._p1.x;
    int x2 = l1._p2.x;
    int y1 = l1._p1.y;
    int y2 = l1._p2.y;
    int x3 = l2._p1.x, x4 = l2._p2.x, y3 = l2._p1.y, y4 = l2._p2.y;
    if (float d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
    {
        cv::Point2f pt;
        pt.x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d;
        pt.y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d;
        return pt;
    }
    return cv::Point2f(-1, -1);
}

cv::Rect rectScale(cv::Rect& rect, float x_scale, float y_scale, int i)
{
    cv::Rect cv_rect;
    if (i >= 0)
    {
        cv_rect.x = rect.x * x_scale;
        cv_rect.y = rect.y * y_scale;
        cv_rect.width = rect.width * x_scale;
        cv_rect.height = rect.height * y_scale;
    }
    else
    {
        cv_rect.x = rect.x / x_scale;
        cv_rect.y = rect.y / y_scale;
        cv_rect.width = rect.width / x_scale;
        cv_rect.height = rect.height / y_scale;
    }

    return cv_rect;
}

void face_subarea(std::vector<cv::Mat>& cv_parts,std::vector<cv::Rect> &parts)
{
    float x_s = cv_parts[0].cols / 512.00;
    float y_s = cv_parts[0].rows / 512.00;
    std::vector<cv::Mat> cv_featurs(cv_parts.size());
    for (int i = 0; i < cv_parts.size(); i++)
    {
        cv::resize(cv_parts[i], cv_featurs[i], cv::Size(512, 512));
    }

    cv::Mat cv_eb;
    drawpoly(cv_featurs[4], cv_eb,cv::Size(115,3));

    cv::Rect eb_rect = findContoursArea(cv_eb);

    cv::Rect face_rect = findContoursArea(cv_featurs[0]);
    cv::Rect nose_rect = findContoursArea(cv_featurs[1]);
    cv::Rect leb_rect = findContoursArea(cv_featurs[5]);
    cv::Rect reb_rect = findContoursArea(cv_featurs[6]);
    cv::Rect dlip_rect = findContoursArea(cv_featurs[3]);
    cv::Rect ulip_rect = findContoursArea(cv_featurs[2]);

    int b = leb_rect.tl().y - reb_rect.tl().y;
    Line L4(cv::Point(0,0),cv::Point(0,0));
    if (b <= 0)
    {
        L4 = Line(cv::Point(0, leb_rect.tl().y + abs((b / 2))),
            cv::Point(cv_featurs[0].cols, leb_rect.tl().y + abs((b / 2))));
    }
    else
    {
        L4 = Line(cv::Point(0, leb_rect.tl().y - (b / 2)),
            cv::Point(cv_featurs[0].cols, leb_rect.tl().y - (b / 2)));
    }

    cv::Point c_leb = getCenterPoint(leb_rect);
    cv::Point c_reb = getCenterPoint(reb_rect);
    
    //额头
    Line L1(cv::Point(c_leb.x, 0),cv::Point(c_leb.x, cv_featurs[0].rows));
    Line L0(cv::Point(0, face_rect.tl().y), 
        cv::Point(cv_featurs[0].cols, face_rect.tl().y));
    cv::Point p0 =  computeIntersect(L0,L1);

    Line L3(cv::Point(c_reb.x, 0),cv::Point(c_reb.x, cv_featurs[0].rows));
   
    cv::Point p2 = computeIntersect(L3, L4);

    int s = cv::Rect(p0, p2).width / 7;
    Line L2(cv::Point(0, face_rect.tl().y + s), 
        cv::Point(cv_featurs[0].cols, face_rect.tl().y + s));
    cv::Point p1 = computeIntersect(L2, L1);

    int p = nose_rect.height / 2;
    int x = eb_rect.width / 13;

    //左脸
    Line L5(cv::Point(0, nose_rect.tl().y + p),
        cv::Point(cv_featurs[0].cols, nose_rect.tl().y + p));
    Line L6(cv::Point(eb_rect.tl().x + x, 0),
        cv::Point(eb_rect.tl().x + x, cv_featurs[0].rows));

    cv::Point p3 = computeIntersect(L5, L6);


   Line L7(cv::Point(0, nose_rect.br().y),
       cv::Point(cv_featurs[0].cols, nose_rect.br().y));
   Line L8(cv::Point(nose_rect.tl().x, 0),
       cv::Point(nose_rect.tl().x, cv_featurs[0].rows));

   cv::Point p4 = computeIntersect(L7, L8);

   Line L9(cv::Point(0, nose_rect.tl().y + p),
       cv::Point(cv_featurs[0].cols, nose_rect.tl().y + p));
   Line L10(cv::Point(nose_rect.br().x, 0),
       cv::Point(nose_rect.br().x, cv_featurs[0].rows));
   Line L11(cv::Point(0, nose_rect.br().y),
       cv::Point(cv_featurs[0].cols, nose_rect.br().y));
   Line L12(cv::Point(eb_rect.br().x - x, 0),
       cv::Point(eb_rect.br().x - x, cv_featurs[0].rows));

   cv::Point p5 = computeIntersect(L9, L10);
   cv::Point p6 = computeIntersect(L11, L12);

   Line L13( cv::Point(nose_rect.tl().x, 0),
       cv::Point(nose_rect.tl().x, cv_featurs[0].rows));
   Line L14(cv::Point(0, dlip_rect.br().y),
       cv::Point(cv_featurs[0].cols, dlip_rect.br().y));

   cv::Point p7 = computeIntersect(L13, L14);

   Line L15(cv::Point(nose_rect.br().x, 0),
       cv::Point(nose_rect.br().x, cv_featurs[0].rows));
   Line L16(cv::Point(0, face_rect.br().y),
       cv::Point(cv_featurs[0].cols, face_rect.br().y));

   cv::Point p8 = computeIntersect(L15, L16);

   int n_h = nose_rect.height / 6;
   int n_w = nose_rect.width / 4;

   Line L17(cv::Point(0, nose_rect.tl().y + (n_h * 3)),
       cv::Point(cv_featurs[0].cols, nose_rect.tl().y + (n_h * 3)));
   Line L18(cv::Point(nose_rect.tl().x + n_w, 0),
       cv::Point(nose_rect.tl().x + n_w, cv_featurs[0].rows));
   cv::Point p9 = computeIntersect(L17, L18);

   Line L19(cv::Point(0, nose_rect.br().y - n_h),
       cv::Point(cv_featurs[0].cols, nose_rect.br().y - n_h));
   Line L20(cv::Point(nose_rect.br().x - n_w, 0),
       cv::Point(nose_rect.br().x - n_w, cv_featurs[0].rows));
   cv::Point p10 = computeIntersect(L19, L20);

   parts.push_back(cv::Rect(p1, p2));
   parts.push_back(cv::Rect(p3, p4));
   parts.push_back(cv::Rect(p5, p6));
   parts.push_back(cv::Rect(p7, p8)); 
   parts.push_back(cv::Rect(p9, p10));
   parts.push_back(ulip_rect);
   parts.push_back(dlip_rect);

   for (int i = 0; i < parts.size(); i++)
   {
       parts[i] = rectScale(parts[i], x_s, y_s, 1);
   }
}


void morph(cv::Mat& cv_src, cv::Mat& cv_dst)
{
    cv::Mat cv_dilate;
    cv::Mat element_d = getStructuringElement(cv::MORPH_RECT,
        cv::Size(211, 211), cv::Point(-1, -1));
    cv::dilate(cv_src, cv_dilate, element_d);
    cv::Mat element_e = getStructuringElement(cv::MORPH_RECT,
        cv::Size(211, 211), cv::Point(-1, -1));
    cv::erode(cv_dilate, cv_dst, element_e);
}

void lip_seg(cv::Mat& cv_src, std::vector<cv::Mat>& cv_featurs,
    std::vector<cv::Mat> &cv_parts,std::vector<cv::Rect> &parts)
{
    cv::Mat cv_dlip, cv_ulip, cv_dlipc, cv_ulipc;

    cv_src.copyTo(cv_dlip, cv_featurs[3]);
    cv_src.copyTo(cv_ulip, cv_featurs[2]);

    cv_parts.push_back(cv_src(parts[0]));
    cv_parts.push_back(cv_src(parts[1]));
    cv_parts.push_back(cv_src(parts[2]));
    cv_parts.push_back(cv_src(parts[3]));
    cv_parts.push_back(cv_src(parts[4]));
    cv_parts.push_back(cv_dlip(parts[6]));
    cv_parts.push_back(cv_ulip(parts[5]));    
}


void draw_line(std::vector<cv::Mat> cv_featurs,cv::Mat &cv_face)
{
    cv::Mat cv_eb;
    drawpoly(cv_featurs[4], cv_eb,cv::Size(115,3));

    cv::Rect eb_rect = findContoursArea(cv_eb);
    cv::Rect face_rect = findContoursArea(cv_featurs[0]);
    cv::Rect nose_rect = findContoursArea(cv_featurs[1]);
    cv::Rect leb_rect = findContoursArea(cv_featurs[5]);
    cv::Rect reb_rect = findContoursArea(cv_featurs[6]);
    cv::Rect dlip_rect = findContoursArea(cv_featurs[3]);

    cv::Point c_leb = getCenterPoint(leb_rect);
    cv::Point c_reb = getCenterPoint(reb_rect);

    cv::line(cv_face, cv::Point(c_leb.x, 0),
       cv::Point(c_leb.x, cv_face.rows), cv::Scalar(255));
   cv::line(cv_face, cv::Point(0, face_rect.tl().y),
       cv::Point(512, face_rect.tl().y), cv::Scalar(255));
   cv::line(cv_face, cv::Point(c_reb.x, 0), 
       cv::Point(c_reb.x, cv_face.rows), cv::Scalar(255));

  cv::line(cv_face, cv::Point(0, eb_rect.tl().y),cv::Point(512, eb_rect.tl().y), cv::Scalar(255));

  int p = nose_rect.height / 3;
  int x = eb_rect.width / 15;

  cv::line(cv_face, cv::Point(eb_rect.tl().x + x, 0),
      cv::Point(eb_rect.tl().x + x, 512), cv::Scalar(255));

  //眼睛下的横线
   cv::line(cv_face, cv::Point(0,nose_rect.tl().y + p),
      cv::Point(512, nose_rect.tl().y + p),cv::Scalar(255));

   //鼻子旁边右竖线
   cv::line(cv_face, cv::Point(nose_rect.br().x, 0),
       cv::Point(nose_rect.br().x, 512), cv::Scalar(255));

   cv::line(cv_face, cv::Point(0, nose_rect.br().y),
             cv::Point(512,nose_rect.br().y), cv::Scalar(255));
   cv::line(cv_face, cv::Point(nose_rect.tl().x,0),
                   cv::Point(nose_rect.tl().x,512), cv::Scalar(255));
   //横线
   cv::line(cv_face, cv::Point(0, face_rect.br().y),
      cv::Point(512, face_rect.br().y), cv::Scalar(255));

   cv::line(cv_face, cv::Point(0, dlip_rect.br().y),
          cv::Point(512, dlip_rect.br().y), cv::Scalar(255));

   cv::line(cv_face, cv::Point(eb_rect.br().x - x, 0),
         cv::Point(eb_rect.br().x - x, 512), cv::Scalar(255));

   int n_h = nose_rect.height / 6;
   int n_w = nose_rect.width / 4;

   cv::line(cv_face, cv::Point(0, nose_rect.tl().y + (n_h * 3)),
       cv::Point(512, nose_rect.tl().y + (n_h * 3)), cv::Scalar(255));
   cv::line(cv_face, cv::Point(0, nose_rect.br().y - n_h),
       cv::Point(512, nose_rect.br().y -n_h), cv::Scalar(255));
   cv::line(cv_face, cv::Point(nose_rect.tl().x + n_w, 0),
       cv::Point(nose_rect.tl().x + n_w, 512), cv::Scalar(255));
   cv::line(cv_face, cv::Point(nose_rect.br().x - n_w, 0),
       cv::Point(nose_rect.br().x - n_w, 512), cv::Scalar(255));

}

在这里插入图片描述

皮肤分析

1.纹理

1.1 面部纹理和皱纹分析:

面部区域的皱纹,如额纹、川字纹、眼下纹、法令纹、嘴角纹等。这些的参数包括数量、长度、深浅,以及角二阶矩、对比度、相关、熵等来描述这些皱纹的特征。这些参数能够帮助定量地衡量皱纹的不同方面,从而更准确地分析和描述面部皮肤的老化和纹理特征。
还可以使用灰度共生矩阵来计算纹理特征,如角二阶矩、对比度、相关、熵等。这些参数可以帮助捕捉图像中不同区域的纹理差异,进而区分纹理和皱纹。需要注意的是,纹理和皱纹确实在某种程度上是相关的,但纹理主要关注整体表面特征,而皱纹更侧重于特定区域的褶皱。
在进行定量检测时。要提取面部的纹理,可参考我之前的博客:基于语义分割实现人脸图像的皱纹检测定位与分割

1.2 唇纹分析:

对于唇纹,定量的参数包括数量、长度、深浅,以及角二阶矩、对比度、相关、熵等,用于描述唇纹的特征。这些参数可以帮助准确地评估唇部区域的纹理和皱纹。

2.毛孔、黑头

#include "PoreDetect.h"

PoreDetect::PoreDetect()
{

}

PoreDetect::~PoreDetect()
{

}
PoreDetect::PoreDetect(bool use_gpu)
{
	bool has_GPU = false;
#if NCNN_VULKAN
	ncnn::create_gpu_instance();
	has_GPU = ncnn::get_gpu_count() > 0;
#endif
	bool to_use_GPU = has_GPU && use_gpu;
	net.opt.use_vulkan_compute = to_use_GPU;
#if FT_MEMFP16
	net.opt.use_fp16_arithmetic = true;
	ncnn_net.load_param(__TB210218FP16_param_bin);
	ncnn_net.load_model(__TB210218FP16_bin);
#else
	net.load_param("porefp.param");
	net.load_model("porefp.bin");
#endif
}

void PoreDetect::detect(const cv::Mat& cv_src, cv::Mat& cv_dst, int in_size, int num_threads)
{
    cv::Mat cv_gray;
    cv::cvtColor(cv_src, cv_gray, cv::COLOR_BGR2GRAY);
    ncnn::Mat nc_in = ncnn::Mat::from_pixels_resize(cv_gray.data, 
        ncnn::Mat::PIXEL_GRAY, cv_gray.cols, cv_gray.rows, in_size, in_size);

    const float norm_vals[3] = { 1 / 255.f, 1 / 255.f, 1 / 255.f };
    nc_in.substract_mean_normalize(0, norm_vals);
    ncnn::Extractor ex = net.create_extractor();
    ex.set_num_threads(num_threads);
    ncnn::Mat out;

#if FT_MEMFP16
    using namespace __TB210218FP16_param_id;
    ex.input(BLOB_input, nc_in);
    ex.extract(BLOB_output, out);
#else
    ex.input("input", nc_in);
    ex.extract("output", out);
#endif

    cv::Mat cv_seg = cv::Mat::zeros(cv::Size(out.w, out.h), CV_8UC1);
    for (int i = 0; i < out.h; ++i)
    {
        for (int j = 0; j < out.w; ++j)
        {
            const float* bg = out.channel(1);
            const float* fg = out.channel(0);
            if (bg[i * out.w + j] < fg[i * out.w + j])
            {
                cv_seg.data[i * out.w + j] = 255;
            }
        }
    }

    cv::resize(cv_seg, cv_dst, cv_src.size());
}

void PoreDetect::draw(const cv::Mat& cv_src,cv::Mat &cv_unet, cv::Mat& cv_draw)
{
    cv::Mat cv_dst = ~cv_unet.clone();
    cv::threshold(cv_dst, cv_dst, 0, 255, cv::THRESH_OTSU);

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarcy;
    findContours(cv_dst, contours, hierarcy, 
        cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); //查找轮廓

    cv_draw = cv_src.clone();

    std::vector<cv::Rect> boundRect(contours.size()); //定义外接矩形集合
    int x0 = 0, y0 = 0, w0 = 0, h0 = 0, num = 0;
    for (int i = 0; i < contours.size(); i++)
    {
        boundRect[i] = cv::boundingRect((cv::Mat)contours[i]); //查找每个轮廓的外接矩形
        //drawContours(cv_src, contours, i, cv::Scalar(0, 0, 255), 2, 8);  //绘制轮廓
        x0 = boundRect[i].x;
        y0 = boundRect[i].y;
        w0 = boundRect[i].width;
        h0 = boundRect[i].height;
        if (w0 > 1 && h0 > 1 && w0 < 10 && h0 < 10)//筛选
        {
            cv::rectangle(cv_draw, cv::Point(x0, y0), 
                cv::Point(x0 + w0, y0 + h0), cv::Scalar(0, 255, 0), 2, 8); //绘制第i个外接矩形
        }
        else if (w0 > 10 && h0 > 10)
        {
            cv::rectangle(cv_draw, cv::Point(x0, y0), 
                cv::Point(x0 + w0, y0 + h0), cv::Scalar(255, 255, 0), 2, 8); //绘制第i个外接矩形
        }
    }
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/938924.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据库相关知识2

数据库知识2 关系完整性 数据完整性 指的是数据库中的数据的准确性和可靠性 实体完整性约束&#xff1a; 目的&#xff1a; 在表中至少有一个唯一的 标识&#xff0c;主属性字段中&#xff0c;不为空&#xff0c;不重复 主键约束&#xff1a;唯一 不重复 不为空 primary k…

Java:HashMap、LinkedHashMap、TreeMap集合的底层原理和集合的嵌套

HashMap的底层原理 LinkedHashMap的底层原理 TreeMap集合的底层原理 集合的嵌套

汽车制造行业,配电柜如何实施监控?

工业领域的生产过程依赖于高效、稳定的电力供应&#xff0c;而配电柜作为电力分配和控制的关键组件&#xff0c;其监控显得尤为重要。 配电柜监控通过实时监测、数据收集和远程控制&#xff0c;为工业企业提供了一种有效管理电能的手段&#xff0c;从而确保生产的连续性、安全性…

C++ 改善程序的具体做法 学习笔记

1、尽量用const enum inline替换#define 因为#define是做预处理操作&#xff0c;编译器从未看见该常量&#xff0c;编译器刚开始编译&#xff0c;它就被预处理器移走了&#xff0c;而#define的本质就是做替换&#xff0c;它可能从来未进入记号表 解决方法是用常量替换宏 语言…

linux 设置与命令基础(二)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、系统基本操作 二、命令类型 三、命令语法 四、命令补齐 五、命令帮助 六、系统基本操作命令 总结 前言 这是本人学习Linux的第二天&#xff0c;今天主…

Unity插件---Dotween

1.什么是DOTween DoTween 是由 Demigiant 开发的&#xff0c;被广泛应用于 Unity 游戏开发中。它是一个流行的动画插件&#xff0c;被许多开发者用于创建流畅、高效的动画效果&#xff0c;提升游戏体验。 2.DOTween的初始配置 ①set up 首先找到DOTween Unity Panel 的面板 点…

机器学习资料汇总

一 卷积 原来卷积是这么计算的 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/268179286 最核心的部分是要知道&#xff0c;通道数和输出特征图无关&#xff0c;是卷积核的个数&#xff0c;决定了输出特征图的个数

【进程】状态模型(三态、五态、七态)

进程的三态模型 按进程在执行过程中的不同情况至少要定义三种状态&#xff1a; 运行&#xff08;running&#xff09;态&#xff1a;进程占有处理器正在运行的状态。进程已获得CPU&#xff0c;其程序正在执行。在单处理机系统中&#xff0c;只有一个进程处于执行状态&#xf…

eclipse工作区打开了很多文件,如何快速找到某个文件

例如&#xff0c;我在eclipse工作区打开了几十个文件&#xff0c;其中有的文件还不是eclipse某个工程中的文件&#xff0c;而是从外部直接拖到工作区打开的&#xff1a; 如果要找到某个文件&#xff0c;可以用鼠标单击打开文件的数字的地方&#xff0c;如下面红框的地方&…

设计模式(一)

1、适配器模式 &#xff08;1&#xff09;概述 适配器中有一个适配器包装类Adapter&#xff0c;其包装的对象为适配者Adaptee&#xff0c;适配器作用就是将客户端请求转化为调用适配者中的接口&#xff1b;当调用适配器中的方法时&#xff0c;适配器内部会调用适配者类的方法…

分享2个u盘还原方法,轻松恢复u盘数据!

U盘&#xff0c;作为便捷的存储设备&#xff0c;经常用于传输和存储重要文件。然而&#xff0c;由于误操作、病毒感染或其他原因&#xff0c;U盘上的数据可能会丢失。在这种情况下&#xff0c;进行u盘还原成为救回丢失数据的关键一步。 本文将解释U盘还原的意义&#xff0c;并…

利用数据透视表统计出现的次数

一、统计出现的次数 注意&#xff1a;字段是直接使用左键拖动到求和栏中即可。 二、表内数据求和 三、按时间统计&#xff08;参考该文&#xff09;

RT_Thread内核机制学习(二)

对于RTT来说&#xff0c;每个线程创建时都自带一个定时器 rt_err_t rt_thread_sleep(rt_tick_t tick) {register rt_base_t temp;struct rt_thread *thread;/* set to current thread */thread rt_thread_self();RT_ASSERT(thread ! RT_NULL);RT_ASSERT(rt_object_get_type(…

Protobuf高性能接口ZeroCopyStream

Protobuf ZeroCopyStream 1.ZeroCopyStream protobuf在io接口上有一个叫做ZeroCopyStream&#xff0c;对于IO的接口设计&#xff0c;pb提供了相关序列化与反序列化接口&#xff0c;如&#xff1a; // Read a protocol buffer from the given zero-copy input stream. If // su…

Bigemap在公路勘察设计行业中如何应用呢?

选择Bigemap的原因&#xff1a; Google 已经停止使用&#xff0c;天地图清晰度偏低&#xff0c;bigmap可以提供多种不同源的影像、矢量数据。奥维&#xff0c;有接触&#xff0c;做了比选&#xff0c;后来从技术本身去考虑这个问题&#xff0c;影像、矢量数据下载方便&#xf…

命令行环境题目

在后台产生一个ID 不会输出任何东西

Golang Gorm 一对多的添加

一对多的添加有两种情况&#xff1a; 一种是添加用户的时候同时创建文章其次是创建文章关联已经存在的用户。 package mainimport ("gorm.io/driver/mysql""gorm.io/gorm" )// User 用户表 一个用户拥有多篇文章 type User struct {ID int64Name …

强强联袂,产教融合丨知了汇智与成都世纪超体达成战略合作

为了共培数字营销人才&#xff0c;实现产业、教育双向赋能&#xff0c;为地方乃至全国电商发展注入源源不断的行业性人才&#xff0c;8月24日&#xff0c;成都知了汇智科技有限公司&#xff08;以下简称“知了汇智”&#xff09;与成都世纪超体科技有限公司&#xff08;以下简称…

科技资讯|苹果发布新专利:可在车内定位苹果的智能设备

根据美国商标和专利局近期公示的清单&#xff0c;苹果公司获得了一项名为《车内定位移动设备的系统和方式》专利&#xff0c;概述了在车内狭窄空间内如何定位 iPhone 等移动设备。 Find My 服务现阶段没有使用 UWB 来追踪 iPhone 或者 iPad&#xff0c;而是依赖 GPS 等相关辅…

iTubeGo for Mac视频下载器

iTubeGo YouTube Downloader是一款功能强大的YouTube视频下载工具&#xff0c; 它具有以下特色&#xff1a; 多种格式支持&#xff1a;iTubeGo YouTube Downloader可以将YouTube视频下载为多种常见的视频和音频格式&#xff0c;包括MP4、MP3、AVI、FLV、MOV、WMV等&#xff0c…