convert protobuf i/o to flat,row-like data 89/1789/1
authorEric Z <ezavesky@research.att.com>
Mon, 7 May 2018 18:50:13 +0000 (13:50 -0500)
committerEric Z <ezavesky@research.att.com>
Mon, 7 May 2018 18:50:13 +0000 (13:50 -0500)
- convert images to row-based nested items
- update javascript demo pages to accomodate
- update documentation for simple instructions with model runner

Change-Id: I55ddbe9e3a14bee6ead79baf6a0d4eb65e2bbe9e
Signed-off-by: Eric Z <ezavesky@research.att.com>
Issue-ID: ACUMOS-786

docs/release-notes.md
docs/tutorials/lesson1.md
docs/tutorials/lesson2.md
docs/tutorials/lesson3.md
face_privacy_filter/_version.py
face_privacy_filter/filter_image.py
face_privacy_filter/transform_detect.py
face_privacy_filter/transform_region.py
web_demo/face-privacy.js
web_demo/model.detect.proto
web_demo/model.pixelate.proto

index b937424..bf71121 100644 (file)
 -->
 
 # Face Privacy Filter Release Notes
+## 0.3
+### 0.3.0
+* Documentation (lesson1) updated with model runner examples.  Deprecation notice
+  in using explicit proto- and swagger-based serves.
+* Update the structure of the protobuf input and output to use flattened (row-based)
+  structure instead of columnar data for all i/o channels.  This should allow
+  other inspecting applications to more easily understand and reuse implementations
+  for image data.
+* Update the demonstration HTML pages for similar modifications.
+
 ## 0.2
 ### 0.2.3
 * Documentation and package update to use install instructions instead of installing
   this package directly into a user's environment.
-* License addition 
+* License addition
 
 ### 0.2.2
 * Refactor documentation into sections and tutorials.
index e32551b..b854c4c 100644 (file)
@@ -37,11 +37,11 @@ composed together for operation.
 
 * Dump the `detect` model to disk.
 ```
-python face_privacy_filter/filter_image.py -d model_detect -f detect
+python face_privacy_filter/filter_image.py -f detect  -d model_detect
 ```
 * Dump the `pixelate` model to disk.
 ```
-python face_privacy_filter/filter_image.py -d model_pix -f pixelate
+python face_privacy_filter/filter_image.py -f pixelate -d model_pix
 ```
 
 
@@ -59,6 +59,46 @@ python face_privacy_filter/filter_image.py -d model_detect -p output.csv -i web_
 python face_privacy_filter/filter_image.py -d model_pix -i detect.csv -p output.jpg --csv_input
 ```
 
+## Using the client model runner
+Getting even closer to what it looks like in a deployed model, you can also use
+the model runner code to run your full cascade (detection + pixelate) transform
+locally. *(added v0.3.0)*
+
+1. First, decide the ports to run your detection and pixelate models. In the example
+below, detection runs on port `8884` and pixelation runs on port `8885`.  For the
+runner to properly forward requests for you, provide a simple JSON file example
+called `runtime.json` in the working directory that you run the model runner.
+
+```
+# cat runtime.json
+{downstream": ["http://127.0.0.1:8885/pixelate"]}
+```
+
+2. Second, dump and launch the face detection model. If you modify the ports to
+run the models, please change them accordingly.  This command example assumes
+that you have cloned the client library in a relative path of `../acumos-python-client`.
+The first line removes any prior model directory, the second dumps the detect
+model to disk, and the third runs the model.
+
+```
+rm -rf model_detect/;  \
+    python face_privacy_filter/filter_image.py -d model_detect -f detect; \
+    python ../acumos-python-client/testing/wrap/runner.py --port 8884 --modeldir model_detect/face_privacy_filter_detect
+
+```
+
+3. Finally, dump and launch the face pixelation model. Again, if you modify the ports to
+run the models, please change them accordingly.  Aside from the model and port,
+the main difference between the above line is that the model runner is instructed
+to *ignore* the downstream forward (`runtime.json`) file so that it doesn't attempt
+to forward the request to itself.
+
+```
+rm -rf model_pix;  \
+    python face_privacy_filter/filter_image.py -d model_pix -f pixelate; \
+    python ../acumos-python-client/testing/wrap/runner.py --port 8885 --modeldir model_pix/face_privacy_filter_pixelate  --no_downstream
+```
+
 
 # Installation Troubleshoting
 Using some environment-based versions of python (e.g. conda),
index 60e335d..bea69ef 100644 (file)
@@ -24,6 +24,10 @@ additional components are included in this repository:
 a simple [swagger-based webserver](../../testing) (documented here) and
 a [demo web page](../../web_demo) (documented in the [next tutorial](lesson3.md).
 
+**NOTE: These steps are now deprecated and a direct protobuf interface from
+web page to the model (the next tutorial) is the preferred operational step.
+*(added v0.2.3)* **
+
 ## Swagger API
 Using a simple [flask-based connexion server](https://github.com/zalando/connexion),
 an API scaffold has been built to host a serialized/dumped model.
index cc6de37..360e7df 100644 (file)
@@ -72,7 +72,7 @@ image that was sent to the remote service.  When available, the <strong>Download
 button will be enabled and a binary file will be generated in the browser.
 
 ```
-protoc --decode=sapLzHrujUMPBGCBEMWQFxEIMsxocFrG.FaceImage model.pixelate.proto < protobuf.bin
+protoc --decode=HipTviKTkIkcmyuMCIAIDkeOOQQYyJne.FaceImage model.pixelate.proto < protobuf.bin
 ```
 
 **NOTE** The specific package name may have changed since the time of writing,
index 5d5137c..3f69a1e 100644 (file)
@@ -1,3 +1,3 @@
 # -*- coding: utf-8 -*-
-__version__ = "0.2.3"
+__version__ = "0.3.0"
 MODEL_NAME = 'face_privacy_filter'
index c30b0c2..20e20f8 100644 (file)
@@ -37,21 +37,35 @@ def model_create_pipeline(transformer, funcName):
 
     # derive the input type from the transformer
     type_list, type_name = transformer._type_in  # it looked like this {'test': int, 'tag': str}
-    input_type = [(k, List[type_list[k]]) for k in type_list]
+    input_type = [(k, type_list[k]) for k in type_list]   # flat, no lists
     type_in = create_namedtuple(type_name, input_type)
+    name_multiple_in = type_name + "s"
+    input_set = create_namedtuple(type_name + "Set", [(name_multiple_in, List[type_in])])
 
     # derive the output type from the transformer
     type_list, type_name = transformer._type_out
-    output_type = [(k, List[type_list[k]]) for k in type_list]
+    output_type = [(k, type_list[k]) for k in type_list]  # flat, no lists
     type_out = create_namedtuple(type_name, output_type)
+    name_multiple_out = type_name + "s"
+    output_set = create_namedtuple(type_name + "Set", [(name_multiple_out, List[type_out])])
 
-    def predict_class(val_wrapped: type_in) -> type_out:
+    def predict_class(val_wrapped: input_set) -> output_set:
         '''Returns an array of float predictions'''
-        df = pd.DataFrame(list(zip(*val_wrapped)), columns=val_wrapped._fields)
-        # df = pd.DataFrame(np.column_stack(val_wrapped), columns=val_wrapped._fields)  # numpy doesn't like binary
+        # print("-===== input -===== ")
+        # print(input_set)
+        df = pd.DataFrame(getattr(val_wrapped, name_multiple_in), columns=type_in._fields)
+        # print("-===== df -===== ")
+        # print(df)
         tags_df = transformer.predict(df)
-        tags_list = type_out(*(col for col in tags_df.values.T))  # flatten to tag set
-        return tags_list
+        # print("-===== out df -===== ")
+        # print(tags_df)
+        tags_parts = tags_df.to_dict('split')
+        # print("-===== out list -===== ")
+        # print(output_set)
+        print("[{}]: Input {} row(s) ({}), output {} row(s) ({}))".format(
+              funcName, len(df), input_set, len(tags_df), output_set))
+        tags_list = [type_out(*r) for r in tags_parts['data']]
+        return output_set(tags_list)
 
     # compute path of this package to add it as a dependency
     package_path = path.dirname(path.realpath(__file__))
index 22c0bfd..b5ad6aa 100644 (file)
@@ -116,13 +116,13 @@ class FaceDetectTransform(BaseEstimator, ClassifierMixin):
     @property
     def _type_in(self):
         """Custom input type for this processing transformer"""
-        return {FaceDetectTransform.COL_IMAGE_MIME: str, FaceDetectTransform.COL_IMAGE_DATA: bytes}, "FaceImage"
+        return {FaceDetectTransform.COL_IMAGE_MIME: str, FaceDetectTransform.COL_IMAGE_DATA: bytes}, "Image"
 
     @property
     def _type_out(self):
         """Custom input type for this processing transformer"""
         output_dict = FaceDetectTransform.generate_out_dict()
-        return {k: type(output_dict[k]) for k in output_dict}, "DetectionFrames"
+        return {k: type(output_dict[k]) for k in output_dict}, "DetectionFrame"
 
     def score(self, X, y=None):
         return 0
index 6924c36..e791a32 100644 (file)
@@ -65,12 +65,12 @@ class RegionTransform(BaseEstimator, ClassifierMixin):
     def _type_in(self):
         """Custom input type for this processing transformer"""
         input_dict = RegionTransform.generate_in_dict()
-        return {k: type(input_dict[k]) for k in input_dict}, "DetectionFrames"
+        return {k: type(input_dict[k]) for k in input_dict}, "DetectionFrame"
 
     @property
     def _type_out(self):
         """Custom input type for this processing transformer"""
-        return {FaceDetectTransform.COL_IMAGE_MIME: str, FaceDetectTransform.COL_IMAGE_DATA: bytes}, "TransformedImage"
+        return {FaceDetectTransform.COL_IMAGE_MIME: str, FaceDetectTransform.COL_IMAGE_DATA: bytes}, "Image"
 
     def score(self, X, y=None):
         return 0
@@ -105,7 +105,7 @@ class RegionTransform(BaseEstimator, ClassifierMixin):
             img_mime = 'image/jpeg'  # image_data['mime']
 
             listData.append(RegionTransform.generate_out_dict(media=img_mime, bin_stream=img_binary))
-            print("IMAGE {:} found {:} total rows".format(image_data['image'], len(image_data['regions'])))
+            print("IMAGE {:} found {:} total rows".format(image_data['image'], len(image_data['regions'])))
         return pd.DataFrame(listData, columns=RegionTransform.output_names_())
 
     @staticmethod
index 4e431bf..ca66edb 100644 (file)
@@ -27,6 +27,7 @@
  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
+ E. Zavesky 05/05/18 adapted for row-based image and other results
  */
 
 "use strict";
@@ -44,7 +45,6 @@ $(document).ready(function() {
                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
@@ -128,6 +128,7 @@ function protobuf_load(pathProto, forceSelect) {
     protobuf.load(pathProto, function(err, root) {
         if (err) {
             console.log("[protobuf]: Error!: "+err);
+            domResult.html("<strong>"+"[protobuf]: Error!: "+err+"</strong>");
             throw err;
         }
         var domSelect = $("#protoMethod");
@@ -315,6 +316,7 @@ function doPostImage(srcCanvas, dstImg, dataPlaceholder) {
     var serviceURL = hd.classificationServer;
     var request = new XMLHttpRequest();     // create request to manipulate
     request.open("POST", serviceURL, true);
+    var domResult = $("#resultText");
 
     //console.log("[doPostImage]: Selected method ... '"+typeInput+"'");
     if (nameProtoMethod && nameProtoMethod.length) {     //valid protobuf type?
@@ -327,14 +329,16 @@ function doPostImage(srcCanvas, dstImg, dataPlaceholder) {
         //    }
 
         //TODO: should we always assume this is input? answer: for now, YES, always image input!
-        var inputPayload = { "mimeType": [blob.type], "imageBinary": [blob.bytes] };
+        var inputPayload = {'Images':[{ "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.");
+            var errStr = "[doPostImage]: Error during type verify for object input into protobuf method."+errMsg;
+            console.log(errStr);
+            domResult.html("<strong>"+errStr+"</strong>");
             throw Error(errMsg);
         }
         // Create a new message
@@ -402,59 +406,92 @@ function doPostImage(srcCanvas, dstImg, dataPlaceholder) {
 
                 // ---- 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;
+                var objOutput = null;
+                try {
+                    objOutput = msgOutput.decode(hd.protoPayloadOutput);
+                }
+                catch(err) {
+                    var errStr = "Error: Failed to parse protobuf response, was the right method chosen? (err: "+err.message+")";
+                    console.log(errStr);
+                    domResult.html(errStr);
+                    hd.imageIsWaiting = false;
+                    return false;
+                }
+                var nameRepeated = null;
+
+                // NOTE: this code expects one top-level item to be an array of nested results
+                //  e.g.   ImageSet [ Image{mime_type, image_binary}, .... ]
+                //  e.g.   DetectionFrameSet [ DetectionFrame{x, y, ...., mime_type, image_binary}, .... ]
 
-                // 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');
+                //try to crawl the fields in the protobuf....
+                var numFields = 0;
+                $.each(msgOutput.fields, function(name, val) {           //collect field names
+                    if (val.repeated) {     //indicates it's a repeated field (likely an array)
+                        nameRepeated = name;      //save this as last repeated field (ideally there is just one)
+                    }
+                    numFields += 1;
+                });
+                if (numFields > 1) {
+                    var errStr = "Error: Expected array/repeated structure in response, but got non-flat array result ("+numFields+" fields)";
+                    console.log(errStr);
+                    domResult.html(errStr);
+                    hd.imageIsWaiting = false;
+                    return false;
                 }
-                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);
+                var objRecv = objOutput[nameRepeated];
+
+                //grab the nested array type and print out the fields of interest
+                var typeNested = methodKeys[0]+"."+msgOutput.fields[nameRepeated].type;
+                var msgOutputNested = hd.protoObj[methodKeys[0]]['root'].lookupType(typeNested);
+                //console.log(msgOutputNested);
+                var domTable = $("<tr />");
+                var arrNames = [];
+                $.each(msgOutputNested.fields, function(name, val) {           //collect field names
+                    var nameClean = val.name;
+                    if (nameClean != 'imageBinary') {
+                        domTable.append($("<th />").html(nameClean));
+                        arrNames.push(nameClean);
                     }
-                    else {                                  //got images, get that chunk directly
-                        idxImg = 0;
+                });
+                domTable = $("<table />").append(domTable);     // create embedded table
+
+                // loop through all members of array to do two things:
+                //  (1) find the biggest/best image
+                //  (2) print out the textual fields
+                var objBest = null;
+                $.each(objRecv, function(idx, val) {
+                    if ('imageBinary' in val) {
+                        // at this time, we only support ONE output image, so we will loop through
+                        //  to grab the largest image (old code could grab the one with region == -1)
+                        if (objBest==null || val.imageBinary.length>objBest.imageBinary.length) {
+                            objBest = val;
+                        }
                     }
 
-                    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');
-                    }
-                }
+                    var domRow = $("<tr />");
+                    $.each(arrNames, function(idx, name) {      //collect data from each column
+                        domRow.append($("<td />").html(val[name]));
+                    });
+                    domTable.append(domRow);
+                });
+                domResult.empty().append($("<strong />").html("Results")).show();
+                domResult.append(domTable);
 
+
+                //did we find an image? show it now!
+                if (objBest != null) {
+                    var strImage = btoa(String.fromCharCode.apply(null, objBest.imageBinary));
+                    $dstImg.attr('src', "data:"+objBest.mimeType+";base64,"+strImage).removeClass('workingImage');
+                }
+                else {
+                    var errStr = "Error: No valid image data was found, aborting display.";
+                    console.log(errStr);
+                    domResult.html(errStr);
+                    hd.imageIsWaiting = false;
+                    return false;
+                }
             }
-            else {
+            else {       //legacy code where response was in base64 encoded image...
                 var responseJson = $.parseJSON(request.responseText);
                 var respImage = responseJson[0];
                 // https://stackoverflow.com/questions/21227078/convert-base64-to-image-in-javascript-jquery
index 79f8384..ed664f8 100644 (file)
@@ -1,22 +1,30 @@
 syntax = "proto3";
-package yieGOMVkeFReJTCdImEYAzGjafuIsmjG;
+package nnjTkneQgiZzmyiPFLQEudNBTxOvOBrK;
 
 service Model {
-  rpc detect (FaceImage) returns (DetectionFrames);
+  rpc detect (ImageSet) returns (DetectionFrameSet);
 }
 
-message FaceImage {
-  repeated string mime_type = 1;
-  repeated bytes image_binary = 2;
+message ImageSet {
+  repeated Image Images = 1;
 }
 
-message DetectionFrames {
-  repeated int64 image = 1;
-  repeated int64 region = 2;
-  repeated int64 x = 3;
-  repeated int64 y = 4;
-  repeated int64 w = 5;
-  repeated int64 h = 6;
-  repeated string mime_type = 7;
-  repeated bytes image_binary = 8;
+message Image {
+  string mime_type = 1;
+  bytes image_binary = 2;
+}
+
+message DetectionFrameSet {
+  repeated DetectionFrame DetectionFrames = 1;
+}
+
+message DetectionFrame {
+  int64 image = 1;
+  int64 region = 2;
+  int64 x = 3;
+  int64 y = 4;
+  int64 w = 5;
+  int64 h = 6;
+  string mime_type = 7;
+  bytes image_binary = 8;
 }
\ No newline at end of file
index 1239fa7..fcc0a50 100644 (file)
@@ -1,29 +1,31 @@
 syntax = "proto3";
-package sapLzHrujUMPBGCBEMWQFxEIMsxocFrG;
+package HipTviKTkIkcmyuMCIAIDkeOOQQYyJne;
 
 service Model {
-  rpc pixelate (FaceImage) returns (TransformedImage);
-  rpc detect (FaceImage) returns (TransformedImage);
+  rpc pixelate (DetectionFrameSet) returns (ImageSet);
+  rpc detect (ImageSet) returns (ImageSet);
 }
 
-message FaceImage {
-  repeated string mime_type = 1;
-  repeated bytes image_binary = 2;
+message DetectionFrameSet {
+  repeated DetectionFrame DetectionFrames = 1;
 }
 
-message DetectionFrames {
-  repeated int64 image = 1;
-  repeated int64 region = 2;
-  repeated int64 x = 3;
-  repeated int64 y = 4;
-  repeated int64 w = 5;
-  repeated int64 h = 6;
-  repeated string mime_type = 7;
-  repeated bytes image_binary = 8;
+message DetectionFrame {
+  int64 image = 1;
+  int64 region = 2;
+  int64 x = 3;
+  int64 y = 4;
+  int64 w = 5;
+  int64 h = 6;
+  string mime_type = 7;
+  bytes image_binary = 8;
 }
 
-message TransformedImage {
-  repeated string mime_type = 1;
-  repeated bytes image_binary = 2;
+message ImageSet {
+  repeated Image Images = 1;
 }
 
+message Image {
+  string mime_type = 1;
+  bytes image_binary = 2;
+}
\ No newline at end of file