当前位置:   article > 正文

人脸识别之人眼定位、人脸矫正、人脸尺寸标准化---_csdn 根据眼睛判断人脸角度

csdn 根据眼睛判断人脸角度

http://blog.csdn.net/eastlhu/article/details/25063193


代码来源于<Mastering OpenCV with Practical Computer Vision Projects >

她的另外几篇文章,也翻译的很好

http://blog.csdn.net/raby_gyl/article/details/12611861

http://blog.csdn.net/raby_gyl/article/details/12623539

http://blog.csdn.net/raby_gyl/article/details/12338371

我感觉下面的程序——对人眼定位,人脸矫正,人脸尺寸化,对于初学人脸识别,做人脸的预处理非常有帮助~

程序的思路是:首先通过人脸检测级联器检测到人脸区域,对于人脸区域我们采用经验理论(即不同的人眼检测器在不同的人脸搜索区域具有最优性),也即人眼在人脸区域中的位置,得到人眼的大体位置,采用opencv的人眼级联检测器检测人眼,获取每一个人眼的中心位置,两个人眼的连线与水平位置的夹角来确定人脸旋转、矫正的角度。同时通过我们想要得到的目标图像来计算得到仿射矩阵的scale尺度因子,即图像缩放的比例,以及通过平移来计算得到的距离,进而实现定位人眼在目的图像的位置。

代码如下:

[cpp]  view plain copy
  1. #include "stdafx.h"  
  2. #include "opencv2/imgproc/imgproc.hpp"  
  3. #include "opencv2/highgui/highgui.hpp"  
  4. #include "opencv2/opencv.hpp"  
  5. #include<iostream>  
  6. #include<vector>  
  7. using namespace std;  
  8. using namespace cv;  
  9.   
  10. const double DESIRED_LEFT_EYE_X = 0.16;     // 控制处理后人脸的多少部分是可见的  
  11. const double DESIRED_LEFT_EYE_Y = 0.14;  
  12. const double FACE_ELLIPSE_CY = 0.40;  
  13. const double FACE_ELLIPSE_W = 0.50;         // 应当至少为0.5  
  14. const double FACE_ELLIPSE_H = 0.80;         //控制人脸掩码的高度  
  15.   
  16. /*--------------------------------------目标检测-------------------------------------*/  
  17. void detectObjectsCustom(const Mat &img, CascadeClassifier &cascade, vector<Rect> &objects, int scaledWidth, int flags, Size minFeatureSize, float searchScaleFactor, int minNeighbors);  
  18. void detectLargestObject(const Mat &img, CascadeClassifier &cascade, Rect &largestObject, int scaledWidth);  
  19. void detectManyObjects(const Mat &img, CascadeClassifier &cascade, vector<Rect> &objects, int scaledWidth);  
  20. /*------------------------------------- end------------------------------------------*/  
  21.   
  22. void detectBothEyes(const Mat &face, CascadeClassifier &eyeCascade1, CascadeClassifier &eyeCascade2, Point &leftEye, Point &rightEye, Rect *searchedLeftEye, Rect *searchedRightEye);  
  23. Mat getPreprocessedFace(Mat &srcImg, int desiredFaceWidth, CascadeClassifier &faceCascade, CascadeClassifier &eyeCascade1, CascadeClassifier &eyeCascade2, bool doLeftAndRightSeparately, Rect *storeFaceRect, Point *storeLeftEye, Point *storeRightEye, Rect *searchedLeftEye, Rect *searchedRightEye);  
  24.   
  25. int main(int argc,char **argv)  
  26. {  
  27.     CascadeClassifier faceDetector;    
  28.     CascadeClassifier  eyeDetector1;  
  29.     CascadeClassifier  eyeDetector2;//未初始化不用  
  30.   
  31.     try{    
  32.         //faceDetector.load("E:\\OpenCV-2.3.0\\data\\haarcascades\\haarcascade_frontalface_alt.xml");    
  33.         faceDetector.load("E:\\OpenCV-2.3.0\\data\\lbpcascades\\lbpcascade_frontalface.xml");    
  34.         eyeDetector1.load("E:\\OpenCV-2.3.0\\data\\haarcascades\\haarcascade_eye.xml");  
  35.         eyeDetector2.load("E:\\OpenCV-2.3.0\\data\\haarcascades\\haarcascade_eye_tree_eyeglasses.xml");  
  36.   
  37.   
  38.     }catch (cv::Exception e){}    
  39.     if(faceDetector.empty())    
  40.     {    
  41.         cerr<<"error:couldn't load face detector (";    
  42.         cerr<<"lbpcascade_frontalface.xml)!"<<endl;    
  43.         exit(1);    
  44.     }    
  45.   
  46.     Mat img=imread(argv[1],1);  
  47.     Rect largestObject;  
  48.     const int scaledWidth=320;  
  49.     detectLargestObject(img,faceDetector,largestObject,scaledWidth);  
  50.     Mat img_rect(img,largestObject);  
  51.     Point leftEye,rightEye;  
  52.     Rect searchedLeftEye,searchedRightEye;  
  53.     detectBothEyes(img_rect,eyeDetector1,eyeDetector2,leftEye,rightEye,&searchedLeftEye,&searchedRightEye);  
  54.     //仿射变换  
  55.     Point2f eyesCenter;  
  56.     eyesCenter.x=(leftEye.x+rightEye.x)*0.5f;  
  57.     eyesCenter.y=(leftEye.y+rightEye.y)*0.5f;  
  58.     cout<<"左眼中心坐标 "<<leftEye.x<<" and "<<leftEye.y<<endl;  
  59.     cout<<"右眼中心坐标 "<<rightEye.x<<" and "<<rightEye.y<<endl;  
  60.     //获取两个人眼的角度  
  61.     double dy=(rightEye.y-leftEye.y);  
  62.     double dx=(rightEye.x-leftEye.x);  
  63.     double len=sqrt(dx*dx+dy*dy);  
  64.     cout<<"dx is "<<dx<<endl;  
  65.     cout<<"dy is "<<dy<<endl;  
  66.     cout<<"len is "<<len<<endl;  
  67.     double angle=atan2(dy,dx)*180.0/CV_PI;  
  68.     const double DESIRED_RIGHT_EYE_X=1.0f-0.16;  
  69.     //得到我们想要的尺度化大小  
  70.     const int DESIRED_FACE_WIDTH=70;  
  71.     const int DESIRED_FACE_HEIGHT=70;  
  72.     double desiredLen=(DESIRED_RIGHT_EYE_X-0.16);  
  73.     cout<<"desiredlen is "<<desiredLen<<endl;  
  74.     double scale=desiredLen*DESIRED_FACE_WIDTH/len;  
  75.     cout<<"the scale is "<<scale<<endl;  
  76.     Mat rot_mat = getRotationMatrix2D(eyesCenter, angle, scale);  
  77.     double ex=DESIRED_FACE_WIDTH * 0.5f - eyesCenter.x;  
  78.     double ey = DESIRED_FACE_HEIGHT * DESIRED_LEFT_EYE_Y-eyesCenter.y;  
  79.     rot_mat.at<double>(0, 2) += ex;  
  80.     rot_mat.at<double>(1, 2) += ey;  
  81.     Mat warped = Mat(DESIRED_FACE_HEIGHT, DESIRED_FACE_WIDTH,CV_8U, Scalar(128));  
  82.     warpAffine(img_rect, warped, rot_mat, warped.size());  
  83.   
  84.     imshow("warped",warped);  
  85.   
  86.     rectangle(img,Point(largestObject.x,largestObject.y),Point(largestObject.x+largestObject.width,largestObject.y+largestObject.height),Scalar(0,0,255),2,8);  
  87.     rectangle(img_rect,Point(searchedLeftEye.x,searchedLeftEye.y),Point(searchedLeftEye.x+searchedLeftEye.width,searchedLeftEye.y+searchedLeftEye.height),Scalar(0,255,0),2,8);  
  88.     rectangle(img_rect,Point(searchedRightEye.x,searchedRightEye.y),Point(searchedRightEye.x+searchedRightEye.width,searchedRightEye.y+searchedRightEye.height),Scalar(0,255,0),2,8);  
  89.   
  90.   
  91.     //getPreprocessedFace  
  92.     imshow("img_rect",img_rect);  
  93.     imwrite("img_rect.jpg",img_rect);  
  94.     imshow("img",img);  
  95.     waitKey();  
  96.   
  97. }  
  98.   
  99.   
  100.   
  101.   
  102.   
  103.   
  104. /* 
  105. 1、采用给出的参数在图像中寻找目标,例如人脸 
  106. 2、可以使用Haar级联器或者LBP级联器做人脸检测,或者甚至眼睛,鼻子,汽车检测 
  107. 3、为了使检测更快,输入图像暂时被缩小到'scaledWidth',因为寻找人脸200的尺度已经足够了。 
  108. */  
  109. void detectObjectsCustom(const Mat &img, CascadeClassifier &cascade, vector<Rect> &objects, int scaledWidth, int flags, Size minFeatureSize, float searchScaleFactor, int minNeighbors)  
  110. {  
  111.   
  112.     //如果输入的图像不是灰度图像,那么将BRG或者BGRA彩色图像转换为灰度图像  
  113.     Mat gray;  
  114.     if (img.channels() == 3) {  
  115.         cvtColor(img, gray, CV_BGR2GRAY);  
  116.     }  
  117.     else if (img.channels() == 4) {  
  118.         cvtColor(img, gray, CV_BGRA2GRAY);  
  119.     }  
  120.     else {  
  121.         // 直接使用输入图像,既然它已经是灰度图像  
  122.         gray = img;  
  123.     }  
  124.   
  125.     // 可能的缩小图像,是检索更快  
  126.     Mat inputImg;  
  127.   
  128.     float scale = img.cols / (float)scaledWidth;  
  129.     if (img.cols > scaledWidth) {  
  130.         // 缩小图像并保持同样的宽高比  
  131.         int scaledHeight = cvRound(img.rows / scale);  
  132.         resize(gray, inputImg, Size(scaledWidth, scaledHeight));  
  133.     }  
  134.     else {  
  135.         // 直接使用输入图像,既然它已经小了  
  136.         inputImg = gray;  
  137.     }  
  138.   
  139.     //标准化亮度和对比度来改善暗的图像  
  140.     Mat equalizedImg;  
  141.     equalizeHist(inputImg, equalizedImg);  
  142.   
  143.     // 在小的灰色图像中检索目标  
  144.     cascade.detectMultiScale(equalizedImg, objects, searchScaleFactor, minNeighbors, flags, minFeatureSize);  
  145.   
  146.     // 如果图像在检测之前暂时的被缩小了,则放大结果图像  
  147.     if (img.cols > scaledWidth) {  
  148.         for (int i = 0; i < (int)objects.size(); i++ ) {  
  149.             objects[i].x = cvRound(objects[i].x * scale);  
  150.             objects[i].y = cvRound(objects[i].y * scale);  
  151.             objects[i].width = cvRound(objects[i].width * scale);  
  152.             objects[i].height = cvRound(objects[i].height * scale);  
  153.         }  
  154.     }  
  155.   
  156.     //确保目标全部在图像内部,以防它在边界上   
  157.     for (int i = 0; i < (int)objects.size(); i++ ) {  
  158.         if (objects[i].x < 0)  
  159.             objects[i].x = 0;  
  160.         if (objects[i].y < 0)  
  161.             objects[i].y = 0;  
  162.         if (objects[i].x + objects[i].width > img.cols)  
  163.             objects[i].x = img.cols - objects[i].width;  
  164.         if (objects[i].y + objects[i].height > img.rows)  
  165.             objects[i].y = img.rows - objects[i].height;  
  166.     }  
  167.   
  168.     // 返回检测到的人脸矩形,存储在objects中  
  169. }  
  170.   
  171. /* 
  172. 1、仅寻找图像中的单个目标,例如最大的人脸,存储结果到largestObject 
  173. 2、可以使用Haar级联器或者LBP级联器做人脸检测,或者甚至眼睛,鼻子,汽车检测 
  174. 3、为了使检测更快,输入图像暂时被缩小到'scaledWidth',因为寻找人脸200的尺度已经足够了。 
  175. 4、注释:detectLargestObject()要比 detectManyObjects()快。 
  176. */  
  177. void detectLargestObject(const Mat &img, CascadeClassifier &cascade, Rect &largestObject, int scaledWidth)  
  178. {  
  179.     //仅寻找一个目标 (图像中最大的).  
  180.     int flags = CV_HAAR_FIND_BIGGEST_OBJECT;// | CASCADE_DO_ROUGH_SEARCH;  
  181.     // 最小的目标大小.  
  182.     Size minFeatureSize = Size(20, 20);  
  183.     // 寻找细节,尺度因子,必须比1大  
  184.     float searchScaleFactor = 1.1f;  
  185.     // 多少检测结果应当被滤掉,这依赖于你的检测系统是多坏,如果minNeighbors=2 ,大量的good or bad 被检测到。如果  
  186.     // minNeighbors=6,意味着只good检测结果,但是一些将漏掉。即可靠性 VS  检测人脸数量  
  187.     int minNeighbors = 4;  
  188.   
  189.     // 执行目标或者人脸检测,仅寻找一个目标(图像中最大的)  
  190.     vector<Rect> objects;  
  191.     detectObjectsCustom(img, cascade, objects, scaledWidth, flags, minFeatureSize, searchScaleFactor, minNeighbors);  
  192.     if (objects.size() > 0) {  
  193.         // 返回仅检测到的目标  
  194.         largestObject = (Rect)objects.at(0);  
  195.     }  
  196.     else {  
  197.         // 返回一个无效的矩阵  
  198.         largestObject = Rect(-1,-1,-1,-1);  
  199.     }  
  200. }  
  201.   
  202. void detectManyObjects(const Mat &img, CascadeClassifier &cascade, vector<Rect> &objects, int scaledWidth)  
  203. {  
  204.     // 寻找图像中的许多目标  
  205.     int flags = CV_HAAR_SCALE_IMAGE;  
  206.   
  207.     // 最小的目标大小.  
  208.     Size minFeatureSize = Size(20, 20);  
  209.     //  寻找细节,尺度因子,必须比1大  
  210.     float searchScaleFactor = 1.1f;  
  211.     // 多少检测结果应当被滤掉,这依赖于你的检测系统是多坏,如果minNeighbors=2 ,大量的good or bad 被检测到。如果  
  212.     // minNeighbors=6,意味着只good检测结果,但是一些将漏掉。即可靠性 VS  检测人脸数量  
  213.     int minNeighbors = 4;  
  214.   
  215.     // 执行目标或者人脸检测,寻找图像中的许多目标  
  216.     detectObjectsCustom(img, cascade, objects, scaledWidth, flags, minFeatureSize, searchScaleFactor, minNeighbors);  
  217. }  
  218. /* 
  219. 1、在给出的人脸图像中寻找双眼,返回左眼和右眼的中心,如果当找不到人眼时,或者设置为Point(-1,-1) 
  220. 2、注意如果你想用两个不同的级联器寻找人眼,你可以传递第二个人眼检测器,例如如果你使用的一个常规人眼检测器和带眼镜的人眼检测器一样好,或者左眼检测器和右眼检测器一样好, 
  221. 或者如果你不想第二个检测器,仅传一个未初始化级联检测器。 
  222. 3、如果需要的话,也可以存储检测到的左眼和右眼的区域 
  223. */  
  224. void detectBothEyes(const Mat &face, CascadeClassifier &eyeCascade1, CascadeClassifier &eyeCascade2, Point &leftEye, Point &rightEye, Rect *searchedLeftEye, Rect *searchedRightEye)  
  225. {  
  226.     //跳过人脸边界,因为它们经常是头发和耳朵,这不是我们关心的   
  227.     /* 
  228.     // For "2splits.xml": Finds both eyes in roughly 60% of detected faces, also detects closed eyes. 
  229.     const float EYE_SX = 0.12f; 
  230.     const float EYE_SY = 0.17f; 
  231.     const float EYE_SW = 0.37f; 
  232.     const float EYE_SH = 0.36f; 
  233.     */  
  234.     /* 
  235.     // For mcs.xml: Finds both eyes in roughly 80% of detected faces, also detects closed eyes. 
  236.     const float EYE_SX = 0.10f; 
  237.     const float EYE_SY = 0.19f; 
  238.     const float EYE_SW = 0.40f; 
  239.     const float EYE_SH = 0.36f; 
  240.     */  
  241.   
  242.     // For default eye.xml or eyeglasses.xml: Finds both eyes in roughly 40% of detected faces, but does not detect closed eyes.  
  243.     //haarcascade_eye.xml检测器在由下面确定的人脸区域内搜索最优。  
  244.     const float EYE_SX = 0.16f;//x  
  245.     const float EYE_SY = 0.26f;//y  
  246.     const float EYE_SW = 0.30f;//width  
  247.     const float EYE_SH = 0.28f;//height  
  248.   
  249.     int leftX = cvRound(face.cols * EYE_SX);  
  250.     int topY = cvRound(face.rows * EYE_SY);  
  251.     int widthX = cvRound(face.cols * EYE_SW);  
  252.     int heightY = cvRound(face.rows * EYE_SH);  
  253.     int rightX = cvRound(face.cols * (1.0-EYE_SX-EYE_SW) );  // 右眼的开始区域  
  254.   
  255.     Mat topLeftOfFace = face(Rect(leftX, topY, widthX, heightY));  
  256.     Mat topRightOfFace = face(Rect(rightX, topY, widthX, heightY));  
  257.     Rect leftEyeRect, rightEyeRect;  
  258.   
  259.     // 如果需要的话,然后搜索到的窗口给调用者  
  260.     if (searchedLeftEye)  
  261.         *searchedLeftEye = Rect(leftX, topY, widthX, heightY);  
  262.     if (searchedRightEye)  
  263.         *searchedRightEye = Rect(rightX, topY, widthX, heightY);  
  264.   
  265.     // 寻找左区域,然后右区域使用第一个人眼检测器  
  266.     detectLargestObject(topLeftOfFace, eyeCascade1, leftEyeRect, topLeftOfFace.cols);  
  267.     detectLargestObject(topRightOfFace, eyeCascade1, rightEyeRect, topRightOfFace.cols);  
  268.   
  269.     // 如果人眼没有检测到,尝试另外一个不同的级联检测器  
  270.     if (leftEyeRect.width <= 0 && !eyeCascade2.empty()) {  
  271.         detectLargestObject(topLeftOfFace, eyeCascade2, leftEyeRect, topLeftOfFace.cols);  
  272.         //if (leftEyeRect.width > 0)  
  273.         //    cout << "2nd eye detector LEFT SUCCESS" << endl;  
  274.         //else  
  275.         //    cout << "2nd eye detector LEFT failed" << endl;  
  276.     }  
  277.     //else  
  278.     //    cout << "1st eye detector LEFT SUCCESS" << endl;  
  279.   
  280.     // 如果人眼没有检测到,尝试另外一个不同的级联检测器  
  281.     if (rightEyeRect.width <= 0 && !eyeCascade2.empty()) {  
  282.         detectLargestObject(topRightOfFace, eyeCascade2, rightEyeRect, topRightOfFace.cols);  
  283.         //if (rightEyeRect.width > 0)  
  284.         //    cout << "2nd eye detector RIGHT SUCCESS" << endl;  
  285.         //else  
  286.         //    cout << "2nd eye detector RIGHT failed" << endl;  
  287.     }  
  288.     //else  
  289.     //    cout << "1st eye detector RIGHT SUCCESS" << endl;  
  290.   
  291.     if (leftEyeRect.width > 0) {   // 检查眼是否被检测到  
  292.         leftEyeRect.x += leftX;    //矫正左眼矩形,因为人脸边界被去除掉了   
  293.         leftEyeRect.y += topY;  
  294.         leftEye = Point(leftEyeRect.x + leftEyeRect.width/2, leftEyeRect.y + leftEyeRect.height/2);  
  295.     }  
  296.     else {  
  297.         leftEye = Point(-1, -1);    // 返回一个无效的点  
  298.     }  
  299.   
  300.     if (rightEyeRect.width > 0) { //检查眼是否被检测到  
  301.         rightEyeRect.x += rightX; // 矫正左眼矩形,因为它从图像的右边界开始  
  302.         rightEyeRect.y += topY;  // 矫正右眼矩形,因为人脸边界被去除掉了  
  303.         rightEye = Point(rightEyeRect.x + rightEyeRect.width/2, rightEyeRect.y + rightEyeRect.height/2);  
  304.     }  
  305.     else {  
  306.         rightEye = Point(-1, -1);    // 返回一个无效的点  
  307.     }  
  308. }  

运行效果图:

1、


2、检测到的人脸矩形区域:

3、人脸矫正和尺寸归一化到70*70后的结果图:

 

我要说的:

1、代码是截取的原文中的一小部分,搭配好环境可以直接运行,人家的程序可能适用于网络摄像头拍的正对着人脸的,一个人脸图像。而不是针对一般的有一群人,人脸小一些的,或者人脸不是正面的图像,你可以那lena图像试一下,它只能检测到一只左眼(真实的右眼),而另外一只检测不到,那么就会返回一个无效的点Point(-1,-1)作为眼睛的中心,那么更别提后面的旋转了,即后面的旋转肯定也是不对的。在你用本程序测试的时候,一定要选择一个合理的图像。

2、我讲一下关于旋转平移的代码的理解:

首先我们看下图:

这是我们的目的图像,满足的要求为:

(1)大小为70*70;

(2)两个眼睛直接的距离为(1-0.16)*70;(图中的(0.16,0.14)是左眼中心在图像中比例位置,由于人眼时对称的,则右眼所在比例位置为(0.68,0.14),要想获得真实的位置乘以70即可,对于本例是这样的)

(3)两个人眼连线的中心位置;

有了上述三个条件,我们就可以固定一双人眼在一个大小(这里是70*70)固定的图像中的具体位置。下面的代码就是要实现这个功能:

[cpp]  view plain copy
  1. Point2f eyesCenter;//原图像两个眼睛连续的中心点(参考下图参)  
  2.     eyesCenter.x=(leftEye.x+rightEye.x)*0.5f;  
  3.     eyesCenter.y=(leftEye.y+rightEye.y)*0.5f;  
  4. double dy=(rightEye.y-leftEye.y);  
  5.     double dx=(rightEye.x-leftEye.x);  
  6.     double len=sqrt(dx*dx+dy*dy);//原图像两个眼睛之间的距离  
  7. double angle=atan2(dy,dx)*180.0/CV_PI;//计算出来的旋转角度  
[cpp]  view plain copy
  1. //目标图像的位置  
  2. const double DESIRED_RIGHT_EYE_X=1.0f-0.16;  
  3. const int DESIRED_FACE_WIDTH=70;  
  4.     const int DESIRED_FACE_HEIGHT=70;  
  5.     double desiredLen=(DESIRED_RIGHT_EYE_X-0.16);//目标图像两个眼睛直接的比例距离,乘以WIDTH=70即得到距离  
  6. double scale=desiredLen*DESIRED_FACE_WIDTH/len;//通过目的图像两个眼睛距离除以原图像两个眼睛的距离,得到旋转矩阵的尺度因子.  
[cpp]  view plain copy
  1. Mat rot_mat = getRotationMatrix2D(eyesCenter, angle, scale);//绕原图像两眼连线中心点旋转,旋转角度为angle,缩放尺度为scale  
[cpp]  view plain copy
  1. //难点部分理解,实现中心点的平移,来控制两个眼睛在图像中的位置:  
  2.            double ex=DESIRED_FACE_WIDTH * 0.5f - eyesCenter.x;//获取x方向的平移因子,即目标两眼连线中心点的x坐标—原图像两眼连线中心点x坐标  
  3. double ey = DESIRED_FACE_HEIGHT * DESIRED_LEFT_EYE_Y-eyesCenter.y;//获取x方向的平移因子,即目标两眼连线中心点的x坐标—原图像两眼连线中心点x坐标  
  4.     rot_mat.at<double>(0, 2) += ex;//将上述结果加到旋转矩阵中控制x平移的位置  
  5.     rot_mat.at<double>(1, 2) += ey;//将上述结果加到旋转矩阵中控制y平移的位置  
  6.     Mat warped = Mat(DESIRED_FACE_HEIGHT, DESIRED_FACE_WIDTH,CV_8U, Scalar(128));  
  7.     warpAffine(img_rect, warped, rot_mat, warped.size());  


我们可以假想一下,上边的代码如果没有最后一个中心点的平移,之前的旋转矩阵只能控制图像的缩放和两个眼睛直接的相对位置,但是控制不了两个眼睛在图像中的位置,即固定两个眼在图像中的位置。

补充知识(仿射变换):

上图中a0,b0为控制平移的因子,如果我们领a2=1,a1=0,b2=0,b1=1,即变为u=x+a0,v=y+b0;
参考图:


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/327197
推荐阅读
相关标签
  

闽ICP备14008679号