4e431bf18efb314a61dd3929aec26553f90ebbea
[face-privacy-filter.git] / web_demo / face-privacy.js
1 /*
2   ===============LICENSE_START=======================================================
3   Acumos Apache-2.0
4   ===================================================================================
5   Copyright (C) 2017-2018 AT&T Intellectual Property & Tech Mahindra. All rights reserved.
6   ===================================================================================
7   This Acumos software file is distributed by AT&T and Tech Mahindra
8   under the Apache License, Version 2.0 (the "License");
9   you may not use this file except in compliance with the License.
10   You may obtain a copy of the License at
11
12   http://www.apache.org/licenses/LICENSE-2.0
13
14   This file is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18   ===============LICENSE_END=========================================================
19 */
20 /**
21  face-privacy.js - send frames to an face privacy service
22
23  Videos or camera are displayed locally and frames are periodically sent to GPU image-net classifier service (developed by Zhu Liu) via http post.
24  For webRTC, See: https://gist.github.com/greenido/6238800
25
26  D. Gibbon 6/3/15
27  D. Gibbon 4/19/17 updated to new getUserMedia api, https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
28  D. Gibbon 8/1/17 adapted for system
29  E. Zavesky 10/19/17 adapted for video+image
30  */
31
32 "use strict";
33
34 /**
35  * main entry point
36  */
37 $(document).ready(function() {
38     var urlDefault = getUrlParameter('url-image');
39     if (!urlDefault)
40         urlDefault = "http://localhost:8884/transform";
41
42         $(document.body).data('hdparams', {     // store global vars in the body element
43                 classificationServer: urlDefault,
44                 protoObj: null,   // to be back-filled after protobuf load {'root':obj, 'methods':{'xx':{'typeIn':x, 'typeOut':y}} }
45                 protoPayloadInput: null,   //payload for encoded message download (if desired)
46                 protoPayloadOutput: null,   //payload for encoded message download (if desired)
47                 protoRes: null,  //TEMPORARY
48                 frameCounter: 0,
49                 frameInterval: 500,             // Milliseconds to sleep between sending frames to reduce server load and reduce results updates
50                 frameTimer: -1,         // frame clock for processing
51                 imageIsWaiting: false,  // blocking to prevent too many queued frames
52                 // Objects from DOM elements
53                 srcImgCanvas: document.getElementById('srcImgCanvas'),  // we have a 'src' source image
54                 destImg: document.getElementById('destImg'),    // we have a 'src' source image
55                 video: document.getElementById('srcVideo'),
56         });
57     $(document.body).data('hdparams')['canvasMaxH'] = $(document.body).data('hdparams')['srcImgCanvas'].height;
58     $(document.body).data('hdparams')['canvasMaxW'] = $(document.body).data('hdparams')['srcImgCanvas'].width;
59
60     $("#protoInput").prop("disabled",true).click(downloadBlobIn);
61     $("#protoOutput").prop("disabled",true).click(downloadBlobOut);
62     $("#resultText").hide();
63
64         //add text input tweak
65         $("#serverUrl").change(function() {
66             $(document.body).data('hdparams')['classificationServer'] = $(this).val();
67         updateLink("serverLink");
68         }).val($(document.body).data('hdparams')['classificationServer'])
69         //set launch link at first
70     updateLink("serverLink");
71
72         // add buttons to change video
73         $("#sourceRibbon div").click(function() {
74             var $this = $(this);
75             $this.siblings().removeClass('selected'); //clear other selection
76             $this.addClass('selected');
77             var objImg = $this.children('img')[0];
78             var hd = $(document.body).data('hdparams');
79             if (objImg) {
80                 switchImage(objImg.src);
81             clearInterval(hd.frameTimer);       // stop the processing
82
83             var movieAttr = $(objImg).attr('movie');
84             if (movieAttr) {
85                 // Set the video source based on URL specified in the 'videos' list, or select camera input
86                 $(hd.video).show();
87                 $(srcImgCanvas).hide();
88                 if (movieAttr == "Camera") {
89                     var constraints = {audio: false, video: true};
90                     navigator.mediaDevices.getUserMedia(constraints)
91                         .then(function(mediaStream) {
92                             hd.video.srcObject = mediaStream;
93                             hd.video.play();
94                         })
95                         .catch(function(err) {
96                             console.log(err.name + ": " + err.message);
97                         });
98                 } else {
99                     var mp4 = document.getElementById("mp4");
100                     mp4.setAttribute("src", movieAttr);
101                     hd.video.load();
102                     newVideo();
103                 }
104             }
105             else {
106                 hd.video.pause();
107                 $(hd.video).hide();
108                 $(srcImgCanvas).show();
109             }
110             }
111         });
112
113         //allow user-uploaded images
114     var imageLoader = document.getElementById('imageLoader');
115     imageLoader.addEventListener('change', handleImage, false);
116
117     //if protobuf is enabled, fire load event for it as well
118     $(document.body).data('hdparams').protoObj = {};  //clear from last load
119     protobuf_load("model.pixelate.proto", true);
120     protobuf_load("model.detect.proto");
121
122     //trigger first click
123     $("#sourceRibbon div")[0].click();
124 });
125
126
127 function protobuf_load(pathProto, forceSelect) {
128     protobuf.load(pathProto, function(err, root) {
129         if (err) {
130             console.log("[protobuf]: Error!: "+err);
131             throw err;
132         }
133         var domSelect = $("#protoMethod");
134         var numMethods = domSelect.children().length;
135         $.each(root.nested, function(namePackage, objPackage) {    // walk all
136             if ('Model' in objPackage && 'methods' in objPackage.Model) {    // walk to model and functions...
137                 var typeSummary = {'root':root, 'methods':{} };
138                 $.each(objPackage.Model.methods, function(nameMethod, objMethod) {  // walk methods
139                     typeSummary['methods'][nameMethod] = {};
140                     typeSummary['methods'][nameMethod]['typeIn'] = namePackage+'.'+objMethod.requestType;
141                     typeSummary['methods'][nameMethod]['typeOut'] = namePackage+'.'+objMethod.responseType;
142                     typeSummary['methods'][nameMethod]['service'] = namePackage+'.'+nameMethod;
143
144                     //create HTML object as well
145                     var namePretty = namePackage+"."+nameMethod;
146                     var domOpt = $("<option />").attr("value", namePretty).text(
147                         nameMethod+ " (input: "+objMethod.requestType
148                         +", output: "+objMethod.responseType+")");
149                     if (numMethods==0) {    // first method discovery
150                         domSelect.append($("<option />").attr("value","").text("(disabled, not loaded)")); //add 'disabled'
151                     }
152                     if (forceSelect) {
153                         domOpt.attr("selected", 1);
154                     }
155                     domSelect.append(domOpt);
156                     numMethods++;
157                 });
158                 $(document.body).data('hdparams').protoObj[namePackage] = typeSummary;   //save new method set
159                 $("#protoContainer").show();
160             }
161         });
162         console.log("[protobuf]: Load successful, found "+numMethods+" model methods.");
163     });
164 }
165
166 /**
167  * Called after a new video has loaded (at least the video metadata has loaded)
168  */
169 function newVideo() {
170         var hd = $(document.body).data('hdparams');
171         hd.frameCounter = 0;
172         hd.imageIsWaiting = false;
173         hd.video.play();
174
175         // set processing canvas size based on source video
176         var pwidth = hd.video.videoWidth;
177         var pheight = hd.video.videoHeight;
178         if (pwidth > hd.maxSrcVideoWidth) {
179                 pwidth = hd.maxSrcVideoWidth;
180                 pheight = Math.floor((pwidth / hd.video.videoWidth) * pheight); // preserve aspect ratio
181         }
182         hd.srcImgCanvas.width = pwidth;
183         hd.srcImgCanvas.height = pheight;
184
185         hd.frameTimer = setInterval(nextFrame, hd.frameInterval); // start the processing
186 }
187
188 /**
189  * process the next video frame
190  */
191 function nextFrame() {
192         var hd = $(document.body).data('hdparams');
193         if (hd.video.ended || hd.video.paused) {
194                 return;
195         }
196     switchImage(hd.video, true);
197 }
198
199 function updateLink(domId, newServer) {
200     var sPageURL = decodeURIComponent(window.location.search.split('?')[0]);
201     if (newServer==undefined) {
202         newServer = $(document.body).data('hdparams')['classificationServer'];
203     }
204     else {
205         $("#serverUrl").val(newServer);
206     }
207     var sNewUrl = sPageURL+"?url-image="+newServer;
208     $("#"+domId).attr('href', sNewUrl);
209 }
210
211 function switchImage(imgSrc, isVideo) {
212     var canvas = $(document.body).data('hdparams')['srcImgCanvas'];
213     if (!isVideo) {
214         var img = new Image();
215         img.onload = function () {
216             var ctx = canvas.getContext('2d');
217             var canvasCopy = document.createElement("canvas");
218             var copyContext = canvasCopy.getContext("2d");
219
220             var ratio = 1;
221
222             //console.log( $(document.body).data('hdparams'));
223             //console.log( [ img.width, img.height]);
224             // https://stackoverflow.com/a/2412606
225             if(img.width > $(document.body).data('hdparams')['canvasMaxW'])
226                 ratio = $(document.body).data('hdparams')['canvasMaxW'] / img.width;
227             if(ratio*img.height > $(document.body).data('hdparams')['canvasMaxH'])
228                 ratio = $(document.body).data('hdparams')['canvasMaxH'] / img.height;
229
230             canvasCopy.width = img.width;
231             canvasCopy.height = img.height;
232             copyContext.drawImage(img, 0, 0);
233
234             canvas.width = img.width * ratio;
235             canvas.height = img.height * ratio;
236             ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
237             //document.removeChild(canvasCopy);
238             doPostImage(canvas, '#destImg', canvas.toDataURL());
239         }
240         img.src = imgSrc;  //copy source, let image load
241     }
242     else if (!$(document.body).data('hdparams').imageIsWaiting) {
243         var ctx = canvas.getContext('2d');
244         var canvasCopy = document.createElement("canvas");
245         var copyContext = canvasCopy.getContext("2d");
246         var ratio = 1;
247
248         if(imgSrc.videoWidth > $(document.body).data('hdparams')['canvasMaxW'])
249             ratio = $(document.body).data('hdparams')['canvasMaxW'] / imgSrc.videoWidth;
250         if(ratio*imgSrc.videoHeight > $(document.body).data('hdparams')['canvasMaxH'])
251             ratio = $(document.body).data('hdparams')['canvasMaxH'] / canvasCopy.height;
252
253         //console.log("Canvas Copy:"+canvasCopy.width+"/"+canvasCopy.height);
254         //console.log("Canvas Ratio:"+ratio);
255         //console.log("Video: "+imgSrc.videoWidth+"x"+imgSrc.videoHeight);
256         canvasCopy.width = imgSrc.videoWidth;     //large as possible
257         canvasCopy.height = imgSrc.videoHeight;
258         copyContext.drawImage(imgSrc, 0, 0);
259
260         canvas.width = canvasCopy.width * ratio;
261         canvas.height = canvasCopy.height * ratio;
262         ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
263         //document.removeChild(canvasCopy);
264         doPostImage(canvas, '#destImg', canvas.toDataURL());
265     }
266 }
267
268
269 //load image that has been uploaded into a vancas
270 function handleImage(e){
271     var reader = new FileReader();
272     reader.onload = function(event){
273         switchImage(event.target.result);
274     }
275     reader.readAsDataURL(e.target.files[0]);
276 }
277
278
279
280 // https://stackoverflow.com/questions/19491336/get-url-parameter-jquery-or-how-to-get-query-string-values-in-js
281 function getUrlParameter(sParam) {
282     var sPageURL = decodeURIComponent(window.location.search.substring(1)),
283         sURLVariables = sPageURL.split('&'),
284         sParameterName,
285         i;
286
287     for (i = 0; i < sURLVariables.length; i++) {
288         sParameterName = sURLVariables[i].split('=');
289
290         if (sParameterName[0] === sParam) {
291             return sParameterName[1] === undefined ? true : sParameterName[1];
292         }
293     }
294 };
295
296
297 /**
298  * post an image from the canvas to the service
299  */
300 function doPostImage(srcCanvas, dstImg, dataPlaceholder) {
301     var dataURL = srcCanvas.toDataURL('image/jpeg', 1.0);
302     var hd = $(document.body).data('hdparams');
303     var sendPayload = null;
304
305     var nameProtoMethod = $("#protoMethod option:selected").attr('value');
306     var methodKeys = null;
307     if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
308         var partsURL = hd.classificationServer.split("/");
309         methodKeys = nameProtoMethod.split(".", 2);       //modified for multiple detect/pixelate models
310         partsURL[partsURL.length-1] = methodKeys[1];
311         hd.classificationServer = partsURL.join("/");   //rejoin with new endpoint
312         updateLink("serverLink", hd.classificationServer);
313     }
314
315     var serviceURL = hd.classificationServer;
316     var request = new XMLHttpRequest();     // create request to manipulate
317     request.open("POST", serviceURL, true);
318
319     //console.log("[doPostImage]: Selected method ... '"+typeInput+"'");
320     if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
321         var blob = dataURItoBlob(dataURL, true);
322
323         // fields from .proto file at time of writing...
324         //    message FaceImage {
325         //      repeated string mime_type = 1;   -> becomes "mimeType" (NOTE repeated type)
326         //      repeated bytes image_binary = 2; -> becomes "imageBinary"
327         //    }
328
329         //TODO: should we always assume this is input? answer: for now, YES, always image input!
330         var inputPayload = { "mimeType": [blob.type], "imageBinary": [blob.bytes] };
331
332         // ---- method for processing from a type ----
333         var msgInput = hd.protoObj[methodKeys[0]]['root'].lookupType(hd.protoObj[methodKeys[0]]['methods'][methodKeys[1]]['typeIn']);
334         // Verify the payload if necessary (i.e. when possibly incomplete or invalid)
335         var errMsg = msgInput.verify(inputPayload);
336         if (errMsg) {
337             console.log("[doPostImage]: Error during type verify for object input into protobuf method.");
338             throw Error(errMsg);
339         }
340         // Create a new message
341         var msgTransmit = msgInput.create(inputPayload);
342         // Encode a message to an Uint8Array (browser) or Buffer (node)
343         sendPayload = msgInput.encode(msgTransmit).finish();
344
345         // ----------
346
347         /*
348         // ---- method for processing from a service ----
349         var serviceInput = hd.protoObj['root'].lookup(hd.protoObj['methods'][nameProtoMethod]['service']);
350
351         function rpcImpl(method, requestData, callback) {
352             // perform the request using an HTTP request or a WebSocket for example
353             var responseData = ...;
354             // and call the callback with the binary response afterwards:
355             callback(null, responseData);
356         }
357         var serviceCall = serviceInput.create(rpcImpl, false, false); //request dlimited? response delimited?
358
359         serviceCall.sayHello(sendPayload).then(response) {
360             console.log('Greeting:', response.message);
361         });
362         // ---------------------------
363         */
364
365         //downloadBlob(sendPayload, 'protobuf.bin', 'application/octet-stream');
366         // NOTE: TO TEST THIS BINARY BLOB, use some command-line magic like this...
367         //  protoc --decode=mMJuVapnmIbrHlZGKyuuPDXsrkzpGqcr.FaceImage model.proto < protobuf.bin
368         $("#protoInput").prop("disabled",false);
369         hd.protoPayloadInput = sendPayload;
370
371         // append our encoded chunk
372         //console.log(sendPayload);
373         //console.log(typeof(blob.type));
374         // console.log(nameProtoMethod);
375         request.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
376         request.responseType = 'arraybuffer';
377     }
378     else {
379         var blob = dataURItoBlob(dataURL, false);
380         sendPayload = new FormData();
381         sendPayload.append("image_binary", blob);
382         sendPayload.append("mime_type", blob.type);
383     }
384     //$(dstImg).addClaas('workingImage').siblings('.spinner').remove().after($("<span class='spinner'>&nbsp;</span>"));
385     $(document.body).data('hdparams').imageIsWaiting = true;
386     var $dstImg = $(dstImg);
387     if ($dstImg.attr('src')=='') {
388         $dstImg.attr('src', dataPlaceholder);
389         //$(dstImg).addClass('workingImage').attr('src', dataPlaceholder);
390     }
391
392     hd.imageIsWaiting = true;
393     request.onreadystatechange=function() {
394         if (request.readyState==4 && request.status>=200 && request.status<300) {
395             if (methodKeys!=null) {     //valid protobuf type?
396                 //console.log(request);
397                 var bodyEncodedInString = new Uint8Array(request.response);
398                 //console.log(bodyEncodedInString);
399                 //console.log(bodyEncodedInString.length);
400                 $("#protoOutput").prop("disabled",false);
401                 hd.protoPayloadOutput = bodyEncodedInString;
402
403                 // ---- method for processing from a type ----
404                 var msgOutput = hd.protoObj[methodKeys[0]]['root'].lookupType(hd.protoObj[methodKeys[0]]['methods'][methodKeys[1]]['typeOut']);
405                 var objRecv = msgOutput.decode(hd.protoPayloadOutput);
406                 //console.log(objRecv);
407                 hd.protoRes = objRecv;
408
409                 // detect what mode we're in (detect alone or processed?)...
410                 if (!Array.isArray(objRecv.mimeType)) {
411                     $dstImg.attr('src', "data:"+objRecv.mimeType+";base64,"+objRecv.imageBinary).removeClass('workingImage');
412                 }
413                 else {
414                     var domResult = $("#resultText");
415                     var domTable = $("<tr />");
416                     var arrNames = [];
417                     $.each(msgOutput.fields, function(name, val) {           //collect field names
418                         var nameClean = val.name;
419                         if (nameClean != 'imageBinary') {
420                             domTable.append($("<th />").html(nameClean));
421                             arrNames.push(nameClean);
422                         }
423                     });
424                     domTable = $("<table />").append(domTable);     // create embedded table
425
426                     var idxImg = -1;
427                     if ('region' in msgOutput.fields) {             // did we get regions?
428                         for (var i=0; i<objRecv.region.length; i++) {       //find the right region
429                             if (objRecv.region[i]==-1) {                    //special indicator for original image
430                                 idxImg = i;
431                             }
432                             var domRow = $("<tr />");
433                             var strDisplay = [];
434                             $.each(arrNames, function(idx, name) {      //collect data from each column
435                                 domRow.append($("<td />").html(objRecv[name][i]));
436                             });
437                             domTable.append(domRow);
438                             //domResult.append($("div").html(objRecv.region));
439                         }
440                         domResult.empty().append($("<strong />").html("Results")).show();
441                         domResult.append(domTable);
442                     }
443                     else {                                  //got images, get that chunk directly
444                         idxImg = 0;
445                     }
446
447                     if (idxImg != -1) {                     //got any valid image? display it
448                         //console.log(objRecv.mimeType[idxImg]);
449                         //console.log(objRecv.imageBinary[idxImg]);
450                         //var strImage = Uint8ToString(objRecv.imageBinary[idxImg]);
451                         var strImage = btoa(String.fromCharCode.apply(null, objRecv.imageBinary[idxImg]));
452                         $dstImg.attr('src', "data:"+objRecv.mimeType[idxImg]+";base64,"+strImage).removeClass('workingImage');
453                     }
454                 }
455
456             }
457             else {
458                 var responseJson = $.parseJSON(request.responseText);
459                 var respImage = responseJson[0];
460                 // https://stackoverflow.com/questions/21227078/convert-base64-to-image-in-javascript-jquery
461                 $dstImg.attr('src', "data:"+respImage['mime_type']+";base64,"+respImage['image_binary']).removeClass('workingImage');
462                 //genClassTable($.parseJSON(request.responseText), dstDiv);
463             }
464             hd.imageIsWaiting = false;
465         }
466         }
467         request.send(sendPayload);
468         $(document.body).data('hdparams').imageIsWaiting = false;
469 }
470
471
472 /**
473  * convert base64/URLEncoded data component to raw binary data held in a string
474  *
475  * Stoive, http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata
476  */
477 function dataURItoBlob(dataURI, wantBytes) {
478     // convert base64/URLEncoded data component to raw binary data held in a string
479     var byteString;
480     if (dataURI.split(',')[0].indexOf('base64') >= 0)
481         byteString = atob(dataURI.split(',')[1]);
482     else
483         byteString = unescape(dataURI.split(',')[1]);
484
485     // separate out the mime component
486     var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
487
488     // write the bytes of the string to a typed array
489     var ia = new Uint8Array(byteString.length);
490     for (var i = 0; i < byteString.length; i++) {
491         ia[i] = byteString.charCodeAt(i);
492     }
493     //added for returning bytes directly
494     if (wantBytes) {
495         return {'bytes':ia, 'type':mimeString};
496     }
497     return new Blob([ia], {type:mimeString});
498 }
499
500 function Uint8ToString(u8a){
501   var CHUNK_SZ = 0x8000;
502   var c = [];
503   for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
504     c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
505   }
506   return c.join("");
507 }
508
509
510 // ----- diagnostic tool to download binary blobs ----
511 function downloadBlobOut() {
512     return downloadBlob($(document.body).data('hdparams').protoPayloadOutput, "protobuf.out.bin");
513 }
514
515 function downloadBlobIn() {
516     return downloadBlob($(document.body).data('hdparams').protoPayloadInput, "protobuf.in.bin");
517 }
518
519 //  https://stackoverflow.com/a/33622881
520 function downloadBlob(data, fileName, mimeType) {
521   //if there is no data, filename, or mime provided, make our own
522   if (!data)
523       data = $(document.body).data('hdparams').protoPayloadInput;
524   if (!fileName)
525       fileName = "protobuf.bin";
526   if (!mimeType)
527       mimeType = "application/octet-stream";
528
529   var blob, url;
530   blob = new Blob([data], {
531     type: mimeType
532   });
533   url = window.URL.createObjectURL(blob);
534   downloadURL(url, fileName, mimeType);
535   setTimeout(function() {
536     return window.URL.revokeObjectURL(url);
537   }, 1000);
538 };
539
540 function downloadURL(data, fileName) {
541   var a;
542   a = document.createElement('a');
543   a.href = data;
544   a.download = fileName;
545   document.body.appendChild(a);
546   a.style = 'display: none';
547   a.click();
548   a.remove();
549 };