Source: visual-recognition/v3.js

/**
 * Copyright 2014 IBM Corp. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

var extend = require('extend');
var pick = require('object.pick');
var isStream = require('isstream');
var requestFactory = require('../lib/requestwrapper');
var util = require('util');
var BaseService = require('../lib/base_service');

var NEGATIVE_EXAMPLES = 'negative_examples';

/**
 * JS-style logical XOR - works on objects, booleans, strings, etc following normal js truthy/falsy conventions
 * @private
 * @param {*} a
 * @param {*} b
 * @returns {boolean}
 * @constructor
 */
function xor(a, b) {
  return ( a || b ) && !( a && b );
}

/**
 * Determine content-type header for .zip, .png, and .jpg files
 * (The only formats currently supported by the service)
 *
 * based on https://github.com/watson-developer-cloud/node-sdk/issues/333
 *
 * @param {Buffer} buffer
 * @returns {String|undefined}
 */
function detectContentType(buffer) {
  var signature = buffer.readUInt32BE();
  switch(signature) { // eslint-disable-line default-case
    case 0x504B0304:
    case 0x504B0506:
    case 0x504B0708:
      return 'application/zip';
    case 0x89504E47:
      return 'image/png';
    case 0xFFD8FFE0:
    case 0xFFD8FFE1:
    case 0xFFD8FFE8:
      return 'image/jpeg';
    // default is `return undefined`
  }
}

/**
 * Verifies that a stream images_file or a string url is included
 * also gracefully handles cases of image_file instead of images_file
 *
 * @private
 */
function fixupImageParam(params) {
  if (params && params.image_file && !params.images_file) {
    params.images_file = params.image_file;
  }

  if (!params || !xor(params.images_file, params.url)) {
    throw new Error('Watson VisualRecognition.classify() requires either an images_file or a url parameter');
  }

  if (Buffer.isBuffer(params.images_file)) {
    params.images_file = {
      value: params.images_file,
      options: {
        contentType: detectContentType(params.images_file)
      }
    }
  }
}

/**
 * Formats error
 *
 * @private
 */
function errorFormatter(cb) {
  var callback = typeof cb === 'function' ? cb : function() { /* no op */};
  return function(err, result) {
    if (err) {
      callback(err, result);
    } else {
      if (result.status === 'ERROR') {
        if (result.statusInfo === 'invalid-api-key') {
          callback({
            error: result.statusInfo,
            code: result.statusInfo === 'invalid-api-key' ? 401 : 400
          }, null);
        }
      } else {
        callback(err, result);
      }
    }
  };
}

/**
 *
 * @param options
 * @constructor
 */
function VisualRecognitionV3(options) {
  BaseService.call(this, options);
  // Check if 'version_date' was provided
  if (typeof this._options.version_date === 'undefined') {
    throw new Error('Argument error: version_date was not specified, use 2016-05-20');
  }
  this._options.qs.version = this._options.version_date; // todo: confirm service expects version not version_date
}
util.inherits(VisualRecognitionV3, BaseService);
VisualRecognitionV3.prototype.name = 'visual_recognition';
VisualRecognitionV3.prototype.version = 'v3';
VisualRecognitionV3.URL = 'https://gateway-a.watsonplatform.net/visual-recognition/api';
VisualRecognitionV3.prototype.serviceDefaults = {
  alchemy: true
};


/**
 * Grab the api key
 *
 * @param options
 * @private
 */
VisualRecognitionV3.prototype.initCredentials = function(options) {
  options.api_key = options.api_key || options.apikey;
  options = extend(
    {},
    this.getCredentialsFromBluemix(this.name), // todo: test if this works
    this.getCredentialsFromEnvironment(this.name),
    options
  );
  if (!options.use_unauthenticated) {
    if (!options.api_key) {
      throw new Error('Argument error: api_key was not specified');
    }
    // Per documentation, Alchemy* services use `apikey`, but Visual Recognition uses (`api_key`)
    // (Either will work in most cases, but the VR Collections & Similarity Search beta only supports `api_key`)
    options.qs = extend({ api_key : options.api_key }, options.qs);
  }
  return options
};

/**
 * Pulls api_key from SERVICE_NAME_API_KEY env property
 *
 * @param {String} name
 * @returns {{api_key: String|undefined}}
 */
VisualRecognitionV3.prototype.getCredentialsFromEnvironment = function(name) {
  return {
    api_key: process.env[name.toUpperCase() + '_API_KEY']
  }
};

/**
 * Bluemix uses a different naming convention for VR v3 than for other services
 * @returns {*}
 */
VisualRecognitionV3.prototype.getCredentialsFromBluemix = function() {
  return BaseService.prototype.getCredentialsFromBluemix.call(this, 'watson_vision_combined');
};

/**
 * Accepts either a url, a single image file, or a zip file with multiple
 * images (.jpeg, .png, .gif) and scores every available classifier
 * on each image. It then applies a threshold and returns the list
 * of relevant classifier scores for each image.
 *
 * @example
 * * {
 *   "images": [{
 *     "classifiers": [{
 *       "classes": [{
 *         "class": "animal",
 *         "score": 0.998771,
 *         "type_hierarchy": "/animals"
 *       }, {
 *         "class": "mammal",
 *         "score": 0.998499,
 *         "type_hierarchy": "/animals/mammal"
 *       }, {
 *         "class": "dog",
 *         "score": 0.900249,
 *         "type_hierarchy": "/animals/pets/dog"
 *       }, {
 *         "class": "puppy",
 *         "score": 0.5,
 *         "type_hierarchy": "/animals/pets/puppy"
 *       }],
 *       "classifier_id": "default",
 *       "name": "default"
 *     }],
 *     "image": "dog.jpg"
 *   }],
 *   "images_processed": 1
 * }
 *
 * @param {Object} params
 * @param {ReadStream|Buffer|Object} [params.images_file] The image file (.jpg, .png, .gif) or compressed (.zip) file of images to classify. The total number of images is limited to 20. Either images_file or url must be specified. The SDK attempts to determine content-type automatically, but this may be overridden by providing an object: {value: Buffer|Stream, options: {filename: 'file.ext', contentType: 'image/type'}}
 * @param {String} [params.url] The URL of an image (.jpg, .png, .gif). Redirects are followed, so you can use shortened URLs. The resolved URL is returned in the response. Either images_file or url must be specified.
 * @param {Array} [params.classifier_ids=['default']] An array of classifier IDs to classify the images against.
 * @param {Array} [params.owners=['me','IBM']] An array with the value(s) "IBM" and/or "me" to specify which classifiers to run.
 * @param {Number} [params.threshold] A floating point value that specifies the minimum score a class must have to be displayed in the response.
 * @param {Function} callback
 *
 * @returns {ReadableStream|undefined}
 *
 */
VisualRecognitionV3.prototype.classify = function(params, callback) {

  try {
    fixupImageParam(params);
  } catch (e) {
    callback(e);
    return;
  }

  params = extend({
    classifier_ids: ['default'],
    owners: ['me','IBM']
  }, params);

  var parameters;

  if(params.images_file) {
    parameters = {
      options: {
        url: '/v3/classify',
        method: 'POST',
        formData: {
          images_file: params.images_file,
          parameters: {
            value: JSON.stringify(pick(params, ['classifier_ids', 'owners', 'threshold'])),
            options: {
              contentType: 'application/json'
            }
          }
        },
        headers: pick(params, 'Accept-Language')
      },
      defaultOptions: this._options
    };
  } else {
    parameters = {
      options: {
        url: '/v3/classify',
        method: 'GET',
        json: true,
        qs: pick(params, ['url', 'classifier_ids', 'owners', 'threshold']),
        headers: pick(params, 'Accept-Language')
      },
      defaultOptions: this._options
    };
  }

  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Accepts either a url, a single image file, or a zip file with multiple
 * images (.jpeg, .png, .gif) and attempts to extract faces and
 * identities. It then applies a threshold
 * and returns the list of relevant identities, locations, and metadata
 * for found faces for each image.
 *
 * @example
 * {
 *   "images": [{
 *     "faces": [{
 *       "age": {
 *         "max": 54,
 *         "min": 45,
 *         "score": 0.40459
 *       },
 *       "face_location": {
 *         "height": 131,
 *         "left": 80,
 *         "top": 68,
 *         "width": 123
 *       },
 *       "gender": {
 *         "gender": "MALE",
 *         "score": 0.993307
 *       },
 *       "identity": {
 *         "name": "Barack Obama",
 *         "score": 0.970688,
 *         "type_hierarchy": "/people/politicians/democrats/barack obama"
 *       }
 *     }],
 *     "image": "obama.jpg"
 *   }],
 *   "images_processed": 1
 * }
 *
 * @param {Object} params
 * @param {ReadStream} [params.images_file] The image file (.jpg, .png, .gif) or compressed (.zip) file of images to classify. The total number of images is limited to 15. Either images_file or url must be specified.
 * @param {String} [params.url] The URL of an image (.jpg, .png, .gif). Redirects are followed, so you can use shortened URLs. The resolved URL is returned in the response. Either images_file or url must be specified.
 * @param {Function} callback
 *
 * @returns {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.detectFaces = function(params, callback) {
  try {
    fixupImageParam(params);
  } catch (e) {
    callback(e);
    return;
  }

  var parameters;

  if(params.images_file) {
    parameters = {
      options: {
        url: '/v3/detect_faces',
        method: 'POST',
        json: true,
        formData: pick(params, ['images_file'])
      },
      defaultOptions: this._options
    };

  } else {
    parameters = {
      options: {
        url: '/v3/detect_faces',
        method: 'GET',
        json: true,
        qs: pick(params, ['url'])
      },
      defaultOptions: this._options
    };
  }

  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Accepts either a url, single image file, or a zip file with multiple
 * images (.jpeg, .png, .gif) and attempts to recognize text
 * found in the image. It then applies a threshold
 * and returns the list of relevant locations, strings,  and metadata
 * for discovered text in each image.
 *
 * @example
 * {
 *   "images": [{
 *     "image": "car.png",
 *     "text": "3 jag [brio]",
 *     "words": [{
 *       "line_number": 0,
 *       "location": {
 *         "height": 53,
 *         "left": 204,
 *         "top": 294,
 *         "width": 27
 *       },
 *       "score": 0.50612,
 *       "word": "3"
 *     }, {
 *       "line_number": 0,
 *       "location": {
 *         "height": 32,
 *         "left": 264,
 *         "top": 288,
 *         "width": 56
 *       },
 *       "score": 0.958628,
 *       "word": "jag"
 *     }, {
 *       "line_number": 0,
 *       "location": {
 *         "height": 40,
 *         "left": 324,
 *         "top": 288,
 *         "width": 92
 *       },
 *       "score": 0.00165806,
 *       "word": "brio"
 *     }]
 *   }],
 *   "images_processed": 1
 * }
 *
 * @param {Object} params
 * @param {ReadStream} [params.images_file] The image file (.jpg, .png, .gif) or compressed (.zip) file of images to classify. The total number of images is limited to 10. Either images_file or url must be specified.
 * @param {String} [params.url] The URL of an image (.jpg, .png, .gif). Redirects are followed, so you can use shortened URLs. The resolved URL is returned in the response. Either images_file or url must be specified.
 * @param {Function} callback
 *
 * @returns {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.recognizeText = function(params, callback) {
  try {
    fixupImageParam(params);
  } catch (e) {
    callback(e);
    return;
  }

  var parameters;

  if(params.images_file) {
    parameters = {
      options: {
        url: '/v3/recognize_text',
        method: 'POST',
        json: true,
        formData: pick(params, ['images_file'])
      },
      defaultOptions: this._options
    };
  } else {
    parameters = {
      options: {
        url: '/v3/recognize_text',
        method: 'GET',
        json: true,
        qs: pick(params, ['url'])
      },
      defaultOptions: this._options
    };
  }

  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Train a new classifier from example images which are uploaded.
 * This call returns before training has completed.  You'll need to use the
 * getClassifer method to make sure the classifier has completed training and
 * was successful before you can classify any images with the newly created
 * classifier.
 *
 * @example
 * {
 *   foo_positive_examples: fs.createReadStream('./foo-pics.zip'),
 *   negative_examples: fs.createReadStream('./not-foo-pics.zip'),
 *   name: 'to-foo-or-not'
 * }
 * @example
 * {
 *  foo_positive_examples: fs.createReadStream('./foo-pics.zip'),
 *  bar_positive_examples: fs.createReadStream('./bar-pics.zip'),
 *  name: 'foo-vs-bar'
 * }
 * @example
 * {
 *  foo_positive_examples: fs.createReadStream('./foo-pics.zip'),
 *  bar_positive_examples: fs.createReadStream('./bar-pics.zip'),
 *  negative_examples: fs.createReadStream('./not-foo-pics.zip'),
 *  name: 'foo-bar-not'
 * }
 *
 * @example
 * {
 *   "classifier_id": "fruit_679357912",
 *   "name": "fruit",
 *   "owner": "a3a48ea7-492b-448b-87d7-9dade8bde5a9",
 *   "status": "training",
 *   "created": "2016-05-23T21:50:41.680Z",
 *   "classes": [{
 *     "class": "banana"
 *   }, {
 *     "class": "apple"
 *   }]
 * }
 *
 * @param {Object} params
 * @param {String} params.name The desired short name of the new classifier.
 * @param {ReadStream} params.classname_positive_examples <your_class_name>_positive_examples One or more compressed (.zip) files of images that depict the visual subject for a class within the new classifier. Must contain a minimum of 10 images. You may supply multiple files with different class names in the key.
 * @param {ReadStream} [params.negative_examples] A compressed (.zip) file of images that do not depict the visual subject of any of the classes of the new classifier. Must contain a minimum of 10 images. Required if only one positive set is provided.
 *
 * @returns {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.createClassifier = function(params, callback) {
  params = params || {};

  var example_keys = Object.keys(params).filter(function(key) {
    return key === NEGATIVE_EXAMPLES || key.match(/^.+_positive_examples$/);
  });

  if (example_keys.length < 2) {
    callback(new Error('Missing required parameters: either two *_positive_examples or one *_positive_examples and one negative_examples must be provided.'));
    return;
  }
  // todo: validate that all *_examples are streams or else objects with buffers and content-types
  var allowed_keys = ['name', NEGATIVE_EXAMPLES].concat(example_keys);

  var parameters = {
    options: {
      url: '/v3/classifiers',
      method: 'POST',
      json: true,
      formData: pick(params, allowed_keys)
    },
    requiredParams: ['name'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Retrain a existing classifier from example images which are uploaded.
 * This call returns before retraining has completed.  You'll need to use the
 * getClassifer method to make sure the classifier has completed retraining and
 * was successful before you can classify any images with the retrained
 * classifier.
 *
 * @example
 * {
 *   "classifier_id": "fruit_679357912",
 *   "name": "fruit",
 *   "owner": "a3a48ea7-492b-448b-87d7-9dade8bde5a9",
 *   "status": "training",
 *   "created": "2016-05-23T21:50:41.680Z",
 *   "classes": [{
 *     "class": "banana"
 *   }, {
 *     "class": "apple"
 *   }]
 * }
 *
 * @param {Object} params
 * @param {ReadStream} params.classname_positive_examples <your_class_name>_positive_examples One or more compressed (.zip) files of images that depict the visual subject for a class within the classifier. You may supply multiple files with different class names in the key.
 * @param {ReadStream} [params.negative_examples] A compressed (.zip) file of images that do not depict the visual subject of any of the classes of the classifier.
 *
 * @returns {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.retrainClassifier = function(params, callback) {
  params = params || {};

  var allowed_keys = Object.keys(params).filter(function(key) {
    return key === NEGATIVE_EXAMPLES || key.match(/^.+_positive_examples$/);
  });

  var parameters = {
    options: {
      url: '/v3/classifiers/' + params.classifier_id,
      method: 'POST',
      json: true,
      formData: pick(params, allowed_keys)
    },
    requiredParams: [],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Retrieve a list of all classifiers, including built-in and
 * user-created classifiers.
 *
 * @example
 * {
 *   "classifiers": [{
 *     "classifier_id": "fruit_679357912",
 *     "name": "fruit",
 *     "status": "ready"
 *   }, {
 *     "classifier_id": "Dogs_2017013066",
 *     "name": "Dogs",
 *     "status": "ready"
 *   }]
 * }
 *
 * @param {Object} params
 * @param {Boolean} [params.verbose=false]
 * @param {Function} callback
 * @return {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.listClassifiers = function(params, callback) {
  var parameters = {
    options: {
      method: 'GET',
      url: '/v3/classifiers',
      qs: pick(params, ['verbose']),
      json: true,
    },
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Retrieves information about a specific classifier.
 *
 * @example
 * {
 *   "classifier_id": "fruit_679357912",
 *   "name": "fruit",
 *   "owner": "a3a42ea7-492b-448b-87d7-9dfde8bde519 ",
 *   "status": "ready",
 *   "created": "2016-05-23T21:50:41.680Z",
 *   "classes": [{
 *     "class": "banana"
 *   }, {
 *     "class": "apple"
 *   }]
 * }
 *
 * @param {Object} params
 * @param {Boolean} params.classifier_id The classifier id
 * @param {Function} callback
 * @return {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.getClassifier = function(params, callback) {
  var parameters = {
    options: {
      method: 'GET',
      url: '/v3/classifiers/{classifier_id}',
      path: params,
      json: true
    },
    requiredParams: ['classifier_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Deletes a custom classifier with the specified classifier id.
 *
 * @param {Object} params
 * @param {String} params.classifier_id The classifier id
 * @param {Function} callback
 * @returns {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.deleteClassifier = function(params, callback) {
  var parameters = {
    options: {
      method: 'DELETE',
      url: '/v3/classifiers/{classifier_id}',
      path: params,
      json: true,
    },
    requiredParams: ['classifier_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};


// collections & similarity search

/**
 * Create a collection
 * Beta. Create a new collection of images to search. You can create a maximum of 5 collections.
 *
 * Example response:

   { collection_id: 'integration_test_1474313373701_d9665f',
     name: 'integration_test_1474313373701',
     status: 'available',
     created: '2016-09-19T19:29:34.019Z',
     images: 0,
     capacity: 1000000 }

 * @param {Object} params
 * @param {String} params.name The name of the new collection. The name can be a maximum of 128 UTF8 characters, with no spaces.
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.createCollection = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections',
      method: 'POST',
      json: true,
      formData: pick(params, ['name'])
    },
    requiredParams: ['name'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};


/**
 * Retrieve collection details
 * Beta. Retrieve information about a specific collection.
 *
 * Example response:

 { collection_id: 'integration_test_1474313373701_d9665f',
   name: 'integration_test_1474313373701',
   status: 'available',
   created: '2016-09-19T19:29:34.019Z',
   images: 0,
   capacity: 1000000 }

 * @param {Object} params
 * @param {String} params.collection_id
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.getCollection = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}',
      method: 'GET',
      json: true,
      path: params
    },
    requiredParams: ['collection_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};



/**
 * List collections
 * Beta. List all custom collections.
 *
 * Example response:

 { collections:
   [ { collection_id: 'integration_test_1474313967414_0e320b',
       name: 'integration_test_1474313967414',
       status: 'available',
       created: '2016-09-19T19:39:27.811Z',
       images: 0,
       capacity: 1000000 } ] }

 * @param {Object} [params]
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.listCollections = function(params, callback) {
  if (typeof params === 'function' && !callback) {
    callback = params;
  }

  var parameters = {
    options: {
      url: '/v3/collections',
      method: 'GET',
      json: true
    },
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};



/**
 * Delete a collection
 * Beta. Delete a user created collection.
 *
 * @param {Object} params
 * @param {String} params.collection_id
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.deleteCollection = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}',
      method: 'DELETE',
      json: true,
      path: params
    },
    requiredParams: ['collection_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};



/**
 * Add an image to a collection
 * Beta. Add images to a collection. Each collection can contain 1000000 images.
 *
 * Example Response:

 {
   "images": [
     {
       "image_id": "9725bc",
       "image_file": "obama.jpg",
       "created": "2016-09-20T14:41:49.927Z",
       "metadata": {
         "foo": "bar"
       }
     }
   ],
   "images_processed": 1
 }

 * @param {Object} params
 * @param {String} params.collection_id
 * @param {ReadableStream} params.image_file The image file (.jpg or .png) of the image to add to the collection. Maximum file size of 2 MB.
 * @param {Object} [params.metadata] optional arbitrary metadata. This can be anything that can be specified in a JSON object. For example, key-value pairs. Maximum 2 KB of metadata for each image.
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.addImage = function(params, callback) {
  params = params || {};


  if (!params.image_file || !isStream(params.image_file)) {
    throw new Error('image_file param must be a standard Node.js Stream');
  }

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images',
      method: 'POST',
      json: true,
      path: params,
      formData: {
        image_file: params.image_file,
        metadata: {
          value: JSON.stringify(params.metadata || {}),
          options: {
            contentType: 'application/json',
            filename: 'metadata.json' // it doesn't matter what the filename is, but the service requires that *some* filename be set or else it ignores the metadata
          }
        }
      }
    },
    requiredParams: ['collection_id', 'image_file'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * List images in a collection
 * Beta. List the first 100 images in a collection. Each collection can contain 1000000 images.
 *
 * Example Response:

{ images:
  [ { image_id: '83f3ff',
     image_file: 'obama.jpg',
     created: '2016-09-19T21:07:15.141Z' } ] }
 *
 * @param {Object} params
 * @param {String} params.collection_id
 * @param {ReadableStream} params.image_file The image file (.jpg or .png) of the image to add to the collection. Maximum file size of 2 MB.
 * @param {Object} [params.metadata] optional arbitrary metadata. This can be anything that can be specified in a JSON object. For example, key-value pairs. Maximum 2 KB of metadata for each image.
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.listImages = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images',
      method: 'GET',
      json: true,
      path: params
    },
    requiredParams: ['collection_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Get image details
 * Beta. List details about a specific image in a collection.
 *
 * Example Response:

{ image_id: '83f3ff',
  image_file: 'obama.jpg',
  created: '2016-09-19T21:07:15.141Z'

 * @param {Object} params
 * @param {String} params.collection_id
 * @param {String} params.image_id
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.getImage = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images/{image_id}',
      method: 'GET',
      json: true,
      path: params
    },
    requiredParams: ['collection_id', 'image_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Delete an image
 * Beta. Delete an image from a collection.
 *
 * @param {Object} params
 * @param {String} params.collection_id
 * @param {String} params.image_id
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.deleteImage = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images/{image_id}',
      method: 'DELETE',
      json: true,
      path: params
    },
    requiredParams: ['collection_id', 'image_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

/**
 * Add or update metadata
 * Beta. Add metadata to a specific image in a collection.
 *
 * @param {Object} params
 * @param {String} params.collection_id
 * @param {String} params.image_id
 * @param {Object} params.metadata Can be anything that can be specified in a JSON object. For example, key-value pairs. Maximum 2 KB of metadata for each image.
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.setImageMetadata = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images/{image_id}/metadata',
      method: 'PUT',
      json: true,
      path: params,
      headers: {"Content-Type": "multipart/form-data"},
      // todo: manually create a body string that looks like a POST form data body even though it's a PUT
      formData: {
        metadata: {
          value: JSON.stringify(params.metadata || {}),
          options: {
            contentType: 'application/json',
            filename: 'metadata.json' // it doesn't matter what the filename is, but the service requires that *some* filename be set or else it gives a confusing "Missing multipart/form-data" error
          }
        }
      }
    },
    requiredParams: ['collection_id', 'image_id', 'metadata'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};


/**
 * Get image metadata
 * Beta. View the metadata for a specific image in a collection.
 *
 * Example Response:

 {"foo": "bar"}

 *
 * @param {Object} params
 * @param {String} params.collection_id
 * @param {ReadableStream} params.image_id
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.getImageMetadata = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images/{image_id}/metadata',
      method: 'GET',
      json: true,
      path: params
    },
    requiredParams: ['collection_id', 'image_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};


/**
 * Delete image metadata
 * Beta. Delete all metadata associated with an image.
 *
 * @param {Object} params
 * @param {String} params.collection_id
 * @param {ReadableStream} params.image_id
 * @param {Function} callback
 */
VisualRecognitionV3.prototype.deleteImageMetadata = function(params, callback) {
  params = params || {};

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/images/{image_id}/metadata',
      method: 'DELETE',
      json: true,
      path: params
    },
    requiredParams: ['collection_id', 'image_id'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};


/**
 * Find similar images
 * Beta. Upload an image to find similar images in your custom collection.
 *
 * Example response:

 {
    "similar_images":[
       {
          "image_id":"dresses_1257263",
          "created":"2016-09-04T21:49:16.908Z",
          "metadata":{
             "weight":10,
             "cut":"a line",
             "color":"red"
          },
          "score":"0.79"
       }
    ],
    "image_file":"red_dress.jpg",
    "images_processed": 1
 }

 * @param {Object} params
 * @param {String} params.collection_id The collection id
 * @param {ReadableStream} params.image_file The image file (.jpg or .png) of the image to search against the collection.
 * @param {Number} [params.limit=10]  limit The number of similar results you want returned. Default limit is 10 results, you can specify a maximum limit of 100 results.
 * @param {Function} callback
 * @returns {ReadableStream|undefined}
 */
VisualRecognitionV3.prototype.findSimilar = function(params, callback) {
  params = params || {};

  if (!params.image_file || !isStream(params.image_file)) {
    throw new Error('image_file param must be a standard Node.js Stream');
  }

  var parameters = {
    options: {
      url: '/v3/collections/{collection_id}/find_similar',
      method: 'POST',
      json: true,
      qs: pick(params, ['limit']),
      formData: pick(params, ['image_file']),
      path: pick(params, ['collection_id'])
    },
    requiredParams: ['collection_id', 'image_file'],
    defaultOptions: this._options
  };
  return requestFactory(parameters, errorFormatter(callback));
};

module.exports = VisualRecognitionV3;