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