Repository: Vantiq/vantiq-extension-sources (language: python)

Issue:
ObjRec: Add option to include the image as a base64 string content to the payload instead of file
Would like an option to include the image file content in the payload itself as a base64 encoded string which can be used directory in HTML img tags <img src="<img.content>". 

@bruden01 created his own one-off that does this but it would be nice to have a flag we could set with the image options where instead of vantiq, local, both we can also do inline or something like that to put the image content into the payload. 



Discussion / hints:
Current plan:  Add `includeEncodedImage` config param & query param.  If set, Base64 encode image will be included in the results sent/returned Vantiq.  This parameter will be orthogonal to the `saveImage` config property/query param in that you can include the B64 encoding and/or save the image.  Or one or the other.

So the option will be there, but not _instead_ of [saving the] file.

Fix patch:
diff --git a/objectRecognitionSource/README.md b/objectRecognitionSource/README.md
index 3573c109..f619bfd8 100644
--- a/objectRecognitionSource/README.md
+++ b/objectRecognitionSource/README.md
@@ -165,21 +165,21 @@ The Configuration document may look similar to the following example:
 ### Options Available for General
 At least one of these options must be set for the source to function
 
-*   pollTime: This indicates how often an image should be captured. A positive number represents the number of
+*   `pollTime`: This indicates how often an image should be captured. A positive number represents the number of
 milliseconds between captures. If the specified time is less than the amount of time it takes to process the image then
 images will be taken as soon as the previous finishes. If this is set to 0, the next image will be captured as soon as
 the previous is sent. 
     *   (**NOTE:** Previously named "pollRate". For a limited amount of time, both "pollTime" and "pollRate"
     will be valid General Config options, but in the future only "pollTime" will be supported.)
-*   allowQueries: This option allows Queries to be received when set to `true'
-*   maxRunningThreads: Optional. Only used if `pollTime` has been specified. The maximum number of threads running at any 
+*   `allowQueries`: This option allows Queries to be received when set to `true'
+*   `maxRunningThreads`: Optional. Only used if `pollTime` has been specified. The maximum number of threads running at any 
 given point for polling requests from the specific VANTIQ source. Must be a positive integer. Default value is 10.
-*   maxQueuedTask: Optional. Only used if `pollTime` has been specified. The maximum number of queued tasks at any given point 
+*   `maxQueuedTask`: Optional. Only used if `pollTime` has been specified. The maximum number of queued tasks at any given point 
 for polling requests from the specific VANTIQ source. Must be a positive integer. Default value is 20.
     *   **NOTE:** The default behavior of the Object Recognition Source is to process up to 10 captured frames in parallel at 
     a time. If you would like the source to only process one frame at a time, then `maxRunningThreads` and `maxQueuedTasks` 
     must both be set to 1.
-*   suppressEmptyNeuralNetResults: Optional. Only used if `pollTime` has been specified. Must be a boolean value. If set to 
+*   `suppressEmptyNeuralNetResults`: Optional. Only used if `pollTime` has been specified. Must be a boolean value. If set to 
 `true`, only the neural net results containing recognitions, (from polled frames), will be sent back to the VANTIQ Source as a 
 Notificiation.
 
@@ -189,7 +189,7 @@ Most of the options required for dataSource are dependent on the specific implem
 [ImageRetrieverInterface](#retrieveInterface). The ones that are the same across all implementations
 are:
 
-*   type: Optional. Can be one of three situations
+*   `type`: Optional. Can be one of three situations
     1.  The fully qualified class name of an implementation of ImageRetrieverInterface, e.g.
         "io.vantiq.extsrc.objectRecognition.imageRetriever.CameraRetriever".
     2.  The short name of one of the standard implementations, i.e. one of "[file](#fileRet)", "[camera](#camRet)",
@@ -203,7 +203,8 @@ are:
 Most of the options required for neuralNet are dependent on the specific implementation of
 [NeuralNetInterface](#netInterface). For an example of neural net specific configurations, please look at the [YOLO Processor 
 configuration options](#yoloNet). The ones that are the same across all implementations are:
-*   type: Optional. Can be one of three situations
+
+*   `type`: Optional. Can be one of three situations
     1.  The fully qualified class name of an implementation of NeuralNetInterface, e.g.
         "io.vantiq.extsrc.objectRecognition.neuralNet.YoloProcessor".
     2.  The short name of one of the standard implementations, i.e. one of "[yolo](#yoloNet)", "[none](#noNet)" or
@@ -211,34 +212,35 @@ configuration options](#yoloNet). The ones that are the same across all implemen
     3.  Empty, in which case the program will try to find an implementation with the name "DefaultProcessor" in
         the `io.vantiq.objectRecognition.neuralNet` package. This implementation is not provided, and must be written by
         the user.
-*   threshold: Optional. Threshold is used to decide if the Neural Net's result is a valid one, by comparing the resulting confidence of the recognition against the threshold value. A high threshold will lead to fewer results, all with a higher confidence. A low threshold will lead to more results, some of which having a lower confidence. Threshold defaults to 0.5 if not specified, or if invalid. There are two ways to specify this value:
+*   `threshold`: Optional. Threshold is used to decide if the Neural Net's result is a valid one, by comparing the resulting confidence of the recognition against the threshold value. A high threshold will lead to fewer results, all with a higher confidence. A low threshold will lead to more results, some of which having a lower confidence. Threshold defaults to 0.5 if not specified, or if invalid. There are two ways to specify this value:
     1.  The value can be a number between 0 and 1 (i.e. 0.4, or 0.2, etc...)
     2.  The value can be a number between 0 and 100 (i.e. 40, or 20, etc...)
-*   cropBeforeAnalysis: Optional. Used to crop the retrieved images before they are processed by the Neural Net. This option 
+*   `cropBeforeAnalysis`: Optional. Used to crop the retrieved images before they are processed by the Neural Net. This option 
 *must* contain the following values:
     *   **x**: The top left x-coordinate used to crop the image.
     *   **y**: The top left y-coordinate used to crop the image.
     *   **width**: The width of the cropped image, (starting at the x-coordinate specified above).
     *   **height**: The height of the cropped image, (starting at the y-coordinate specified above).
-*   saveImage: Optional. The value can be one of the following three options:
+*   `saveImage`: Optional. The value can be one of the following three options:
     1.  "local"     - This will save images to the disk (outputDir must be specified in order for this to work).
     2.  "vantiq"    - This will save images as documents in VANTIQ. No images will be saved locally even if outputDir is specified.
     3.  "both"      - This will save the images both to the disk and as documents in VANTIQ. (outputDir must be specified in order to save locally)
+* `includeEncodedImage`: Optional.  If set, the Base64-encoded image will be included in the results. The image encoded and included is the same one that would be saved if `saveImage` is/was set. _E.g._, if `labelImage` (see below) is set, the encoded image will contain the labeling.  Note that `includeEncodedImage` and `saveImage` are independent of one another;  either or both can be set.
     
-**NOTE:** All of the following options are relevant only if the "saveImage" option has been set.
+**NOTE:** All of the following options are relevant only if the `saveImage` or `includedEncodedImage` (for `labelImage` and `savedResolution`) options are set.
 
-*   outputDir: Optional. The directory in which images will be saved locally. Images will only be saved locally if saveImage 
+*   `outputDir`: Optional. The directory in which images will be saved locally. Images will only be saved locally if saveImage 
 is set to be either "local" or "both".
-*   saveRate: Optional. The rate at which images will be saved (i.e. "saveRate": 3 - This will save every 3rd image that is 
+*   `saveRate`: Optional. The rate at which images will be saved (i.e. "saveRate": 3 - This will save every 3rd image that is 
 captured). If not specified, the value will default to 1 which saves every captured image.
-*   labelImage: Optional. If set to "true", images will be saved with bounding boxes and labels. If set to "false", or if not 
+*   `labelImage`: Optional. If set to "true", images will be saved with bounding boxes and labels. If set to "false", or if not 
 set at all, the images will be saved with no bounding boxes or labels.
-*   uploadAsImage: Optional. If set to "true", images will be uploaded to VANTIQ as VANTIQ Images, (the default behavior 
+*   `uploadAsImage`: Optional. If set to "true", images will be uploaded to VANTIQ as VANTIQ Images, (the default behavior 
 uploads images as VANTIQ Documents).
     *   **NOTE**: This option is only relevant if "saveImage" has been set to either "vantiq" or "both".
-*   savedResolution: Optional. A map containing options for adjusting the resolution of the saved images.
+*   `savedResolution`: Optional. A map containing options for adjusting the resolution of the saved images.
     * *Options for savedResolution:*
-    1. longEdge: Optional. This sets the maximum long edge dimension for saved images. Must be a non-negative integer that is 
+    1. `longEdge`: Optional. This sets the maximum long edge dimension for saved images. Must be a non-negative integer that is 
     smaller than the long edge of the image to be saved. This value will become the new long edge dimension, and the short 
     edge will be scaled down to maintain the same aspect ratio as the original image.
         *   **NOTE:** We do not support enlarging images. This feature can be used *only* to resize images to smaller 
@@ -535,7 +537,7 @@ identified, in the format used by the neural [net implementation](#netInterface)
 
 Options available for all Queries (not prepended by anything) are:
 
-*   sendFullResponse: Optional. Specifies that this request should send back data in the same format as a notification
+*   `sendFullResponse`: Optional. Specifies that this request should send back data in the same format as a notification
     instead of only the objects recognized. Note that the data will be the sole occupant of a 1-element array when
     received, instead of being immediately available as a JSON object. This is because Query results are mandated
     to be arrays. Default is false.
@@ -597,12 +599,14 @@ it will be set to the default value, "processNextFrame".
         *   "cropBeforeAnalysis": Optional. This value can be set exactly like the "cropBeforeAnalysis" value in the source 
         configuration. If no cropBeforeAnalysis value is set as a query parameter, the source configuration's 
         cropBeforeAnalysis value will be used.
-        *   uploadAsImage: Optional. If set to "true", images will be uploaded to VANTIQ as VANTIQ Images, (the default 
+        *   "uploadAsImage": Optional. If set to "true", images will be uploaded to VANTIQ as VANTIQ Images, (the default 
         behavior uploads images as VANTIQ Documents).
+        *   "includeEncodedImage": Optional.  If set to "true", a Base64-encoded image will be included in the results when `sendFullResponse` is set.
     
 **EXAMPLE QUERIES**:
 
 *   Upload Query using imageName:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
     	operation:"upload",
@@ -611,14 +615,16 @@ SELECT * FROM SOURCE Camera1 AS results WITH
 ```
 
 *   Upload Query using imageDate, uploading as VANTIQ Images:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
-    	operation:"upload",
-    	imageDate:["2019-02-08--10-33-36", "2019-02-08--12-45-18"],
+        operation:"upload",
+        imageDate:["2019-02-08--10-33-36", "2019-02-08--12-45-18"],
         uploadAsImage: true
 ```
 
 *   Delete Query using imageName:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
     	operation:"delete",
@@ -626,6 +632,7 @@ SELECT * FROM SOURCE Camera1 AS results WITH
 ```
 
 *   Delete Query using imageDate to delete everything after a certain date:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
     	operation:"delete",
@@ -633,6 +640,7 @@ SELECT * FROM SOURCE Camera1 AS results WITH
 ```
 
 *   Delete Query using imageDate to delete all images:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
     	operation:"delete",
@@ -640,21 +648,23 @@ SELECT * FROM SOURCE Camera1 AS results WITH
 ```
 
 *   Process Next Frame Query:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
         operation:"processNextFrame",
-    	NNsaveImage:"local",
-    	NNoutputDir:"testDir",
-    	NNfileName:"testFile"
+        NNsaveImage:"local",
+        NNoutputDir:"testDir",
+        NNfileName:"testFile"
 ```
 
 *   Process Next Frame Query, with preCrop set:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
         operation:"processNextFrame",
-    	NNsaveImage:"local",
-    	NNoutputDir:"testDir",
-    	NNfileName:"testFile",
+        NNsaveImage:"local",
+        NNoutputDir:"testDir",
+        NNfileName:"testFile",
         cropBeforeAnalysis: {
             x: 50,
             y: 100,
@@ -664,20 +674,22 @@ SELECT * FROM SOURCE Camera1 AS results WITH
 ```
 
 * Process Next Frame Query, uploading as VANTIQ Image:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
         operation:"processNextFrame",
-    	NNsaveImage:"vantiq",
-    	NNfileName:"testFile",
+        NNsaveImage:"vantiq",
+        NNfileName:"testFile",
         uploadAsImage: true
 ```
 
 *   Process Next Frame Query without specifying operation *(not recommended)*:
+
 ```
 SELECT * FROM SOURCE Camera1 AS results WITH
-    	NNsaveImage:"local",
-    	NNoutputDir:"testDir",
-    	NNfileName:"testFile"
+        NNsaveImage:"local",
+        NNoutputDir:"testDir",
+        NNfileName:"testFile"
 ```
 
 ### Error Messages
diff --git a/objectRecognitionSource/src/main/java/edu/ml/tensorflow/ObjectDetector.java b/objectRecognitionSource/src/main/java/edu/ml/tensorflow/ObjectDetector.java
index b2481dea..9d8210e1 100644
--- a/objectRecognitionSource/src/main/java/edu/ml/tensorflow/ObjectDetector.java
+++ b/objectRecognitionSource/src/main/java/edu/ml/tensorflow/ObjectDetector.java
@@ -28,6 +28,7 @@
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import static edu.ml.tensorflow.Config.MEAN;
@@ -35,6 +36,7 @@
 /**
  * ObjectDetector class to detect objects using pre-trained models with TensorFlow Java API.
  */
+@SuppressWarnings("PMD.TooManyFields")
 public class ObjectDetector {
     private final static Logger LOGGER = LoggerFactory.getLogger(ObjectDetector.class);
     private byte[] graph_def;
@@ -42,7 +44,8 @@ public class ObjectDetector {
 
     // This will be used to create
     // "year-month-date-hour-minute-seconds"
-    private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss");
+    private static final  SimpleDateFormat format =
+            new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss", Locale.getDefault());
     
     // Getting meta config options for YOLO Processor
     public MetaBasedConfig metaConfigOptions = new MetaBasedConfig();
@@ -57,7 +60,7 @@ public class ObjectDetector {
     private Vantiq vantiq = null;
     private String sourceName = null;
     private double[] anchorArray;
-    private Map<String,Object> metaFileMap;    
+    private Map<String, Object> metaFileMap;
     
     private Graph yoloGraph;
     private Session yoloSession;
@@ -69,6 +72,11 @@ public class ObjectDetector {
     
     public String lastFilename;
 
+    public class ResultHolder {
+        public List<Map<String, ?>> results;
+        public byte[] image;
+    }
+
     /**
      * Initializes the ObjectDetector with the given graph and and labels.
      * <br>Edited to initialize and save the graph for reuse, and allow the files to be specified dynamically.
@@ -79,7 +87,6 @@ public class ObjectDetector {
      * @param metaFile      The location of the meta file used to retrieve anchors and labels if a labelFile is not provided.
      * @param anchorArray   The list of anchor pairs used by the YOLOClassifier to label recognitions.
      * @param imageUtil     The instance of the ImageUtil class used to save images. Either initialized, or set to null.
-     * @param outputDir     The directory to which images will be saved.
      * @param labelImage    The boolean flag signifying if images should be saved with or without bounding boxes. If true,
      *                      the frames will be saved with bounding boxes, and vice versa.     
      * @param saveRate      The rate at which images will be saved, once per every saveRate frames. Non-positive values are
@@ -87,15 +94,17 @@ public class ObjectDetector {
      * @param vantiq        The Vantiq variable used to connect to the VANTIQ SDK. Either authenticated, or set to null.
      * @param sourceName    The name of the VANTIQ Source
      */
-    public ObjectDetector(float thresh, String graphFile, String labelFile, String metaFile, double[] anchorArray, ImageUtil imageUtil, String outputDir, 
-            Boolean labelImage, int saveRate, Vantiq vantiq, String sourceName) {
+    @SuppressWarnings({"checkstyle.ParameterNumberCheck", "PMD.CognitiveComplexity", "PMD.ExcessiveParameterList"})
+    public ObjectDetector(float thresh, String graphFile, String labelFile, String metaFile, double[] anchorArray,
+                          ImageUtil imageUtil, Boolean labelImage, int saveRate,
+                          Vantiq vantiq, String sourceName) {
         try {
             graph_def = IOUtil.readAllBytesOrExit(graphFile);
             // Parse meta file if it exists, and get all general information we need
             if (metaFile != null) {
                 parseMetaFile(metaFile);
-                int frameHeight = (int) ((Map<String,Object>) metaFileMap.get("net")).get("height");
-                int frameWidth = (int) ((Map<String,Object>) metaFileMap.get("net")).get("width");
+                int frameHeight = (int) ((Map<String, Object>) metaFileMap.get("net")).get("height");
+                int frameWidth = (int) ((Map<String, Object>) metaFileMap.get("net")).get("width");
                 // Check that meta file has appropriate frame size, and that user has not overwritten frame size in Config
                 if (frameHeight == frameWidth && frameHeight % 32 == 0 && metaConfigOptions.useMetaIfAvailable) {
                     // Set config value to the .meta file's frame size
@@ -138,9 +147,7 @@ public ObjectDetector(float thresh, String graphFile, String labelFile, String m
             }
         } catch (ServiceException ex) {
             throw new IllegalArgumentException(ex.getLocalizedMessage(), ex);
-        } catch (JsonParseException ex) {
-            throw new IllegalArgumentException("Problem reading the meta file.", ex);
-        } catch (JsonMappingException ex) {
+        } catch (JsonParseException | JsonMappingException ex) {
             throw new IllegalArgumentException("Problem reading the meta file.", ex);
         } catch (IOException ex) {
             throw new IllegalArgumentException("Problem reading one of the model files.", ex);
@@ -160,15 +167,17 @@ public ObjectDetector(float thresh, String graphFile, String labelFile, String m
      * <br>Edited to return the results as a map and conditionally save the image
      * @param image     The image in jpeg format
      * @param timestamp The timestamp corresponding to when the frame was captured. Used to name the image if it is being saved
-     * @return          A List of Maps, each of which has a {@code label} stating the type of the object identified,
-     *                  a {@code confidence} specifying on a scale of 0-1 how confident the neural net is that the
-     *                  identification is accurate, and a {@code location} containing the coordinates for the
-     *                  {@code top},{@code left}, {@code bottom}, and {@code right} edges of the bounding box for the object.
+     * @return ResultHolder containing a List of Maps, each of which has a {@code label} stating the type
+     *                      of the object identified, a {@code confidence} specifying on a scale of 0-1 how confident
+     *                      the neural net is that the identification is accurate, and a {@code location} containing
+     *                      the coordinates for the {@code top},{@code left}, {@code bottom}, and {@code right} edges
+     *                      of the bounding box for the object; and the bufferedImage (possibly labeled) that may
+     *                      was returned.
      */
-    public List<Map<String, ?>> detect(final byte[] image, Date timestamp) {
+    public ResultHolder detect(final byte[] image, Date timestamp) {
         try (Tensor<Float> normalizedImage = normalizeImage(image)) {
             List<Recognition> recognitions = YOLOClassifier.getInstance(threshold, anchorArray, frameSize).classifyImage(executeYOLOGraph(normalizedImage), labels);
-            BufferedImage buffImage = imageUtil.createImageFromBytes(image);
+            BufferedImage buffImage = ImageUtil.createImageFromBytes(image);
             
             // Saves an image every saveRate frames
             if (imageUtil.saveImage && ++frameCount >= saveRate) {
@@ -185,6 +194,9 @@ public ObjectDetector(float thresh, String graphFile, String labelFile, String m
                 }
                 imageUtil.saveImage(buffImage, fileName);
                 frameCount = 0;
+            } else if (labelImage) {
+                buffImage = imageUtil.labelImage(buffImage, recognitions);
+                lastFilename = null;
             } else {
                 lastFilename = null;
             }
@@ -203,17 +215,22 @@ public ObjectDetector(float thresh, String graphFile, String labelFile, String m
      *                      and {@code outputDir} is non-null, then the file is saved as
      *                      "&lt;year&gt;-&lt;month&gt;-&lt;day&gt;--&lt;hour&gt;-&lt;minute&gt;-&lt;second&gt;.jpg"
      * @param vantiq        The Vantiq variable used to connect to the VANTIQ SDK. Either authenticated, or set to null.
-     * @param uploadAsImage The boolean flag used to specify if images should be uploaded to VANTIQ as Documents or VANTIQ Images
-     * @return              A List of Maps, each of which has a {@code label} stating the type of the object identified, a
-     *                      {@code confidence} specifying on a scale of 0-1 how confident the neural net is that the
-     *                      identification is accurate, and a {@code location} containing the coordinates for the
-     *                      {@code top},{@code left}, {@code bottom}, and {@code right} edges of the bounding box for
-     *                      the object.
+     * @param uploadAsImage The boolean flag used to specify if images should be uploaded to VANTIQ as
+     *                      Documents or VANTIQ Images
+     * @param localLabelImage Boolean indicating whether this query should produce labels, overriding
+     *                      the connector setting.
+     * @return              ResultHolder containing a List of Maps, each of which has a {@code label} stating the type
+     *                      of the object identified, a {@code confidence} specifying on a scale of 0-1 how confident
+     *                      the neural net is that the identification is accurate, and a {@code location} containing
+     *                      the coordinates for the {@code top},{@code left}, {@code bottom}, and {@code right} edges
+     *                      of the bounding box for the object; and the bufferedImage (possibly labeled) that may
+     *                      was returned.
      */
-    public List<Map<String, ?>> detect(final byte[] image, String outputDir, String fileName, Vantiq vantiq, boolean uploadAsImage) {
+    public ResultHolder detect(final byte[] image, String outputDir, String fileName, Vantiq vantiq,
+                               boolean uploadAsImage, boolean localLabelImage) {
         try (Tensor<Float> normalizedImage = normalizeImage(image)) {
             List<Recognition> recognitions = YOLOClassifier.getInstance(threshold, anchorArray, frameSize).classifyImage(executeYOLOGraph(normalizedImage), labels);
-            BufferedImage buffImage = imageUtil.createImageFromBytes(image);
+            BufferedImage buffImage = ImageUtil.createImageFromBytes(image);
             
             // Saves an image if requested
             if (outputDir != null || vantiq != null || this.imageUtil.saveImage) {
@@ -224,10 +241,13 @@ public ObjectDetector(float thresh, String graphFile, String labelFile, String m
                 imageUtil.frameSize = frameSize;
                 imageUtil.uploadAsImage = uploadAsImage;
                 lastFilename = fileName;
-                if (labelImage) {
+                if (labelImage || localLabelImage) {
                     buffImage = imageUtil.labelImage(buffImage, recognitions);
                 }
                 imageUtil.saveImage(buffImage, fileName);
+            } else if (labelImage || localLabelImage) {
+                buffImage = imageUtil.labelImage(buffImage, recognitions);
+                lastFilename = null;
             } else {
                 lastFilename = null;
             }
@@ -246,7 +266,7 @@ public ObjectDetector(float thresh, String graphFile, String labelFile, String m
      */
     private void parseMetaFile(String metaFile) throws JsonParseException, JsonMappingException, IOException {
         byte[] metaFileData = Files.readAllBytes(Paths.get(metaFile));
-        this.metaFileMap = new HashMap<String,Object>();
+        this.metaFileMap = new HashMap<String, Object>();
         ObjectMapper objectMapper = new ObjectMapper();
         this.metaFileMap = objectMapper.readValue(metaFileData, HashMap.class);
     }
@@ -323,8 +343,9 @@ private float[] executeYOLOGraph(final Tensor<Float> image) {
     /**
      * ADDED BY NAMIR - Used to convert recognitions to JSON
      * @param recognitions
+     * @return ResultHolder including the list of recognitions & the resulting image.
      */
-    private List<Map<String, ?>> returnJSON(final List<Recognition> recognitions, BufferedImage buffImage) {
+    private ResultHolder returnJSON(final List<Recognition> recognitions, BufferedImage buffImage) {
         List<Map<String, ?>> jsonRecognitions = new ArrayList<>();
         for (Recognition recognition : recognitions) {
         	HashMap map = new HashMap();
@@ -348,8 +369,12 @@ private float[] executeYOLOGraph(final Tensor<Float> image) {
         	
         	LOGGER.info("{}", map);
         }
+
+        ResultHolder res = new ResultHolder();
+        res.results = jsonRecognitions;
+        res.image = ImageUtil.getBytesForImage(buffImage);
         
-        return jsonRecognitions;
+        return res;
     }
     
     /**
diff --git a/objectRecognitionSource/src/main/java/edu/ml/tensorflow/util/ImageUtil.java b/objectRecognitionSource/src/main/java/edu/ml/tensorflow/util/ImageUtil.java
index c9de5372..5dd139a5 100644
--- a/objectRecognitionSource/src/main/java/edu/ml/tensorflow/util/ImageUtil.java
+++ b/objectRecognitionSource/src/main/java/edu/ml/tensorflow/util/ImageUtil.java
@@ -14,6 +14,7 @@
 import java.awt.*;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
@@ -33,7 +34,8 @@ public class ImageUtil {
     public Boolean uploadAsImage = false;
 
     // Used to upload image to VANTIQ as VANTIQ Image
-    final static String IMAGE_RESOURCE_PATH = "/resources/images";
+    static final String IMAGE_RESOURCE_PATH = "/resources/images";
+    static final String IMAGE_SAVE_FORMAT = "jpg";
 
     /**
      * Label image with classes and predictions given by the TensorFLow YOLO Implementation
@@ -64,16 +66,19 @@ public BufferedImage labelImage(BufferedImage bufferedImage, final List<Recognit
      * @param image     The image to save
      * @param target    The name of the file to be written
      */
+    @SuppressWarnings({"PMD.CognitiveComplexity"})
     public void saveImage(final BufferedImage image, final String target) {
         File fileToUpload = null;
         if (outputDir != null) {
             try {
                 IOUtil.createDirIfNotExists(new File(outputDir));
                 if (longEdge == 0) {
-                    ImageIO.write(image, "jpg", new File(outputDir + File.separator + target));
+                    ImageIO.write(image, IMAGE_SAVE_FORMAT,
+                            new File(outputDir + File.separator + target));
                 } else {
                     BufferedImage resizedImage = resizeImage(image);
-                    ImageIO.write(resizedImage, "jpg", new File(outputDir + File.separator + target));
+                    ImageIO.write(resizedImage, IMAGE_SAVE_FORMAT,
+                            new File(outputDir + File.separator + target));
                 }
                 fileToUpload = new File(outputDir + File.separator + target);
             } catch (IOException e) {
@@ -88,10 +93,10 @@ public void saveImage(final BufferedImage image, final String target) {
                     File imgFile = File.createTempFile("tmp", ".jpg");
                     imgFile.deleteOnExit();
                     if (longEdge == 0) {
-                        ImageIO.write(image, "jpg", imgFile);
+                        ImageIO.write(image, IMAGE_SAVE_FORMAT, imgFile);
                     } else {
                         BufferedImage resizedImage = resizeImage(image);
-                        ImageIO.write(resizedImage, "jpg", imgFile);
+                        ImageIO.write(resizedImage, IMAGE_SAVE_FORMAT, imgFile);
                     }
                     uploadImage(imgFile, target);
                 } catch (IOException e) {
@@ -113,10 +118,13 @@ public void uploadImage(File imgFile, String target) {
                 BufferedImage resizedImage = resizeImage(ImageIO.read(imgFile));
                 File tmpFile = File.createTempFile("tmp", ".jpg");
                 tmpFile.deleteOnExit();
-                ImageIO.write(resizedImage, "jpg", tmpFile);
+                ImageIO.write(resizedImage, IMAGE_SAVE_FORMAT, tmpFile);
                 fileToUpload = tmpFile;
             } catch (IOException e) {
-                LOGGER.error("An error occured while reading and/or resizing the locally saved image file. " + e.getMessage());
+                if (LOGGER.isErrorEnabled()) {
+                    LOGGER.error("An error occurred while reading and/or resizing the locally saved image file. "
+                            + e.getMessage());
+                }
             }
         }
         uploadToVantiq(fileToUpload, target);
@@ -217,12 +225,23 @@ public BufferedImage resizeHelper(int width, int height, BufferedImage image) {
         return resizedImage;
     }
 
-    public BufferedImage createImageFromBytes(final byte[] imageData) {
-        ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
-        try {
+    public static BufferedImage createImageFromBytes(final byte[] imageData) {
+
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
             return ImageIO.read(bais);
         } catch (IOException ex) {
-            throw new ServiceException("Unable to create image from bytes!", ex);
+            throw new ServiceException("Unable to create image from bytes.", ex);
+        }
+    }
+
+    public static byte[] getBytesForImage(final BufferedImage bi) {
+
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            ImageIO.write(bi, IMAGE_SAVE_FORMAT, baos);
+            baos.flush();
+            return baos.toByteArray();
+        } catch (IOException e) {
+            return new byte[0];
         }
     }
 }
\ No newline at end of file
diff --git a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/ObjectRecognitionCore.java b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/ObjectRecognitionCore.java
index df6f2397..0af60b11 100644
--- a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/ObjectRecognitionCore.java
+++ b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/ObjectRecognitionCore.java
@@ -678,6 +678,11 @@ Map<String, Object> createMapFromResults(ImageRetrieverResults imageResults, Neu
        if (neuralNetResults.getLastFilename() != null) {
            map.put("filename", neuralNetResults.getLastFilename());
        }
+
+       String encodedImage = neuralNetResults.getEncodedImage();
+       if (encodedImage != null) {
+           map.put("encodedImage", encodedImage);
+       }
        
        map.put("sourceName", sourceName);
        map.put("timestamp", imageResults.getTimestamp());
@@ -695,6 +700,7 @@ Map<String, Object> createMapFromResults(ImageRetrieverResults imageResults, Neu
        } else {
            map.put("neuralNet", new LinkedHashMap<>());
        }
+
        if (locationMapper != null) {
            // Then our source is configured to create a set of mapped results in addition to the NN results.
            List<Map<String, ?>> mappedResults = locationMapper.mapResults(nnRes);
diff --git a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetResults.java b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetResults.java
index 4dce2f98..db5424e5 100644
--- a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetResults.java
+++ b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetResults.java
@@ -20,6 +20,9 @@ public class NeuralNetResults {
      * A Map containing any data that should be passed to the source
      */
     protected Map<String, ?>          otherData   = null;
+
+    /* String containing the base64-encoded image that was just evaluated, assuming that's desired */
+    protected String                  encodedImage = null;
     
     public NeuralNetResults() {
         // Do nothing
@@ -76,10 +79,23 @@ public String getLastFilename() {
     }
 
     /**
-     * @param otherData A Map containing any data that should be passed to the source
+     * @param lastFilename A String containing the name of the last saved image file.  Used in testing
      */
     public void setLastFilename(String lastFilename) {
         this.lastFilename = lastFilename;
     }
-    
+
+    /**
+     * @param ei String the base64-encoded image to return
+     */
+    public void setEncodedImage(String ei) {
+        this.encodedImage = ei;
+    }
+
+    /**
+     * @return String the base64-encoded image to return
+     */
+    public String getEncodedImage() {
+        return encodedImage;
+    }
 }
diff --git a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetUtils.java b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetUtils.java
index f863e9bb..d3f6149f 100644
--- a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetUtils.java
+++ b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetUtils.java
@@ -5,12 +5,15 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 
 import javax.imageio.ImageIO;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+@SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod")
 public abstract class NeuralNetUtils {
     
     Logger log = LoggerFactory.getLogger(this.getClass());
@@ -37,7 +40,7 @@ public byte[] cropImage(byte[] image, int x, int y, int w, int h) {
             // Convert BufferedImage to byte[] and save it
             ImageIO.write(croppedImage, "jpg", baos);
             baos.flush();
-            image = baos.toByteArray();
+            return baos.toByteArray();
         } catch (IOException e) {
             log.error("An error occured when trying to crop the captured image. Image will not be cropped.");
         } catch (RasterFormatException e) {
@@ -49,4 +52,14 @@ public byte[] cropImage(byte[] image, int x, int y, int w, int h) {
         
         return image;
     }
+
+    /**
+     * Converts the incoming image (well, really, any byte array) to a base 64 string
+     * @param image byte[] byte encoding of the image
+     * @return String containing the base64 encoding of the image bytes
+     */
+    public static String convertToBase64(byte[] image) {
+        byte[] encBytes = Base64.getEncoder().encode(image);
+        return new String(encBytes, StandardCharsets.UTF_8);
+    }
 }
diff --git a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NoProcessor.java b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NoProcessor.java
index 8a6928bc..1ad6427f 100644
--- a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NoProcessor.java
+++ b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/NoProcessor.java
@@ -10,9 +10,9 @@
 
 import java.awt.image.BufferedImage;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
-import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import org.slf4j.Logger;
@@ -32,7 +32,8 @@ public class NoProcessor implements NeuralNetInterface {
     private static final String OUTPUT_DIR = "outputDir";
     private static final String SAVE_RATE = "saveRate";
     private static final String UPLOAD_AS_IMAGE = "uploadAsImage";
-
+    private static final String INCLUDE_ENCODED_IMAGE = "includeEncodedImage";
+    
     // Constants for Query Parameter options
     private static final String NN_OUTPUT_DIR = "NNoutputDir";
     private static final String NN_FILENAME = "NNfileName";
@@ -40,7 +41,8 @@ public class NoProcessor implements NeuralNetInterface {
     
     // This will be used to create
     // "year-month-date-hour-minute-seconds"
-    private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss");
+    private static final SimpleDateFormat format =
+            new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss", Locale.getDefault());
     
     public String lastFilename;
     public Boolean isSetup = false;
@@ -57,7 +59,9 @@ public class NoProcessor implements NeuralNetInterface {
     int frameCount = 0;
     int fileCount = 0; // Used for saving files with same name
     boolean uploadAsImage = false;
-
+    boolean includeEncodedImage = false;
+    
+    @SuppressWarnings("PMD.CognitiveComplexity")
     @Override
     public void setupImageProcessing(Map<String, ?> neuralNetConfig, String sourceName, String modelDirectory, String authToken, String server) {
         this.server = server;
@@ -102,7 +106,9 @@ public void setupImageProcessing(Map<String, ?> neuralNetConfig, String sourceNa
             log.info("The Neural Net Config did not specify a method of saving images. No images will be saved when polling. "
                     + "If allowQueries is set, then the user can query the source and save images based on the query options.");
         }
-        
+    
+        includeEncodedImage = checkEncodedImageParam(neuralNetConfig);
+
         isSetup = true;
     }
 
@@ -111,7 +117,7 @@ public void setupImageProcessing(Map<String, ?> neuralNetConfig, String sourceNa
     public NeuralNetResults processImage(byte[] image) throws ImageProcessingException {
         if (imageUtil.saveImage && ++frameCount >= saveRate) {
             Date now = new Date(); // Saves the time before
-            BufferedImage buffImage = imageUtil.createImageFromBytes(image);
+            BufferedImage buffImage = ImageUtil.createImageFromBytes(image);
             String fileName = format.format(now);
             // If filename is same as previous name, add parentheses containing the count
             if (lastFilename != null && lastFilename.contains(fileName)) {
@@ -122,13 +128,21 @@ public NeuralNetResults processImage(byte[] image) throws ImageProcessingExcepti
             }
             lastFilename = fileName;
             imageUtil.saveImage(buffImage, fileName);
+            image = ImageUtil.getBytesForImage(buffImage);
             frameCount = 0;
         }
-        NeuralNetResults emptyResults = new NeuralNetResults();
-        return emptyResults;
+        
+        NeuralNetResults results = new NeuralNetResults();
+        // If the processor was configured to include the Base64 encoded image in the results, do so now.
+        // We include the pre-cropped image as we'd like the image to correspond to the reported object locations.
+        if (includeEncodedImage) {
+            results.setEncodedImage(NeuralNetUtils.convertToBase64(image));
+        }
+        return results;
     }
 
     // Does no processing, just saves images
+    @SuppressWarnings("PMD.CognitiveComplexity")
     @Override
     public NeuralNetResults processImage(byte[] image, Map<String, ?> request) throws ImageProcessingException {
         String saveImage = null;
@@ -173,10 +187,12 @@ public NeuralNetResults processImage(byte[] image, Map<String, ?> request) throw
         } else {
             queryImageUtil.saveImage = false;
         }
+    
+        includeEncodedImage = checkEncodedImageParam(request);
         
         if (queryImageUtil.saveImage) {
             Date now = new Date(); // Saves the time before
-            BufferedImage buffImage = imageUtil.createImageFromBytes(image);
+            BufferedImage buffImage = ImageUtil.createImageFromBytes(image);
             if (fileName == null) {
                 fileName = format.format(now) + ".jpg";
             }
@@ -186,16 +202,44 @@ public NeuralNetResults processImage(byte[] image, Map<String, ?> request) throw
             lastFilename = null;
         }
         
-        NeuralNetResults emptyResults = new NeuralNetResults();
-        List emptyList = new ArrayList();
-        emptyResults.setResults(emptyList);
-        emptyResults.setLastFilename("objectRecognition/" + sourceName + "/" + lastFilename);
-        return emptyResults;
+        NeuralNetResults results = new NeuralNetResults();
+        results.setResults(Collections.emptyList());
+        results.setLastFilename("objectRecognition/" + sourceName + "/" + lastFilename);
+    
+        // If the processor was configured to include the Base64 encoded image in the results, do so now.
+        // We include the pre-cropped image as we'd like the image to correspond to the reported object locations.
+        if (includeEncodedImage) {
+            results.setEncodedImage(NeuralNetUtils.convertToBase64(image));
+        }
+        return results;
+    }
+    
+    private boolean checkEncodedImageParam(Map<String, ?> config) {
+        boolean iei = false;
+        if (config.get(INCLUDE_ENCODED_IMAGE) instanceof String) {
+            String ieiString = (String) config.get(INCLUDE_ENCODED_IMAGE);
+            try {
+                iei = Boolean.parseBoolean(ieiString);
+            } catch (Exception e) {
+                if (log.isErrorEnabled()) {
+                    log.error("The config value for " + INCLUDE_ENCODED_IMAGE + " must be a boolean value ('" +
+                            ieiString + "' was provided). The encoded image will not be included in the results.");
+                }
+            }
+        } else if (config.get(INCLUDE_ENCODED_IMAGE) instanceof Boolean) {
+            iei = (Boolean) config.get(INCLUDE_ENCODED_IMAGE);
+        } else if (config.get(INCLUDE_ENCODED_IMAGE) != null) {
+            if (log.isErrorEnabled()) {
+                log.error("The value for " + INCLUDE_ENCODED_IMAGE + " must be a boolean value ('" +
+                        config.get(INCLUDE_ENCODED_IMAGE) + "' was provided). The encoded image will" +
+                        "not be included in the results.");
+            }
+        }
+        return iei;
     }
 
     @Override
     public void close() {
         // Nothing to close here
     }
-
 }
diff --git a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/YoloProcessor.java b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/YoloProcessor.java
index 27c7051d..0b61d096 100644
--- a/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/YoloProcessor.java
+++ b/objectRecognitionSource/src/main/java/io/vantiq/extsrc/objectRecognition/neuralNet/YoloProcessor.java
@@ -79,7 +79,8 @@ public class YoloProcessor extends NeuralNetUtils implements NeuralNetInterface2
     float threshold = 0.5f;
     int saveRate = 1;
     boolean uploadAsImage = false;
-    
+    boolean includeEncodedImage = false;
+
     // Variables for pre crop
     int x = -1;
     int y = -1; 
@@ -103,6 +104,7 @@ public class YoloProcessor extends NeuralNetUtils implements NeuralNetInterface2
     private static final String SAVE_IMAGE = "saveImage";
     private static final String BOTH = "both";
     private static final String LOCAL = "local";
+    private static final String INCLUDE_ENCODED_IMAGE = "includeEncodedImage";
     private static final String VANTIQ = "vantiq";
     private static final String OUTPUT_DIR = "outputDir";
     private static final String LABEL_IMAGE = "labelImage";
@@ -126,7 +128,7 @@ public void setupImageProcessing(Map<String, ?> neuralNetConfig, String sourceNa
         setup(neuralNetConfig, sourceName, modelDirectory, authToken, server);
         try {
             objectDetector = new ObjectDetector(threshold, pbFile, labelsFile, metaFile, anchorArray,
-                    imageUtil, outputDir, labelImage, saveRate, vantiq, sourceName);
+                    imageUtil, labelImage, saveRate, vantiq, sourceName);
         } catch (Exception e) {
             throw new Exception(this.getClass().getCanonicalName() + ".yoloBackendSetupError: " 
                     + "Failed to create new ObjectDetector", e);
@@ -141,7 +143,9 @@ public void setupImageProcessing(Map<String, ?> neuralNetConfig, String sourceNa
      * @param authToken         The authToken used to with the VANTIQ SDK
      * @throws Exception        Thrown when an invalid configuration is requested
      */
-    private void setup(Map<String, ?> neuralNet, String sourceName, String modelDirectory, String authToken, String server) throws Exception {
+    @SuppressWarnings({"PMD.CognitiveComplexity", "Checkstyle:MethodLengthCheck"})
+    private void setup(Map<String, ?> neuralNet, String sourceName, String modelDirectory,
+                       String authToken, String server) throws Exception {
         this.server = server;
         this.authToken = authToken;
         this.sourceName = sourceName;
@@ -163,10 +167,12 @@ private void setup(Map<String, ?> neuralNet, String sourceName, String modelDire
            
        } else {
            throw new Exception(this.getClass().getCanonicalName() + ".missingConfig: Invalid Configuration: " 
-                   + "A YOLO NeuralNet configuration requires a pbFile and a metaFile and/or labelFile. " 
-                   + "pbFile: " + (neuralNet.get(PB_FILE) instanceof String ? (String) neuralNet.get(PB_FILE)  : " ") 
-                   + ", metaFile: " + (neuralNet.get(META_FILE) instanceof String ? (String) neuralNet.get(META_FILE)  : " ") 
-                   + ", labelFile: " + (neuralNet.get(LABEL_FILE) instanceof String ? (String) neuralNet.get(LABEL_FILE)  : " "));
+               + "A YOLO NeuralNet configuration requires a pbFile and a metaFile and/or labelFile. "
+               + "pbFile: " + (neuralNet.get(PB_FILE) instanceof String ? (String) neuralNet.get(PB_FILE)  : " ")
+               + ", metaFile: " + (neuralNet.get(META_FILE) instanceof String ?
+                   (String) neuralNet.get(META_FILE)  : " ")
+               + ", labelFile: " + (neuralNet.get(LABEL_FILE) instanceof String ?
+                   (String) neuralNet.get(LABEL_FILE)  : " "));
        }
        
        if (neuralNet.get(THRESHOLD) instanceof Number) {
@@ -186,6 +192,7 @@ private void setup(Map<String, ?> neuralNet, String sourceName, String modelDire
        
        // Get anchor values if they exist
        if (neuralNet.get(ANCHORS) instanceof List) {
+           @SuppressWarnings("rawtypes")
            List tempAnchorList = (List) neuralNet.get(ANCHORS);
            // Checking that the number of anchor pairs matches NUMBER_OF_BOUNDING_BOXES * 2
            if (tempAnchorList.size() != YOLOClassifier.NUMBER_OF_BOUNDING_BOX * 2) {
@@ -223,12 +230,6 @@ private void setup(Map<String, ?> neuralNet, String sourceName, String modelDire
        if (neuralNet.get(SAVE_IMAGE) instanceof String) {
            saveImage = (String) neuralNet.get(SAVE_IMAGE);
            
-           // Check if user wants to save original image without labels
-           if (neuralNet.get(LABEL_IMAGE) instanceof String) {
-               String labelImageString = (String) neuralNet.get(LABEL_IMAGE);
-               labelImage = labelImageString.equalsIgnoreCase("true");
-           }
-           
            // Check which method of saving the user requests
            if (!saveImage.equalsIgnoreCase(VANTIQ) && !saveImage.equalsIgnoreCase(BOTH) && !saveImage.equalsIgnoreCase(LOCAL)) {
                log.error("The config value for saveImage was invalid. Images will not be saved.");
@@ -250,6 +251,7 @@ private void setup(Map<String, ?> neuralNet, String sourceName, String modelDire
            
            // Check if any resolution configurations have been set
            if (neuralNet.get(SAVED_RESOLUTION) instanceof Map) {
+               @SuppressWarnings("rawtypes")
                Map savedResolution = (Map) neuralNet.get(SAVED_RESOLUTION);
                if (savedResolution.get(LONG_EDGE) instanceof Integer) {
                    int longEdge = (Integer) savedResolution.get(LONG_EDGE);
@@ -277,9 +279,21 @@ private void setup(Map<String, ?> neuralNet, String sourceName, String modelDire
            // Flag to mark that we should not save images
            imageUtil.saveImage = false;
        }
-       
-       // Checking if pre cropping was specified in config
+
+       includeEncodedImage = checkEncodedImageParam(neuralNet);
+
+       // We can label the saved image, but we'll also label an included, encoded image if desired.
+       if (imageUtil.saveImage || includeEncodedImage) {
+           // Check if user wants to include image with labels in the upload.
+           if (neuralNet.get(LABEL_IMAGE) instanceof String) {
+               String labelImageString = (String) neuralNet.get(LABEL_IMAGE);
+               labelImage = "true".equalsIgnoreCase(labelImageString);
+           }
+       }
+
+        // Checking if pre cropping was specified in config
        if (neuralNet.get(CROP_BEFORE) instanceof Map) {
+           @SuppressWarnings("rawtypes")
            Map preCrop = (Map) neuralNet.get(CROP_BEFORE);
            if (preCrop.get(X) instanceof Integer && (Integer) preCrop.get(X) >= 0) {
                x = (Integer) preCrop.get(X);
@@ -301,9 +315,34 @@ private void setup(Map<String, ?> neuralNet, String sourceName, String modelDire
            }
        }
    }
+   
+   private boolean checkEncodedImageParam(Map<String, ?> config) {
+       boolean iei = false;
+       if (config.get(INCLUDE_ENCODED_IMAGE) instanceof String) {
+           String ieiString = (String) config.get(INCLUDE_ENCODED_IMAGE);
+           try {
+               iei = Boolean.parseBoolean(ieiString);
+           } catch (Exception e) {
+               if (log.isErrorEnabled()) {
+                   log.error("The config value for " + INCLUDE_ENCODED_IMAGE + " must be a boolean value ('" +
+                           ieiString + "' was provided). The encoded image will not be included in the results.");
+               }
+           }
+       } else if (config.get(INCLUDE_ENCODED_IMAGE) instanceof Boolean) {
+           iei = (Boolean) config.get(INCLUDE_ENCODED_IMAGE);
+       } else if (config.get(INCLUDE_ENCODED_IMAGE) != null) {
+           if (log.isErrorEnabled()) {
+               log.error("The value for " + INCLUDE_ENCODED_IMAGE + " must be a boolean value ('" +
+                       config.get(INCLUDE_ENCODED_IMAGE) + "' was provided). The encoded image will" +
+                       "not be included in the results.");
+           }
+       }
+       return iei;
+   }
 
     /**
      * Deprecated method, should not be used anymore.
+     * @ Deprecated at interface level.
      */
     @Override
     public NeuralNetResults processImage(byte[] image) throws ImageProcessingException {
@@ -313,6 +352,7 @@ public NeuralNetResults processImage(byte[] image) throws ImageProcessingExcepti
     
     /**
      * Deprecated method, should not be used anymore.
+     * @ Deprecated at interface level.
      */
     @Override
     public NeuralNetResults processImage(byte[] image,  Map<String, ?> request) throws ImageProcessingException {
@@ -322,6 +362,8 @@ public NeuralNetResults processImage(byte[] image,  Map<String, ?> request) thro
     
     /**
      * Run the image through a YOLO net. May save the resulting image depending on the settings.
+     * This overload of processImage() operates as the connector fetches new images from its image source.
+     * As such, its configuration is provided via the setup() method above.
      */
     @Override
     public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] image) throws ImageProcessingException {
@@ -342,7 +384,11 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
         }
 
         try {
-            foundObjects = objectDetector.detect(image, timestamp);
+            ObjectDetector.ResultHolder rh = objectDetector.detect(image, timestamp);
+            foundObjects = rh.results;
+            if (rh.image != null) {
+                image = rh.image;
+            }
         } catch (IllegalArgumentException e) {
             throw new ImageProcessingException(this.getClass().getCanonicalName() + ".invalidImage: " 
                     + "Data to be processed was invalid. Most likely it was not correctly encoded as a jpg.", e);
@@ -359,21 +405,35 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
             results.setLastFilename("objectRecognition/" + sourceName + '/' + objectDetector.lastFilename);
         }
         results.setResults(foundObjects);
+
+        // If the processor was configured to include the Base64 encoded image in the results, do so now.
+        // We include the pre-cropped image as we'd like the image to correspond to the reported object locations.
+        if (includeEncodedImage) {
+            results.setEncodedImage(NeuralNetUtils.convertToBase64(image));
+        }
         return results;
     }
     
     /**
      * Run the image through a YOLO net. May save the resulting image depending on the request.
+     * This version of processImage() is used for processing queries from Vantiq.  The other is used as images are
+     * fetched by the connector.
      */
+    @SuppressWarnings("PMD.CognitiveComplexity")
     @Override
     public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] image, Map<String, ?> request) throws ImageProcessingException {
         List<Map<String, ?>> foundObjects;
         NeuralNetResults results = new NeuralNetResults();
+        // This method duplicates a number of the config value settings. These are determined on a per-query basis,
+        // but duplicating them here makes the code easier to move about.
         String saveImage = null;
         String outputDir = null;
         String fileName = null;
         Vantiq vantiq = null;
+        boolean includeEncodedImage;
         boolean uploadAsImage = false;
+        boolean localLabelRequest = false;
+        boolean savingSomewhere = false;
 
         Date timestamp;
         if (processingParams != null && processingParams.get(IMAGE_TIMESTAMP) instanceof Date) {
@@ -381,6 +441,7 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
         } else {
             timestamp = new Date();
         }
+
         fileName = format.format(timestamp);
 
         if (request.get(NN_SAVE_IMAGE) instanceof String) {
@@ -388,6 +449,7 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
             if (!saveImage.equalsIgnoreCase(VANTIQ) && !saveImage.equalsIgnoreCase(BOTH) && !saveImage.equalsIgnoreCase(LOCAL)) {
                 log.error("The config value for saveImage was invalid. Images will not be saved.");
             } else {
+                savingSomewhere = true;
                 if (saveImage.equalsIgnoreCase(VANTIQ) || saveImage.equalsIgnoreCase(BOTH)) {
                     vantiq = new io.vantiq.client.Vantiq(server);
                     vantiq.setAccessToken(authToken);
@@ -407,12 +469,28 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
                 }
             }
         }
+
+        includeEncodedImage = checkEncodedImageParam(request);
+
+        if (includeEncodedImage || savingSomewhere) {
+            // If we are saving somewhere, we'll allow a local override of the label request
+
+            if (!request.containsKey(LABEL_IMAGE)) {
+                // If not local request, inherit the state from the connector configuration
+                localLabelRequest = labelImage;
+            } else if (request.get(LABEL_IMAGE) instanceof String) {
+                localLabelRequest = Boolean.parseBoolean((String) request.get(LABEL_IMAGE));
+            } else if (request.get(LABEL_IMAGE) instanceof Boolean) {
+                localLabelRequest = (Boolean) request.get(LABEL_IMAGE);
+            }
+        }
                 
         // Checking if pre cropping was specified in query parameters
         boolean queryCrop = false;
         int x, y, w, h;
         x = y = w = h = -1;
         if (request.get(CROP_BEFORE) instanceof Map) {
+            @SuppressWarnings("rawtypes")
             Map preCrop = (Map) request.get(CROP_BEFORE);
             if (preCrop.get(X) instanceof Integer && (Integer) preCrop.get(X) >= 0) {
                 x = (Integer) preCrop.get(X);
@@ -447,12 +525,16 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
         long after;
         long before = System.currentTimeMillis();
         try {
-            foundObjects = objectDetector.detect(image, outputDir, fileName, vantiq, uploadAsImage);
+            ObjectDetector.ResultHolder rh = objectDetector.detect(image, outputDir, fileName, vantiq,
+                    uploadAsImage, localLabelRequest);
+            foundObjects = rh.results;
+            if (rh.image != null) {
+                image = rh.image;
+            }
         } catch (IllegalArgumentException e) {
             throw new ImageProcessingException(this.getClass().getCanonicalName() + ".queryInvalidImage: " 
                     + "Data to be processed was invalid. Most likely it was not correctly encoded as a jpg.", e);
         }
-        
 
         after = System.currentTimeMillis();
         log.debug("Image processing time: {}.{} seconds"
@@ -464,7 +546,21 @@ public NeuralNetResults processImage(Map<String, ?> processingParams, byte[] ima
         } else {
             results.setLastFilename("objectRecognition/" + sourceName + '/' + objectDetector.lastFilename);
         }
+
         results.setResults(foundObjects);
+
+        // If the processor was configured to include the Base64 encoded image in the results, do so now.
+        // We include the pre-cropped image as we'd like the image to correspond to the reported object locations.
+        if (includeEncodedImage) {
+            log.debug("Attempting to encode the image from a query");
+            results.setEncodedImage(NeuralNetUtils.convertToBase64(image));
+            if (log.isTraceEnabled()) {
+                log.trace("Encoded image in results: " + results.getEncodedImage());
+            }
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("Returning {} results.", results.getResults().size());
+        }
         return results;
     }
 


Test patch:
diff --git a/jdbcSource/src/test/java/io/vantiq/extsrc/jdbcSource/TestJDBCBase.java b/jdbcSource/src/test/java/io/vantiq/extsrc/jdbcSource/TestJDBCBase.java
index f5cba73f..725a0b60 100644
--- a/jdbcSource/src/test/java/io/vantiq/extsrc/jdbcSource/TestJDBCBase.java
+++ b/jdbcSource/src/test/java/io/vantiq/extsrc/jdbcSource/TestJDBCBase.java
@@ -93,7 +93,6 @@ protected static void createSourceImpl(Vantiq vantiq) throws Exception {
     protected static void deleteSourceImpl(Vantiq vantiq) {
         if (createdImpl) {
             vantiq.deleteOne(VANTIQ_SOURCE_IMPL, JDBC_SRC_TYPE);
-            createdImpl = false;
         }
 
         VantiqResponse resp = vantiq.selectOne(VANTIQ_SOURCE_IMPL, JDBC_SRC_TYPE);
@@ -102,7 +101,7 @@ protected static void deleteSourceImpl(Vantiq vantiq) {
             if (errors.size() != 1 || !errors.get(0).getCode().equals("io.vantiq.resource.not.found")) {
                 fail("Error deleting source impl" + resp.getErrors());
             }
-        } else {
+        } else if (createdImpl) {
             fail(JDBC_SRC_TYPE + " source impl found after deletion.");
         }
     }
diff --git a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/TestObjRecConfig.java b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/TestObjRecConfig.java
index 25cf634a..219add1f 100644
--- a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/TestObjRecConfig.java
+++ b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/TestObjRecConfig.java
@@ -227,7 +227,7 @@ public void testValidThreadConfigs() {
     @Test
     public void testInvalidImageUploadConfig() {
         Map<String, Object> conf = minimalConfig();
-        neuralNet.put("uploadAsImage", "jibberish");
+        neuralNet.put("uploadAsImage", "gibberish");
         sendConfig(conf);
         assertFalse("Should not fail when image upload config is a string", configIsFailed());
 
@@ -247,7 +247,39 @@ public void testValidImageUploadConfig() {
         sendConfig(conf);
         assertFalse("Should not fail when image upload config is false", configIsFailed());
     }
-    
+
+    @Test
+    public void testInvalidIncludeEncodedConfig() {
+        Map<String, Object> conf = minimalConfig();
+        neuralNet.put("includeEncodedImage", "gibberish");
+        sendConfig(conf);
+        assertFalse("Should not fail when image include is garbage", configIsFailed());
+
+        neuralNet.put("includeEncodedImage", 10);
+        sendConfig(conf);
+        assertFalse("Should not fail when image include config is an int", configIsFailed());
+    }
+
+    @Test
+    public void testValidIncludeEncodedConfig() {
+        Map<String, Object> conf = minimalConfig();
+        neuralNet.put("includeEncodedImage", true);
+        sendConfig(conf);
+        assertFalse("Should not fail when image include config is true", configIsFailed());
+
+        neuralNet.put("includeEncodedImage", false);
+        sendConfig(conf);
+        assertFalse("Should not fail when image include config is false", configIsFailed());
+
+        neuralNet.put("includeEncodedImage", "true");
+        sendConfig(conf);
+        assertFalse("Should not fail when image include config is true String", configIsFailed());
+
+        neuralNet.put("includeEncodedImage", "false");
+        sendConfig(conf);
+        assertFalse("Should not fail when image include config is false String", configIsFailed());
+    }
+
     @Test
     public void testMinimalConfig() {
         nCore.start(5); // Need a client to avoid NPEs on sends
diff --git a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetTestBase.java b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetTestBase.java
index c17a35d0..ad9e98a8 100644
--- a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetTestBase.java
+++ b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/NeuralNetTestBase.java
@@ -91,7 +91,6 @@ protected static void createSourceImpl(Vantiq vantiq) throws Exception {
     protected static void deleteSourceImpl(Vantiq vantiq) throws Exception {
         if (createdImpl) {
             vantiq.deleteOne(VANTIQ_SOURCE_IMPL, OR_SRC_TYPE);
-            createdImpl = false;
         }
 
         VantiqResponse resp = vantiq.selectOne(VANTIQ_SOURCE_IMPL, OR_SRC_TYPE);
@@ -100,9 +99,10 @@ protected static void deleteSourceImpl(Vantiq vantiq) throws Exception {
             if (errors.size() != 1 || !errors.get(0).getCode().equals("io.vantiq.resource.not.found")) {
                 fail("Error deleting source impl" + resp.getErrors());
             }
-        } else {
+        } else if (createdImpl) {
             fail(OR_SRC_TYPE + " source impl found after deletion.");
         }
+        createdImpl = false;
     }
 
     public static byte[] getTestImage() {
@@ -114,13 +114,13 @@ public static byte[] getTestImage() {
             return null;
         }
     }
-
+    
+    @SuppressWarnings("PMD.CognitiveComplexity")
     public static void checkUploadToVantiq(String name, Vantiq vantiq, String resourceName) throws InterruptedException {
         boolean done = false;
         int retries = 0;
         int maxRetries = WAIT_FOR_ASYNC_MILLIS / 50;
         while (!done) {
-            done = true;
             VantiqResponse vantiqResponse = vantiq.selectOne(resourceName, name);
             if (vantiqResponse.hasErrors()) {
                 if (++retries < maxRetries) {
@@ -134,6 +134,11 @@ public static void checkUploadToVantiq(String name, Vantiq vantiq, String resour
                         }
                     }
                 }
+            } else if (vantiqResponse.isSuccess()) {
+                done = true;
+            } else {
+                retries += 1;
+                Thread.sleep(50);
             }
         }
     }
@@ -169,7 +174,7 @@ public static boolean checkSourceExists(Vantiq vantiq) {
     }
 
     public static void deleteSource(Vantiq vantiq) {
-        Map<String,Object> where = new LinkedHashMap<String,Object>();
+        Map<String, Object> where = new LinkedHashMap<>();
         where.put("name", testSourceName);
         VantiqResponse response = vantiq.delete("system.sources", where);
     }
@@ -187,7 +192,7 @@ public static boolean checkTypeExists(Vantiq vantiq) {
     }
 
     public static void deleteType(Vantiq vantiq) {
-        Map<String,Object> where = new LinkedHashMap<String,Object>();
+        Map<String, Object> where = new LinkedHashMap<>();
         where.put("name", testTypeName);
         VantiqResponse response = vantiq.delete("system.types", where);
     }
@@ -205,7 +210,7 @@ public static boolean checkRuleExists(Vantiq vantiq) {
     }
 
     public static void deleteRule(Vantiq vantiq) {
-        Map<String,Object> where = new LinkedHashMap<String,Object>();
+        Map<String, Object> where = new LinkedHashMap<>();
         where.put("name", testRuleName);
         VantiqResponse response = vantiq.delete("system.rules", where);
     }
diff --git a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestNoProcessor.java b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestNoProcessor.java
index 6ae22b99..10c11bfe 100644
--- a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestNoProcessor.java
+++ b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestNoProcessor.java
@@ -12,12 +12,17 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
+import java.awt.image.BufferedImage;
 import java.io.File;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import edu.ml.tensorflow.util.ImageUtil;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.FixMethodOrder;
@@ -317,6 +322,72 @@ public void testImageSavingBoth() throws ImageProcessingException, InterruptedEx
             npProcessor.close();
         }
     }
+    
+    // Similar to testImageSavingVantiq(), but includes returning the image in a Base64-encoded form.
+    // No images should be saved locally.
+    @Test
+    public void testImageSavingVantiqPlusEncoded() throws ImageProcessingException, InterruptedException {
+        
+        // Only run test with intended vantiq availability
+        assumeTrue(testAuthToken != null && testVantiqServer != null);
+        
+        String lastFilename;
+        Map<String, Object> config = new LinkedHashMap<>();
+        NoProcessor npProcessor = new NoProcessor();
+        
+        config.put("outputDir", OUTPUT_DIR);
+        config.put("saveRate", SAVE_RATE);
+        config.put("saveImage", "vantiq");
+        config.put("includeEncodedImage", true);
+        npProcessor.setupImageProcessing(config, SOURCE_NAME, MODEL_DIRECTORY, testAuthToken, testVantiqServer);
+        assert npProcessor.includeEncodedImage;
+        
+        File d = new File(OUTPUT_DIR);
+        try {
+            // Ensure no results from previous tests
+            if (d.exists()) {
+                deleteDirectory(OUTPUT_DIR);
+            }
+            
+            byte[] testImage = getTestImage();
+            NeuralNetResults results = npProcessor.processImage(testImage);
+            assert results.getResults() == null;
+            assert results.getEncodedImage() != null;
+    
+            String incImage = results.getEncodedImage();
+            assert incImage != null;
+            byte[] retBytes = Base64.getDecoder().decode(incImage.getBytes(StandardCharsets.UTF_8));
+    
+            // For insurance, we'll also ensure that we can decode the bits & that our images are at
+            // least the same size.
+            BufferedImage encodedImage = ImageUtil.createImageFromBytes(retBytes);
+            BufferedImage origImage = ImageUtil.createImageFromBytes(testImage);
+            assert origImage.getWidth() == encodedImage.getWidth();
+            assert origImage.getHeight() == encodedImage.getHeight();
+            // Round trip from bytes to base64 to bytes for images can have slightly varying lengths, probably
+            // due to image compression, etc.  Semantically, the images are the same, so we'll look at them as a JPEG,
+            // then  get the bytes back to compare.
+            testImage = ImageUtil.getBytesForImage(origImage);
+            assert testImage.length == retBytes.length;
+            assert Arrays.equals(testImage, retBytes);
+            
+            // Should not exist since images are not being saved locally.
+            assert !d.exists();
+            
+            // Checking that image was saved to VANTIQ
+            Thread.sleep(1000);
+            lastFilename = "objectRecognition/" + SOURCE_NAME + '/' + npProcessor.lastFilename;
+            checkUploadToVantiq(lastFilename, vantiq, VANTIQ_DOCUMENTS);
+            vantiqSavedFiles.add(lastFilename);
+        } finally {
+            // delete the directory even if the test fails
+            if (d.exists()) {
+                deleteDirectory(OUTPUT_DIR);
+            }
+            
+            npProcessor.close();
+        }
+    }
 
     // Similar to testImageSavingLocal() and testImageSavingBoth(), but saveImage is set to "vantiq".
     // No images should be saved locally.
@@ -366,6 +437,15 @@ public void testImageSavingVantiq() throws ImageProcessingException, Interrupted
 
     @Test
     public void testQuery() throws ImageProcessingException, InterruptedException {
+        doQueryTest(false);
+    }
+    
+    @Test
+    public void testQueryWithEncoded() throws ImageProcessingException, InterruptedException {
+        doQueryTest(true);
+    }
+    
+    void doQueryTest(boolean includeEncoded) throws ImageProcessingException, InterruptedException {
         // Only run test with intended vantiq availability
         assumeTrue(testAuthToken != null && testVantiqServer != null);
 
@@ -401,10 +481,29 @@ public void testQuery() throws ImageProcessingException, InterruptedException {
             // Test when saveImage is not set correctly
             request.put("NNsaveImage", "jibberish");
             request.put("NNoutputDir", queryOutputDir);
-            results = null;
-            results = npProcessor.processImage(getTestImage(), request);
+            if (includeEncoded) {
+                request.put("includeEncodedImage", "true");
+            }
+            
+            byte[] testImage = getTestImage();
+            results = npProcessor.processImage(testImage, request);
             assert results.getResults().isEmpty();
-
+            assert (results.getEncodedImage() == null) != includeEncoded;
+
+            if (includeEncoded) {
+                String incImage = results.getEncodedImage();
+                assert incImage != null;
+                byte[] retBytes = Base64.getDecoder().decode(incImage.getBytes(StandardCharsets.UTF_8));
+    
+                // For insurance, we'll also ensure that we can decode the bits & that our images are at
+                // least the same size.
+                BufferedImage encodedImage = ImageUtil.createImageFromBytes(retBytes);
+                BufferedImage origImage = ImageUtil.createImageFromBytes(testImage);
+                assert origImage.getWidth() == encodedImage.getWidth();
+                assert origImage.getHeight() == encodedImage.getHeight();
+                assert testImage != null;
+                assert testImage.length == retBytes.length;
+            }
             // Should not have saved the image
             assert !dNew.exists();
 
diff --git a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloProcessor.java b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloProcessor.java
index 836a2e4e..0261eefb 100644
--- a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloProcessor.java
+++ b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloProcessor.java
@@ -26,6 +26,7 @@
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -38,6 +39,7 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 
+import edu.ml.tensorflow.util.ImageUtil;
 import io.vantiq.client.BaseResponseHandler;
 import io.vantiq.client.Vantiq;
 import io.vantiq.client.VantiqError;
@@ -45,6 +47,7 @@
 import io.vantiq.extjsdk.ExtensionServiceMessage;
 import io.vantiq.extsrc.objectRecognition.exception.ImageProcessingException;
 import io.vantiq.extsrc.objectRecognition.ObjectRecognitionCore;
+import lombok.extern.slf4j.Slf4j;
 import okhttp3.Response;
 import okio.BufferedSource;
 import org.junit.AfterClass;
@@ -54,7 +57,8 @@
 import org.junit.runners.MethodSorters;
 
 
-@SuppressWarnings({"PMD.ExcessiveClassLength", "PMD.AbbreviationAsWordInNameCheck"})
+@SuppressWarnings({"PMD.ExcessiveClassLength", "PMD.AbbreviationAsWordInNameCheck", "PMD.ExcessivePublicCount"})
+@Slf4j
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class TestYoloProcessor extends NeuralNetTestBase {
 
@@ -348,7 +352,6 @@ public void testRealJsonConfig() throws IOException {
         YoloProcessor ypImageSaver = new YoloProcessor();
         ExtensionServiceMessage msg = createRealConfig(neuralNetJSON1);
 
-
         Map<String, Object> config = (Map<String, Object>) msg.getObject();
         Map<String, ?> neuralNetConfig = (Map<String, ?>) config.get("neuralNet");
 
@@ -518,7 +521,6 @@ public void testInvalidData() {
 
     @Test
     public void testImageSavingWithoutLabels() throws ImageProcessingException {
-
         // Only run test with intended vantiq availability
         assumeTrue(testAuthToken != null && testVantiqServer != null);
         labelTestHelper(false);
@@ -526,7 +528,6 @@ public void testImageSavingWithoutLabels() throws ImageProcessingException {
 
     @Test
     public void testImageSavingWithLabels() throws ImageProcessingException {
-
         // Only run test with intended vantiq availability
         assumeTrue(testAuthToken != null && testVantiqServer != null);
         labelTestHelper(true);
@@ -573,10 +574,13 @@ public void labelTestHelper(Boolean labelOption) throws ImageProcessingException
             assert lf.length == 1;
             assert lf[0].getName().matches(timestampPattern);
 
-            try {
+            byte[] imgBytes = getTestImage();
+            assert imgBytes != null;
+
+            try (ByteArrayInputStream bis = new ByteArrayInputStream(imgBytes);
+                    ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                 // Converting original test image to buffered image, since this is how we save images
-                BufferedImage tempOriginal = ImageIO.read(new ByteArrayInputStream(getTestImage()));
-                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                BufferedImage tempOriginal = ImageIO.read(bis);
                 ImageIO.write(tempOriginal, "jpg", baos);
                 baos.flush();
 
@@ -590,8 +594,6 @@ public void labelTestHelper(Boolean labelOption) throws ImageProcessingException
                 } else {
                     assertArrayEquals("Images should be identical", originalImg, savedImg);
                 }
-
-                baos.close();
             } catch (IOException e) {
                 fail("Should not catch exception when checking saved image vs. original.");
             }
@@ -626,7 +628,7 @@ public void testInvalidResizingSavedImage() throws ImageProcessingException, IOE
         
         // The longEdge is a negative number which is not allowed, should not do any resizing
         savedResolution.put("longEdge", -100);
-        config.put("savedResolution", savedResolution); 
+        config.put("savedResolution", savedResolution);
         checkInvalidResizing(config);
         
         // The longEdge is not an integer which is not allowed, should not do any resizing
@@ -677,7 +679,6 @@ public void checkInvalidResizing(Map<String, Object> config) throws ImageProcess
             // Original dimensions
             assert resizedImage.getWidth() == TEST_IMAGE_WIDTH;
             assert resizedImage.getHeight() == TEST_IMAGE_HEIGHT;
-
         } finally {
             if (d.exists()) {
                 deleteDirectory(OUTPUT_DIR);
@@ -733,7 +734,6 @@ public void testResizingSavedImageLocal() throws ImageProcessingException, IOExc
             
             assert resizedImage.getWidth() == RESIZED_IMAGE_WIDTH;
             assert resizedImage.getHeight() == RESIZED_IMAGE_HEIGHT;
-
         } finally {
             if (d.exists()) {
                 deleteDirectory(OUTPUT_DIR);
@@ -766,7 +766,6 @@ public void testResizingSavedImageVantiq() throws ImageProcessingException, IOEx
             fail("Could not setup the JSON YoloProcessor");
         }
         try {
-
             NeuralNetResults results = ypImageSaver.processImage(getTestImage());
             assert results != null;
             assert results.getResults() != null;
@@ -789,10 +788,8 @@ public void testResizingSavedImageVantiq() throws ImageProcessingException, IOEx
             InputStream imageStream = new ByteArrayInputStream(imageBytes);
             BufferedImage resizedImage = ImageIO.read(imageStream);
             
-            
             assert resizedImage.getWidth() == RESIZED_IMAGE_WIDTH;
             assert resizedImage.getHeight() == RESIZED_IMAGE_HEIGHT;
-
         } finally {
             ypImageSaver.close();
         }
@@ -863,22 +860,18 @@ public void testResizingSavedImageBoth() throws ImageProcessingException, IOExce
             InputStream imageStream = new ByteArrayInputStream(imageBytes);
             resizedImage = ImageIO.read(imageStream);
             
-            
             assert resizedImage.getWidth() == RESIZED_IMAGE_WIDTH;
             assert resizedImage.getHeight() == RESIZED_IMAGE_HEIGHT;
-
         } finally {
             if (d.exists()) {
                 deleteDirectory(OUTPUT_DIR);
             }
-
             ypImageSaver.close();
         }
     }
 
     @Test
     public void testImageSavingLocal() throws ImageProcessingException, InterruptedException {
-
         // Only run test with intended vantiq availability
         assumeTrue(testAuthToken != null && testVantiqServer != null);
 
@@ -1298,7 +1291,7 @@ public void testInvalidPreCropping() throws ImageProcessingException, IOExceptio
         preCrop.put("y", -1);
         preCrop.put("width", CROPPED_WIDTH);
         preCrop.put("height", CROPPED_HEIGHT);
-        config.put("cropBeforeAnalysis", preCrop); 
+        config.put("cropBeforeAnalysis", preCrop);
         checkInvalidPreCropping(config);
         
         // Just x coordinate is negative, should not do any resizing
@@ -1306,7 +1299,7 @@ public void testInvalidPreCropping() throws ImageProcessingException, IOExceptio
         preCrop.put("y", PRECROP_TOP_LEFT_Y_COORDINATE);
         preCrop.put("width", CROPPED_WIDTH);
         preCrop.put("height", CROPPED_HEIGHT);
-        config.put("cropBeforeAnalysis", preCrop); 
+        config.put("cropBeforeAnalysis", preCrop);
         checkInvalidPreCropping(config);
         
         // Just y coordinate is negative, should not do any resizing
@@ -1314,7 +1307,7 @@ public void testInvalidPreCropping() throws ImageProcessingException, IOExceptio
         preCrop.put("y", -1);
         preCrop.put("width", CROPPED_WIDTH);
         preCrop.put("height", CROPPED_HEIGHT);
-        config.put("cropBeforeAnalysis", preCrop); 
+        config.put("cropBeforeAnalysis", preCrop);
         checkInvalidPreCropping(config);
         
         // Two values are not integers, should not do any resizing
@@ -1466,7 +1459,6 @@ public void testPreCroppingVantiq() throws ImageProcessingException, Interrupted
             fail("Could not setup the YoloProcessor");
         }
         try {
-
             NeuralNetResults results = ypImageSaver.processImage(getTestImage());
             assert results != null;
             assert results.getResults() != null;
@@ -1821,6 +1813,123 @@ public void testUploadAsVantiqImage() throws ImageProcessingException, Interrupt
         }
     }
 
+    @Test
+    public void testEncodeImage() throws ImageProcessingException, InterruptedException {
+        performTestEncodedImage(false);
+    }
+
+    @Test
+    public void testEncodeImageWithSave() throws ImageProcessingException, InterruptedException {
+        performTestEncodedImage(true);
+    }
+
+    public void performTestEncodedImage(boolean shouldSaveImage) throws ImageProcessingException, InterruptedException {
+        // Only run test with intended vantiq availability
+        assumeTrue(testAuthToken != null && testVantiqServer != null);
+
+        Map<String, Object> config = new LinkedHashMap<>();
+        YoloProcessor ypImageSaver = new YoloProcessor();
+
+        config.put("pbFile", PB_FILE);
+        config.put("metaFile", META_FILE);
+        config.put("outputDir", OUTPUT_DIR);
+        config.put("saveRate", SAVE_RATE);
+        if (shouldSaveImage) {
+            config.put("saveImage", "vantiq");
+            config.put("uploadAsImage", true);
+        }
+        config.put("includeEncodedImage", true);
+
+        try {
+            ypImageSaver.setupImageProcessing(config, SOURCE_NAME, MODEL_DIRECTORY, testAuthToken, testVantiqServer);
+            log.debug("Saving spec: {}", ypImageSaver.imageUtil.saveImage);
+            assert ypImageSaver.imageUtil.saveImage == shouldSaveImage;
+        } catch (Exception e) {
+            fail("Could not setup the YoloProcessor");
+        }
+        try {
+            byte[] image = getTestImage();
+            BufferedImage testImage = ImageUtil.createImageFromBytes(image);
+
+            // The image gets re-encoded before processing (to force format, amongst other reasons), so we cannot
+            // just compare the base64 rendering of the image we thought we had.  Instead, we'll do what the
+            // image processor does & compare those results.
+
+            byte[] imageAsProcessed = ImageUtil.getBytesForImage(testImage);
+            String imageBase64 = NeuralNetUtils.convertToBase64(imageAsProcessed);
+
+            // Process the image...
+            NeuralNetResults results = ypImageSaver.processImage(null, image);
+
+            assert results != null;
+            assert results.getResults() != null;
+            assert results.getEncodedImage() != null;
+            assert results.getEncodedImage().equals(imageBase64);
+
+            // Fetch the byte array representing the uploaded image.
+            byte[] decodedBytes = Base64.getDecoder().decode(results.getEncodedImage());
+            // For insurance, we'll also ensure that we can decode the bits & that our images are at
+            // least the same size.
+            BufferedImage encodedImage = ImageUtil.createImageFromBytes(decodedBytes);
+            assert testImage.getWidth() == encodedImage.getWidth();
+            assert testImage.getHeight() == encodedImage.getHeight();
+
+            if (shouldSaveImage) {
+                // Checking that image was saved to VANTIQ
+                Thread.sleep(1000);
+                if (log.isDebugEnabled()) {
+                    log.debug("Checking for image file: {}", results.getLastFilename());
+                }
+                checkUploadToVantiq(results.getLastFilename(), vantiq, VANTIQ_IMAGES);
+                vantiqSavedImageFiles.add(results.getLastFilename());
+            }
+        } finally {
+            ypImageSaver.close();
+        }
+    }
+
+    @Test
+    public void testPreCroppingIncludeEncoded() {
+        performPreCroppingIncludeEncoded(false);
+    }
+
+    @Test
+    public void testPreCroppingLabelIncludeEncoded() {
+        performPreCroppingIncludeEncoded(true);
+    }
+
+    public void performPreCroppingIncludeEncoded(boolean labelOption) {
+        // Only run test with intended vantiq availability
+        assumeTrue(testAuthToken != null && testVantiqServer != null);
+
+        Map<String, Object> config = new LinkedHashMap<>();
+        Map<String, Object> preCrop = new LinkedHashMap<>();
+        YoloProcessor ypImageSaver = new YoloProcessor();
+
+        preCrop.put("x", PRECROP_KEYBOARD_TOP_LEFT_X_COORDINATE);
+        preCrop.put("y", PRECROP_KEYBOARD_TOP_LEFT_Y_COORDINATE);
+        preCrop.put("width", KEYBOARD_CROPPED_WIDTH);
+        preCrop.put("height", KEYBOARD_CROPPED_HEIGHT);
+
+        config.put("pbFile", PB_FILE);
+        config.put("metaFile", META_FILE);
+        config.put("cropBeforeAnalysis", preCrop);
+        config.put("includeEncodedImage", true);
+        if (labelOption) {
+            config.put("labelImage", "true");
+        }
+
+        // Config with meta file, label file, pb file, and anchors
+        try {
+            ypImageSaver.setupImageProcessing(config, SOURCE_NAME, MODEL_DIRECTORY, testAuthToken, testVantiqServer);
+            verifyProcessing(ypImageSaver, croppedImageResultsAsString);
+        } catch (Exception e) {
+            fail("Should not fail with valid config: " + e.getClass().getName() + "::" + e.getMessage());
+        } finally {
+            ypImageSaver.close();
+        }
+    }
+
     // ================================================= Helper functions =================================================
 
     String imageResultsAsString = "[" +
@@ -1934,9 +2043,33 @@ List<Map> getExpectedResults(String resultsAsString) throws IOException {
     }
 
     void verifyProcessing(YoloProcessor ypImageSaver, String resultsAsString) throws ImageProcessingException {
-        NeuralNetResults results = ypImageSaver.processImage(getTestImage());
+
+        byte[] image = getTestImage();
+        String encoded = null;
+        if (ypImageSaver.includeEncodedImage) {
+            if (ypImageSaver.preCropping) {
+                byte[] croppedImage =
+                        ypImageSaver.cropImage(image, ypImageSaver.x, ypImageSaver.y, ypImageSaver.w, ypImageSaver.h);
+                encoded = NeuralNetUtils.convertToBase64(imageAsSeenByProcessor(croppedImage));
+            } else {
+                encoded = NeuralNetUtils.convertToBase64(imageAsSeenByProcessor(image));
+            }
+        }
+        NeuralNetResults results = ypImageSaver.processImage(image);
         assert results != null;
         assert results.getResults() != null;
+        // If the image is labeled, our copy from above should be different from the returned value.
+        // Otherwise, they should be the same.
+        if (encoded != null) {
+            if (log.isDebugEnabled()) {
+                log.debug("Labeling: {}, length expected encoded: {}, length returned: {}", ypImageSaver.labelImage,
+                        encoded.length(), results.getEncodedImage().length());
+            }
+            assert ((encoded.length() == results.getEncodedImage().length()) != ypImageSaver.labelImage);
+            assert (results.getEncodedImage().equals(encoded) != ypImageSaver.labelImage);
+        } else {
+            assert results.getEncodedImage() == null;
+        }
         try {
             resultsEquals(results.getResults(), getExpectedResults(resultsAsString)); // Will throw assert error with a message when not equivalent
         } catch (IOException e) {
@@ -1944,6 +2077,15 @@ void verifyProcessing(YoloProcessor ypImageSaver, String resultsAsString) throws
         }
     }
 
+    byte[] imageAsSeenByProcessor(byte[] input) {
+        try (ByteArrayInputStream bis = new ByteArrayInputStream(input)) {
+            BufferedImage bi = ImageIO.read(bis);
+            return ImageUtil.getBytesForImage(bi);
+        } catch (IOException e) {
+            return new byte[0];
+        }
+    }
+
     @SuppressWarnings({"rawtypes", "unchecked"})
     void resultsEquals(List<Map<String, ?>> list, List<Map> expectedRes) {
         assertEquals("list Size: " + list.size() + ", expected: " + expectedRes.size() + " :: " + list,
diff --git a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloQueries.java b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloQueries.java
index cc9a4798..3cce373e 100644
--- a/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloQueries.java
+++ b/objectRecognitionSource/src/test/java/io/vantiq/extsrc/objectRecognition/neuralNet/TestYoloQueries.java
@@ -16,13 +16,18 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
 import javax.imageio.ImageIO;
 
+import lombok.extern.slf4j.Slf4j;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -38,6 +43,7 @@
 import okio.BufferedSource;
 
 @SuppressWarnings({"PMD.ExcessiveClassLength"})
+@Slf4j
 public class TestYoloQueries extends NeuralNetTestBase {
     
     static Vantiq vantiq;
@@ -206,11 +212,11 @@ public void testProcessNextFrameLocal() {
         assert !outputDir.exists();
         
         // Run query without setting "operation":"processNextFrame"
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("NNsaveImage", "local");
         params.put("NNoutputDir", OUTPUT_DIR);
         params.put("NNfileName", IMAGE_8.get("name"));
-                
+        
         querySource(params);
         
         // Check we saved a file in the output directory
@@ -270,10 +276,10 @@ public void testProcessNextFrameVantiq() throws InterruptedException {
         assumeTrue(testAuthToken != null && testVantiqServer != null);
         
         // Run query without setting "operation":"processNextFrame"
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("NNsaveImage", "vantiq");
         params.put("NNfileName", QUERY_FILENAME);
-                
+        
         querySource(params);
         
         // Check that file was saved to Vantiq
@@ -316,11 +322,11 @@ public void testProcessNextFrameBoth() throws InterruptedException {
         assert !outputDir.exists();
         
         // Run query without setting "operation":"processNextFrame"
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("NNsaveImage", "both");
         params.put("NNoutputDir", OUTPUT_DIR);
         params.put("NNfileName", IMAGE_8.get("name"));
-                
+        
         querySource(params);
         
         // Check we saved a file in the output directory
@@ -402,7 +408,7 @@ public void testImageNameUploadOne() throws IOException, InterruptedException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         params.put("imageName", IMAGE_2.get("date"));
         
@@ -428,14 +434,14 @@ public void testImageDateUploadAll() throws IOException, InterruptedException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         
         List<String> imageDate = new ArrayList<String>();
         imageDate.add("-");
         imageDate.add("-");
         params.put("imageDate", imageDate);
-                
+        
         querySource(params);
         
         // Checking that all images were uploaded to VANTIQ
@@ -456,16 +462,16 @@ public void testImageDateUploadOne() throws InterruptedException, IOException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         
         List<String> imageDate = new ArrayList<String>();
         imageDate.add(START_DATE);
         imageDate.add(START_DATE);
         params.put("imageDate", imageDate);
-                
+        
         querySource(params);
-                        
+        
         // Checking that all images were uploaded to VANTIQ
         Thread.sleep(1000);
         checkUploadToVantiq(IMAGE_2.get("filename"), vantiq, VANTIQ_DOCUMENTS);
@@ -486,16 +492,16 @@ public void testImageDateUploadBefore() throws InterruptedException, IOException
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         
         List<String> imageDate = new ArrayList<String>();
         imageDate.add("-");
         imageDate.add(START_DATE);
         params.put("imageDate", imageDate);
-                
+        
         querySource(params);
-                
+        
         // Checking that all images were uploaded to VANTIQ
         Thread.sleep(1000);
         checkUploadToVantiq(IMAGE_1.get("filename"), vantiq, VANTIQ_DOCUMENTS);
@@ -516,16 +522,16 @@ public void testImageDateUploadAfter() throws InterruptedException, IOException
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         
         List<String> imageDate = new ArrayList<String>();
         imageDate.add(END_DATE);
         imageDate.add("-");
         params.put("imageDate", imageDate);
-                
+        
         querySource(params);
-                
+        
         // Checking that all images were uploaded to VANTIQ
         Thread.sleep(1000);
         checkUploadToVantiq(IMAGE_4.get("filename"), vantiq, VANTIQ_DOCUMENTS);
@@ -546,16 +552,16 @@ public void testImageDateUploadRange() throws InterruptedException, IOException
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         
         List<String> imageDate = new ArrayList<String>();
         imageDate.add(START_DATE);
         imageDate.add(END_DATE);
         params.put("imageDate", imageDate);
-                
+        
         querySource(params);
-                
+        
         // Checking that all images were uploaded to VANTIQ
         Thread.sleep(3000);
         checkUploadToVantiq(IMAGE_2.get("filename"), vantiq, VANTIQ_DOCUMENTS);
@@ -576,7 +582,7 @@ public void testImageUploadChangeResolution() throws InterruptedException, IOExc
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         
         Map<String, Object> savedResolution = new LinkedHashMap<>();
@@ -584,7 +590,7 @@ public void testImageUploadChangeResolution() throws InterruptedException, IOExc
         
         params.put("imageName", IMAGE_2.get("date"));
         params.put("savedResolution", savedResolution);
-                
+        
         querySource(params);
         
         // Checking that image was saved to VANTIQ
@@ -611,7 +617,6 @@ public void testImageUploadChangeResolution() throws InterruptedException, IOExc
         InputStream imageStream = new ByteArrayInputStream(imageBytes);
         BufferedImage resizedImage = ImageIO.read(imageStream);
         
-        
         assert resizedImage.getWidth() == RESIZED_IMAGE_WIDTH;
         assert resizedImage.getHeight() == RESIZED_IMAGE_HEIGHT;
     }
@@ -623,7 +628,7 @@ public void testImageNameDeleteOne() throws IOException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "delete");
         params.put("imageName", IMAGE_3.get("date"));
         
@@ -652,7 +657,7 @@ public void testImageDateDeleteAll() throws IOException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "delete");
         
         List<String> imageDate = new ArrayList<String>();
@@ -677,7 +682,7 @@ public void testImageDateDeleteBefore() throws IOException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "delete");
         
         List<String> imageDate = new ArrayList<String>();
@@ -707,7 +712,7 @@ public void testImageDateDeleteAfter() throws IOException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "delete");
         
         List<String> imageDate = new ArrayList<String>();
@@ -734,7 +739,7 @@ public void testImageDateDeleteRange() throws IOException {
         
         addLocalTestImages();
         
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "delete");
         
         List<String> imageDate = new ArrayList<String>();
@@ -744,7 +749,7 @@ public void testImageDateDeleteRange() throws IOException {
         params.put("imageDate", imageDate);
         
         querySource(params);
-
+        
         File d = new File(OUTPUT_DIR);
         File[] dList = d.listFiles();
         assert dList != null;
@@ -766,8 +771,8 @@ public void testInvalidPreCroppingQuery() throws IOException {
         // Make sure that output directory has not yet been created
         File outputDir = new File(OUTPUT_DIR);
         assert !outputDir.exists();
-                
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        
+        Map<String, Object> params = new LinkedHashMap<>();
         
         // Invalid preCrop, it isn't a map
         params.put("cropBeforeAnalysis", "jibberish");
@@ -776,7 +781,7 @@ public void testInvalidPreCroppingQuery() throws IOException {
         params.put("NNsaveImage", "local");
         params.put("NNoutputDir", OUTPUT_DIR);
         params.put("NNfileName", "testInvalidPreCrop");
-                
+        
         querySource(params);
         
         // Check we saved a file in the output directory
@@ -794,7 +799,184 @@ public void testInvalidPreCroppingQuery() throws IOException {
         // If we're here, then we can read the image which is what we expect
     }
     
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPreCroppingQueryEncode() throws IOException {
+        // Only run test with intended vantiq availability
+        assumeTrue(testAuthToken != null && testVantiqServer != null);
+        
+        // Make sure that output directory has not yet been created
+        File outputDir = new File(OUTPUT_DIR);
+        assert !outputDir.exists();
+        
+        Map<String, Object> params = new LinkedHashMap<>();
+        Map<String, Object> preCrop = new LinkedHashMap<>();
+        
+        preCrop.put("x", PRECROP_TOP_LEFT_X_COORDINATE);
+        preCrop.put("y", PRECROP_TOP_LEFT_Y_COORDINATE);
+        preCrop.put("width", CROPPED_WIDTH);
+        preCrop.put("height", CROPPED_HEIGHT);
+        
+        params.put("cropBeforeAnalysis", preCrop);
+        
+        // Run query without setting "operation":"processNextFrame"
+        params.put("NNsaveImage", "local");
+        params.put("NNoutputDir", OUTPUT_DIR);
+        params.put("NNfileName", "testPreCrop");
+        params.put("includeEncodedImage", true);
+        params.put("sendFullResponse", true);
+        
+        VantiqResponse resp = querySourceWithResponse(params);
+        
+        // Check we saved a file in the output directory
+        outputDir = new File(OUTPUT_DIR);
+        assert outputDir.exists();
+        
+        // Check there is only one file, and it's name is equivalent to QUERY_FILENAME
+        File[] outputDirFiles = outputDir.listFiles();
+        assert outputDirFiles != null;
+        assert outputDirFiles.length == 1;
+        assert outputDirFiles[0].getName().equals("testPreCrop.jpg");
+        
+        File resizedImageFile = new File(OUTPUT_DIR + "/" + outputDirFiles[0].getName());
+        byte[] resizedImageBytes = Files.readAllBytes(Paths.get(resizedImageFile.getAbsolutePath()));
+        
+        ByteArrayInputStream img = new ByteArrayInputStream(resizedImageBytes);
+        BufferedImage resizedImage = ImageIO.read(img);
+        
+        assert resizedImage.getWidth() == CROPPED_WIDTH;
+        assert resizedImage.getHeight() == CROPPED_HEIGHT;
+        
+        // Now, check that this image has been encoded correctly.
+        String encodedImage = NeuralNetUtils.convertToBase64(resizedImageBytes);
+        
+        if (resp.hasErrors()) {
+            if (log.isErrorEnabled()) {
+                for (VantiqError err : resp.getErrors()) {
+                    log.error("Query had errors: {}::{}", err.getCode(), err.getMessage());
+                }
+            }
+        }
+        assert resp.isSuccess();
+        assert resp.getBody() != null;
+        assert resp.getBody() instanceof JsonObject;
+        JsonObject responseObj = (JsonObject) resp.getBody();
+        assert responseObj.has("encodedImage");
+        String returnedImage = responseObj.getAsJsonPrimitive("encodedImage").getAsString();
+        if (log.isDebugEnabled()) {
+            log.debug("Encoded image size: {} -- returned image size: {}", encodedImage.length(),
+                    returnedImage.length());
+            if (log.isTraceEnabled()) {
+                log.trace("Encoded image: {} -- returned image: {}", encodedImage, returnedImage);
+            }
+        }
+        byte[] retBytes = Base64.getDecoder().decode(returnedImage.getBytes(StandardCharsets.UTF_8));
+        try (ByteArrayInputStream retImgStream = new ByteArrayInputStream(retBytes)) {
+            BufferedImage retImg = ImageIO.read(retImgStream);
+            
+            assert retImg.getWidth() == CROPPED_WIDTH;
+            assert retImg.getHeight() == CROPPED_HEIGHT;
+            assert encodedImage.length() == returnedImage.length();
+            assert returnedImage.equals(encodedImage);
+        }
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testLocalLabelQueryEncode() throws IOException {
+        doLabelTest(true, true, true);
+    }
+    
+    @SuppressWarnings("rawtypes")
     @Test
+    public void testLocalLabelNoSaveQueryEncode() throws IOException {
+        doLabelTest(true, true, false);
+    }
+    
+    @SuppressWarnings("PMD.CognitiveComplexity")
+    public void doLabelTest(boolean localLabelRequest, boolean includeEncoded, boolean saveImage) throws IOException {
+        // Only run test with intended vantiq availability
+        assumeTrue(testAuthToken != null && testVantiqServer != null);
+        
+        // Make sure that output directory has not yet been created
+        File outputDir = new File(OUTPUT_DIR);
+        assert !outputDir.exists();
+        
+        Map<String, Object> params = new LinkedHashMap<>();
+        
+        // Run query without setting "operation":"processNextFrame"
+        if (saveImage) {
+            params.put("NNsaveImage", "local");
+            params.put("NNoutputDir", OUTPUT_DIR);
+            params.put("NNfileName", "testLabel");
+        }
+        params.put("includeEncodedImage", includeEncoded);
+        params.put("sendFullResponse", true);
+        if (localLabelRequest) {
+            params.put("labelImage", "true");
+        }
+        
+        VantiqResponse resp = querySourceWithResponse(params);
+        if (resp.hasErrors()) {
+            if (log.isErrorEnabled()) {
+                for (VantiqError err : resp.getErrors()) {
+                    log.error("Query had errors: {}::{}", err.getCode(), err.getMessage());
+                }
+            }
+        }
+        assert resp.isSuccess();
+        assert resp.getBody() != null;
+        assert resp.getBody() instanceof JsonObject;
+        JsonObject responseObj = (JsonObject) resp.getBody();
+        assert responseObj.has("encodedImage") == includeEncoded;
+        
+        BufferedImage image = null;
+        String encodedImage = null;
+        if (saveImage) {
+            // Check we saved a file in the output directory
+            outputDir = new File(OUTPUT_DIR);
+            assert outputDir.exists();
+            
+            // Check there is only one file, and it's name is equivalent to QUERY_FILENAME
+            File[] outputDirFiles = outputDir.listFiles();
+            assert outputDirFiles != null;
+            assert outputDirFiles.length == 1;
+            assert outputDirFiles[0].getName().equals("testLabel.jpg");
+            
+            File imageFile = new File(OUTPUT_DIR + "/" + outputDirFiles[0].getName());
+            byte[] imageBytes = Files.readAllBytes(Paths.get(imageFile.getAbsolutePath()));
+            
+            ByteArrayInputStream img = new ByteArrayInputStream(imageBytes);
+            image = ImageIO.read(img);
+            // Now, check that this image has been encoded correctly.
+            encodedImage = NeuralNetUtils.convertToBase64(imageBytes);
+        }
+        
+        if (includeEncoded) {
+            String returnedImage = responseObj.getAsJsonPrimitive("encodedImage").getAsString();
+            
+            if (encodedImage != null && log.isDebugEnabled()) {
+                log.debug("Encoded image size: {} -- returned image size: {}",
+                        encodedImage.length(), returnedImage.length());
+            }
+            byte[] retBytes = Base64.getDecoder().decode(returnedImage.getBytes(StandardCharsets.UTF_8));
+            try (ByteArrayInputStream retImgStream = new ByteArrayInputStream(retBytes)) {
+                BufferedImage retImg = ImageIO.read(retImgStream);
+                
+                // In the no-save case, we'll just validate that we got an encoded image back & that we can
+                // turn it into an image.
+                
+                if (image != null) {
+                    assert retImg.getWidth() == image.getWidth();
+                    assert retImg.getHeight() == image.getHeight();
+                    assert (encodedImage.length() == returnedImage.length());
+                    assert returnedImage.equals(encodedImage);
+                }
+            }
+        }
+    }
+    
+   @Test
     public void testPreCroppingQuery() throws IOException {
         // Only run test with intended vantiq availability
         assumeTrue(testAuthToken != null && testVantiqServer != null);
@@ -803,7 +985,7 @@ public void testPreCroppingQuery() throws IOException {
         File outputDir = new File(OUTPUT_DIR);
         assert !outputDir.exists();
                 
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         Map<String, Object> preCrop = new LinkedHashMap<>();
         
         preCrop.put("x", PRECROP_TOP_LEFT_X_COORDINATE);
@@ -844,7 +1026,7 @@ public void testUploadAsImage() throws IOException, InterruptedException {
 
         addLocalTestImages();
 
-        Map<String,Object> params = new LinkedHashMap<String,Object>();
+        Map<String, Object> params = new LinkedHashMap<>();
         params.put("operation", "upload");
         params.put("uploadAsImage", true);
 
@@ -879,7 +1061,7 @@ public void testUploadAsImage() throws IOException, InterruptedException {
     
     // ================================================= Helper functions =================================================
     
-    public static void setupSource(Map<String,Object> sourceDef) {
+    public static void setupSource(Map<String, Object> sourceDef) {
         VantiqResponse insertResponse = vantiq.insert("system.sources", sourceDef);
         if (insertResponse.isSuccess()) {
             core = new ObjectRecognitionCore(SOURCE_NAME, testAuthToken, testVantiqServer, MODEL_DIRECTORY);;
@@ -887,16 +1069,20 @@ public static void setupSource(Map<String,Object> sourceDef) {
         }
     }
     
-    public static void querySource(Map<String,Object> params) {
+    public static void querySource(Map<String, Object> params) {
         vantiq.query(SOURCE_NAME, params);
     }
+
+    public static VantiqResponse querySourceWithResponse(Map<String, Object> params) {
+        return vantiq.query(SOURCE_NAME, params);
+    }
     
     public static void deleteFileFromVantiq(String filename) {
         vantiq.deleteOne(VANTIQ_DOCUMENTS, filename);
         vantiq.deleteOne(VANTIQ_IMAGES, filename);
     }
     
-    public void checkQueryError(Map<String,Object> params, String operation) {
+    public void checkQueryError(Map<String, Object> params, String operation) {
         vantiqResponse = vantiq.query(SOURCE_NAME, params);
         assert vantiqResponse.hasErrors();
         String errorMessage = vantiqResponse.getErrors().get(0).getMessage();
@@ -906,7 +1092,7 @@ public void checkQueryError(Map<String,Object> params, String operation) {
         }
     }
     
-    public void checkQueryErrorImageDateListSize(Map<String,Object> params) {
+    public void checkQueryErrorImageDateListSize(Map<String, Object> params) {
         vantiqResponse = vantiq.query(SOURCE_NAME, params);
         assert vantiqResponse.hasErrors();
         String errorMessage = vantiqResponse.getErrors().get(0).getMessage();
@@ -916,7 +1102,7 @@ public void checkQueryErrorImageDateListSize(Map<String,Object> params) {
         }
     }
     
-    public void checkQueryErrorInvalidDate(Map<String,Object> params) {
+    public void checkQueryErrorInvalidDate(Map<String, Object> params) {
         vantiqResponse = vantiq.query(SOURCE_NAME, params);
         assert vantiqResponse.hasErrors();
         String errorMessage = vantiqResponse.getErrors().get(0).getMessage();
@@ -926,7 +1112,7 @@ public void checkQueryErrorInvalidDate(Map<String,Object> params) {
         }
     }
     
-    public void invalidParametersHelper(Map<String,Object> params, String operation) {
+    public void invalidParametersHelper(Map<String, Object> params, String operation) {
         // Not including imageName or imageDate
         checkQueryError(params, operation);
         
@@ -969,13 +1155,13 @@ public void invalidParametersHelper(Map<String,Object> params, String operation)
         checkQueryErrorImageDateListSize(params);
     }
     
-    public static Map<String,Object> createSourceDef() {
-        Map<String,Object> sourceDef = new LinkedHashMap<String,Object>();
-        Map<String,Object> sourceConfig = new LinkedHashMap<String,Object>();
-        Map<String,Object> objRecConfig = new LinkedHashMap<String,Object>();
-        Map<String,Object> dataSource = new LinkedHashMap<String,Object>();
-        Map<String,Object> general = new LinkedHashMap<String,Object>();
-        Map<String,Object> neuralNet = new LinkedHashMap<String,Object>();
+    public static Map<String, Object> createSourceDef() {
+        Map<String, Object> sourceDef = new LinkedHashMap<>();
+        Map<String, Object> sourceConfig = new LinkedHashMap<>();
+        Map<String, Object> objRecConfig = new LinkedHashMap<>();
+        Map<String, Object> dataSource = new LinkedHashMap<>();
+        Map<String, Object> general = new LinkedHashMap<>();
+        Map<String, Object> neuralNet = new LinkedHashMap<>();
         
         // Setting up dataSource config options
         dataSource.put("camera", IP_CAMERA_URL);