fe11fdb61712f34a0685e3a95e173160179dd30f
[face-privacy-filter.git] / web_demo / demo-framework.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; clone from image-classes.js
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  E. Zavesky 05/05/18 adapted for row-based image and other results
31  E. Zavesky 05/30/18 adapted for single image input, github preview, safe posting (forked from model-specific code)
32  */
33
34
35 /**
36  * main entry point
37  */
38 function demo_init(objSetting) {
39     if (!objSetting) objSetting = {};
40
41     // clone/extend the default input from our main script
42     $(document.body).data('hdparams', $.extend(true, objSetting, {      // store global vars in the body element
43         classificationServer: getUrlParameter('url-image'), // default to what's in our url prameter
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         protoKeys: null,  // currently selected protobuf method (if any)
48         frameCounter: 0,
49         totalFrames: 900000,    // stop after this many frames just to avoid sending frames forever if someone leaves page up
50         frameInterval: 500,             // Milliseconds to sleep between sending frames to reduce server load and reduce results updates
51         frameTimer: -1,         // frame clock for processing
52         maxSrcVideoWidth: 512,  // maximum image width for processing
53         serverIsLocal: true,    // server is local versus 'firewall' version
54         imageIsWaiting: false,  // blocking to prevent too many queued frames
55         // functional customizations for each demo
56         documentTitle: "Protobuf Demo",
57         mediaList: [],        //relative URLs of media files
58         protoList: [],        //relative URLs of proto files to include
59         // Objects from DOM elements
60         video: document.getElementById('srcVideo'),
61         srcImgCanvas: document.getElementById('srcImgCanvas'),  // we have a 'src' source image
62     }));
63
64     var hd = $(document.body).data('hdparams');
65     if (hd.video) {
66         hd.video.addEventListener("loadedmetadata", newVideo);
67     }
68
69     $("#protoInput").prop("disabled",true).click(downloadBlobIn);
70     $("#protoOutput").prop("disabled",true).click(downloadBlobOut);
71     $("#resultText").hide();
72
73     //add text input tweak
74     $("#serverUrl").change(function() {
75         $(document.body).data('hdparams')['classificationServer'] = $(this).val();
76         updateLink("serverLink");
77     }).val($(document.body).data('hdparams')['classificationServer'])
78     //set launch link at first
79     $("#protoMethod").change(function() {
80         updateProto($(this).attr('id'));
81     });
82
83     //if protobuf is enabled, fire load event for it as well
84     hd.protoObj = {};  //clear from last load
85     $.each(hd.protoList, function(idx, protoTuple) {     //load relevant protobuf tuples
86         protobuf_load.apply(this, protoTuple);      //load each file independently
87     });
88
89     // add buttons to change video
90     $.each(hd.mediaList, function(key) {
91         //TODO: integrarte as DIV instead of button
92         var button = $('<button/>').text(videos[key].name).attr('movie', videos[key].url);
93         $("#sourceRibbon").append(button);
94     });
95
96     // add buttons to change video or image
97         $("#sourceRibbon").children("div,button").click(function() {
98         var $this = $(this);
99         $this.siblings().removeClass('selected'); //clear other selection
100         $this.addClass('selected');
101
102         var movieAttr = $this.attr('movie');
103         var objImg = $this.children('img')[0];
104         if (objImg) {
105             movieAttr = $(objImg).attr('movie');
106             objImg = $(objImg);
107         }
108
109         clearInterval(hd.frameTimer);   // stop the processing
110         hd.video.pause();
111
112         if (movieAttr) {
113             switchVideo(movieAttr);
114         }
115         else {
116             $(hd.video).hide();
117             $(srcImgCanvas).show();
118             if (objImg)
119                 switchImage(objImg.attr('src'));
120         }
121         }).first().click();
122 }
123
124
125 function protobuf_load(pathProto, forceSelect) {
126     protobuf.load(pathProto, function(err, root) {
127         if (err) {
128             console.log("[protobuf]: Error!: "+err);
129             throw err;
130         }
131         var domSelect = $("#protoMethod");
132         var numMethods = domSelect.children().length;
133         $.each(root.nested, function(namePackage, objPackage) {    // walk all
134             if ('Model' in objPackage && 'methods' in objPackage.Model) {    // walk to model and functions...
135                 var typeSummary = {'root':root, 'methods':{} };
136                 $.each(objPackage.Model.methods, function(nameMethod, objMethod) {  // walk methods
137                     typeSummary['methods'][nameMethod] = {};
138                     typeSummary['methods'][nameMethod]['typeIn'] = namePackage+'.'+objMethod.requestType;
139                     typeSummary['methods'][nameMethod]['typeOut'] = namePackage+'.'+objMethod.responseType;
140                     typeSummary['methods'][nameMethod]['service'] = namePackage+'.'+nameMethod;
141
142                     //create HTML object as well
143                     var namePretty = namePackage+"."+nameMethod;
144                     var domOpt = $("<option />").attr("value", namePretty).text(
145                         nameMethod+ " (input: "+objMethod.requestType
146                         +", output: "+objMethod.responseType+")");
147                     if (numMethods==0) {    // first method discovery
148                         domSelect.append($("<option />").attr("value","").text("(disabled, not loaded)")); //add 'disabled'
149                     }
150                     if (forceSelect) {
151                         domOpt.attr("selected", 1);
152                     }
153                     domSelect.append(domOpt);
154                     numMethods++;
155                 });
156                 $(document.body).data('hdparams').protoObj[namePackage] = typeSummary;   //save new method set
157                 $("#protoContainer").show();
158             }
159         });
160         console.log("[protobuf]: Load successful, found "+numMethods+" model methods.");
161     });
162 }
163
164 // modify the link and update our url
165 function updateLink(domId) {
166     var newServer = $(document.body).data('hdparams')['classificationServer'];
167     var sNewUrl = updateQueryStringParameter(window.location.href, "url-image", newServer, "?");
168     $("#"+domId).attr('href', sNewUrl);
169     $("#serverUrl").val(newServer);
170     //window.history.pushState({}, $(document.body).data('hdparams')['documentTitle'], sNewUrl);
171 }
172
173 // update proto link
174 function updateProto(domProtoCombo) {
175     var nameProtoMethod = $("#"+domProtoCombo+" option:selected").attr('value');
176     $(document.body).data('hdparams').protoKeys = null;
177     if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
178         var partsURL = $(document.body).data('hdparams').classificationServer.split("/");
179         var protoKeys = nameProtoMethod.split(".", 2);       //modified for multiple detect/pixelate models
180         $(document.body).data('hdparams').protoKeys = protoKeys;
181         partsURL[partsURL.length-1] = protoKeys[1];
182         $(document.body).data('hdparams').classificationServer = partsURL.join("/");   //rejoin with new endpoint
183         updateLink("serverLink", $(document.body).data('hdparams').classificationServer);
184     }
185 }
186
187 // https://stackoverflow.com/a/6021027
188 function updateQueryStringParameter(uri, key, value, separator) {
189     var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
190     if (uri.match(re)) {
191         return uri.replace(re, '$1' + key + "=" + value + '$2');
192     }
193     if (!separator) //allow forced/override
194        separator = uri.indexOf('?') !== -1 ? "&" : "?";
195     return uri + separator + key + "=" + value;
196 }
197
198 // https://stackoverflow.com/a/3354511
199 window.onpopstate = function(e){
200     if(e.state){
201         //document.getElementById("content").innerHTML = e.state.html;
202         $(document.body).data('hdparams')['documentTitle'] = e.state.pageTitle;
203     }
204 };
205
206 function getUrlParameter(key) {
207     var re = new RegExp("([?&])" + key + "=(.*?)(&|$)", "i");
208     var match = window.location.search.match(re)
209     if (match) {
210         //console.log(match);
211         return match[match.length-2];
212     }
213 };
214
215 /**
216  * Change the video source and restart
217  */
218 function switchVideo(movieAttr) {
219         var hd = $(document.body).data('hdparams');
220
221     // Set the video source based on URL specified in the 'videos' list, or select camera input
222     $(hd.video).show();
223     $(srcImgCanvas).hide();
224     if (movieAttr == "Camera") {
225         var constraints = {audio: false, video: true};
226         navigator.mediaDevices.getUserMedia(constraints)
227             .then(function(mediaStream) {
228                 hd.video.srcObject = mediaStream;
229                 hd.video.play();
230             })
231             .catch(function(err) {
232                 console.log(err.name + ": " + err.message);
233             });
234     } else {
235         var mp4 = document.getElementById("mp4");
236         mp4.setAttribute("src", movieAttr);
237         hd.video.load();
238         hd.video.autoplay = true;
239         newVideo();
240     }
241 }
242
243 /**
244  * Called after a new video has loaded (at least the video metadata has loaded)
245  */
246 function newVideo() {
247         var hd = $(document.body).data('hdparams');
248         hd.frameCounter = 0;
249         hd.imageIsWaiting = false;
250
251         // set processing canvas size based on source video
252         var pwidth = hd.video.videoWidth;
253         var pheight = hd.video.videoHeight;
254         if (pwidth > hd.maxSrcVideoWidth) {
255                 pwidth = hd.maxSrcVideoWidth;
256                 pheight = Math.floor((pwidth / hd.video.videoWidth) * pheight); // preserve aspect ratio
257         }
258         hd.srcImgCanvas.width = pwidth;
259         hd.srcImgCanvas.height = pheight;
260
261     updateProto("protoMethod");
262     hd.frameTimer = setInterval(nextFrame, hd.frameInterval); // start the processing
263 }
264
265
266 /**
267  * process the next video frame
268  */
269 function nextFrame() {
270         var hd = $(document.body).data('hdparams');
271         if (hd.video.ended || hd.video.paused) {
272                 return;
273         }
274     switchImage(hd.video, true);
275 }
276
277 function switchImage(imgSrc, isVideo) {
278     var canvas = $(document.body).data('hdparams')['srcImgCanvas'];
279     if (!isVideo) {
280         var img = new Image();
281         img.crossOrigin = "Anonymous";
282         img.onload = function () {
283             var ctx = canvas.getContext('2d');
284             var canvasCopy = document.createElement("canvas");
285             var copyContext = canvasCopy.getContext("2d");
286
287             var ratio = 1;
288
289             //console.log( $(document.body).data('hdparams'));
290             //console.log( [ img.width, img.height]);
291             // https://stackoverflow.com/a/2412606
292             if(img.width > $(document.body).data('hdparams')['canvasMaxW'])
293                 ratio = $(document.body).data('hdparams')['canvasMaxW'] / img.width;
294             if(ratio*img.height > $(document.body).data('hdparams')['canvasMaxH'])
295                 ratio = $(document.body).data('hdparams')['canvasMaxH'] / img.height;
296
297             canvasCopy.width = img.width;
298             canvasCopy.height = img.height;
299             copyContext.drawImage(img, 0, 0);
300
301             canvas.width = img.width * ratio;
302             canvas.height = img.height * ratio;
303             ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
304             //document.removeChild(canvasCopy);
305             updateProto("protoMethod");
306             doPostImage(canvas, '#resultsDiv', '#destImg', canvas.toDataURL());
307         }
308         img.src = imgSrc;  //copy source, let image load
309     }
310     else if (!$(document.body).data('hdparams').imageIsWaiting) {
311         var ctx = canvas.getContext('2d');
312         var canvasCopy = document.createElement("canvas");
313         var copyContext = canvasCopy.getContext("2d");
314         var ratio = 1;
315
316         if(imgSrc.videoWidth > $(document.body).data('hdparams')['canvasMaxW'])
317             ratio = $(document.body).data('hdparams')['canvasMaxW'] / imgSrc.videoWidth;
318         if(ratio*imgSrc.videoHeight > $(document.body).data('hdparams')['canvasMaxH'])
319             ratio = $(document.body).data('hdparams')['canvasMaxH'] / canvasCopy.height;
320
321         //console.log("Canvas Copy:"+canvasCopy.width+"/"+canvasCopy.height);
322         //console.log("Canvas Ratio:"+ratio);
323         //console.log("Video: "+imgSrc.videoWidth+"x"+imgSrc.videoHeight);
324         canvasCopy.width = imgSrc.videoWidth;     //large as possible
325         canvasCopy.height = imgSrc.videoHeight;
326         copyContext.drawImage(imgSrc, 0, 0);
327
328         canvas.width = canvasCopy.width * ratio;
329         canvas.height = canvasCopy.height * ratio;
330         ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
331         //document.removeChild(canvasCopy);
332         doPostImage(canvas, '#resultsDiv', '#destImg', canvas.toDataURL());
333     }
334 }
335
336
337 /**
338  * post an image from the canvas to the service
339  */
340 function doPostImage(srcCanvas, dstDiv, dstImg, imgPlaceholder) {
341     var dataURL = srcCanvas.toDataURL('image/jpeg', 1.0);
342     var hd = $(document.body).data('hdparams');
343     var sendPayload = null;
344
345     hd.imageIsWaiting = true;
346     var domHeaders = {};
347     dstDiv = $(dstDiv);
348     $("#postSpinner").remove();     //erase previously existing one
349     dstDiv.append($("<div id='postSpinner' class='spinner'>&nbsp;</div>"));
350     if (dstImg)     //convert to jquery dom object
351         dstImg = $(dstImg);
352
353     //console.log("[doPostImage]: Selected method ... '"+typeInput+"'");
354     if (hd.protoKeys) {     //valid protobuf type?
355         var blob = dataURItoBlob(dataURL, true);
356
357         // fields from .proto file at time of writing...
358         // message Image {
359         //   string mime_type = 1;
360         //   bytes image_binary = 2;
361         // }
362
363         //TODO: should we always assume this is input? answer: for now, YES, always image input!
364         var inputPayload = { "mimeType": blob.type, "imageBinary": blob.bytes };
365
366         // ---- method for processing from a type ----
367         var msgInput = hd.protoObj[hd.protoKeys[0]]['root'].lookupType(hd.protoObj[hd.protoKeys[0]]['methods'][hd.protoKeys[1]]['typeIn']);
368         // Verify the payload if necessary (i.e. when possibly incomplete or invalid)
369         var errMsg = msgInput.verify(inputPayload);
370         if (errMsg) {
371             var strErr = "[doPostImage]: Error during type verify for object input into protobuf method. ("+errMsg+")";
372             dstDiv.empty().html(strErr);
373             console.log(strErr);
374             throw Error(strErr);
375         }
376         // Create a new message
377         var msgTransmit = msgInput.create(inputPayload);
378         // Encode a message to an Uint8Array (browser) or Buffer (node)
379         sendPayload = msgInput.encode(msgTransmit).finish();
380
381         //downloadBlob(sendPayload, 'protobuf.bin', 'application/octet-stream');
382         // NOTE: TO TEST THIS BINARY BLOB, use some command-line magic like this...
383         //  protoc --decode=mMJuVapnmIbrHlZGKyuuPDXsrkzpGqcr.FaceImage model.proto < protobuf.bin
384         $("#protoInput").prop("disabled",false);
385         hd.protoPayloadInput = sendPayload;
386
387         //request.setRequestHeader("Content-type", "application/octet-stream;charset=UTF-8");
388         domHeaders["Content-type"] = "text/plain;charset=UTF-8";
389         //request.responseType = 'arraybuffer';
390     }
391     else if (hd.protoList.length) {
392         var strErr = "[doPostImage]: Proto method expected but unavailable in POST, aborting send.";
393         console.log(strErr);
394         throw Error(strErr);
395     }
396     else {
397         var blob = dataURItoBlob(dataURL, false);
398         sendPayload = new FormData();
399         if (hd.serverIsLocal) {
400             serviceURL = hd.classificationServer;
401             sendPayload.append("image_binary", blob);
402             sendPayload.append("mime_type", blob.type);
403         }
404         else {      //disabled now for direct URL specification
405             serviceURL = hd.classificationServerFirewall;
406             sendPayload.append("myFile", blob);
407             sendPayload.append("rtnformat", "json");
408             sendPayload.append("myList", "5");  // limit the number of classes (max 1000)
409         }
410     }
411
412     //$(dstImg).addClaas('workingImage').siblings('.spinner').remove().after($("<span class='spinner'>&nbsp;</span>"));
413     $.ajax({
414         type: 'POST',
415         url: hd.classificationServer,
416         data: sendPayload,
417         crossDomain: true,
418         dataType: 'native',
419         xhrFields: {
420             responseType: 'arraybuffer'
421         },
422         processData: false,
423         headers: domHeaders,
424         error: function (data, textStatus, errorThrown) {
425             //console.log(textStatus);
426             if (textStatus=="error") {
427                 textStatus += " (Was the transform URL valid? Was the right method selected?) ";
428             }
429             var errStr = "Error: Failed javascript POST (err: "+textStatus+","+errorThrown+")";
430             console.log(errStr);
431             dstDiv.html(errStr);
432             hd.imageIsWaiting = false;
433             return false;
434         },
435         success: function(data, textStatus, jqXHR) {
436             // what do we do with a good processing result?
437             //
438             //  data: the raw body from the response
439             //  dstImg: the dom element of a destination image
440             //  methodKeys: which protomethod was selected
441             //  dstImg: the dom element of a destination image (if available)
442             //  imgPlaceholder: the exported canvas image from last source
443             //
444             var returnState = processResult(data, dstDiv, hd.protoKeys, dstImg, imgPlaceholder);
445             hd.imageIsWaiting = false;
446             return returnState;
447         }
448         });
449 }
450
451 /**
452  * convert base64/URLEncoded data component to raw binary data held in a string
453  *
454  * Stoive, http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata
455  */
456 function dataURItoBlob(dataURI, wantBytes) {
457     // convert base64/URLEncoded data component to raw binary data held in a string
458     var byteString;
459     if (dataURI.split(',')[0].indexOf('base64') >= 0)
460         byteString = atob(dataURI.split(',')[1]);
461     else
462         byteString = unescape(dataURI.split(',')[1]);
463
464     // separate out the mime component
465     var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
466
467     // write the bytes of the string to a typed array
468     var ia = new Uint8Array(byteString.length);
469     for (var i = 0; i < byteString.length; i++) {
470         ia[i] = byteString.charCodeAt(i);
471     }
472     //added for returning bytes directly
473     if (wantBytes) {
474         return {'bytes':ia, 'type':mimeString};
475     }
476     return new Blob([ia], {type:mimeString});
477 }
478
479 // https://stackoverflow.com/a/12713326
480 function Uint8ToString(u8a){
481   var CHUNK_SZ = 0x8000;
482   var c = [];
483   for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
484     c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
485   }
486   return c.join("");
487 }
488
489 function BlobToDataURI(data, mime) {
490     var b64encoded = btoa(Uint8ToString(data));
491     return "data:"+mime+";base64,"+b64encoded;
492 }
493
494 // ----- diagnostic tool to download binary blobs ----
495 function downloadBlobOut() {
496     return downloadBlob($(document.body).data('hdparams').protoPayloadOutput, "protobuf.out.bin");
497 }
498
499 function downloadBlobIn() {
500     return downloadBlob($(document.body).data('hdparams').protoPayloadInput, "protobuf.in.bin");
501 }
502
503 //  https://stackoverflow.com/a/33622881
504 function downloadBlob(data, fileName, mimeType) {
505     //if there is no data, filename, or mime provided, make our own
506     if (!data)
507         data = $(document.body).data('hdparams').protoPayloadInput;
508     if (!fileName)
509         fileName = "protobuf.bin";
510     if (!mimeType)
511         mimeType = "application/octet-stream";
512
513     var blob, url;
514     blob = new Blob([data], {
515         type: mimeType
516     });
517     url = window.URL.createObjectURL(blob);
518     downloadURL(url, fileName, mimeType);
519     setTimeout(function() {
520         return window.URL.revokeObjectURL(url);
521     }, 1000);
522 };
523
524 function downloadURL(data, fileName) {
525     var a;
526     a = document.createElement('a');
527     a.href = data;
528     a.download = fileName;
529     document.body.appendChild(a);
530     a.style = 'display: none';
531     a.click();
532     a.remove();
533 };
534
535
536 //load image that has been uploaded into a canvas
537 function handleImage(e){
538     var reader = new FileReader();
539     reader.onload = function(event){
540         switchImage(event.target.result);
541     }
542     reader.readAsDataURL(e.target.files[0]);
543 }
544
545