计算机视觉与人工智能在医美人脸皮肤诊断方面的应用
一、人脸皮肤诊断方法
近年来,随着计算机技术和人工智能的不断发展,中医领域开始逐渐探索利用这些先进技术来辅助面诊和诊断。在皮肤望诊方面,也出现了一些现代研究,尝试通过图像分析技术和人工智能算法来客观化地获取皮肤相关的色形参数,从而辅助中医面诊。
一些研究将计算机视觉和图像处理技术应用于皮肤望诊,旨在提取皮肤颜色、纹理、斑点等特征,然后通过模式识别算法来进行分析和诊断。这些研究通常需要大量的医学图像数据作为基础,使用机器学习和深度学习技术,如卷积神经网络(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 tensorOrt::Value input_tensor = this->transform(mat);// 2. inferenceauto output_tensors = ort_session->Run(Ort::RunOptions{nullptr}, input_node_names.data(),&input_tensor, 1, output_node_names.data(), num_outputs);// 3. generate maskthis->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); // allocatefor (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 面部纹理和皱纹分析:
面部区域的皱纹,如额纹、川字纹、眼下纹、法令纹、嘴角纹等。这些的参数包括数量、长度、深浅,以及角二阶矩、对比度、相关、熵等来描述这些皱纹的特征。这些参数能够帮助定量地衡量皱纹的不同方面,从而更准确地分析和描述面部皮肤的老化和纹理特征。
还可以使用灰度共生矩阵来计算纹理特征,如角二阶矩、对比度、相关、熵等。这些参数可以帮助捕捉图像中不同区域的纹理差异,进而区分纹理和皱纹。需要注意的是,纹理和皱纹确实在某种程度上是相关的,但纹理主要关注整体表面特征,而皱纹更侧重于特定区域的褶皱。
在进行定量检测时。要提取面部的纹理,可参考我之前的博客:基于语义分割实现人脸图像的皱纹检测定位与分割
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>void show_img(std::string name, const cv::Mat& img) {cv::namedWindow(name, 0);int max_rows = 500;int max_cols = 600;if (img.rows >= img.cols) {cv::resizeWindow(name, cv::Size(img.cols * max_rows / img.rows, max_rows));}else {cv::resizeWindow(name, cv::Size(max_cols, img.rows * max_cols / img.cols));}cv::imshow(name, img);
}int main(int argc, char* argv[])
{std::string path = "demo";std::vector<std::string> filenames;cv::glob(path, filenames, false);for (auto img_name : filenames){cv::Mat img = cv::imread(img_name);cv::Size reso(512, 512);cv::Mat blob = cv::dnn::blobFromImage(img, 1.0 / 255, reso,cv::Scalar(0, 0, 0), false, false);cv::dnn::Net net = cv::dnn::readNet("model/Enet.onnx");net.setInput(blob);auto t0 = cv::getTickCount();cv::Mat out = net.forward();std::cout << out.size << std::endl;cv::Mat segm = cv::Mat::zeros(cv::Size(512, 512), CV_8UC1);std::cout << out.size[3] << std::endl;for (int i = 0; i < 512*512; ++i){if (out.ptr<float>(0, 0)[i] < out.ptr<float>(0, 1)[i]){segm.data[i] = 255;}}show_img("img", img);show_img("out", segm);cv::waitKey();}return 0;
}
1.2 唇纹分析:
对于唇纹,定量的参数包括数量、长度、深浅,以及角二阶矩、对比度、相关、熵等,用于描述唇纹的特征。这些参数可以帮助准确地评估唇部区域的纹理和皱纹。
2.毛孔、黑头
#include "PoreDetect.h"PoreDetect::PoreDetect()
{}PoreDetect::~PoreDetect()
{}
PoreDetect::PoreDetect(bool use_gpu)
{bool has_GPU = false;
#if NCNN_VULKANncnn::create_gpu_instance();has_GPU = ncnn::get_gpu_count() > 0;
#endifbool to_use_GPU = has_GPU && use_gpu;net.opt.use_vulkan_compute = to_use_GPU;
#if FT_MEMFP16net.opt.use_fp16_arithmetic = true;ncnn_net.load_param(__TB210218FP16_param_bin);ncnn_net.load_model(__TB210218FP16_bin);
#elsenet.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_MEMFP16using namespace __TB210218FP16_param_id;ex.input(BLOB_input, nc_in);ex.extract(BLOB_output, out);
#elseex.input("input", nc_in);ex.extract("output", out);
#endifcv::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个外接矩形}}
}
相关文章:
计算机视觉与人工智能在医美人脸皮肤诊断方面的应用
一、人脸皮肤诊断方法 近年来,随着计算机技术和人工智能的不断发展,中医领域开始逐渐探索利用这些先进技术来辅助面诊和诊断。在皮肤望诊方面,也出现了一些现代研究,尝试通过图像分析技术和人工智能算法来客观化地获取皮肤相关的…...
RCU501 RMP201-8 KONGSBERG 分布式处理单元
RCU501 RMP201-8 KONGSBERG 分布式处理单元 AutoChief600使用直接安装在主机接线盒中的分布式处理单元。进出发动机的所有信号都在双冗余CAN线路(发动机总线)上传输。 所有不重要的传感器都可以与K-Chief 600报警和监控系统共享,只需要一个主机接口。这一原则大大…...
说说 MVCC 的工作原理?
分析&回答 多版本并发控制(MVCC) InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现。这两个列,一个保存了行的创建时间,一个保存行的删除时间,并不是实际的时间,而是系统版本号。每开始一个新的事务&am…...
微信小程序请求接口返回的二维码(图片),本地工具和真机测试都能显示,上线之后不显示问题
请求后端接口返回的图片: 页面展示: 代码实现: :show-menu-by-longpress"true" 是长按保存图片 base64Code 是转为base64的地址 <image class"code" :src"base64Code" alt"" :show-menu-by-long…...
Python小知识 - 1. Python装饰器(decorator)
Python装饰器(decorator) Python装饰器是一个很有用的功能,它可以让我们在不修改原有代码的情况下,为已有的函数或类添加额外的功能。 常见的使用场景有: a. 函数缓存:对于一些计算量较大的函数,…...
如何访问GitHub
1、手动修改hosts 1.1、查找到最新的GitHub的hosts信息 通过链接:https://raw.hellogithub.com/hosts 进行查找最新的GitHub的hosts信息 1.2、查找到hosts文件位置 先找到 hosts 文件的位置,不同操作系统,hosts 文件的存储位置也不同&…...
【广州华锐互动】智能变电站AR仿真实训系统大大提高培训的效率和质量
随着电力行业的不断发展,变电站的建设和运维变得越来越重要。传统的变电站运维培训方式存在着诸多问题,如难以真实模拟变电站运行环境、信息传递不及时、难以掌握实际操作技能等问题。而智能变电站AR仿真实训系统可以为变电站运维人员带来全新的培训方式…...
手写Mybatis:第11章-流程解耦,封装结果集处理器
文章目录 一、目标:结果集处理器二、设计:结果集处理器三、实现:结果集处理器3.1 工程结构3.2 结果集处理器关系图3.3 出参参数处理3.3.1 结果映射Map3.3.2 结果映射封装3.3.3 修改映射器语句类3.3.4 映射构建器助手3.3.5 语句构建器调用助手…...
金融风控数据分析-信用评分卡建模(附数据集下载地址)
本文引用自: 金融风控:信用评分卡建模流程 - 知乎 (zhihu.com) 在原文的基础上加上了一部分自己的理解,转载在CSDN上作为保留记录。 本文涉及到的数据集可直接从天池上面下载: Give Me Some Credit给我一些荣誉_数据集-阿里云…...
ceph对象三元素data、xattr、omap
这里有一个ceph的原则,就是所有存储的不管是块设备、对象存储、文件存储最后都转化成了底层的对象object,这个object包含3个元素data,xattr,omap。data是保存对象的数据,xattr是保存对象的扩展属性,每个对象…...
使用 BERT 进行文本分类 (03/3)
一、说明 在使用BERT(2)进行文本分类时,我们讨论了什么是PyTorch以及如何预处理我们的数据,以便可以使用BERT模型对其进行分析。在这篇文章中,我将向您展示如何训练分类器并对其进行评估。 二、准备数据的又一个步骤 …...
Leetcode Top 100 Liked Questions(序号236~347)
236. Lowest Common Ancestor of a Binary Tree 题意:二叉树,求最近公共祖先,All Node.val are unique. 我的思路 首先把每个节点的深度得到,之后不停向上,直到val相同,存深度就用map存吧 但是它没有向…...
MySQL数据库学习【基础篇】
📃基础篇 下方链接使用科学上网速度可能会更加快一点哦! 请点击查看数据库MySQL笔记大全 通用语法及分类 DDL: 数据定义语言,用来定义数据库对象(数据库、表、字段)DML: 数据操作语言,用来对数据库表中的…...
Kubernetes技术--k8s核心技术Service服务
1.service概述 Service 是 Kubernetes 最核心概念,通过创建 Service,可以为一组具有相同功能的容器应用提供一个统一的入口地址,并且将请求负载分发到后端的各个容器应用上。 2.service存在的意义 -1:防止pod失联(服务发现) 我们先说一下什么叫pod失联。 -2:...
OpenHarmony 应用 ArkUI 状态管理开发范例
本文转载自《#2023 盲盒码 # OpenHarmony 应用 ArkUI 状态管理开发范例》,作者:zhushangyuan_ 本文根据橘子购物应用,实现 ArkUI 中的状态管理。 在声明式 UI 编程框架中,UI 是程序状态的运行结果,用户构建了一个 UI …...
二、QTableWidget 类 clear() 和 clearContents() 的区别及程序崩溃原因分析
问题描述:区分 QTableWidget 类的 clear() 和 clearContents() 的用法,以及可能由于这两个方法使用不当导致程序崩溃的原因分析 Qt 官方文档对 QTableWidget 类的 clear() 方法描述如下: [slot] void QTableWidget::clear() Removes all ite…...
spring boot 项目中搭建 ElasticSearch 中间件 一 postman 操作 es
postman 操作 es 1. 简介2. 环境3. postman操作索引3.1 创建索引3.2 查看索引3.3 查看所有索引3.4 删除索引 4. postman操作文档4.1 添加文档4.2 查询文档4.3 查询全部文档4.4 更新文档4.5 局部更新文档4.6 删除文档4.7 条件查询文档14.8 条件查询文档24.9 条件查询文档 limit4…...
设计模式—观察者模式(Observer)
目录 思维导图 一、什么是观察者模式? 二、有什么优点吗? 三、有什么缺点吗? 四、什么时候使用观察者模式? 五、代码展示 ①、双向耦合的代码 ②、解耦实践一 ③、解耦实践二 ④、观察者模式 六、这个模式涉及到了哪些…...
分类算法系列③:模型选择与调优 (Facebook签到位置预测)
目录 模型选择与调优 1、介绍 模型选择(Model Selection): 调优(Hyperparameter Tuning): 本章重点 2、交叉验证 介绍 为什么需要交叉验证 数据处理 3、⭐超参数搜索-网格搜索(Grid Search) 介绍…...
PCL RANSAC分割提取多个空间圆
目录 一、概述二、代码实现三、结果展示1、原始数据2、提取结果四、测试数据本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、概述 使用PCL分割提取多个空间圆,其核心原理仍然是RANSAC拟合空间圆,这里只是做简单修改…...
Java八股文学习笔记day01
01.和equals区别 对于字符串变量来说,使用""和"equals"比较字符串时,其比较方法不同。""比较两个变量本身的值,即两个对象在内存中的首地址,"equals"比较字符串包含内容是否相同。 对于非…...
vant的NavBar导航栏可以自定义背景图片吗
可以的,Vant的NavBar导航栏提供了一个background-image属性,可以设置自定义背景图片。例 如: <van-nav-bar title"标题" left-text"返回" left-arrow background-image"url(https://example.com/image.jpg)&qu…...
深入浅出AXI协议(5)——数据读写结构读写响应结构
目录 一、前言 二、写选通(Write strobes) 三、窄传输(Narrow transfers) 1、示例1 2、示例2 四、字节不变性(Byte invariance) 五、未对齐的传输(Unaligned transfers) 六…...
IntelliJ Idea开发Vue遇到的几个问题
IntelliJ Idea开发Vue遇到的几个问题 确保 idea已安装插件【Vue.js】 问题1:ts方法错误 或 提示导入 import xxx.vue标红 解决办法:在 env.d.ts中添加以下代码(若无此文件,重新创建): /* eslint-disable */ declare module *.…...
sql查找最晚一天/日期最大的一条记录 两种方法
例:查找最晚入职员工的所有信息 建表: CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMA…...
详解python的
详解& 在Python中,使用&符号可以求取两种数据类型的交集: 集合(Set):你可以使用&来计算两个集合的交集。例如: set1 {1, 2, 3, 4} set2 {3, 4, 5, 6} common_elements set1 & set2 pri…...
Modbus TCP通信笔记
目录 1 Modbus TCP 数据协议1.1 数据格式1.2 报文头(MBAP头)1.3 功能码1.4 Modbus 地址映射到 CPU 地址 2 Modbus TCP 通讯数据示例2.1 功能码01 读离散输出线圈2.2 功能码02 读离散输入线圈2.3 功能码03 读保持寄存器2.4 功能码04 读输入寄存器2.5 功能码05 写单个离散输出寄存…...
CIM和websockt-实现实时消息通信:双人聊天和消息列表展示
欢迎大佬的来访,给大佬奉茶 一、文章背景 有一个业务需求是:实现一个聊天室,我和对方可以聊天;以及有一个消息列表展示我和对方(多个人)的聊天信息和及时接收到对方发来的消息并展示在列表上。 项目框架概…...
useLayoutEffect和useEffect有什么作用?
useEffect 和 useLayoutEffect 都是 React 中的钩子函数,用于在组件渲染过程中执行副作用操作。它们的主要区别在于执行时机。 useEffect: useEffect 是异步执行的,它在浏览器渲染完成之后才执行。这意味着它不会阻塞浏览器的渲染过程,因此适合用于处理副作用,如数据获取、…...
django中配置使用websocket终极解决方案
django ASGI/Channels 启动和 ASGI/daphne的区别 Django ASGI/Channels 是 Django 框架的一个扩展,它提供了异步服务器网关接口(ASGI)协议的支持,以便处理实时应用程序的并发连接。ASGI 是一个用于构建异步 Web 服务器和应用程序…...
网站开发人员名片/如何宣传网站
每一个oracle数据库都有一个控制文件。控制文件是一个小型的二进制文件,可以记录数据库的物理结构,包含以下的内容:数据库名称、相关数据文件和联机重做日志文件的名称和位置、数据库创建的时标、当前日志的序号、检验点信息。 无论何时打开数…...
在线制图网/seo排名谁教的好
Talk Is Cheap和Java一样,python也提供了对于checked exception和unchecked exception. 对于checked exception,我们通常使用try except可以显示解决,对于unchecked 异常,其实也是提供回调或者是钩子来帮助我们处理的,我们可以在钩子里面记录崩溃栈追踪或者发送崩溃数据.下面代…...
php 数据库 wordpress/seo怎么才能做好
1:寻找类定义jvm会在自己的一个名叫“方法区”的内存块中,寻找名叫“MyObject”的Class对象(注意class也是一个对象,该对象记录了所有类的定义),如果有,则按照Class对象的定义,生成一个MyObject对象。2:加载…...
山东省建设协会网站/如何自己开个网站平台
我们现在测试环境与生产环境内容越来越多,导致研发环境发布测试环境,测试环境发布生成环境时每次更改访问域名或者固定数据文件时每次都需要大范围更改,所以使用脚本动态判断动态引入js文件,通过配置js文件来达到研发环境、测试环…...
网站优化定做/百度服务
训练赛1(第十一届山东省大学生程序设计竞赛复现)导语涉及的知识点题目GDHBCM总结导语 第一次训练赛,根据队内安排,选择值得参考的题目进行整理 涉及的知识点 整数除法、01背包、思维、数据量&最小生成树、树、矩阵 题目&a…...
wordpress的安装目录结构/网络广告策划书范文
大神们可以写出“深入浅出”系列,小白就写点"真浅尝辄止"系列的吧,主要便于自己理解和巩固,毕竟一开始就上源码还是会头大滴,于是就准备浅尝辄止的了解下"React是如何工作的?" React是怎么工作的&…...