Commit | Line | Data |
---|---|---|
830d0ba4 AIL |
1 | /**** |
2 | Copyright (c) 2020 Adrian I. Lam | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in all | |
12 | copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
20 | SOFTWARE. | |
21 | ****/ | |
22 | ||
23 | #include <stdexcept> | |
24 | #include <fstream> | |
25 | #include <string> | |
26 | #include <sstream> | |
27 | #include <cmath> | |
28 | ||
29 | #include <opencv2/opencv.hpp> | |
30 | ||
31 | #include <dlib/opencv.h> | |
32 | #include <dlib/image_processing/frontal_face_detector.h> | |
33 | #include <dlib/image_processing.h> | |
34 | #include <dlib/image_processing/render_face_detections.h> | |
35 | ||
36 | #include "facial_landmark_detector.h" | |
37 | #include "math_utils.h" | |
38 | ||
39 | ||
40 | static void filterPush(std::deque<double>& buf, double newval, | |
41 | std::size_t numTaps) | |
42 | { | |
43 | buf.push_back(newval); | |
44 | while (buf.size() > numTaps) | |
45 | { | |
46 | buf.pop_front(); | |
47 | } | |
48 | } | |
49 | ||
50 | FacialLandmarkDetector::FacialLandmarkDetector(std::string cfgPath) | |
51 | : m_stop(false) | |
52 | { | |
53 | parseConfig(cfgPath); | |
54 | ||
55 | if (!webcam.open(m_cfg.cvVideoCaptureId)) | |
56 | { | |
57 | throw std::runtime_error("Unable to open webcam"); | |
58 | } | |
59 | ||
60 | detector = dlib::get_frontal_face_detector(); | |
61 | dlib::deserialize(m_cfg.predictorPath) >> predictor; | |
62 | } | |
63 | ||
64 | FacialLandmarkDetector::Params FacialLandmarkDetector::getParams(void) const | |
65 | { | |
66 | Params params; | |
67 | ||
68 | params.faceXAngle = avg(m_faceXAngle); | |
69 | params.faceYAngle = avg(m_faceYAngle) + m_cfg.faceYAngleCorrection; | |
70 | // + 10 correct for angle between computer monitor and webcam | |
71 | params.faceZAngle = avg(m_faceZAngle); | |
72 | params.mouthOpenness = avg(m_mouthOpenness); | |
73 | params.mouthForm = avg(m_mouthForm); | |
74 | ||
75 | double leftEye = avg(m_leftEyeOpenness, 1); | |
76 | double rightEye = avg(m_rightEyeOpenness, 1); | |
77 | // Just combine the two to get better synchronized blinks | |
78 | // This effectively disables winks, so if we want to | |
79 | // support winks in the future (see below) we will need | |
80 | // a better way to handle this out-of-sync blinks. | |
81 | double bothEyes = (leftEye + rightEye) / 2; | |
82 | leftEye = bothEyes; | |
83 | rightEye = bothEyes; | |
84 | // Detect winks and make them look better | |
85 | // Commenting out - winks are difficult to be detected by the | |
86 | // dlib data set anyway... maybe in the future we can | |
87 | // add a runtime option to enable/disable... | |
88 | /*if (right == 0 && left > 0.2) | |
89 | { | |
90 | left = 1; | |
91 | } | |
92 | else if (left == 0 && right > 0.2) | |
93 | { | |
94 | right = 1; | |
95 | } | |
96 | */ | |
97 | params.leftEyeOpenness = leftEye; | |
98 | params.rightEyeOpenness = rightEye; | |
99 | ||
100 | if (leftEye <= m_cfg.eyeSmileEyeOpenThreshold && | |
101 | rightEye <= m_cfg.eyeSmileEyeOpenThreshold && | |
102 | params.mouthForm > m_cfg.eyeSmileMouthFormThreshold && | |
103 | params.mouthOpenness > m_cfg.eyeSmileMouthOpenThreshold) | |
104 | { | |
105 | params.leftEyeSmile = 1; | |
106 | params.rightEyeSmile = 1; | |
107 | } | |
108 | else | |
109 | { | |
110 | params.leftEyeSmile = 0; | |
111 | params.rightEyeSmile = 0; | |
112 | } | |
113 | ||
114 | return params; | |
115 | } | |
116 | ||
117 | void FacialLandmarkDetector::stop(void) | |
118 | { | |
119 | m_stop = true; | |
120 | } | |
121 | ||
122 | void FacialLandmarkDetector::mainLoop(void) | |
123 | { | |
124 | while (!m_stop) | |
125 | { | |
126 | cv::Mat frame; | |
127 | if (!webcam.read(frame)) | |
128 | { | |
129 | throw std::runtime_error("Unable to read from webcam"); | |
130 | } | |
131 | cv::Mat flipped; | |
132 | if (m_cfg.lateralInversion) | |
133 | { | |
134 | cv::flip(frame, flipped, 1); | |
135 | } | |
136 | else | |
137 | { | |
138 | flipped = frame; | |
139 | } | |
140 | dlib::cv_image<dlib::bgr_pixel> cimg(flipped); | |
141 | ||
142 | if (m_cfg.showWebcamVideo) | |
143 | { | |
144 | win.set_image(cimg); | |
145 | } | |
146 | ||
147 | std::vector<dlib::rectangle> faces = detector(cimg); | |
148 | ||
149 | if (faces.size() > 0) | |
150 | { | |
151 | dlib::rectangle face = faces[0]; | |
152 | dlib::full_object_detection shape = predictor(cimg, face); | |
153 | ||
154 | /* The coordinates seem to be rather noisy in general. | |
155 | * We will push everything through some moving average filters | |
156 | * to reduce noise. The number of taps is determined empirically | |
157 | * until we get something good. | |
158 | * An alternative method would be to get some better dataset | |
159 | * for dlib - perhaps even to train on a custom data set just for the user. | |
160 | */ | |
161 | ||
162 | // Face rotation: X direction (left-right) | |
163 | double faceXRot = calcFaceXAngle(shape); | |
164 | filterPush(m_faceXAngle, faceXRot, m_cfg.faceXAngleNumTaps); | |
165 | ||
166 | // Mouth form (smile / laugh) detection | |
167 | double mouthForm = calcMouthForm(shape); | |
168 | filterPush(m_mouthForm, mouthForm, m_cfg.mouthFormNumTaps); | |
169 | ||
170 | // Face rotation: Y direction (up-down) | |
171 | double faceYRot = calcFaceYAngle(shape, faceXRot, mouthForm); | |
172 | filterPush(m_faceYAngle, faceYRot, m_cfg.faceYAngleNumTaps); | |
173 | ||
174 | // Face rotation: Z direction (head tilt) | |
175 | double faceZRot = calcFaceZAngle(shape); | |
176 | filterPush(m_faceZAngle, faceZRot, m_cfg.faceZAngleNumTaps); | |
177 | ||
178 | // Mouth openness | |
179 | double mouthOpen = calcMouthOpenness(shape, mouthForm); | |
180 | filterPush(m_mouthOpenness, mouthOpen, m_cfg.mouthOpenNumTaps); | |
181 | ||
182 | // Eye openness | |
183 | double eyeLeftOpen = calcEyeOpenness(LEFT, shape, faceYRot); | |
184 | filterPush(m_leftEyeOpenness, eyeLeftOpen, m_cfg.leftEyeOpenNumTaps); | |
185 | double eyeRightOpen = calcEyeOpenness(RIGHT, shape, faceYRot); | |
186 | filterPush(m_rightEyeOpenness, eyeRightOpen, m_cfg.rightEyeOpenNumTaps); | |
187 | ||
188 | // TODO eyebrows? | |
189 | ||
190 | if (m_cfg.showWebcamVideo && m_cfg.renderLandmarksOnVideo) | |
191 | { | |
192 | win.clear_overlay(); | |
193 | win.add_overlay(dlib::render_face_detections(shape)); | |
194 | } | |
195 | } | |
196 | else | |
197 | { | |
198 | if (m_cfg.showWebcamVideo && m_cfg.renderLandmarksOnVideo) | |
199 | { | |
200 | win.clear_overlay(); | |
201 | } | |
202 | } | |
203 | ||
204 | cv::waitKey(m_cfg.cvWaitKeyMs); | |
205 | } | |
206 | } | |
207 | ||
208 | double FacialLandmarkDetector::calcEyeAspectRatio( | |
209 | dlib::point& p1, dlib::point& p2, | |
210 | dlib::point& p3, dlib::point& p4, | |
211 | dlib::point& p5, dlib::point& p6) const | |
212 | { | |
213 | double eyeWidth = dist(p1, p4); | |
214 | double eyeHeight1 = dist(p2, p6); | |
215 | double eyeHeight2 = dist(p3, p5); | |
216 | ||
217 | return (eyeHeight1 + eyeHeight2) / (2 * eyeWidth); | |
218 | } | |
219 | ||
220 | double FacialLandmarkDetector::calcEyeOpenness( | |
221 | LeftRight eye, | |
222 | dlib::full_object_detection& shape, | |
223 | double faceYAngle) const | |
224 | { | |
225 | double eyeAspectRatio; | |
226 | if (eye == LEFT) | |
227 | { | |
228 | eyeAspectRatio = calcEyeAspectRatio(shape.part(42), shape.part(43), shape.part(44), | |
229 | shape.part(45), shape.part(46), shape.part(47)); | |
230 | } | |
231 | else | |
232 | { | |
233 | eyeAspectRatio = calcEyeAspectRatio(shape.part(36), shape.part(37), shape.part(38), | |
234 | shape.part(39), shape.part(40), shape.part(41)); | |
235 | } | |
236 | ||
237 | // Apply correction due to faceYAngle | |
238 | double corrEyeAspRat = eyeAspectRatio / std::cos(degToRad(faceYAngle)); | |
239 | ||
240 | return linearScale01(corrEyeAspRat, m_cfg.eyeClosedThreshold, m_cfg.eyeOpenThreshold); | |
241 | } | |
242 | ||
243 | ||
244 | ||
245 | double FacialLandmarkDetector::calcMouthForm(dlib::full_object_detection& shape) const | |
246 | { | |
247 | /* Mouth form parameter: 0 for normal mouth, 1 for fully smiling / laughing. | |
248 | * Compare distance between the two corners of the mouth | |
249 | * to the distance between the two eyes. | |
250 | */ | |
251 | ||
252 | /* An alternative (my initial attempt) was to compare the corners of | |
253 | * the mouth to the top of the upper lip - they almost lie on a | |
254 | * straight line when smiling / laughing. But that is only true | |
255 | * when facing straight at the camera. When looking up / down, | |
256 | * the angle changes. So here we'll use the distance approach instead. | |
257 | */ | |
258 | ||
259 | auto eye1 = centroid(shape.part(36), shape.part(37), shape.part(38), | |
260 | shape.part(39), shape.part(40), shape.part(41)); | |
261 | auto eye2 = centroid(shape.part(42), shape.part(43), shape.part(44), | |
262 | shape.part(45), shape.part(46), shape.part(47)); | |
263 | double distEyes = dist(eye1, eye2); | |
264 | double distMouth = dist(shape.part(48), shape.part(54)); | |
265 | ||
266 | double form = linearScale01(distMouth / distEyes, | |
267 | m_cfg.mouthNormalThreshold, | |
268 | m_cfg.mouthSmileThreshold); | |
269 | ||
270 | return form; | |
271 | } | |
272 | ||
273 | double FacialLandmarkDetector::calcMouthOpenness( | |
274 | dlib::full_object_detection& shape, | |
275 | double mouthForm) const | |
276 | { | |
277 | // Use points for the bottom of the upper lip, and top of the lower lip | |
278 | // We have 3 pairs of points available, which give the mouth height | |
279 | // on the left, in the middle, and on the right, resp. | |
280 | // First let's try to use an average of all three. | |
281 | double heightLeft = dist(shape.part(63), shape.part(65)); | |
282 | double heightMiddle = dist(shape.part(62), shape.part(66)); | |
283 | double heightRight = dist(shape.part(61), shape.part(67)); | |
284 | ||
285 | double avgHeight = (heightLeft + heightMiddle + heightRight) / 3; | |
286 | ||
287 | // Now, normalize it with the width of the mouth. | |
288 | double width = dist(shape.part(60), shape.part(64)); | |
289 | ||
290 | double normalized = avgHeight / width; | |
291 | ||
292 | double scaled = linearScale01(normalized, | |
293 | m_cfg.mouthClosedThreshold, | |
294 | m_cfg.mouthOpenThreshold, | |
295 | true, false); | |
296 | ||
297 | // Apply correction according to mouthForm | |
298 | // Notice that when you smile / laugh, width is increased | |
299 | scaled *= (1 + m_cfg.mouthOpenLaughCorrection * mouthForm); | |
300 | ||
301 | return scaled; | |
302 | } | |
303 | ||
304 | double FacialLandmarkDetector::calcFaceXAngle(dlib::full_object_detection& shape) const | |
305 | { | |
306 | // This function will be easier to understand if you refer to the | |
307 | // diagram in faceXAngle.png | |
308 | ||
309 | // Construct the y-axis using (1) average of four points on the nose and | |
310 | // (2) average of four points on the upper lip. | |
311 | ||
312 | auto y0 = centroid(shape.part(27), shape.part(28), shape.part(29), | |
313 | shape.part(30)); | |
314 | auto y1 = centroid(shape.part(50), shape.part(51), shape.part(52), | |
315 | shape.part(62)); | |
316 | ||
317 | // Now drop a perpedicular from the left and right edges of the face, | |
318 | // and calculate the ratio between the lengths of these perpendiculars | |
319 | ||
320 | auto left = centroid(shape.part(14), shape.part(15), shape.part(16)); | |
321 | auto right = centroid(shape.part(0), shape.part(1), shape.part(2)); | |
322 | ||
323 | // Constructing a perpendicular: | |
324 | // Join the left/right point and the upper lip. The included angle | |
325 | // can now be determined using cosine rule. | |
326 | // Then sine of this angle is the perpendicular divided by the newly | |
327 | // created line. | |
328 | double opp = dist(right, y0); | |
329 | double adj1 = dist(y0, y1); | |
330 | double adj2 = dist(y1, right); | |
331 | double angle = solveCosineRuleAngle(opp, adj1, adj2); | |
332 | double perpRight = adj2 * std::sin(angle); | |
333 | ||
334 | opp = dist(left, y0); | |
335 | adj2 = dist(y1, left); | |
336 | angle = solveCosineRuleAngle(opp, adj1, adj2); | |
337 | double perpLeft = adj2 * std::sin(angle); | |
338 | ||
339 | // Model the head as a sphere and look from above. | |
340 | double theta = std::asin((perpRight - perpLeft) / (perpRight + perpLeft)); | |
341 | ||
342 | theta = radToDeg(theta); | |
343 | if (theta < -30) theta = -30; | |
344 | if (theta > 30) theta = 30; | |
345 | return theta; | |
346 | } | |
347 | ||
348 | double FacialLandmarkDetector::calcFaceYAngle(dlib::full_object_detection& shape, double faceXAngle, double mouthForm) const | |
349 | { | |
350 | // Use the nose | |
351 | // angle between the two left/right points and the tip | |
352 | double c = dist(shape.part(31), shape.part(35)); | |
353 | double a = dist(shape.part(30), shape.part(31)); | |
354 | double b = dist(shape.part(30), shape.part(35)); | |
355 | ||
356 | double angle = solveCosineRuleAngle(c, a, b); | |
357 | ||
358 | // This probably varies a lot from person to person... | |
359 | ||
360 | // Best is probably to work out some trigonometry again, | |
361 | // but just linear interpolation seems to work ok... | |
362 | ||
363 | // Correct for X rotation | |
364 | double corrAngle = angle * (1 + (std::abs(faceXAngle) / 30 | |
365 | * m_cfg.faceYAngleXRotCorrection)); | |
366 | ||
367 | // Correct for smiles / laughs - this increases the angle | |
368 | corrAngle *= (1 - mouthForm * m_cfg.faceYAngleSmileCorrection); | |
369 | ||
370 | if (corrAngle >= m_cfg.faceYAngleZeroValue) | |
371 | { | |
372 | return -30 * linearScale01(corrAngle, | |
373 | m_cfg.faceYAngleZeroValue, | |
374 | m_cfg.faceYAngleDownThreshold, | |
375 | false, false); | |
376 | } | |
377 | else | |
378 | { | |
379 | return 30 * (1 - linearScale01(corrAngle, | |
380 | m_cfg.faceYAngleUpThreshold, | |
381 | m_cfg.faceYAngleZeroValue, | |
382 | false, false)); | |
383 | } | |
384 | } | |
385 | ||
386 | double FacialLandmarkDetector::calcFaceZAngle(dlib::full_object_detection& shape) const | |
387 | { | |
388 | // Use average of eyes and nose | |
389 | ||
390 | auto eyeRight = centroid(shape.part(36), shape.part(37), shape.part(38), | |
391 | shape.part(39), shape.part(40), shape.part(41)); | |
392 | auto eyeLeft = centroid(shape.part(42), shape.part(43), shape.part(44), | |
393 | shape.part(45), shape.part(46), shape.part(47)); | |
394 | ||
395 | auto noseLeft = shape.part(35); | |
396 | auto noseRight = shape.part(31); | |
397 | ||
398 | double eyeYDiff = eyeRight.y() - eyeLeft.y(); | |
399 | double eyeXDiff = eyeRight.x() - eyeLeft.x(); | |
400 | ||
401 | double angle1 = std::atan(eyeYDiff / eyeXDiff); | |
402 | ||
403 | double noseYDiff = noseRight.y() - noseLeft.y(); | |
404 | double noseXDiff = noseRight.x() - noseLeft.x(); | |
405 | ||
406 | double angle2 = std::atan(noseYDiff / noseXDiff); | |
407 | ||
408 | return radToDeg((angle1 + angle2) / 2); | |
409 | } | |
410 | ||
411 | void FacialLandmarkDetector::parseConfig(std::string cfgPath) | |
412 | { | |
413 | populateDefaultConfig(); | |
414 | if (cfgPath != "") | |
415 | { | |
416 | std::ifstream file(cfgPath); | |
417 | ||
418 | if (!file) | |
419 | { | |
420 | throw std::runtime_error("Failed to open config file"); | |
421 | } | |
422 | ||
423 | std::string line; | |
424 | unsigned int lineNum = 0; | |
425 | ||
426 | while (std::getline(file, line)) | |
427 | { | |
428 | lineNum++; | |
429 | ||
430 | if (line[0] == '#') | |
431 | { | |
432 | continue; | |
433 | } | |
434 | ||
435 | std::istringstream ss(line); | |
436 | std::string paramName; | |
437 | if (ss >> paramName) | |
438 | { | |
439 | if (paramName == "cvVideoCaptureId") | |
440 | { | |
441 | if (!(ss >> m_cfg.cvVideoCaptureId)) | |
442 | { | |
443 | throwConfigError(paramName, "int", | |
444 | line, lineNum); | |
445 | } | |
446 | } | |
447 | else if (paramName == "predictorPath") | |
448 | { | |
449 | if (!(ss >> m_cfg.predictorPath)) | |
450 | { | |
451 | throwConfigError(paramName, "std::string", | |
452 | line, lineNum); | |
453 | } | |
454 | } | |
455 | else if (paramName == "faceYAngleCorrection") | |
456 | { | |
457 | if (!(ss >> m_cfg.faceYAngleCorrection)) | |
458 | { | |
459 | throwConfigError(paramName, "double", | |
460 | line, lineNum); | |
461 | } | |
462 | } | |
463 | else if (paramName == "eyeSmileEyeOpenThreshold") | |
464 | { | |
465 | if (!(ss >> m_cfg.eyeSmileEyeOpenThreshold)) | |
466 | { | |
467 | throwConfigError(paramName, "double", | |
468 | line, lineNum); | |
469 | } | |
470 | } | |
471 | else if (paramName == "eyeSmileMouthFormThreshold") | |
472 | { | |
473 | if (!(ss >> m_cfg.eyeSmileMouthFormThreshold)) | |
474 | { | |
475 | throwConfigError(paramName, "double", | |
476 | line, lineNum); | |
477 | } | |
478 | } | |
479 | else if (paramName == "eyeSmileMouthOpenThreshold") | |
480 | { | |
481 | if (!(ss >> m_cfg.eyeSmileMouthOpenThreshold)) | |
482 | { | |
483 | throwConfigError(paramName, "double", | |
484 | line, lineNum); | |
485 | } | |
486 | } | |
487 | else if (paramName == "showWebcamVideo") | |
488 | { | |
489 | if (!(ss >> m_cfg.showWebcamVideo)) | |
490 | { | |
491 | throwConfigError(paramName, "bool", | |
492 | line, lineNum); | |
493 | } | |
494 | } | |
495 | else if (paramName == "renderLandmarksOnVideo") | |
496 | { | |
497 | if (!(ss >> m_cfg.renderLandmarksOnVideo)) | |
498 | { | |
499 | throwConfigError(paramName, "bool", | |
500 | line, lineNum); | |
501 | } | |
502 | } | |
503 | else if (paramName == "lateralInversion") | |
504 | { | |
505 | if (!(ss >> m_cfg.lateralInversion)) | |
506 | { | |
507 | throwConfigError(paramName, "bool", | |
508 | line, lineNum); | |
509 | } | |
510 | } | |
511 | else if (paramName == "faceXAngleNumTaps") | |
512 | { | |
513 | if (!(ss >> m_cfg.faceXAngleNumTaps)) | |
514 | { | |
515 | throwConfigError(paramName, "std::size_t", | |
516 | line, lineNum); | |
517 | } | |
518 | } | |
519 | else if (paramName == "faceYAngleNumTaps") | |
520 | { | |
521 | if (!(ss >> m_cfg.faceYAngleNumTaps)) | |
522 | { | |
523 | throwConfigError(paramName, "std::size_t", | |
524 | line, lineNum); | |
525 | } | |
526 | } | |
527 | else if (paramName == "faceZAngleNumTaps") | |
528 | { | |
529 | if (!(ss >> m_cfg.faceZAngleNumTaps)) | |
530 | { | |
531 | throwConfigError(paramName, "std::size_t", | |
532 | line, lineNum); | |
533 | } | |
534 | } | |
535 | else if (paramName == "mouthFormNumTaps") | |
536 | { | |
537 | if (!(ss >> m_cfg.mouthFormNumTaps)) | |
538 | { | |
539 | throwConfigError(paramName, "std::size_t", | |
540 | line, lineNum); | |
541 | } | |
542 | } | |
543 | else if (paramName == "mouthOpenNumTaps") | |
544 | { | |
545 | if (!(ss >> m_cfg.mouthOpenNumTaps)) | |
546 | { | |
547 | throwConfigError(paramName, "std::size_t", | |
548 | line, lineNum); | |
549 | } | |
550 | } | |
551 | else if (paramName == "leftEyeOpenNumTaps") | |
552 | { | |
553 | if (!(ss >> m_cfg.leftEyeOpenNumTaps)) | |
554 | { | |
555 | throwConfigError(paramName, "std::size_t", | |
556 | line, lineNum); | |
557 | } | |
558 | } | |
559 | else if (paramName == "rightEyeOpenNumTaps") | |
560 | { | |
561 | if (!(ss >> m_cfg.rightEyeOpenNumTaps)) | |
562 | { | |
563 | throwConfigError(paramName, "std::size_t", | |
564 | line, lineNum); | |
565 | } | |
566 | } | |
567 | else if (paramName == "cvWaitKeyMs") | |
568 | { | |
569 | if (!(ss >> m_cfg.cvWaitKeyMs)) | |
570 | { | |
571 | throwConfigError(paramName, "int", | |
572 | line, lineNum); | |
573 | } | |
574 | } | |
575 | else if (paramName == "eyeClosedThreshold") | |
576 | { | |
577 | if (!(ss >> m_cfg.eyeClosedThreshold)) | |
578 | { | |
579 | throwConfigError(paramName, "double", | |
580 | line, lineNum); | |
581 | } | |
582 | } | |
583 | else if (paramName == "eyeOpenThreshold") | |
584 | { | |
585 | if (!(ss >> m_cfg.eyeOpenThreshold)) | |
586 | { | |
587 | throwConfigError(paramName, "double", | |
588 | line, lineNum); | |
589 | } | |
590 | } | |
591 | else if (paramName == "mouthNormalThreshold") | |
592 | { | |
593 | if (!(ss >> m_cfg.mouthNormalThreshold)) | |
594 | { | |
595 | throwConfigError(paramName, "double", | |
596 | line, lineNum); | |
597 | } | |
598 | } | |
599 | else if (paramName == "mouthSmileThreshold") | |
600 | { | |
601 | if (!(ss >> m_cfg.mouthSmileThreshold)) | |
602 | { | |
603 | throwConfigError(paramName, "double", | |
604 | line, lineNum); | |
605 | } | |
606 | } | |
607 | else if (paramName == "mouthClosedThreshold") | |
608 | { | |
609 | if (!(ss >> m_cfg.mouthClosedThreshold)) | |
610 | { | |
611 | throwConfigError(paramName, "double", | |
612 | line, lineNum); | |
613 | } | |
614 | } | |
615 | else if (paramName == "mouthOpenThreshold") | |
616 | { | |
617 | if (!(ss >> m_cfg.mouthOpenThreshold)) | |
618 | { | |
619 | throwConfigError(paramName, "double", | |
620 | line, lineNum); | |
621 | } | |
622 | } | |
623 | else if (paramName == "mouthOpenLaughCorrection") | |
624 | { | |
625 | if (!(ss >> m_cfg.mouthOpenLaughCorrection)) | |
626 | { | |
627 | throwConfigError(paramName, "double", | |
628 | line, lineNum); | |
629 | } | |
630 | } | |
631 | else if (paramName == "faceYAngleXRotCorrection") | |
632 | { | |
633 | if (!(ss >> m_cfg.faceYAngleXRotCorrection)) | |
634 | { | |
635 | throwConfigError(paramName, "double", | |
636 | line, lineNum); | |
637 | } | |
638 | } | |
639 | else if (paramName == "faceYAngleSmileCorrection") | |
640 | { | |
641 | if (!(ss >> m_cfg.faceYAngleSmileCorrection)) | |
642 | { | |
643 | throwConfigError(paramName, "double", | |
644 | line, lineNum); | |
645 | } | |
646 | } | |
647 | else if (paramName == "faceYAngleZeroValue") | |
648 | { | |
649 | if (!(ss >> m_cfg.faceYAngleZeroValue)) | |
650 | { | |
651 | throwConfigError(paramName, "double", | |
652 | line, lineNum); | |
653 | } | |
654 | } | |
655 | else if (paramName == "faceYAngleUpThreshold") | |
656 | { | |
657 | if (!(ss >> m_cfg.faceYAngleUpThreshold)) | |
658 | { | |
659 | throwConfigError(paramName, "double", | |
660 | line, lineNum); | |
661 | } | |
662 | } | |
663 | else if (paramName == "faceYAngleDownThreshold") | |
664 | { | |
665 | if (!(ss >> m_cfg.faceYAngleDownThreshold)) | |
666 | { | |
667 | throwConfigError(paramName, "double", | |
668 | line, lineNum); | |
669 | } | |
670 | } | |
671 | else | |
672 | { | |
673 | std::ostringstream oss; | |
674 | oss << "Unrecognized parameter name at line " << lineNum | |
675 | << ": " << paramName; | |
676 | throw std::runtime_error(oss.str()); | |
677 | } | |
678 | } | |
679 | } | |
680 | } | |
681 | } | |
682 | ||
683 | void FacialLandmarkDetector::populateDefaultConfig(void) | |
684 | { | |
685 | // These are values that I've personally tested to work OK for my face. | |
686 | // Your milage may vary - hence the config file. | |
687 | ||
688 | m_cfg.cvVideoCaptureId = 0; | |
689 | m_cfg.predictorPath = "shape_predictor_68_face_landmarks.dat"; | |
690 | m_cfg.faceYAngleCorrection = 10; | |
691 | m_cfg.eyeSmileEyeOpenThreshold = 0.6; | |
692 | m_cfg.eyeSmileMouthFormThreshold = 0.75; | |
693 | m_cfg.eyeSmileMouthOpenThreshold = 0.5; | |
694 | m_cfg.showWebcamVideo = true; | |
695 | m_cfg.renderLandmarksOnVideo = true; | |
696 | m_cfg.lateralInversion = true; | |
697 | m_cfg.cvWaitKeyMs = 5; | |
698 | m_cfg.faceXAngleNumTaps = 11; | |
699 | m_cfg.faceYAngleNumTaps = 11; | |
700 | m_cfg.faceZAngleNumTaps = 11; | |
701 | m_cfg.mouthFormNumTaps = 3; | |
702 | m_cfg.mouthOpenNumTaps = 3; | |
703 | m_cfg.leftEyeOpenNumTaps = 3; | |
704 | m_cfg.rightEyeOpenNumTaps = 3; | |
705 | m_cfg.eyeClosedThreshold = 0.2; | |
706 | m_cfg.eyeOpenThreshold = 0.25; | |
707 | m_cfg.mouthNormalThreshold = 0.75; | |
708 | m_cfg.mouthSmileThreshold = 1.0; | |
709 | m_cfg.mouthClosedThreshold = 0.1; | |
710 | m_cfg.mouthOpenThreshold = 0.4; | |
711 | m_cfg.mouthOpenLaughCorrection = 0.2; | |
712 | m_cfg.faceYAngleXRotCorrection = 0.15; | |
713 | m_cfg.faceYAngleSmileCorrection = 0.075; | |
714 | m_cfg.faceYAngleZeroValue = 1.8; | |
715 | m_cfg.faceYAngleDownThreshold = 2.3; | |
716 | m_cfg.faceYAngleUpThreshold = 1.3; | |
717 | } | |
718 | ||
719 | void FacialLandmarkDetector::throwConfigError(std::string paramName, | |
720 | std::string expectedType, | |
721 | std::string line, | |
722 | unsigned int lineNum) | |
723 | { | |
724 | std::ostringstream ss; | |
725 | ss << "Error parsing config file for parameter " << paramName | |
726 | << "\nAt line " << lineNum << ": " << line | |
727 | << "\nExpecting value of type " << expectedType; | |
728 | ||
729 | throw std::runtime_error(ss.str()); | |
730 | } | |
731 |