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