/** * Photography_with_detected_features.js */ 'use strict'; var Width_used_for_displaying_canvas = (function () { return function () { return 200; }; })(); var Height_used_for_displaying_canvas = (function () { return function () { return 200; }; })(); var Photography_with_detected_features = function (image /* Image */, image_name /* String */) { chai.assert.isTrue(image !== undefined && image !== null && image instanceof Image && image.complete, 'Photography_with_detected_features._image'); // 'image_name instanceof String === false' when 'var image_name = "Not constructed through constructor!";' chai.assert.isTrue(image_name !== undefined && image_name !== null /* && image_name instanceof String */, 'Photography_with_detected_features._image_name'); /** Check image resizing here that has to occur in calling context */ /** Not sure 'image' has to be recorded since it is immediately transformed into canvas: */ this._image = image; this._image_name = image_name; /** JSFeat: */ // jsfeat.matrix_t(columns, rows, data_type, data_buffer = undefined); // jsfeat.U8_t | jsfeat.C1_t; // 1 channel with unsigned char: this._matrix_char8_C1 = new jsfeat.matrix_t(this._image.width, this._image.height, /*jsfeat.U8_t | jsfeat.C1_t*/ jsfeat.U8C1_t); /** Canvases */ this.$_image_canvas = $('').attr({// Canvas as jQuery object id: "image-canvas-" + this._image_name }); // Configuring canvas' size with jQuery is not reliable using 'innerWidth(this._image.width)' or 'outerWidth(this._image.width)': this.$_image_canvas.get(0).width = this._image.width; this.$_image_canvas.get(0).height = this._image.height; this.$_image_canvas.get(0).getContext('2d').drawImage(this._image, 0, 0, this._image.width, this._image.height); // Display: this.$_image_canvas.css("width", Width_used_for_displaying_canvas() + "px"); this.$_image_canvas.css("height", Width_used_for_displaying_canvas() + "px"); this.$_image_canvas.get(0).getContext('2d').font = "italic 10px Arial"; this.$_image_canvas.get(0).getContext('2d').strokeText("Original: " + this._image_name, 10, 10); $("body").prepend(this.$_image_canvas); this._image_data = this.$_image_canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height); this._image_data_buffer = new Uint32Array(this._image_data.data.buffer); this.$_grayscale_canvas = $('').attr({// Canvas as jQuery object id: "grayscale-canvas-" + this._image_name }); this.$_grayscale_canvas.get(0).width = this._image.width; this.$_grayscale_canvas.get(0).height = this._image.height; this._grayscale_image_data = this.$_grayscale_canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height); this._grayscale_image_data_buffer = new Uint32Array(this._grayscale_image_data.data.buffer); this.$_gaussian_blur_canvas = $('').attr({// Canvas as jQuery object id: "gaussian_blur-canvas-" + this._image_name }); this.$_gaussian_blur_canvas.get(0).width = this._image.width; this.$_gaussian_blur_canvas.get(0).height = this._image.height; this.$_gaussian_blur_canvas.get(0).getContext('2d').drawImage(this._image, 0, 0, this._image.width, this._image.height); this._gaussian_blur_image_data = this.$_gaussian_blur_canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height); this._gaussian_blur_image_data_buffer = new Uint32Array(this._gaussian_blur_image_data.data.buffer); /** Canny: */ this.$_canny_canvas = $('').attr({// Canvas as jQuery object id: "canny-canvas-" + this._image_name }); this.$_canny_canvas.get(0).width = this._image.width; this.$_canny_canvas.get(0).height = this._image.height; this.$_canny_canvas.get(0).getContext('2d').drawImage(this._image, 0, 0, this._image.width, this._image.height); this._canny_image_data = this.$_canny_canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height); this._canny_image_data_buffer = new Uint32Array(this._canny_image_data.data.buffer); /** Sobel: */ this.$_sobel_canvas = $('').attr({ id: "sobel-canvas-" + this._image_name }); this.$_sobel_canvas.get(0).width = this._image.width; this.$_sobel_canvas.get(0).height = this._image.height; this.$_sobel_canvas.get(0).getContext('2d').drawImage(this._image, 0, 0, this._image.width, this._image.height); this._sobel_image_data = this.$_sobel_canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height); this._sobel_image_data_buffer = new Uint32Array(this._sobel_image_data.data.buffer); /** Detection: */ this._gaussian_blur(); this._canny_processing(); this._canny_postprocessing(); this._sobel_processing(); this._sobel_postprocessing(); /** BBF: */ this._brightness_binary_face = null // Sample: '[{"x":44.86460414486096,"y":40.36860185248175,"width":51.39751761778363,"height":51.39751761778363,"neighbors":14,"confidence":8.938965569999997}]' this._brightness_binary_face_detecting(chroma('blue').hex()); /** Haar: */ this._haar_feature = null; // Classifiers: 'jsfeat.haar.eye', 'jsfeat.haar.frontalface' and 'jsfeat.haar.mouth' this._haar_feature_detecting(chroma('green').hex(), jsfeat.haar.frontalface, 1); this._haar_feature_detecting(chroma('yellow').hex(), jsfeat.haar.mouth, 1); this._haar_feature_detecting(chroma('magenta').hex(), jsfeat.haar.eye, 2); // Bad result /** tracking.js */ this._tracking_face(); }; Photography_with_detected_features.prototype._gaussian_blur = function () { var source = new jsfeat.matrix_t(this._image.width, this._image.height, /*jsfeat.U8_t | jsfeat.C1_t*/ jsfeat.U8C1_t); var pixel_number = this._image.width * this._image.height; while (--pixel_number >= 0) { var pixel = this._image_data_buffer[pixel_number]; source.data[pixel_number] = (pixel << 24) | (pixel << 16) | (pixel << 8) | pixel; } var target = new jsfeat.matrix_t(this._image.width, this._image.height, /*jsfeat.U8_t | jsfeat.C1_t*/ jsfeat.U8C1_t); var sigma = 2; // gui.add(options, 'sigma', 0, 10).step(0.5); // It increases bluring var radius = 3; // [1 - 11] var kernel_size = (radius + 1) << 1; jsfeat.imgproc.gaussian_blur(source, target, kernel_size, sigma); var pixel_number = target.cols * target.rows; while (--pixel_number >= 0) { var pixel = target.data[pixel_number]; this._gaussian_blur_image_data_buffer[pixel_number] = (pixel << 24) | (pixel << 16) | (pixel << 8) | pixel; } this.$_gaussian_blur_canvas.get(0).getContext('2d').putImageData(this._gaussian_blur_image_data, 0, 0); // Display: this.$_gaussian_blur_canvas.css("width", Width_used_for_displaying_canvas() + "px"); this.$_gaussian_blur_canvas.css("height", Width_used_for_displaying_canvas() + "px"); this.$_gaussian_blur_canvas.get(0).getContext('2d').font = "italic 10px Arial"; this.$_gaussian_blur_canvas.get(0).getContext('2d').strokeText("Gaussian blur: " + this._image_name, 10, 10); $("body").prepend(this.$_gaussian_blur_canvas); }; Photography_with_detected_features.prototype._canny_processing = function () { chai.assert.isNotNull(this._canny_image_data, 'Photography_with_detected_features._canny_image_data'); chai.assert.isNotNull(this._canny_image_data_buffer, 'Photography_with_detected_features._canny_image_data_buffer'); jsfeat.imgproc.grayscale(this._canny_image_data.data, this._image.width, this._image.height, this._matrix_char8_C1); var blur_radius = 3; var low_threshold = 40; var high_threshold = 40; // Bitwise OR with '0' is equivalent to 'Math.floor()': var r = blur_radius | 0; var kernel_size = (r + 1) << 1; jsfeat.imgproc.gaussian_blur(this._matrix_char8_C1, this._matrix_char8_C1, kernel_size, 0); // Bitwise OR with '0' is equivalent to 'Math.floor()': jsfeat.imgproc.canny(this._matrix_char8_C1, this._matrix_char8_C1, low_threshold | 0, high_threshold | 0); // Render result back to canvas: var alpha = (0x000000FF << 24); // 'alpha' is set to max., i.e., '255' var pixel_number = this._matrix_char8_C1.cols * this._matrix_char8_C1.rows; while (--pixel_number >= 0) { var pixel = this._matrix_char8_C1.data[pixel_number]; this._canny_image_data_buffer[pixel_number] = alpha | (pixel << 16) | (pixel << 8) | pixel; } }; Photography_with_detected_features.prototype._canny_postprocessing = function () { chai.assert.isNotNull(this._canny_image_data, 'Photography_with_detected_features._canny_image_data'); chai.assert.isNotNull(this._canny_image_data_buffer, 'Photography_with_detected_features._canny_image_data_buffer'); for (var pixel_number = 0; pixel_number < this._canny_image_data_buffer.length; pixel_number++) { // Décalage sur la droite SANS préservation du signe (i.e., remplissage à gauche par des '0'): var alpha = this._canny_image_data_buffer[pixel_number] >>> 24; // Most left byte var blue = (this._canny_image_data_buffer[pixel_number] & 0x00FF0000) >> 16; var green = (this._canny_image_data_buffer[pixel_number] & 0x0000FF00) >> 8; var red = this._canny_image_data_buffer[pixel_number] & 0x000000FF; // Most right byte if (red === 255 && green === 255 && blue === 255) { // white green = 0; blue = 0; alpha = (0x000000FF << 24); // 'alpha' is set to max., i.e., '255' } else alpha = (0x00000000 << 24); // 'alpha' is set to min., i.e., '0' this._canny_image_data_buffer[pixel_number] = alpha | (blue << 16) | (green << 8) | red; } this.$_canny_canvas.get(0).getContext('2d').putImageData(this._canny_image_data, 0, 0); // Display: this.$_canny_canvas.css("width", Width_used_for_displaying_canvas() + "px"); this.$_canny_canvas.css("height", Height_used_for_displaying_canvas() + "px"); this.$_canny_canvas.get(0).getContext('2d').font = "italic 10px Arial"; this.$_canny_canvas.get(0).getContext('2d').strokeText("Canny: " + this._image_name, 10, 10); $("body").prepend(this.$_canny_canvas); }; Photography_with_detected_features.prototype._sobel_processing = function () { chai.assert.isNotNull(this._sobel_image_data, 'Photography_with_detected_features._sobel_image_data'); chai.assert.isNotNull(this._sobel_image_data_buffer, 'Photography_with_detected_features._sobel_image_data_buffer'); // 2 channels with 32 bit integer: var matrix_integer32_C2 = new jsfeat.matrix_t(this._image.width, this._image.height, jsfeat.S32C2_t); // Passe l'image en nuances de gris au lieu des couleurs: jsfeat.imgproc.grayscale(this._sobel_image_data.data, this._image.width, this._image.height, this._matrix_char8_C1, jsfeat.COLOR_RGBA2GRAY); // Image source in 'this._matrix_char8_C1', result in 'matrix_integer32_C2' jsfeat.imgproc.sobel_derivatives(this._matrix_char8_C1, matrix_integer32_C2); // Render result back to canvas: // RGBA pixel's features are packed into 32 bits (https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipulation-with-typed-arrays/): var i = this._sobel_image_data_buffer.length, pixel = 0, gx = 0, gy = 0; while (--i >= 0) { // 'i' = this._image.width * this._image.height, matrix_integer32_C2.data.length = this._image.width * this._image.height // First iteration: // 'i << 1' === matrix_integer32_C2.data.length - 2 // '(i << 1) + 1' === matrix_integer32_C2.data.length - 1 // Gradient de l'intensité sur l'axe x: gx = Math.abs(matrix_integer32_C2.data[i << 1] >> 2) & 0x000000FF; // Gradient de l'intensité sur l'axe y: gy = Math.abs(matrix_integer32_C2.data[(i << 1) + 1] >> 2) & 0x000000FF; pixel = ((gx + gy) >> 1) & 0x000000FF; this._sobel_image_data_buffer[i] = (pixel << 24) | (gx << 16) | (0 << 8) | gy; } }; Photography_with_detected_features.prototype._sobel_postprocessing = function () { chai.assert.isNotNull(this._sobel_image_data, 'Photography_with_detected_features._sobel_image_data'); chai.assert.isNotNull(this._sobel_image_data_buffer, 'Photography_with_detected_features._sobel_image_data_buffer'); chai.assert.isNotNull(this._image_data, 'Photography_with_detected_features._image_data'); chai.assert.isNotNull(this._image_data_buffer, 'Photography_with_detected_features._image_data_buffer'); for (var pixel_number = 0; pixel_number < this._sobel_image_data_buffer.length; pixel_number++) { var alpha = this._sobel_image_data_buffer[pixel_number] >>> 24; // Most left byte var blue = (this._sobel_image_data_buffer[pixel_number] & 0x00FF0000) >> 16; var green = (this._sobel_image_data_buffer[pixel_number] & 0x0000FF00) >> 8; var red = this._sobel_image_data_buffer[pixel_number] & 0x000000FF; // Most right byte /** Gray scale: red-channel == green-channel == blue-channel */ if (alpha > 10) { // Lower: useless details are kept (outside faces especially) while higher: faces lose quality red = alpha; blue = alpha; green = alpha; // Change 'alpha'? } else { alpha = 0; } // Pixel reload: this._sobel_image_data_buffer[pixel_number] = (alpha << 24) | (blue << 16) | (green << 8) | red; /** Original image is setup according to Sobel: */ var sobel_alpha = alpha; alpha = this._image_data_buffer[pixel_number] >>> 24; // Most left byte blue = (this._image_data_buffer[pixel_number] & 0x00FF0000) >> 16; green = (this._image_data_buffer[pixel_number] & 0x0000FF00) >> 8; red = this._image_data_buffer[pixel_number] & 0x000000FF; // Most right byte if (sobel_alpha === 0) { // Pixel reload: //this._image_data_buffer[pixel_number] = (alpha << 24) | (blue << 16) | (green << 8) | red; // Colors are kept } } /** Update pixels into canvas: */ this.$_sobel_canvas.get(0).getContext('2d').putImageData(this._sobel_image_data, 0, 0); // Display for test only: this.$_sobel_canvas.css("width", Width_used_for_displaying_canvas() + "px"); this.$_sobel_canvas.css("height", Height_used_for_displaying_canvas() + "px"); this.$_sobel_canvas.get(0).getContext('2d').font = "italic 10px Arial"; this.$_sobel_canvas.get(0).getContext('2d').strokeText("Sobel: " + this._image_name, 10, 10); $("body").prepend(this.$_sobel_canvas); }; Photography_with_detected_features.prototype._brightness_binary_face_detecting = function (color) { var canvas = $('').attr({ id: "bbf-canvas-" + this._image_name }); canvas.get(0).width = this._image.width; canvas.get(0).height = this._image.height; canvas.get(0).getContext('2d').drawImage(this._image, 0, 0, this._image.width, this._image.height); jsfeat.imgproc.grayscale(canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height).data, this._image.width, this._image.height, this._matrix_char8_C1); // Possible option: // jsfeat.imgproc.equalize_histogram(img_u8, img_u8); /* Build image pyramid using canvas drawImage src - source grayscale matrix_t (U8_t|C1_t) min_width - minimum width to scale pyramid to min_height - minimum height to scale pyramid to interval - number of original scale levels in pyramid */ var pyramid = jsfeat.bbf.build_pyramid(this._matrix_char8_C1, 24 * 2, 24 * 2, 5); // Oscar passe à '5' /* This step needed only once to create local copy of features to prevent multiple Array relocation during detection */ jsfeat.bbf.prepare_cascade(jsfeat.bbf.face_cascade); // 'bbf_face.js' /* Run detection pyramid - 'pyramid_t' object from 'build_pyramid' method cascade - cascade data */ this._brightness_binary_face = jsfeat.bbf.detect(pyramid, jsfeat.bbf.face_cascade); // 'bbf_face.js' /* Groups the object candidate rectangles this._brightness_binary_face - input candidate objects sequence min_neighbors - Minimum possible number of rectangles minus 1, the threshold is used in a group of rectangles to retain it */ // En augmentant 'min_neighbors', on augmente la qualité de la détection pour ensuite choisir le rectangle: this._brightness_binary_face = jsfeat.bbf.group_rectangles(this._brightness_binary_face, 1); // '1' par défaut pour 'min_neighbors' /** JS conventions: */ // All objects are considered to be 'true' // Strings are considered to be 'false' if they are empty // 'null' and 'undefined' are considered to be 'false' // A Number is 'false' if it is zero if (this._brightness_binary_face.length > 0) { jsfeat.math.qsort(this._brightness_binary_face, 0, this._brightness_binary_face.length - 1, function (a, b) { return (b.confidence < a.confidence); }); // Les rectangles sont triés par ordre de confiance // Display: this.$_image_canvas.get(0).getContext('2d').strokeStyle = color; this.$_image_canvas.get(0).getContext('2d').lineWidth = 3; var scale = this._image.width / this._matrix_char8_C1.cols; this.$_image_canvas.get(0).getContext('2d').strokeRect((this._brightness_binary_face[0].x * scale) | 0, (this._brightness_binary_face[0].y * scale) | 0, (this._brightness_binary_face[0].width * scale) | 0, (this._brightness_binary_face[0].height * scale) | 0); } }; Photography_with_detected_features.prototype._haar_feature_detecting = function (color, classifier, feature_number) { var canvas = $('').attr({ id: "frontalface-canvas-" + this._image_name }); canvas.get(0).width = this._image.width; canvas.get(0).height = this._image.height; canvas.get(0).getContext('2d').drawImage(this._image, 0, 0, this._image.width, this._image.height); var edges = new jsfeat.matrix_t(this._image.width, this._image.height, jsfeat.U8_t | jsfeat.C1_t); var ii_sum = new Int32Array((this._image.width + 1) * (this._image.height + 1)); var ii_sqsum = new Int32Array((this._image.width + 1) * (this._image.height + 1)); var ii_tilted = new Int32Array((this._image.width + 1) * (this._image.height + 1)); var ii_canny = new Int32Array((this._image.width + 1) * (this._image.height + 1)); jsfeat.imgproc.grayscale(canvas.get(0).getContext('2d').getImageData(0, 0, this._image.width, this._image.height).data, this._image.width, this._image.height, this._matrix_char8_C1); /** option */ jsfeat.imgproc.equalize_histogram(this._matrix_char8_C1, this._matrix_char8_C1); // jsfeat.imgproc.gaussian_blur(this._matrix_char8_C1, this._matrix_char8_C1, 3); jsfeat.imgproc.compute_integral_image(this._matrix_char8_C1, ii_sum, ii_sqsum, classifier.tilted ? ii_tilted : null); /** option */ jsfeat.imgproc.canny(this._matrix_char8_C1, edges, 10, 50); jsfeat.imgproc.compute_integral_image(edges, ii_canny, null, null); var min_scale = 1.; // '1.' to '4.' step '0.1' var scale_factor = 1.15; // '1.1' to '2.' step '0.025' jsfeat.haar.edges_density = 0.13; // '0.01' to '1.' step '0.005' this._haar_feature = jsfeat.haar.detect_multi_scale(ii_sum, ii_sqsum, ii_tilted, ii_canny /* 'null' if canny is not used */, this._matrix_char8_C1.cols, this._matrix_char8_C1.rows, classifier, scale_factor, min_scale); this._haar_feature = jsfeat.haar.group_rectangles(this._haar_feature, 1); if (this._haar_feature.length > 1) { jsfeat.math.qsort(this._haar_feature, 0, this._haar_feature.length - 1, function (a, b) { return (b.confidence < a.confidence); }); // Les rectangles sont triés par ordre de confiance } // Display: this.$_image_canvas.get(0).getContext('2d').strokeStyle = color; this.$_image_canvas.get(0).getContext('2d').lineWidth = 3; var scale = this._image.width / this._matrix_char8_C1.cols; for (var i = 0; i < Math.min(this._haar_feature.length, feature_number); ++i) { this.$_image_canvas.get(0).getContext('2d').strokeRect((this._haar_feature[i].x * scale) | 0, (this._haar_feature[i].y * scale) | 0, (this._haar_feature[i].width * scale) | 0, (this._haar_feature[i].height * scale) | 0); } }; /** tracking.js */ Photography_with_detected_features.prototype._tracking_face = function () { var tracker = new tracking.ObjectTracker(['face']); tracker.setInitialScale(1.); // gui.add(tracker, 'initialScale', 1.0, 10.0).step(0.1); tracker.setStepSize(1.); // gui.add(tracker, 'stepSize', 1, 5).step(0.1); tracker.setEdgesDensity(0.1); // gui.add(tracker, 'edgesDensity', 0.1, 0.5).step(0.01); var display_face = function (event) { // Display: this.$_sobel_canvas.get(0).getContext('2d').strokeStyle = chroma('red').hex(); this.$_sobel_canvas.get(0).getContext('2d').lineWidth = 3; for (var i = 0; i < event.data.length; ++i) { this.$_sobel_canvas.get(0).getContext('2d').strokeRect(Math.round(event.data[i].x), Math.round(event.data[i].y), Math.round(event.data[i].width), Math.round(event.data[i].height)); } }; tracker.on('track', display_face.bind(this)); tracking.track(this._image, tracker); };