Source: services/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 omit = require('object.omit');
var isStream = require('isstream');
var requestFactory = require('../../lib/requestwrapper');

/**
 * 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 );
}

/**
 * Verifies that a stream images_file or a string url is included
 *
 * also gracefully handles cases of image_file instead of images_file
 *
 * @todo: allow for file to be a Buffer if a content_type parameter is also set
 *
 * @private
 */
function verifyParams(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 (params.images_file && !isStream(params.images_file)) {
    throw new Error('images_file param must be a standard Node.js Stream');
  }
}

/**
 *
 * @param options
 * @constructor
 */
function VisualRecognitionV3(options) {
  // Check if 'version_date' was provided
  if (typeof options.version_date === 'undefined') {
    throw new Error('Argument error: version_date was not specified, use 2015-12-02');
  }

  // Default URL
  // url:
  var serviceDefaults = {
    url: 'http://gateway-a.watsonplatform.net/visual-recognition/api',
    alchemy: true,
    qs: {
      api_key: options.api_key,
      version: options.version_date
    }
  };

  // prevents the requestFactory from inserting it into the url a second time as `apikey`
  delete options.api_key;

  // Replace default options with user provided
  this._options = extend(serviceDefaults, omit(options, ['version_date']));
}





/**
 * 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 response:
 *
{
    "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
}
 *
 * @parma {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 100. 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 {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 {
    verifyParams(params);
  } catch (e) {
    callback(e);
    return;
  }

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

  var parameters;

  if(params.images_file) {
    var stream = params.images_file || params.image_file;

    parameters = {
      options: {
        url: '/v3/classify',
        method: 'POST',
        formData: {
          images_file: stream,
          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, 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 output:
 *
{
  "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
}
 *
 *
 * @parma {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 100. 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 {
    verifyParams(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, 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 output:
 {
   "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
 }
 *
 * @parma {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 100. 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 {
    verifyParams(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, callback);
};

var NEGATIVE_EXAMPLES = 'negative_examples';
/**
 * 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 inputs:
 *
{
  foo_positive_examples: fs.createReadStream('./foo-pics.zip'),
  negative_examples: fs.createReadStream('./not-foo-pics.zip'),
  name: 'to-foo-or-not'
}

{
 foo_positive_examples: fs.createReadStream('./foo-pics.zip'),
 bar_positive_examples: fs.createReadStream('./bar-pics.zip'),
 name: 'foo-vs-bar'
}

{
 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 output:
 {
     "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, callback);
};

/**
 * Retrieve a list of all classifiers, including built-in and
 * user-created classifiers.
 *
 * Example output:

 {"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, callback);
};

/**
 * Retrieves information about a specific classifier.
 *
 * Example output:
{
  classifier_id: 'fruit_679357912',
  name: 'fruit',
  owner: 'a3a48ea7-492b-448b-87d7-9dade8bde5a9',
  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, 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, callback);
};


module.exports = VisualRecognitionV3;