20ec7c837b5b0137fd963575dc721a8d780b526d
[facial-landmarks-for-cubism.git] / src / facial_landmark_detector.cpp
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