├── .gitignore ├── images ├── lenna.png ├── gioconda.jpg └── lenna.features.png ├── package.json ├── LICENSE ├── examples └── identify.js ├── lib ├── faced.js ├── detector.js ├── feature.js └── face.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /images/lenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/faced/master/images/lenna.png -------------------------------------------------------------------------------- /images/gioconda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/faced/master/images/gioconda.jpg -------------------------------------------------------------------------------- /images/lenna.features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmx/faced/master/images/lenna.features.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faced", 3 | "version": "1.1.3", 4 | "description": "light-weight library for face recognition including features such as eyes, nose and mouth.", 5 | "main": "lib/faced.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "dependencies": { 10 | "opencv": "~0.3.1", 11 | "underscore": "~1.4.4" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/gordalina/faced.git" 20 | }, 21 | "keywords": [ 22 | "face", 23 | "face detection", 24 | "face recognition", 25 | "face identification", 26 | "detection", 27 | "recognition" 28 | ], 29 | "author": "Samuel Gordalina", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Samuel Gordalina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /examples/identify.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true*/ 2 | "use strict"; 3 | 4 | var path = require('path'); 5 | var _ = require('underscore'); 6 | var Faced = require('./../lib/faced'); 7 | var faced = new Faced(); 8 | 9 | function worker(faces, image, file) { 10 | var output, colors = { 11 | "face": [0, 0, 0], 12 | "mouth": [255, 0, 0], 13 | "nose": [255, 255, 255], 14 | "eyeLeft": [0, 0, 255], 15 | "eyeRight": [0, 255, 0] 16 | }; 17 | 18 | if (!faces) { 19 | console.error("Could not open %s", file); 20 | return; 21 | } 22 | 23 | function draw(feature, color) { 24 | image.rectangle( 25 | [feature.getX(), feature.getY()], 26 | [feature.getWidth(), feature.getHeight()], 27 | color, 28 | 2 29 | ); 30 | } 31 | 32 | _.each(faces, function (face) { 33 | draw(face, colors.face); 34 | 35 | _.each(face.getFeatures(), function (features, name) { 36 | _.each(features, function (feature) { 37 | draw(feature, colors[name]); 38 | }); 39 | }); 40 | }); 41 | 42 | output = file.split('.'); 43 | output.push('features', output.pop()); 44 | output = output.join('.'); 45 | 46 | console.log('Processed %s', output); 47 | image.save(output); 48 | } 49 | 50 | _.each(process.argv.slice(2), function (file) { 51 | faced.detect(path.resolve(file), worker); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/faced.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen:true*/ 2 | "use strict"; 3 | 4 | var _ = require("underscore"); 5 | var OpenCV = require("opencv"); 6 | var Detector = require("./detector"); 7 | var path = require('path'); 8 | 9 | var hc_path = path.join(path.dirname(require.resolve("opencv")), '..', 'data'); 10 | var cascades = { 11 | "face": path.join(hc_path, "haarcascade_frontalface_alt2.xml"), 12 | "mouth": path.join(hc_path, "haarcascade_mcs_mouth.xml"), 13 | "nose": path.join(hc_path, "haarcascade_mcs_nose.xml"), 14 | "eyeLeft": path.join(hc_path, "haarcascade_mcs_lefteye.xml"), 15 | "eyeRight": path.join(hc_path, "haarcascade_mcs_righteye.xml") 16 | }; 17 | 18 | function Faced() { 19 | this.cascades = {}; 20 | 21 | _.each(cascades, function (path, element) { 22 | this.cascades[element] = new OpenCV.CascadeClassifier(path); 23 | }, this); 24 | } 25 | 26 | Faced.prototype.detect = function (path, fn, context) { 27 | if (!this.cascades) { 28 | throw new Error("Faced has been destroyed"); 29 | } 30 | 31 | OpenCV.readImage(path, _.bind(function (err, img) { 32 | var detector, size; 33 | 34 | if (err || typeof img !== "object") { 35 | return fn.call(context, undefined, undefined, path); 36 | } 37 | 38 | size = img.size(); 39 | 40 | if (size[0] === 0 || size[1] === 0) { 41 | return fn.call(context, undefined, undefined, path); 42 | } 43 | 44 | detector = new Detector(this.cascades); 45 | detector.run(img, function (faces) { 46 | fn.call(context, faces, img, path); 47 | }); 48 | }, this)); 49 | }; 50 | 51 | Faced.prototype.destroy = function () { 52 | delete this.cascades; 53 | }; 54 | 55 | module.exports = Faced; 56 | -------------------------------------------------------------------------------- /lib/detector.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen:true*/ 2 | "use strict"; 3 | 4 | var _ = require("underscore"); 5 | var Face = require("./face"); 6 | var Feature = require("./feature"); 7 | 8 | var getFaces; 9 | 10 | var Detector = function (cascades) { 11 | this.cascades = cascades; 12 | }; 13 | 14 | Detector.prototype.run = function (image, fn, context) { 15 | var detections = {}, 16 | complete; 17 | 18 | complete = _.after(_.keys(this.cascades).length, function () { 19 | fn.call(context, getFaces(detections), image); 20 | }); 21 | 22 | _.each(this.cascades, function (cascade, element) { 23 | cascade.detectMultiScale(image, function (error, objects) { 24 | detections[element] = !error ? objects : []; 25 | complete(); 26 | }); 27 | }); 28 | }; 29 | 30 | getFaces = function (detections) { 31 | var faces = []; 32 | 33 | function outside(input, test) { 34 | return test.x > input.x + input.width || 35 | test.x + test.width < input.x || 36 | test.y > input.y + input.height || 37 | test.y + test.height < input.y; 38 | } 39 | 40 | _.each(detections.face, function (face) { 41 | var currentFace = new Face(face); 42 | 43 | _.each(detections, function (detect, element) { 44 | if (element === "face") { 45 | return; 46 | } 47 | 48 | _.each(detect, function (properties) { 49 | if (!outside(face, properties)) { 50 | currentFace.add(element, new Feature(properties)); 51 | } 52 | }); 53 | }); 54 | 55 | currentFace.normalize(); 56 | 57 | if (currentFace.getFeatureCount() > 0) { 58 | faces.push(currentFace); 59 | } 60 | }); 61 | 62 | return faces; 63 | } 64 | 65 | module.exports = Detector; 66 | -------------------------------------------------------------------------------- /lib/feature.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true*/ 2 | "use strict"; 3 | 4 | function Feature(attributes) { 5 | if (attributes) { 6 | this.x = attributes.x; 7 | this.y = attributes.y; 8 | this.width = attributes.width; 9 | this.height = attributes.height; 10 | } 11 | } 12 | 13 | Feature.prototype.getX = function () { 14 | return this.x; 15 | }; 16 | 17 | Feature.prototype.getY = function () { 18 | return this.y; 19 | }; 20 | 21 | Feature.prototype.getX2 = function () { 22 | return this.getX() + this.getWidth(); 23 | }; 24 | 25 | Feature.prototype.getY2 = function () { 26 | return this.getY() + this.getHeight(); 27 | }; 28 | 29 | Feature.prototype.getWidth = function () { 30 | return this.width; 31 | }; 32 | 33 | Feature.prototype.getHeight = function () { 34 | return this.height; 35 | }; 36 | 37 | Feature.prototype.intersect = function (feature) { 38 | var excessHeightTop, 39 | excessHeightBottom, 40 | excessWidthLeft, 41 | excessWidthRight, 42 | excessHeight, 43 | excessWidth, 44 | excess; 45 | 46 | // test for intersection 47 | if (( 48 | this.getX2() < feature.getX() || 49 | this.getX() > feature.getX2() || 50 | this.getY2() < feature.getY() || 51 | this.getY() > feature.getY2() 52 | )) { 53 | return 0; 54 | } 55 | 56 | excessHeightTop = (this.getY() - feature.getY()); 57 | excessHeightBottom = (feature.getY2() - this.getY2()); 58 | excessWidthLeft = (this.getX() - feature.getX()); 59 | excessWidthRight = (feature.getX2() - this.getX2()); 60 | 61 | excessHeight = ( 62 | (excessHeightTop > 0 ? excessHeightTop : 0) 63 | + 64 | (excessHeightBottom > 0 ? excessHeightBottom : 0) 65 | ); 66 | 67 | excessWidth = ( 68 | (excessWidthLeft > 0 ? excessWidthLeft : 0) 69 | + 70 | (excessWidthRight > 0 ? excessWidthRight : 0) 71 | ); 72 | 73 | excess = ( 74 | excessHeight * feature.getWidth() 75 | + 76 | excessWidth * feature.getHeight() 77 | ); 78 | 79 | return 1 - (excess / (feature.getWidth() * feature.getHeight())); 80 | }; 81 | 82 | module.exports = Feature; 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faced 2 | 3 | faced is a light-weight library for face recognition including features such as eyes, nose and mouth. It requires opencv. 4 | 5 | ![](https://raw.github.com/gordalina/faced/master/images/lenna.features.png) 6 | 7 | Face is outlined in **black**, the eyes are **red** & **green** for left and right respectively, the nose is outlined in **white** and the mouth in **blue**. 8 | 9 | ## Dependencies 10 | 11 | ### OpenCV 12 | Make sure you have [OpenCV](http://opencv.org/downloads.html) installed on your machine. 13 | 14 | For MacOS X you can use [Homebrew](http://brew.sh) 15 | 16 | ``` 17 | $ brew tap homebrew/science 18 | $ brew install opencv 19 | ``` 20 | 21 | ## Installation 22 | 23 | ### As a dependency to your project 24 | Just add `"faced": "1.x",` to your dependencies list in `package.json`. 25 | 26 | ### Globally 27 | `npm install -g faced` 28 | 29 | ## Identify your first face 30 | 31 | ```javascript 32 | var faced = new Faced(); 33 | faced.detect('image.jpg', function (faces, image, file) { 34 | if (!faces) { 35 | return console.log("No faces found!"); 36 | } 37 | 38 | var face = faces[0]; 39 | 40 | console.log( 41 | "Found a face at %d,%d with dimensions %dx%d", 42 | face.getX(), 43 | face.getY(), 44 | face.getWidth(), 45 | face.getHeight() 46 | ); 47 | 48 | console.log( 49 | "What a pretty face, it %s a mouth, it %s a nose, it % a left eye and it %s a right eye!", 50 | face.getMouth() ? "has" : "does not have", 51 | face.getNose() ? "has" : "does not have", 52 | face.getEyeLeft() ? "has" : "does not have", 53 | face.getEyeRight() ? "has" : "does not have" 54 | ); 55 | }); 56 | ``` 57 | 58 | Its that simple! See the program used to [generate the above image](https://github.com/gordalina/faced/blob/master/examples/identify.js) 59 | 60 | ## API 61 | 62 | ### Faced.detect(source, function, context) 63 | 64 | Loads an image from `source` which can be a file path or a buffer and executes `function` upon completion. 65 | 66 | The callback function expects a prototype like `function (faces, image, file) { }`, where the first is an array of `Face`, the second is a `Matrix` [object from opencv](https://npmjs.org/package/opencv#readme) and the third is the path of the image. 67 | 68 | In case of error the arguments `faces` and `image` will be `undefined`. 69 | 70 | ### Class `Feature` 71 | - `Feature.getX()` Returns the upper left corner X position of the face 72 | - `Feature.getY()` Returns the upper left corner Y position of the face 73 | - `Feature.getX2()` Returns the lower right corner X position of the face 74 | - `Feature.getY2()` Returns the lower right conrner Y position of the face 75 | - `Feature.getWidth()` Returns the width 76 | - `Feature.getHeight()` Returns the height 77 | - `Feature.intersect(Feature)` Returns the percentage of shared spaced that the current feature has with the given argument Feature. *Although this is used internally it might be useful for client usage.* 78 | 79 | ### Class `Face` (extends `Feature`) 80 | 81 | All of the following method1s return an instance of `Feture` or `undefined` if it could not detect. 82 | 83 | - `Face.getMouth()` 84 | - `Face.getNose()` 85 | - `Face.getEyeLeft()` 86 | - `Face.getEyeRight()` 87 | 88 | Other significant methods 89 | 90 | - `Face.getFeature(name)` Returns a feature by name. Possible names are: `mouth`, `nose`, `eyeLeft` and `eyeRight`. 91 | - `Face.getFeatures()` Returns an array of `Feature` that the instance has detected. 92 | - `Face.getFeatureCount()` Returns the number of detected features. 93 | 94 | ## Examples 95 | 96 | ```bash 97 | $ node examples/identify.js images/lenna.png 98 | ``` 99 | 100 | Then open `images/lenna.features.png` on your favourite image viewer. 101 | 102 | ## Information 103 | 104 | #### License 105 | 106 | faced is licensed under the [MIT license](http://opensource.org/licenses/MIT) 107 | 108 | #### Copyright 109 | 110 | Copyright (c) 2013, Samuel Gordalina 111 | -------------------------------------------------------------------------------- /lib/face.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true, nomen: true*/ 2 | "use strict"; 3 | 4 | var _ = require("underscore"); 5 | var Feature = require("./feature"); 6 | 7 | var allowedFeatures = [ 8 | "mouth", 9 | "nose", 10 | "eyeLeft", 11 | "eyeRight" 12 | ]; 13 | 14 | function Face(attributes) { 15 | Feature.prototype.constructor.apply(this, arguments); 16 | 17 | _.each(allowedFeatures, function (feature) { 18 | this[feature] = []; 19 | }, this); 20 | } 21 | 22 | Face.prototype = new Feature(); 23 | Face.prototype.constructor = Face; 24 | 25 | Face.prototype.add = function (name, feature) { 26 | if (feature instanceof Feature === false) { 27 | throw new TypeError("feature is not class of Instance"); 28 | } 29 | 30 | if (!_.contains(allowedFeatures, name)) { 31 | throw new Error("feature name is not allowed: " + name); 32 | } 33 | 34 | this[name].push(feature); 35 | }; 36 | 37 | Face.prototype.getFeatureCount = function (feature) { 38 | if (feature) { 39 | return this.getFeatures(feature).length; 40 | } 41 | 42 | return _.filter( 43 | _.map(allowedFeatures, function (feature) { 44 | return this[feature].length; 45 | }, this), 46 | function (count) { 47 | return count > 0; 48 | }, 49 | this 50 | ).length; 51 | }; 52 | 53 | Face.prototype.getFeature = function (feature) { 54 | if (_.contains(allowedFeatures, feature)) { 55 | switch (this[feature].length) { 56 | case 0: 57 | return; 58 | 59 | case 1: 60 | return this[feature][0]; 61 | 62 | default: 63 | return this[feature]; 64 | } 65 | } 66 | }; 67 | 68 | Face.prototype.getFeatures = function (feature) { 69 | var features = {}; 70 | 71 | if (feature && _.contains(allowedFeatures, feature)) { 72 | return this[feature]; 73 | } 74 | 75 | _.each(allowedFeatures, function (feature) { 76 | features[feature] = this[feature]; 77 | }, this); 78 | 79 | return features; 80 | }; 81 | 82 | Face.prototype.getMouth = function () { 83 | return this.getFeature("mouth"); 84 | }; 85 | 86 | Face.prototype.getNose = function () { 87 | return this.getFeature("nose"); 88 | }; 89 | 90 | Face.prototype.getEyeLeft = function () { 91 | return this.getFeature("eyeRight"); 92 | }; 93 | 94 | Face.prototype.getEyeRight = function () { 95 | return this.getFeature("eyeLeft"); 96 | }; 97 | 98 | Face.prototype.remove = function (name, feature) { 99 | this[name] = _.difference(this[name], [feature]); 100 | }; 101 | 102 | Face.prototype.getNoseCenteredness = function (nose) { 103 | var mouth = this.getMouth(), 104 | horizontal, 105 | vertical; 106 | 107 | if (!mouth) { 108 | return 0; 109 | } 110 | 111 | horizontal = ( 112 | (nose.getX() - this.getX()) 113 | + 114 | (nose.getWidth() / 2) 115 | ) / this.getWidth(); 116 | 117 | vertical = ( 118 | (nose.getY() - this.getY()) 119 | + 120 | (nose.getHeight() / 2) 121 | ) / this.getHeight(); 122 | 123 | return Math.abs( 124 | ((horizontal + vertical) / 2) - 0.5 125 | ); 126 | }; 127 | 128 | Face.prototype.normalize = function () { 129 | this.stripExternalFeatures(); 130 | 131 | if (!this.getFeatureCount()) { 132 | return; 133 | } 134 | 135 | this.isolateMouth(); 136 | this.isolateNose(); 137 | this.isolateEyes(); 138 | }; 139 | 140 | Face.prototype.stripExternalFeatures = function () { 141 | _.each(this.getFeatures(), function (features, name) { 142 | _.each(features, function (feature) { 143 | if (this.intersect(feature) < 1) { 144 | this.remove(name, feature); 145 | } 146 | }, this); 147 | }, this); 148 | }; 149 | 150 | Face.prototype.isolateMouth = function () { 151 | var bestMouth; 152 | 153 | _.each(this.getFeatures("mouth"), function (mouth) { 154 | var toRemove; 155 | 156 | if (!bestMouth) { 157 | bestMouth = mouth; 158 | return; 159 | } 160 | 161 | if (mouth.getY() > bestMouth.getY()) { 162 | toRemove = bestMouth; 163 | bestMouth = mouth; 164 | } else { 165 | toRemove = mouth; 166 | } 167 | 168 | if (toRemove) { 169 | this.remove("mouth", toRemove); 170 | } 171 | }, this); 172 | }; 173 | 174 | Face.prototype.isolateNose = function () { 175 | var mouth = this.getMouth(), 176 | bestNose; 177 | 178 | // if we have a mouth lets remove all the noses that do not intersect it 179 | if (mouth) { 180 | _.each(this.getFeatures("nose"), function (nose) { 181 | if (nose.intersect(mouth) === 0) { 182 | this.remove("nose", nose); 183 | } 184 | }, this); 185 | } 186 | 187 | if (this.getFeatures("nose").length <= 1) { 188 | return; 189 | } 190 | 191 | // we have more than one nose, lets select the most centrally-aligned one 192 | _.each(this.getFeatures("nose"), function (nose) { 193 | var toRemove; 194 | 195 | if (!bestNose) { 196 | bestNose = nose; 197 | return; 198 | } 199 | 200 | if (this.getNoseCenteredness(nose) < this.getNoseCenteredness(bestNose)) { 201 | toRemove = bestNose; 202 | bestNose = nose; 203 | } else { 204 | toRemove = nose; 205 | } 206 | 207 | if (toRemove) { 208 | this.remove("nose", nose); 209 | } 210 | }, this); 211 | }; 212 | 213 | Face.prototype.isolateEyes = function () { 214 | var eyes, 215 | maximumY = this.getY() + Math.abs(this.getHeight() / 2), 216 | minimumXforRightEye, 217 | maximumXforLeftEye; 218 | 219 | // first lets discard all the eyes that do no start 220 | // on the upper half of the face 221 | 222 | _.each(this.getFeatures("eyeLeft"), function (eye) { 223 | if (eye.getY() > maximumY) { 224 | this.remove("eyeLeft", eye); 225 | } 226 | }, this); 227 | 228 | _.each(this.getFeatures("eyeRight"), function (eye) { 229 | if (eye.getY() > maximumY) { 230 | this.remove("eyeRight", eye); 231 | } 232 | }, this); 233 | 234 | // jackpot! 235 | if (this.getFeatures("eyeLeft").length === 1 && 236 | this.getFeatures("eyeRight").length === 1) { 237 | return; 238 | } 239 | 240 | eyes = [ 'eyeLeft', 'eyeRight' ]; 241 | 242 | // Lets remove all eyes that are within each other 243 | _.each(eyes, function (eyeName, idx) { 244 | _.each(this.getFeatures(eyeName), function (eye) { 245 | var subEyeName = eyes[idx ? 0 : 1]; 246 | 247 | _.each(this.getFeatures(subEyeName), function (subEye) { 248 | if (eye === subEye) { 249 | return; 250 | } 251 | 252 | if (eye.intersect(subEye)) { 253 | this.remove(subEyeName, subEye); 254 | } 255 | }, this); 256 | }, this); 257 | }, this); 258 | 259 | // If we have a pair of the same eye and none of the other 260 | // set one of them as the other eye 261 | _.each(eyes, function (eyeName, idx) { 262 | var eyeFeatures, otherEyeFeatures, 263 | otherEyeName = eyes[idx ? 0 : 1]; 264 | 265 | eyeFeatures = this.getFeatures(eyeName); 266 | otherEyeFeatures = this.getFeatures(otherEyeName); 267 | 268 | if (eyeFeatures.length === 2 && otherEyeFeatures.length === 0) { 269 | eyeFeatures = this.getFeatures(eyeName); 270 | 271 | 272 | if (eyeFeatures[0].getX() > eyeFeatures[1].getX()) { 273 | this.remove(eyeName, eyeFeatures[0]); 274 | this.add(otherEyeName, eyeFeatures[0]); 275 | } else { 276 | this.remove(eyeName, eyeFeatures[1]); 277 | this.add(otherEyeName, eyeFeatures[1]); 278 | } 279 | } 280 | }, this); 281 | 282 | // jackpot! 283 | if (this.getFeatures("eyeLeft").length === 1 && 284 | this.getFeatures("eyeRight").length === 1) { 285 | return; 286 | } 287 | 288 | // Lets remove right-side eyes from the left side 289 | minimumXforRightEye = this.getX() + Math.abs(this.getWidth() * 0.33); 290 | 291 | _.each(this.getFeatures("eyeRight"), function (eye) { 292 | if (eye.getX() < minimumXforRightEye) { 293 | this.remove("eyeRight", eye); 294 | } 295 | }, this); 296 | 297 | // Lets remove right-side eyes from the left side 298 | maximumXforLeftEye = this.getX() + Math.abs(this.getWidth() * 0.66); 299 | 300 | _.each(this.getFeatures("eyeLeft"), function (eye) { 301 | if (eye.getX2() > maximumXforLeftEye) { 302 | this.remove("eyeLeft", eye); 303 | } 304 | }, this); 305 | }; 306 | 307 | module.exports = Face; 308 | --------------------------------------------------------------------------------