赞
踩
作为图像处理的C++库,图像存储是一个基本数据结构。
接口中图像对象为SeetaImageData。
struct SeetaImageData
{
int width; // 图像宽度
int height; // 图像高度
int channels; // 图像通道
unsigned char *data; // 图像数据
};
这里要说明的是data的存储格式,其存储的是连续存放的试用8位无符号整数表示的像素值,存储为[height, width, channels]顺序。彩色图像时三通道以BGR通道排列。
如下图所示,就是展示了高4宽3的彩色图像内存格式。
Image Layout
该存储在内存中是连续存储。因此,上图中表示的图像data的存储空间为433=36bytes。
提示: BGR是OpenCV默认的图像格式。在大家很多时候看到就是直接将cv::Mat的data直接赋值给SeetaImageData的data。
data的数据类型为uint8,数值范围为[0, 255],表示了对应的灰度值由最暗到最亮。在一些用浮点数 float表示灰度值的平台上,该范围映射到为[0, 1]。
这里详细描述格式是为了对接不同应用场景下的图像库的图像表述格式,这里注意的两点,1. 数据类型是uint8表示的[0, 255]范围;2. 颜色通道是BGR格式,而很多图像库常见的是RGB或者RGBA(A为alpha不透明度)。
提示:颜色通道会影响识别算法的精度,在不同库的转换时需要小心注意。
这种纯C接口不利于一些资源管理的情况。SeetaFace 提供给了对应SeetaImageData的封装seeta::ImageData和seeta::cv::ImageData。
以下就是使用OpenCV加载图像后转换为SeetaImageData的操作。
cv::Mat cvimage = cv::imread("1.jpg", cv::IMREAD_COLOR);
SeetaImageData simage;
simage.width = cvimage.cols;
simage.height = cvimage.rows;
simage.channels = cvimage.channels();
simage.data = cvimage.data;
注意:simage.data拿到的是“借来的”临时数据,在试用的期间,必须保证cvimage的对象不被释放,或者形式的更新。
注意:原始图像可能是空的,为了输入的合法性,需要通过cvimage.empty()方法,判断图像是否为空。
这里通过cv::imread并且第二个参数设置为cv::IMREAD_COLOR,获取到的cv::Mat的图像数据是连续存储的,这个是使用SeetaImageData必须的。如果不确定是否是连续存储的对象,可以调用下述代码段进行转换。
if (!cvimage.isContinuous()) cvimage = cvimage.clone();
当然,根据cv::Mat和SeetaImageData,对象的转换可以逆向。
cv::Mat another_cvimage = cv::Mat(simage.height, simage.width, CV_8UC(simage.channels), simage.data);
seeta::ImageData和seeta::cv::ImageData也是基于这种基本的类型定义进行的封装,并加进了对象声明周期的管理。
这里展示了一种封装:
namespace seeta { namespace cv { // using namespace ::cv; class ImageData : public SeetaImageData { public: ImageData( const ::cv::Mat &mat ) : cv_mat( mat.clone() ) { this->width = cv_mat.cols; this->height = cv_mat.rows; this->channels = cv_mat.channels(); this->data = cv_mat.data; } private: ::cv::Mat cv_mat; }; } }
这样SeetaImageData使用代码就可以简化为:
seeta::cv::ImageData = cv::imread("1.jpg");
因为seeta::cv::ImageData继承了SeetaImageData,因此在需要传入const SeetaImageData &类型的地方,可以直接传入seeta::cv::ImageData对象。
终于经过繁杂的基础特性说明之后,迎来了两个重要的识别器模块。人脸检测和关键点定位。
人脸检测, seeta::FaceDetector 就是输入待检测的图片,输出检测到的每个人脸位置,用矩形表示。
关键点定位,seeta::FaceLandmarker就是输入待检测的图片,和待检测的人脸位置,输出N个关键点的坐标(图片内)。
两个模块分别负责找出可以处理的人脸位置,检测出关键点用于标定人脸的状态,方便后续的人脸对齐后进行对应识别分析。
人脸检测器的效果如图所示:
FaceDetector
这里给出人脸检测器的主要接口:
namespace seeta {
class FaceDetector {
FaceDetector(const SeetaModelSetting &setting);
SeetaFaceInfoArray detect(const SeetaImageData &image) const;
std::vector<SeetaFaceInfo> detect_v2(const SeetaImageData &image) const;
void set(Property property, double value);
double get(Property property) const;
}
}
构造一个检测器的函数参考如下:
#include <seeta/FaceDetector.h>
seeta::FaceDetector *new_fd() {
seeta::ModelSetting setting;
setting.append("face_detector.csta");
return new seeta::FaceDetector(setting);
}
有了检测器,我们就可以对图片检测人脸,检测图片中所有人脸并打印坐标的函数参考如下:
#include <seeta/FaceDetector.h>
void detect(seeta::FaceDetector *fd, const SeetaImageData &image) {
std::vector<SeetaFaceInfo> faces = fd->detect_v2(image);
for (auto &face : faces) {
SeetaRect rect = face.pos;
std::cout << "[" << rect.x << ", " << rect.y << ", "
<< rect.width << ", " << rect.height << "]: "
<< face.score << std::endl;
}
}
这里要说明的是,一般检测返回的所有人脸是按照置信度排序的,当应用需要获取最大的人脸时,可以对检测结果进行一个部分排序获取出最大的人脸,如下代码排序完成后,faces[0]就是最大人脸的位置。
std::partial_sort(faces.begin(), faces.begin() + 1, faces.end(), [](SeetaFaceInfo a, SeetaFaceInfo b) {
return a.pos.width > b.pos.width;
});
人脸检测器可以设置一些参数,通过set方法。可以设置的属性有:
seeta::FaceDetector::PROPERTY_MIN_FACE_SIZE 最小人脸
seeta::FaceDetector::PROPERTY_THRESHOLD 检测器阈值
seeta::FaceDetector::PROPERTY_MAX_IMAGE_WIDTH 可检测的图像最大宽度
seeta::FaceDetector::PROPERTY_MAX_IMAGE_HEIGHT 可检测的图像最大高度
最小人脸是人脸检测器常用的一个概念,默认值为20,单位像素。它表示了在一个输入图片上可以检测到的最小人脸尺度,注意这个尺度并非严格的像素值,例如设置最小人脸80,检测到了宽度为75的人脸是正常的,这个值是给出检测能力的下限。
最小人脸和检测器性能息息相关。主要方面是速度,使用建议上,我们建议在应用范围内,这个值设定的越大越好。SeetaFace采用的是BindingBox Regresion的方式训练的检测器。如果最小人脸参数设置为80的话,从检测能力上,可以将原图缩小的原来的1/4,这样从计算复杂度上,能够比最小人脸设置为20时,提速到16倍。
检测器阈值默认值是0.9,合理范围为[0, 1]。这个值一般不进行调整,除了用来处理一些极端情况。这个值设置的越小,漏检的概率越小,同时误检的概率会提高;
可检测的图像最大宽度和可检测的图像最大高度是相关的设置,默认值都是2000。最大高度和宽度,是算法实际检测的高度。检测器是支持动态输入的,但是输入图像越大,计算所使用的内存越大、计算时间越长。如果不加以限制,一个超高分辨率的图片会轻易的把内存撑爆。这里的限制就是,当输入图片的宽或者高超过限度之后,会自动将图片缩小到限制的分辨率之内。
我们当然希望,一个算法在各种场景下都能够很好的运行,但是自然规律远远不是一个几兆的文件就是能够完全解释的。应用上总会需要取舍,也就是trade-off。
关键点定位器的效果如图所示:
FaceLandmarker
关键定定位输入的是原始图片和人脸检测结果,给出指定人脸上的关键点的依次坐标。
这里检测到的5点坐标循序依次为,左眼中心、右眼中心、鼻尖、左嘴角和右嘴角。
注意这里的左右是基于图片内容的左右,并不是图片中人的左右,即左眼中心就是图片中左边的眼睛的中心。
同样的方式,我们也可以构造关键点定位器:
#include <seeta/FaceLandmarker.h>
seeta::FaceLandmarker *new_fl() {
seeta::ModelSetting setting;
setting.append("face_landmarker_pts5.csta");
return new seeta::FaceLandmarker(setting);
}
根据人脸检测关键点,并将坐标打印出来的代码如下:
#include <seeta/FaceLandmarker.h>
void mark(seeta::FaceLandmarker *fl, const SeetaImageData &image, const SeetaRect &face) {
std::vector<SeetaPointF> points = fl->mark(image, face);
for (auto &point : points) {
std::cout << "[" << point.x << ", " << point.y << "]" << std::endl;
}
}
当然开放版也会有多点的模型放出来,限于篇幅不再对点的位置做过多的文字描述。
例如face_landmarker_pts68.csta就是68个关键点检测的模型。其坐标位置可以通过逐个打印出来进行区分。
这里需要强调说明一下,这里的关键点是指人脸上的关键位置的坐标,在一些表述中也将关键点称之为特征点,但是这个和人脸识别中提取的特征概念没有任何相关性。并不存在结论,关键点定位越多,人脸识别精度越高。
一般的关键点定位和其他的基于人脸的分析是基于5点定位的。而且算法流程确定下来之后,只能使用5点定位。5点定位是后续算法的先验,并不能直接替换。从经验上来说,5点定位已经足够处理人脸识别或其他相关分析的精度需求,单纯增加关键点个数,只是增加方法的复杂度,并不对最终结果产生直接影响。
参考:seeta/FaceDetector.h seeta/FaceLandmarker.h
这两个重要的功能都是seeta::FaceRecognizer模块提供的基本功能。特征提取方式和对比是对应的。
这是人脸识别的一个基本概念,就是将待识别的人脸经过处理变成二进制数据的特征,然后基于特征表示的人脸进行相似度计算,最终与相似度阈值对比,一般超过阈值就认为特征表示的人脸是同一个人。
这里SeetaFace的特征都是float数组,特征对比方式是向量內积。
首先可以构造人脸识别器以备用:
#include <seeta/FaceRecognizer.h>
seeta::FaceRecognizer *new_fr() {
seeta::ModelSetting setting;
setting.append("face_recognizer.csta");
return new seeta::FaceRecognizer(setting);
}
特征提取过程可以分为两个步骤:1. 根据人脸5个关键点裁剪出人脸区域;2. 将人脸区域输入特征提取网络提取特征。
这两个步骤可以分开调用,也可以独立调用。
两个步骤分别对应seeta::FaceRecognizer的CropFaceV2和ExtractCroppedFace。也可以用Extract方法一次完成两个步骤的工作。
这里列举使用Extract进行特征提取的函数:
#include <seeta/FaceRecognizer.h>
#include <memory>
std::shared_ptr<float> extract(
seeta::FaceRecognizer *fr,
const SeetaImageData &image,
const std::vector<SeetaPointF> &points) {
std::shared_ptr<float> features(
new float[fr->GetExtractFeatureSize()],
std::default_delete<float[]>());
fr->Extract(image, points.data(), features.get());
return features;
}
同样可以给出相似度计算的函数:
#include <seeta/FaceRecognizer.h>
#include <memory>
float compare(seeta::FaceRecognizer *fr,
const std::shared_ptr<float> &feat1,
const std::shared_ptr<float> &feat2) {
return fr->CalculateSimilarity(feat1.get(), feat2.get());
}
注意:这里points的关键点个数必须是SeetaFace提取的5点关键点。
特征长度是不同模型可能不同的,要使用GetExtractFeatureSize方法获取当前使用模型提取的特征长度。
相似度的范围是[0, 1],但是需要注意的是,如果是直接用內积计算的话,因为特征中存在复数,所以计算出的相似度可能为负数。识别器内部会将负数映射到0。
在一些特殊的情况下,需要将特征提取分开两步进行,比如前端裁剪处理图片,服务器进行特征提取和对比。下面给出分步骤的特征提取方式:
#include <seeta/FaceRecognizer.h>
#include <memory>
std::shared_ptr<float> extract_v2(
seeta::FaceRecognizer *fr,
const SeetaImageData &image,
const std::vector<SeetaPointF> &points) {
std::shared_ptr<float> features(
new float[fr->GetExtractFeatureSize()],
std::default_delete<float[]>());
seeta::ImageData face = fr->CropFaceV2(image, points.data());
fr->ExtractCroppedFace(face, features.get());
return features;
}
函数中间临时申请的face和features的对象大小,在识别器加载后就已经固定了,所以这部分的内存对象是可以复用的。
特别指出,如果只是对一个图像中最大人脸做特征提取的函数可以实现为:
std::shared_ptr<float> extract(
seeta::FaceDetector *fd,
seeta::FaceLandmarker *fl,
seeta::FaceRecognizer *fr,
const SeetaImageData &image) {
auto faces = fd->detect_v2(image);
if (faces.empty()) return nullptr;
std::partial_sort(faces.begin(), faces.begin() + 1, faces.end(),
[](SeetaFaceInfo a, SeetaFaceInfo b) {
return a.pos.width > b.pos.width;
});
auto points = fl->mark(image, faces[0].pos);
return extract(fr, image, points);
}
实现81特征点检测
/* * @Descripttion: * @version: * @Author: Yueyang * @email: 1700695611@qq.com * @Date: 2020-11-28 01:14:29 * @LastEditors: Yueyang * @LastEditTime: 2020-11-28 11:35:18 */ #include <seeta/FaceDetector.h> #include <seeta/FaceLandmarker.h> #include <seeta/FaceRecognizer.h> #include <seeta/Struct.h> #include <array> #include <map> #include <iostream> #ifdef __cplusplus extern "C" { #endif #include "cv.h" #include "li_image.h" #include "li_painter.h" #ifdef __cplusplus } #endif int test_image(seeta::FaceDetector &FD, seeta::FaceLandmarker &FL) { for(int i=1;i<=4;i++){ std::string image_path = std::to_string(i)+".bmp"; std::cout << "Loading image: " << image_path << std::endl; Li_Image* img=Li_Load_Image((BYTE*)image_path.c_str(),BMP_888); Li_Image* square=Li_ReShape(img,500,500); Li_Image* img2=Li_Rotate(square); Li_Image* img1=Li_Convert_Image(img2,LI_BMP_888_2_LI_BMP_8); SeetaImageData simage; simage.width=img1->width; simage.height=img1->height; simage.data=(unsigned char*)img1->data; simage.channels=1; auto faces = FD.detect(simage); std::cout<<faces.size<<std::endl; for (int i = 0; i < faces.size; ++i) { auto &face = faces.data[i]; auto points = FL.mark(simage, face.pos); Li_Line(img2,0xFF0000,face.pos.x,face.pos.y,face.pos.x,face.pos.y+face.pos.height); Li_Line(img2,0xFF0000,face.pos.x,face.pos.y,face.pos.x+face.pos.width,face.pos.y); Li_Line(img2,0xFF0000,face.pos.x+face.pos.width,face.pos.y,face.pos.x+face.pos.width,face.pos.y+face.pos.height); Li_Line(img2,0xFF0000,face.pos.x,face.pos.y+face.pos.height,face.pos.x+face.pos.width,face.pos.y+face.pos.height); for (auto &point : points) { Li_Circle(img2,0xFF0000,point.x,point.y,1); } } Li_Image*res= Li_Rotate(img2); auto output_path = image_path + ".pts81.bmp"; Li_Save_Image((BYTE*)output_path.c_str(),res); std::cerr << "Saving result into: " << output_path << std::endl; } return EXIT_SUCCESS; } int main() { seeta::ModelSetting::Device device = seeta::ModelSetting::CPU; int id = 0; seeta::ModelSetting FD_model( "./model/fd_2_00.dat", device, id ); seeta::ModelSetting FL_model( "./model/pd_2_00_pts81.dat", device, id ); seeta::FaceDetector FD(FD_model); seeta::FaceLandmarker FL(FL_model); FD.set(seeta::FaceDetector::PROPERTY_VIDEO_STABLE, 1); return test_image(FD, FL); }
代码中有错误的地方还望指出。我已经将项目同步到了github,我会实时更新这个代码仓库。
项目github地址:
LiteCV
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。