code changs for multiple protobuf models
[face-privacy-filter.git] / web_demo / face-privacy.js
index d20654c..e1a597a 100644 (file)
-/**\r
- image-classes.js - send frames to an image classification service\r
-\r
- Videos or camera are displayed locally and frames are periodically sent to GPU image-net classifier service (developed by Zhu Liu) via http post.\r
- For webRTC, See: https://gist.github.com/greenido/6238800\r
\r
- D. Gibbon 6/3/15\r
- D. Gibbon 4/19/17 updated to new getUserMedia api, https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia\r
- D. Gibbon 8/1/17 adapted for Cognita\r
- */\r
\r
-"use strict";\r
-\r
-/**\r
- * main entry point\r
- */\r
-$(document).ready(function() {\r
-    var urlDefault = getUrlParameter('url-image');\r
-    if (!urlDefault)\r
-        urlDefault = "http://localhost:8884/transform";\r
-\r
-       $(document.body).data('hdparams', {     // store global vars in the body element\r
-               classificationServer: urlDefault,\r
-               frameCounter: 0,\r
-               frameInterval: 500,             // Milliseconds to sleep between sending frames to reduce server load and reduce results updates\r
-               frameTimer: -1,         // frame clock for processing\r
-               imageIsWaiting: false,  // blocking to prevent too many queued frames\r
-               // Objects from DOM elements\r
-               srcImgCanvas: document.getElementById('srcImgCanvas'),  // we have a 'src' source image\r
-               destImg: document.getElementById('destImg'),    // we have a 'src' source image\r
-               video: document.getElementById('srcVideo'),\r
-       });\r
-    $(document.body).data('hdparams')['canvasMaxH'] = $(document.body).data('hdparams')['srcImgCanvas'].height;\r
-    $(document.body).data('hdparams')['canvasMaxW'] = $(document.body).data('hdparams')['srcImgCanvas'].width;\r
-\r
-       //add text input tweak\r
-       $("#serverUrl").change(function() {\r
-           $(document.body).data('hdparams')['classificationServer'] = $(this).val();\r
-        updateLink("serverLink");\r
-       }).val($(document.body).data('hdparams')['classificationServer'])\r
-       //set launch link at first\r
-    updateLink("serverLink");\r
-\r
-       // add buttons to change video\r
-       $("#sourceRibbon div").click(function() {\r
-           var $this = $(this);\r
-           $this.siblings().removeClass('selected'); //clear other selection\r
-           $this.addClass('selected');\r
-           var objImg = $this.children('img')[0];\r
-           var hd = $(document.body).data('hdparams');\r
-           if (objImg) {\r
-               switchImage(objImg.src);\r
-            clearInterval(hd.frameTimer);      // stop the processing\r
-\r
-            var movieAttr = $(objImg).attr('movie');\r
-            if (movieAttr) {\r
-                // Set the video source based on URL specified in the 'videos' list, or select camera input\r
-                $(hd.video).show();\r
-                $(srcImgCanvas).hide();\r
-                if (movieAttr == "Camera") {\r
-                    var constraints = {audio: false, video: true};\r
-                    navigator.mediaDevices.getUserMedia(constraints)\r
-                        .then(function(mediaStream) {\r
-                            hd.video.srcObject = mediaStream;\r
-                            hd.video.play();\r
-                        })\r
-                        .catch(function(err) {\r
-                            console.log(err.name + ": " + err.message);\r
-                        });\r
-                } else {\r
-                    var mp4 = document.getElementById("mp4");\r
-                    mp4.setAttribute("src", movieAttr);\r
-                    hd.video.load();\r
-                    newVideo();\r
-                }\r
-            }\r
-            else {\r
-                hd.video.pause();\r
-                $(hd.video).hide();\r
-                $(srcImgCanvas).show();\r
-            }\r
-           }\r
-       });\r
-\r
-       //allow user-uploaded images\r
-    var imageLoader = document.getElementById('imageLoader');\r
-    imageLoader.addEventListener('change', handleImage, false);\r
-\r
-    //trigger first click\r
-    $("#sourceRibbon div")[0].click();\r
-});\r
-\r
-\r
-/**\r
- * Called after a new video has loaded (at least the video metadata has loaded)\r
- */\r
-function newVideo() {\r
-       var hd = $(document.body).data('hdparams');\r
-       hd.frameCounter = 0;\r
-       hd.imageIsWaiting = false;\r
-       hd.video.play();\r
-\r
-       // set processing canvas size based on source video\r
-       var pwidth = hd.video.videoWidth;\r
-       var pheight = hd.video.videoHeight;\r
-       if (pwidth > hd.maxSrcVideoWidth) {\r
-               pwidth = hd.maxSrcVideoWidth;\r
-               pheight = Math.floor((pwidth / hd.video.videoWidth) * pheight); // preserve aspect ratio\r
-       }\r
-       hd.srcImgCanvas.width = pwidth;\r
-       hd.srcImgCanvas.height = pheight;\r
-\r
-       hd.frameTimer = setInterval(nextFrame, hd.frameInterval); // start the processing\r
-}\r
-\r
-/**\r
- * process the next video frame\r
- */\r
-function nextFrame() {\r
-       var hd = $(document.body).data('hdparams');\r
-       if (hd.video.ended || hd.video.paused) {\r
-               return;\r
-       }\r
-    switchImage(hd.video, true);\r
-}\r
-\r
-function updateLink(domId) {\r
-    var sPageURL = decodeURIComponent(window.location.search.split('?')[0]);\r
-    var newServer = $(document.body).data('hdparams')['classificationServer'];\r
-    var sNewUrl = sPageURL+"?url-image="+newServer;\r
-    $("#"+domId).attr('href', sNewUrl);\r
-}\r
-\r
-function switchImage(imgSrc, isVideo) {\r
-    var canvas = $(document.body).data('hdparams')['srcImgCanvas'];\r
-    if (!isVideo) {\r
-        var img = new Image();\r
-        img.onload = function () {\r
-            var ctx = canvas.getContext('2d');\r
-            var canvasCopy = document.createElement("canvas");\r
-            var copyContext = canvasCopy.getContext("2d");\r
-\r
-            var ratio = 1;\r
-\r
-            //console.log( $(document.body).data('hdparams'));\r
-            //console.log( [ img.width, img.height]);\r
-            // https://stackoverflow.com/a/2412606\r
-            if(img.width > $(document.body).data('hdparams')['canvasMaxW'])\r
-                ratio = $(document.body).data('hdparams')['canvasMaxW'] / img.width;\r
-            if(ratio*img.height > $(document.body).data('hdparams')['canvasMaxH'])\r
-                ratio = $(document.body).data('hdparams')['canvasMaxH'] / img.height;\r
-\r
-            canvasCopy.width = img.width;\r
-            canvasCopy.height = img.height;\r
-            copyContext.drawImage(img, 0, 0);\r
-\r
-            canvas.width = img.width * ratio;\r
-            canvas.height = img.height * ratio;\r
-            ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);\r
-            //document.removeChild(canvasCopy);\r
-            doPostImage(canvas, '#destImg', canvas.toDataURL());\r
-        }\r
-        img.src = imgSrc;  //copy source, let image load\r
-    }\r
-    else if (!$(document.body).data('hdparams').imageIsWaiting) {\r
-        var ctx = canvas.getContext('2d');\r
-        var canvasCopy = document.createElement("canvas");\r
-        var copyContext = canvasCopy.getContext("2d");\r
-        var ratio = 1;\r
-\r
-        if(imgSrc.videoWidth > $(document.body).data('hdparams')['canvasMaxW'])\r
-            ratio = $(document.body).data('hdparams')['canvasMaxW'] / imgSrc.videoWidth;\r
-        if(ratio*imgSrc.videoHeight > $(document.body).data('hdparams')['canvasMaxH'])\r
-            ratio = $(document.body).data('hdparams')['canvasMaxH'] / canvasCopy.height;\r
-\r
-        //console.log("Canvas Copy:"+canvasCopy.width+"/"+canvasCopy.height);\r
-        //console.log("Canvas Ratio:"+ratio);\r
-        //console.log("Video: "+imgSrc.videoWidth+"x"+imgSrc.videoHeight);\r
-        canvasCopy.width = imgSrc.videoWidth;     //large as possible\r
-        canvasCopy.height = imgSrc.videoHeight;\r
-        copyContext.drawImage(imgSrc, 0, 0);\r
-\r
-        canvas.width = canvasCopy.width * ratio;\r
-        canvas.height = canvasCopy.height * ratio;\r
-        ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);\r
-        //document.removeChild(canvasCopy);\r
-        doPostImage(canvas, '#destImg', canvas.toDataURL());\r
-    }\r
-}\r
-\r
-\r
-//load image that has been uploaded into a vancas\r
-function handleImage(e){\r
-    var reader = new FileReader();\r
-    reader.onload = function(event){\r
-        switchImage(event.target.result);\r
-    }\r
-    reader.readAsDataURL(e.target.files[0]);\r
-}\r
-\r
-\r
-\r
-// https://stackoverflow.com/questions/19491336/get-url-parameter-jquery-or-how-to-get-query-string-values-in-js\r
-function getUrlParameter(sParam) {\r
-    var sPageURL = decodeURIComponent(window.location.search.substring(1)),\r
-        sURLVariables = sPageURL.split('&'),\r
-        sParameterName,\r
-        i;\r
-\r
-    for (i = 0; i < sURLVariables.length; i++) {\r
-        sParameterName = sURLVariables[i].split('=');\r
-\r
-        if (sParameterName[0] === sParam) {\r
-            return sParameterName[1] === undefined ? true : sParameterName[1];\r
-        }\r
-    }\r
-};\r
-\r
-\r
-/**\r
- * post an image from the canvas to the service\r
- */\r
-function doPostImage(srcCanvas, dstImg, dataPlaceholder) {\r
-       var serviceURL = "";\r
-       var dataURL = srcCanvas.toDataURL('image/jpeg', 1.0);\r
-       var blob = dataURItoBlob(dataURL);\r
-       var hd = $(document.body).data('hdparams');\r
-       var fd = new FormData();\r
-\r
-       $(document.body).data('hdparams').imageIsWaiting = true;\r
-    serviceURL = hd.classificationServer;\r
-    fd.append("base64_data", blob);\r
-    fd.append("mime_type", "image/jpeg");\r
-    var $dstImg = $(dstImg);\r
-    if ($dstImg.attr('src')=='') {\r
-        $dstImg.attr('src', dataPlaceholder);\r
-        //$(dstImg).addClass('workingImage').attr('src', dataPlaceholder);\r
-    }\r
-    //$(dstImg).addClaas('workingImage').siblings('.spinner').remove().after($("<span class='spinner'>&nbsp;</span>"));\r
-\r
-       var request = new XMLHttpRequest();\r
-       hd.imageIsWaiting = true;\r
-       request.onreadystatechange=function() {\r
-               if (request.readyState==4 && request.status==200) {\r
-                   var responseJson = $.parseJSON(request.responseText);\r
-                   var respImage = responseJson[0];\r
-                   // https://stackoverflow.com/questions/21227078/convert-base64-to-image-in-javascript-jquery\r
-            $dstImg.attr('src', "data:"+respImage['mime_type']+";base64,"+respImage['base64_data']).removeClass('workingImage');\r
-                       //genClassTable($.parseJSON(request.responseText), dstDiv);\r
-                       hd.imageIsWaiting = false;\r
-               }\r
-       }\r
-       request.open("POST", serviceURL, true);\r
-       request.send(fd);\r
-       $(document.body).data('hdparams').imageIsWaiting = false;\r
-}\r
-\r
-\r
-/**\r
- * convert base64/URLEncoded data component to raw binary data held in a string\r
- *\r
- * Stoive, http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata\r
- */\r
-function dataURItoBlob(dataURI) {\r
-    // convert base64/URLEncoded data component to raw binary data held in a string\r
-    var byteString;\r
-    if (dataURI.split(',')[0].indexOf('base64') >= 0)\r
-        byteString = atob(dataURI.split(',')[1]);\r
-    else\r
-        byteString = unescape(dataURI.split(',')[1]);\r
-\r
-    // separate out the mime component\r
-    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];\r
-\r
-    // write the bytes of the string to a typed array\r
-    var ia = new Uint8Array(byteString.length);\r
-    for (var i = 0; i < byteString.length; i++) {\r
-        ia[i] = byteString.charCodeAt(i);\r
-    }\r
-\r
-    return new Blob([ia], {type:mimeString});\r
-}\r
-\r
+/**
+ face-privacy.js - send frames to an face privacy service
+
+ Videos or camera are displayed locally and frames are periodically sent to GPU image-net classifier service (developed by Zhu Liu) via http post.
+ For webRTC, See: https://gist.github.com/greenido/6238800
+
+ D. Gibbon 6/3/15
+ D. Gibbon 4/19/17 updated to new getUserMedia api, https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
+ D. Gibbon 8/1/17 adapted for system
+ E. Zavesky 10/19/17 adapted for video+image
+ */
+
+"use strict";
+
+/**
+ * main entry point
+ */
+$(document).ready(function() {
+    var urlDefault = getUrlParameter('url-image');
+    if (!urlDefault)
+        urlDefault = "http://localhost:8884/transform";
+
+       $(document.body).data('hdparams', {     // store global vars in the body element
+               classificationServer: urlDefault,
+               protoObj: null,   // to be back-filled after protobuf load {'root':obj, 'methods':{'xx':{'typeIn':x, 'typeOut':y}} }
+               protoPayloadInput: null,   //payload for encoded message download (if desired)
+               protoPayloadOutput: null,   //payload for encoded message download (if desired)
+               protoRes: null,  //TEMPORARY
+               frameCounter: 0,
+               frameInterval: 500,             // Milliseconds to sleep between sending frames to reduce server load and reduce results updates
+               frameTimer: -1,         // frame clock for processing
+               imageIsWaiting: false,  // blocking to prevent too many queued frames
+               // Objects from DOM elements
+               srcImgCanvas: document.getElementById('srcImgCanvas'),  // we have a 'src' source image
+               destImg: document.getElementById('destImg'),    // we have a 'src' source image
+               video: document.getElementById('srcVideo'),
+       });
+    $(document.body).data('hdparams')['canvasMaxH'] = $(document.body).data('hdparams')['srcImgCanvas'].height;
+    $(document.body).data('hdparams')['canvasMaxW'] = $(document.body).data('hdparams')['srcImgCanvas'].width;
+
+    $("#protoInput").prop("disabled",true).click(downloadBlobIn);
+    $("#protoOutput").prop("disabled",true).click(downloadBlobOut);
+    $("#resultText").hide();
+
+       //add text input tweak
+       $("#serverUrl").change(function() {
+           $(document.body).data('hdparams')['classificationServer'] = $(this).val();
+        updateLink("serverLink");
+       }).val($(document.body).data('hdparams')['classificationServer'])
+       //set launch link at first
+    updateLink("serverLink");
+
+       // add buttons to change video
+       $("#sourceRibbon div").click(function() {
+           var $this = $(this);
+           $this.siblings().removeClass('selected'); //clear other selection
+           $this.addClass('selected');
+           var objImg = $this.children('img')[0];
+           var hd = $(document.body).data('hdparams');
+           if (objImg) {
+               switchImage(objImg.src);
+            clearInterval(hd.frameTimer);      // stop the processing
+
+            var movieAttr = $(objImg).attr('movie');
+            if (movieAttr) {
+                // Set the video source based on URL specified in the 'videos' list, or select camera input
+                $(hd.video).show();
+                $(srcImgCanvas).hide();
+                if (movieAttr == "Camera") {
+                    var constraints = {audio: false, video: true};
+                    navigator.mediaDevices.getUserMedia(constraints)
+                        .then(function(mediaStream) {
+                            hd.video.srcObject = mediaStream;
+                            hd.video.play();
+                        })
+                        .catch(function(err) {
+                            console.log(err.name + ": " + err.message);
+                        });
+                } else {
+                    var mp4 = document.getElementById("mp4");
+                    mp4.setAttribute("src", movieAttr);
+                    hd.video.load();
+                    newVideo();
+                }
+            }
+            else {
+                hd.video.pause();
+                $(hd.video).hide();
+                $(srcImgCanvas).show();
+            }
+           }
+       });
+
+       //allow user-uploaded images
+    var imageLoader = document.getElementById('imageLoader');
+    imageLoader.addEventListener('change', handleImage, false);
+
+    //if protobuf is enabled, fire load event for it as well
+    $(document.body).data('hdparams').protoObj = {};  //clear from last load
+    protobuf_load("model.pixelate.proto", true);
+    protobuf_load("model.detect.proto");
+
+    //trigger first click
+    $("#sourceRibbon div")[0].click();
+});
+
+
+function protobuf_load(pathProto, forceSelect) {
+    protobuf.load(pathProto, function(err, root) {
+        if (err) {
+            console.log("[protobuf]: Error!: "+err);
+            throw err;
+        }
+        var domSelect = $("#protoMethod");
+        var numMethods = domSelect.children().length;
+        $.each(root.nested, function(namePackage, objPackage) {    // walk all
+            if ('Model' in objPackage && 'methods' in objPackage.Model) {    // walk to model and functions...
+                var typeSummary = {'root':root, 'methods':{} };
+                $.each(objPackage.Model.methods, function(nameMethod, objMethod) {  // walk methods
+                    typeSummary['methods'][nameMethod] = {};
+                    typeSummary['methods'][nameMethod]['typeIn'] = namePackage+'.'+objMethod.requestType;
+                    typeSummary['methods'][nameMethod]['typeOut'] = namePackage+'.'+objMethod.responseType;
+                    typeSummary['methods'][nameMethod]['service'] = namePackage+'.'+nameMethod;
+
+                    //create HTML object as well
+                    var namePretty = namePackage+"."+nameMethod;
+                    var domOpt = $("<option />").attr("value", namePretty).text(
+                        nameMethod+ " (input: "+objMethod.requestType
+                        +", output: "+objMethod.responseType+")");
+                    if (numMethods==0) {    // first method discovery
+                        domSelect.append($("<option />").attr("value","").text("(disabled, not loaded)")); //add 'disabled'
+                    }
+                    if (forceSelect) {
+                        domOpt.attr("selected", 1);
+                    }
+                    domSelect.append(domOpt);
+                    numMethods++;
+                });
+                $(document.body).data('hdparams').protoObj[namePackage] = typeSummary;   //save new method set
+                $("#protoContainer").show();
+            }
+        });
+        console.log("[protobuf]: Load successful, found "+numMethods+" model methods.");
+    });
+}
+
+/**
+ * Called after a new video has loaded (at least the video metadata has loaded)
+ */
+function newVideo() {
+       var hd = $(document.body).data('hdparams');
+       hd.frameCounter = 0;
+       hd.imageIsWaiting = false;
+       hd.video.play();
+
+       // set processing canvas size based on source video
+       var pwidth = hd.video.videoWidth;
+       var pheight = hd.video.videoHeight;
+       if (pwidth > hd.maxSrcVideoWidth) {
+               pwidth = hd.maxSrcVideoWidth;
+               pheight = Math.floor((pwidth / hd.video.videoWidth) * pheight); // preserve aspect ratio
+       }
+       hd.srcImgCanvas.width = pwidth;
+       hd.srcImgCanvas.height = pheight;
+
+       hd.frameTimer = setInterval(nextFrame, hd.frameInterval); // start the processing
+}
+
+/**
+ * process the next video frame
+ */
+function nextFrame() {
+       var hd = $(document.body).data('hdparams');
+       if (hd.video.ended || hd.video.paused) {
+               return;
+       }
+    switchImage(hd.video, true);
+}
+
+function updateLink(domId, newServer) {
+    var sPageURL = decodeURIComponent(window.location.search.split('?')[0]);
+    if (newServer==undefined) {
+        newServer = $(document.body).data('hdparams')['classificationServer'];
+    }
+    else {
+        $("#serverUrl").val(newServer);
+    }
+    var sNewUrl = sPageURL+"?url-image="+newServer;
+    $("#"+domId).attr('href', sNewUrl);
+}
+
+function switchImage(imgSrc, isVideo) {
+    var canvas = $(document.body).data('hdparams')['srcImgCanvas'];
+    if (!isVideo) {
+        var img = new Image();
+        img.onload = function () {
+            var ctx = canvas.getContext('2d');
+            var canvasCopy = document.createElement("canvas");
+            var copyContext = canvasCopy.getContext("2d");
+
+            var ratio = 1;
+
+            //console.log( $(document.body).data('hdparams'));
+            //console.log( [ img.width, img.height]);
+            // https://stackoverflow.com/a/2412606
+            if(img.width > $(document.body).data('hdparams')['canvasMaxW'])
+                ratio = $(document.body).data('hdparams')['canvasMaxW'] / img.width;
+            if(ratio*img.height > $(document.body).data('hdparams')['canvasMaxH'])
+                ratio = $(document.body).data('hdparams')['canvasMaxH'] / img.height;
+
+            canvasCopy.width = img.width;
+            canvasCopy.height = img.height;
+            copyContext.drawImage(img, 0, 0);
+
+            canvas.width = img.width * ratio;
+            canvas.height = img.height * ratio;
+            ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
+            //document.removeChild(canvasCopy);
+            doPostImage(canvas, '#destImg', canvas.toDataURL());
+        }
+        img.src = imgSrc;  //copy source, let image load
+    }
+    else if (!$(document.body).data('hdparams').imageIsWaiting) {
+        var ctx = canvas.getContext('2d');
+        var canvasCopy = document.createElement("canvas");
+        var copyContext = canvasCopy.getContext("2d");
+        var ratio = 1;
+
+        if(imgSrc.videoWidth > $(document.body).data('hdparams')['canvasMaxW'])
+            ratio = $(document.body).data('hdparams')['canvasMaxW'] / imgSrc.videoWidth;
+        if(ratio*imgSrc.videoHeight > $(document.body).data('hdparams')['canvasMaxH'])
+            ratio = $(document.body).data('hdparams')['canvasMaxH'] / canvasCopy.height;
+
+        //console.log("Canvas Copy:"+canvasCopy.width+"/"+canvasCopy.height);
+        //console.log("Canvas Ratio:"+ratio);
+        //console.log("Video: "+imgSrc.videoWidth+"x"+imgSrc.videoHeight);
+        canvasCopy.width = imgSrc.videoWidth;     //large as possible
+        canvasCopy.height = imgSrc.videoHeight;
+        copyContext.drawImage(imgSrc, 0, 0);
+
+        canvas.width = canvasCopy.width * ratio;
+        canvas.height = canvasCopy.height * ratio;
+        ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
+        //document.removeChild(canvasCopy);
+        doPostImage(canvas, '#destImg', canvas.toDataURL());
+    }
+}
+
+
+//load image that has been uploaded into a vancas
+function handleImage(e){
+    var reader = new FileReader();
+    reader.onload = function(event){
+        switchImage(event.target.result);
+    }
+    reader.readAsDataURL(e.target.files[0]);
+}
+
+
+
+// https://stackoverflow.com/questions/19491336/get-url-parameter-jquery-or-how-to-get-query-string-values-in-js
+function getUrlParameter(sParam) {
+    var sPageURL = decodeURIComponent(window.location.search.substring(1)),
+        sURLVariables = sPageURL.split('&'),
+        sParameterName,
+        i;
+
+    for (i = 0; i < sURLVariables.length; i++) {
+        sParameterName = sURLVariables[i].split('=');
+
+        if (sParameterName[0] === sParam) {
+            return sParameterName[1] === undefined ? true : sParameterName[1];
+        }
+    }
+};
+
+
+/**
+ * post an image from the canvas to the service
+ */
+function doPostImage(srcCanvas, dstImg, dataPlaceholder) {
+    var dataURL = srcCanvas.toDataURL('image/jpeg', 1.0);
+    var hd = $(document.body).data('hdparams');
+    var sendPayload = null;
+
+    var nameProtoMethod = $("#protoMethod option:selected").attr('value');
+    var methodKeys = null;
+    if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
+        var partsURL = hd.classificationServer.split("/");
+        methodKeys = nameProtoMethod.split(".", 2);       //modified for multiple detect/pixelate models
+        partsURL[partsURL.length-1] = methodKeys[1];
+        hd.classificationServer = partsURL.join("/");   //rejoin with new endpoint
+        updateLink("serverLink", hd.classificationServer);
+    }
+
+    var serviceURL = hd.classificationServer;
+    var request = new XMLHttpRequest();     // create request to manipulate
+    request.open("POST", serviceURL, true);
+
+    //console.log("[doPostImage]: Selected method ... '"+typeInput+"'");
+    if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
+        var blob = dataURItoBlob(dataURL, true);
+
+        // fields from .proto file at time of writing...
+        //    message FaceImage {
+        //      repeated string mime_type = 1;   -> becomes "mimeType" (NOTE repeated type)
+        //      repeated bytes image_binary = 2; -> becomes "imageBinary"
+        //    }
+
+        //TODO: should we always assume this is input? answer: for now, YES, always image input!
+        var inputPayload = { "mimeType": [blob.type], "imageBinary": [blob.bytes] };
+
+        // ---- method for processing from a type ----
+        var msgInput = hd.protoObj[methodKeys[0]]['root'].lookupType(hd.protoObj[methodKeys[0]]['methods'][methodKeys[1]]['typeIn']);
+        // Verify the payload if necessary (i.e. when possibly incomplete or invalid)
+        var errMsg = msgInput.verify(inputPayload);
+        if (errMsg) {
+            console.log("[doPostImage]: Error during type verify for object input into protobuf method.");
+            throw Error(errMsg);
+        }
+        // Create a new message
+        var msgTransmit = msgInput.create(inputPayload);
+        // Encode a message to an Uint8Array (browser) or Buffer (node)
+        sendPayload = msgInput.encode(msgTransmit).finish();
+
+        // ----------
+
+        /*
+        // ---- method for processing from a service ----
+        var serviceInput = hd.protoObj['root'].lookup(hd.protoObj['methods'][nameProtoMethod]['service']);
+
+        function rpcImpl(method, requestData, callback) {
+            // perform the request using an HTTP request or a WebSocket for example
+            var responseData = ...;
+            // and call the callback with the binary response afterwards:
+            callback(null, responseData);
+        }
+        var serviceCall = serviceInput.create(rpcImpl, false, false); //request dlimited? response delimited?
+
+        serviceCall.sayHello(sendPayload).then(response) {
+            console.log('Greeting:', response.message);
+        });
+        // ---------------------------
+        */
+
+        //downloadBlob(sendPayload, 'protobuf.bin', 'application/octet-stream');
+        // NOTE: TO TEST THIS BINARY BLOB, use some command-line magic like this...
+        //  protoc --decode=mMJuVapnmIbrHlZGKyuuPDXsrkzpGqcr.FaceImage model.proto < protobuf.bin
+        $("#protoInput").prop("disabled",false);
+        hd.protoPayloadInput = sendPayload;
+
+        // append our encoded chunk
+        //console.log(sendPayload);
+        //console.log(typeof(blob.type));
+        // console.log(nameProtoMethod);
+        request.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
+        request.responseType = 'arraybuffer';
+    }
+    else {
+        var blob = dataURItoBlob(dataURL, false);
+        sendPayload = new FormData();
+        sendPayload.append("image_binary", blob);
+        sendPayload.append("mime_type", blob.type);
+    }
+    //$(dstImg).addClaas('workingImage').siblings('.spinner').remove().after($("<span class='spinner'>&nbsp;</span>"));
+    $(document.body).data('hdparams').imageIsWaiting = true;
+    var $dstImg = $(dstImg);
+    if ($dstImg.attr('src')=='') {
+        $dstImg.attr('src', dataPlaceholder);
+        //$(dstImg).addClass('workingImage').attr('src', dataPlaceholder);
+    }
+
+    hd.imageIsWaiting = true;
+    request.onreadystatechange=function() {
+        if (request.readyState==4 && request.status>=200 && request.status<300) {
+            if (methodKeys!=null) {     //valid protobuf type?
+                //console.log(request);
+                var bodyEncodedInString = new Uint8Array(request.response);
+                //console.log(bodyEncodedInString);
+                //console.log(bodyEncodedInString.length);
+                $("#protoOutput").prop("disabled",false);
+                hd.protoPayloadOutput = bodyEncodedInString;
+
+                // ---- method for processing from a type ----
+                var msgOutput = hd.protoObj[methodKeys[0]]['root'].lookupType(hd.protoObj[methodKeys[0]]['methods'][methodKeys[1]]['typeOut']);
+                var objRecv = msgOutput.decode(hd.protoPayloadOutput);
+                //console.log(objRecv);
+                hd.protoRes = objRecv;
+
+                // detect what mode we're in (detect alone or processed?)...
+                if (!Array.isArray(objRecv.mimeType)) {
+                    $dstImg.attr('src', "data:"+objRecv.mimeType+";base64,"+objRecv.imageBinary).removeClass('workingImage');
+                }
+                else {
+                    var domResult = $("#resultText");
+                    var domTable = $("<tr />");
+                    var arrNames = [];
+                    $.each(msgOutput.fields, function(name, val) {           //collect field names
+                        var nameClean = val.name;
+                        if (nameClean != 'imageBinary') {
+                            domTable.append($("<th />").html(nameClean));
+                            arrNames.push(nameClean);
+                        }
+                    });
+                    domTable = $("<table />").append(domTable);     // create embedded table
+
+                    var idxImg = -1;
+                    if ('region' in msgOutput.fields) {             // did we get regions?
+                        for (var i=0; i<objRecv.region.length; i++) {       //find the right region
+                            if (objRecv.region[i]==-1) {                    //special indicator for original image
+                                idxImg = i;
+                            }
+                            var domRow = $("<tr />");
+                            var strDisplay = [];
+                            $.each(arrNames, function(idx, name) {      //collect data from each column
+                                domRow.append($("<td />").html(objRecv[name][i]));
+                            });
+                            domTable.append(domRow);
+                            //domResult.append($("div").html(objRecv.region));
+                        }
+                        domResult.empty().append($("<strong />").html("Results")).show();
+                        domResult.append(domTable);
+                    }
+                    else {                                  //got images, get that chunk directly
+                        idxImg = 0;
+                    }
+
+                    if (idxImg != -1) {                     //got any valid image? display it
+                        //console.log(objRecv.mimeType[idxImg]);
+                        //console.log(objRecv.imageBinary[idxImg]);
+                        //var strImage = Uint8ToString(objRecv.imageBinary[idxImg]);
+                        var strImage = btoa(String.fromCharCode.apply(null, objRecv.imageBinary[idxImg]));
+                        $dstImg.attr('src', "data:"+objRecv.mimeType[idxImg]+";base64,"+strImage).removeClass('workingImage');
+                    }
+                }
+
+            }
+            else {
+                var responseJson = $.parseJSON(request.responseText);
+                var respImage = responseJson[0];
+                // https://stackoverflow.com/questions/21227078/convert-base64-to-image-in-javascript-jquery
+                $dstImg.attr('src', "data:"+respImage['mime_type']+";base64,"+respImage['image_binary']).removeClass('workingImage');
+                //genClassTable($.parseJSON(request.responseText), dstDiv);
+            }
+            hd.imageIsWaiting = false;
+        }
+       }
+       request.send(sendPayload);
+       $(document.body).data('hdparams').imageIsWaiting = false;
+}
+
+
+/**
+ * convert base64/URLEncoded data component to raw binary data held in a string
+ *
+ * Stoive, http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata
+ */
+function dataURItoBlob(dataURI, wantBytes) {
+    // convert base64/URLEncoded data component to raw binary data held in a string
+    var byteString;
+    if (dataURI.split(',')[0].indexOf('base64') >= 0)
+        byteString = atob(dataURI.split(',')[1]);
+    else
+        byteString = unescape(dataURI.split(',')[1]);
+
+    // separate out the mime component
+    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
+
+    // write the bytes of the string to a typed array
+    var ia = new Uint8Array(byteString.length);
+    for (var i = 0; i < byteString.length; i++) {
+        ia[i] = byteString.charCodeAt(i);
+    }
+    //added for returning bytes directly
+    if (wantBytes) {
+        return {'bytes':ia, 'type':mimeString};
+    }
+    return new Blob([ia], {type:mimeString});
+}
+
+function Uint8ToString(u8a){
+  var CHUNK_SZ = 0x8000;
+  var c = [];
+  for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
+    c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
+  }
+  return c.join("");
+}
+
+
+// ----- diagnostic tool to download binary blobs ----
+function downloadBlobOut() {
+    return downloadBlob($(document.body).data('hdparams').protoPayloadOutput, "protobuf.out.bin");
+}
+
+function downloadBlobIn() {
+    return downloadBlob($(document.body).data('hdparams').protoPayloadInput, "protobuf.in.bin");
+}
+
+//  https://stackoverflow.com/a/33622881
+function downloadBlob(data, fileName, mimeType) {
+  //if there is no data, filename, or mime provided, make our own
+  if (!data)
+      data = $(document.body).data('hdparams').protoPayloadInput;
+  if (!fileName)
+      fileName = "protobuf.bin";
+  if (!mimeType)
+      mimeType = "application/octet-stream";
+
+  var blob, url;
+  blob = new Blob([data], {
+    type: mimeType
+  });
+  url = window.URL.createObjectURL(blob);
+  downloadURL(url, fileName, mimeType);
+  setTimeout(function() {
+    return window.URL.revokeObjectURL(url);
+  }, 1000);
+};
+
+function downloadURL(data, fileName) {
+  var a;
+  a = document.createElement('a');
+  a.href = data;
+  a.download = fileName;
+  document.body.appendChild(a);
+  a.style = 'display: none';
+  a.click();
+  a.remove();
+};