UNPKG

@google-cloud/vision

Version:
1,648 lines (1,517 loc) 43.3 kB
/*! * Copyright 2015 Google Inc. 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. */ /*! * @module vision */ 'use strict'; var arrify = require('arrify'); var async = require('async'); var common = require('@google-cloud/common'); var extend = require('extend'); var format = require('string-format-obj'); var fs = require('fs'); var is = require('is'); var prop = require('propprop'); var request = require('request'); var rgbHex = require('rgb-hex'); var util = require('util'); var VERY_UNLIKELY = 0; var UNLIKELY = 1; var POSSIBLE = 2; var LIKELY = 3; var VERY_LIKELY = 4; /** * The [Cloud Vision API](https://cloud.google.com/vision/docs) allows easy * integration of vision detection features, including image labeling, face and * landmark detection, optical character recognition (OCR), and tagging of * explicit content. * * @constructor * @alias module:vision * * @resource [Getting Started]{@link https://cloud.google.com/vision/docs/getting-started} * @resource [Image Best Practices]{@link https://cloud.google.com/vision/docs/image-best-practices} * * @param {object} options - [Configuration object](#/docs). */ function Vision(options) { if (!(this instanceof Vision)) { options = common.util.normalizeArguments(this, options); return new Vision(options); } var config = { baseUrl: 'https://vision.googleapis.com/v1', projectIdRequired: false, scopes: [ 'https://www.googleapis.com/auth/cloud-platform' ], packageJson: require('../package.json') }; common.Service.call(this, config, options); } util.inherits(Vision, common.Service); Vision.likelihood = { VERY_UNLIKELY: VERY_UNLIKELY, UNLIKELY: UNLIKELY, POSSIBLE: POSSIBLE, LIKELY: LIKELY, VERY_LIKELY: VERY_LIKELY }; /** * Run image detection and annotation for an image or batch of images. * * This is an advanced API method that requires raw * [`AnnotateImageRequest`](https://cloud.google.com/vision/reference/rest/v1/images/annotate#AnnotateImageRequest) * objects to be provided. If that doesn't sound like what you're looking for, * you'll probably appreciate {module:vision#detect}. * * @resource [images.annotate API Reference]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate} * * @param {object|object[]} requests - An `AnnotateImageRequest` or array of * `AnnotateImageRequest`s. See an * [`AnnotateImageRequest`](https://cloud.google.com/vision/reference/rest/v1/images/annotate#AnnotateImageRequest). * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this request. * @param {object} callback.annotations - See an * [`AnnotateImageResponse`](https://cloud.google.com/vision/reference/rest/v1/images/annotate#AnnotateImageResponse). * @param {object} callback.apiResponse - Raw API response. * * @example * var annotateImageReq = { * // See the link in the parameters for `AnnotateImageRequest`. * }; * * vision.annotate(annotateImageReq, function(err, annotations, apiResponse) { * // annotations = apiResponse.responses * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.annotate(annotateImageReq).then(function(data) { * var annotations = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.annotate = function(requests, callback) { this.request({ method: 'POST', uri: 'images:annotate', json: { requests: arrify(requests) } }, function(err, resp) { if (err) { callback(err, null, resp); return; } callback(null, resp.responses, resp); }); }; // jscs:disable maximumLineLength /** * Detect properties from an image (or images) of one or more types. * * <h4>API simplifications</h4> * * The raw API response will return some values in a range from `VERY_UNLIKELY` * to `VERY_LIKELY`. For simplification, any value less than `LIKELY` is * converted to `false`. * * - **False** * - `VERY_UNLIKELY` * - `UNLIKELY` * - `POSSIBLE` * - **True** * - `LIKELY` * - `VERY_LIKELY` * * The API will also return many values represented in a `[0,1]` range. We * convert these to a `[0,100]` value. E.g, `0.4` is represented as `40`. * * For the response in the original format, review the `apiResponse` argument * your callback receives. * * @param {string|string[]|buffer|buffer[]|module:storage/file|module:storage/file[]} images - The * source image(s) to run the detection on. It can be either a local image * path, a remote image URL, a Buffer, or a @google-cloud/storage File * object. * @param {string[]|object=} options - An array of types or a configuration * object. * @param {object=} options.imageContext - See an * [`ImageContext`](https://cloud.google.com/vision/reference/rest/v1/images/annotate#ImageContext) * resource. * @param {number} options.maxResults - The maximum number of results, per type, * to return in the response. * @param {string[]} options.types - An array of feature types to detect from * the provided images. Acceptable values: `faces`, `landmarks`, `labels`, * `logos`, `properties`, `safeSearch`, `text`. * @param {boolean=} options.verbose - Use verbose mode, which returns a less- * simplistic representation of the annotation (default: `false`). * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this request. * @param {object[]} callback.err.errors - If present, these represent partial * failures. It's possible for part of your request to be completed * successfully, while a single feature request was not successful. * @param {object|object[]} callback.detections - If a single detection type was * asked for, it will be returned in its raw form; either an object or array * of objects. If multiple detection types were requested, you will receive * an object with keys for each detection type (listed above in * `config.types`). Additionally, if multiple images were provided, you will * receive an array of detection objects, each representing an image. See * the examples below for more information. * @param {object} callback.apiResponse - Raw API response. * * @example * var types = [ * 'face', * 'label' * ]; * * vision.detect('image.jpg', types, function(err, detections, apiResponse) { * // detections = { * // faces: [...], * // labels: [...] * // } * }); * * //- * // Run feature detection over a remote image. * // * // *Note: This is not an officially supported feature of the Vision API. Our * // library will make a request to the URL given, convert it to base64, and * // send that upstream.* * //- * var img = 'https://upload.wikimedia.org/wikipedia/commons/5/51/Google.png'; * vision.detect(img, types, function(err, detection, apiResponse) {}); * * //- * // Run feature detection over a Buffer. * //- * var level = require('level'); * var db = level('./users-database'); * * db.get('user-image', { encoding: 'binary' }, function(err, image) { * if (err) { * // Error handling omitted. * } * * vision.detect(image, function(err, detection, apiResponse) {}); * }); * * //- * // Supply multiple images for feature detection. * //- * var images = [ * 'image.jpg', * 'image-two.jpg' * ]; * * var types = [ * 'face', * 'label' * ]; * * vision.detect(images, types, function(err, detections, apiResponse) { * // detections = [ * // // Detections for image.jpg: * // { * // faces: [...], * // labels: [...] * // }, * // * // // Detections for image-two.jpg: * // { * // faces: [...], * // labels: [...] * // } * // ] * }); * * //- * // It's possible for part of your request to be completed successfully, while * // a single feature request was not successful. * //- * vision.detect('malformed-image.jpg', types, function(err, detections) { * if (err) { * // An API error or partial failure occurred. * * if (err.name === 'PartialFailureError') { * // err.errors = [ * // { * // image: 'malformed-image.jpg', * // errors: [ * // { * // code: 400, * // message: 'Bad image data', * // type: 'faces' * // }, * // { * // code: 400, * // message: 'Bad image data', * // type: 'labels' * // } * // ] * // } * // ] * } * } * * // `detections` will still be populated with all of the results that could * // be annotated. * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detect('image.jpg', types).then(function(data) { * var detections = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detect = function(images, options, callback) { var self = this; var isSingleImage = !is.array(images) || images.length === 1; if (!is.object(options)) { options = { types: options }; } var types = arrify(options.types); var typeShortNameToFullName = { face: 'FACE_DETECTION', faces: 'FACE_DETECTION', label: 'LABEL_DETECTION', labels: 'LABEL_DETECTION', landmark: 'LANDMARK_DETECTION', landmarks: 'LANDMARK_DETECTION', logo: 'LOGO_DETECTION', logos: 'LOGO_DETECTION', properties: 'IMAGE_PROPERTIES', safeSearch: 'SAFE_SEARCH_DETECTION', text: 'TEXT_DETECTION' }; var typeShortNameToRespName = { face: 'faceAnnotations', faces: 'faceAnnotations', label: 'labelAnnotations', labels: 'labelAnnotations', landmark: 'landmarkAnnotations', landmarks: 'landmarkAnnotations', logo: 'logoAnnotations', logos: 'logoAnnotations', properties: 'imagePropertiesAnnotation', safeSearch: 'safeSearchAnnotation', text: 'textAnnotations' }; var typeRespNameToShortName = { faceAnnotations: 'faces', imagePropertiesAnnotation: 'properties', labelAnnotations: 'labels', landmarkAnnotations: 'landmarks', logoAnnotations: 'logos', safeSearchAnnotation: 'safeSearch', textAnnotations: 'text' }; Vision.findImages_(images, function(err, foundImages) { if (err) { callback(err); return; } var config = []; foundImages.forEach(function(image) { types.forEach(function(type) { var typeName = typeShortNameToFullName[type]; if (!typeName) { throw new Error('Requested detection feature not found: ' + type); } var cfg = { image: image, features: { type: typeName } }; if (is.object(options.imageContext)) { cfg.imageContext = options.imageContext; } if (is.number(options.maxResults)) { cfg.features.maxResults = options.maxResults; } config.push(cfg); }); }); self.annotate(config, function(err, annotations, resp) { if (err) { callback(err, null, resp); return; } var originalResp = extend(true, {}, resp); var partialFailureErrors = []; var detections = foundImages .map(groupDetectionsByImage) .map(assignTypeToEmptyAnnotations) .map(removeDetectionsWithErrors) .map(flattenAnnotations) .map(decorateAnnotations); if (partialFailureErrors.length > 0) { err = new common.util.PartialFailureError({ errors: partialFailureErrors, response: originalResp }); } if (isSingleImage && detections.length > 0) { // If only a single image was given, expose it from the array. detections = detections[0]; } callback(err, detections, originalResp); function groupDetectionsByImage() { // detections = [ // // Image one: // [ // { // faceAnnotations: {}, // labelAnnotations: {}, // ... // } // ], // // // Image two: // [ // { // faceAnnotations: {}, // labelAnnotations: {}, // ... // } // ] // ] return annotations.splice(0, types.length); } function assignTypeToEmptyAnnotations(annotations) { // Before: // [ // {}, // What annotation type was attempted? // { labelAnnotations: {...} } // ] // // After: // [ // { faceAnnotations: [] }, // { labelAnnotations: {...} } // ] return annotations.map(function(annotation, index) { var detectionType = types[index]; var typeName = typeShortNameToRespName[detectionType]; if (is.empty(annotation) || annotation.error) { var isPlural = typeName.charAt(typeName.length - 1) === 's'; annotation[typeName] = isPlural ? [] : {}; } return annotation; }); } function removeDetectionsWithErrors(annotations, index) { // Before: // [ // { // faceAnnotations: [] // }, // { // error: {...}, // imagePropertiesAnnotation: {} // } // ] // After: // [ // { // faceAnnotations: [] // }, // undefined // ] var errors = []; annotations.forEach(function(annotation, index) { var annotationKey = Object.keys(annotation)[0]; if (annotationKey === 'error') { var userInputType = types[index]; var respNameType = typeShortNameToRespName[userInputType]; annotation.error.type = typeRespNameToShortName[respNameType]; errors.push(Vision.formatError_(annotation.error)); } }); if (errors.length > 0) { partialFailureErrors.push({ image: isSingleImage ? images : images[index], errors: errors }); return; } return annotations; } function flattenAnnotations(annotations) { return extend.apply(null, annotations); } function formatAnnotationBuilder(type) { return function(annotation) { if (is.empty(annotation)) { return annotation; } var formatMethodMap = { errors: Vision.formatError_, faceAnnotations: Vision.formatFaceAnnotation_, imagePropertiesAnnotation: Vision.formatImagePropertiesAnnotation_, labelAnnotations: Vision.formatEntityAnnotation_, landmarkAnnotations: Vision.formatEntityAnnotation_, logoAnnotations: Vision.formatEntityAnnotation_, safeSearchAnnotation: Vision.formatSafeSearchAnnotation_, textAnnotations: Vision.formatEntityAnnotation_ }; return formatMethodMap[type](annotation, options); }; } function decorateAnnotations(annotations) { for (var annotationType in annotations) { if (annotations.hasOwnProperty(annotationType)) { var annotationGroup = arrify(annotations[annotationType]); var formattedAnnotationGroup = annotationGroup .map(formatAnnotationBuilder(annotationType)); // An annotation can be singular, e.g. SafeSearch. It is either // violent or not. Unlike face detection, where there can be // multiple results. // // Be sure the original type (object or array) is preserved and // not wrapped in an array if it wasn't originally. if (!is.array(annotations[annotationType])) { formattedAnnotationGroup = formattedAnnotationGroup[0]; } delete annotations[annotationType]; var typeShortName = typeRespNameToShortName[annotationType]; annotations[typeShortName] = formattedAnnotationGroup; } } if (types.length === 1) { // Only a single detection type was asked for, so no need to box in // the results. Make them accessible without using a key. var key = Object.keys(annotations)[0]; annotations = annotations[key]; } return annotations; } }); }); }; // jscs:enable maximumLineLength /** * Run face detection against an image. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @resource [FaceAnnotation JSON respresentation]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate#FaceAnnotation} * * @example * vision.detectFaces('image.jpg', function(err, faces, apiResponse) { * // faces = [ * // { * // angles: { * // pan: -8.1090336, * // roll: -5.0002542, * // tilt: 18.012161 * // }, * // bounds: { * // head: [ * // { * // x: 1 * // }, * // { * // x: 295 * // }, * // { * // x: 295, * // y: 301 * // }, * // { * // x: 1, * // y: 301 * // } * // ], * // face: [ * // { * // x: 28, * // y: 40 * // }, * // { * // x: 250, * // y: 40 * // }, * // { * // x: 250, * // y: 262 * // }, * // { * // x: 28, * // y: 262 * // } * // ] * // }, * // features: { * // confidence: 34.489909, * // chin: { * // center: { * // x: 143.34183, * // y: 262.22998, * // z: -57.388493 * // }, * // left: { * // x: 63.102425, * // y: 248.99081, * // z: 44.207638 * // }, * // right: { * // x: 241.72728, * // y: 225.53488, * // z: 19.758242 * // } * // }, * // ears: { * // left: { * // x: 54.872219, * // y: 207.23712, * // z: 97.030685 * // }, * // right: { * // x: 252.67567, * // y: 180.43124, * // z: 70.15992 * // } * // }, * // eyebrows: { * // left: { * // left: { * // x: 58.790176, * // y: 113.28249, * // z: 17.89735 * // }, * // right: { * // x: 106.14151, * // y: 98.593758, * // z: -13.116687 * // }, * // top: { * // x: 80.248711, * // y: 94.04303, * // z: 0.21131183 * // } * // }, * // right: { * // left: { * // x: 148.61565, * // y: 92.294594, * // z: -18.804882 * // }, * // right: { * // x: 204.40808, * // y: 94.300117, * // z: -2.0009689 * // }, * // top: { * // x: 174.70135, * // y: 81.580917, * // z: -12.702137 * // } * // } * // }, * // eyes: { * // left: { * // bottom: { * // x: 84.883934, * // y: 134.59479, * // z: -2.8677137 * // }, * // center: { * // x: 83.707092, * // y: 128.34, * // z: -0.00013388535 * // }, * // left: { * // x: 72.213913, * // y: 132.04138, * // z: 9.6985674 * // }, * // pupil: { * // x: 86.531624, * // y: 126.49807, * // z: -2.2496929 * // }, * // right: { * // x: 105.28892, * // y: 125.57655, * // z: -2.51554 * // }, * // top: { * // x: 86.706947, * // y: 119.47144, * // z: -4.1606765 * // } * // }, * // right: { * // bottom: { * // x: 179.30353, * // y: 121.03307, * // z: -14.843414 * // }, * // center: { * // x: 181.17694, * // y: 115.16437, * // z: -12.82961 * // }, * // left: { * // x: 158.2863, * // y: 118.491, * // z: -9.723031 * // }, * // pupil: { * // x: 175.99976, * // y: 114.64407, * // z: -14.53744 * // }, * // right: { * // x: 194.59413, * // y: 115.91954, * // z: -6.952745 * // }, * // top: { * // x: 173.99446, * // y: 107.94287, * // z: -16.050705 * // } * // } * // }, * // forehead: { * // x: 126.53813, * // y: 93.812057, * // z: -18.863352 * // }, * // lips: { * // bottom: { * // x: 137.28528, * // y: 219.23564, * // z: -56.663128 * // }, * // top: { * // x: 134.74164, * // y: 192.50438, * // z: -53.876408 * // } * // }, * // mouth: { * // center: { * // x: 136.43481, * // y: 204.37952, * // z: -51.620205 * // }, * // left: { * // x: 104.53558, * // y: 214.05037, * // z: -30.056231 * // }, * // right: { * // x: 173.79134, * // y: 204.99333, * // z: -39.725758 * // } * // }, * // nose: { * // bottom: { * // center: { * // x: 133.81947, * // y: 173.16437, * // z: -48.287724 * // }, * // left: { * // x: 110.98372, * // y: 173.61331, * // z: -29.7784 * // }, * // right: { * // x: 161.31354, * // y: 168.24527, * // z: -36.1628 * // } * // }, * // tip: { * // x: 128.14919, * // y: 153.68129, * // z: -63.198204 * // }, * // top: { * // x: 127.83745, * // y: 110.17557, * // z: -22.650913 * // } * // } * // }, * // confidence: 56.748849, * // anger: false, * // blurred: false, * // headwear: false, * // joy: false, * // sorrow: false, * // surprise: false, * // underExposed: false * // } * // ] * }); * * //- * // Our library simplifies the response from the API. Use the map below to see * // each response name's original name. * //- * var shortNameToLongNameMap = { * chin: { * center: 'CHIN_GNATHION', * left: 'CHIN_LEFT_GONION', * right: 'CHIN_RIGHT_GONION' * }, * * ears: { * left: 'LEFT_EAR_TRAGION', * right: 'RIGHT_EAR_TRAGION' * }, * * eyebrows: { * left: { * left: 'LEFT_OF_LEFT_EYEBROW', * right: 'RIGHT_OF_LEFT_EYEBROW', * top: 'LEFT_EYEBROW_UPPER_MIDPOINT' * }, * right: { * left: 'LEFT_OF_RIGHT_EYEBROW', * right: 'RIGHT_OF_RIGHT_EYEBROW', * top: 'RIGHT_EYEBROW_UPPER_MIDPOINT' * } * }, * * eyes: { * left: { * bottom: 'LEFT_EYE_BOTTOM_BOUNDARY', * center: 'LEFT_EYE', * left: 'LEFT_EYE_LEFT_CORNER', * pupil: 'LEFT_EYE_PUPIL', * right: 'LEFT_EYE_RIGHT_CORNER', * top: 'LEFT_EYE_TOP_BOUNDARY' * }, * right: { * bottom: 'RIGHT_EYE_BOTTOM_BOUNDARY', * center: 'RIGHT_EYE', * left: 'RIGHT_EYE_LEFT_CORNER', * pupil: 'RIGHT_EYE_PUPIL', * right: 'RIGHT_EYE_RIGHT_CORNER', * top: 'RIGHT_EYE_TOP_BOUNDARY' * } * }, * * forehead: 'FOREHEAD_GLABELLA', * * lips: { * bottom: 'LOWER_LIP', * top: 'UPPER_LIP' * }, * * mouth: { * center: 'MOUTH_CENTER', * left: 'MOUTH_LEFT', * right: 'MOUTH_RIGHT' * }, * * nose: { * bottom: { * center: 'NOSE_BOTTOM_CENTER', * left: 'NOSE_BOTTOM_LEFT', * right: 'NOSE_BOTTOM_RIGHT' * }, * tip: 'NOSE_TIP', * top: 'MIDPOINT_BETWEEN_EYES' * } * }; * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectFaces('image.jpg').then(function(data) { * var faces = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectFaces = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['faces'] }); this.detect(images, options, callback); }; /** * Annotate an image with descriptive labels. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @resource [EntityAnnotation JSON representation]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate#EntityAnnotation} * * @example * vision.detectLabels('image.jpg', function(err, labels, apiResponse) { * // labels = [ * // 'classical sculpture', * // 'statue', * // 'landmark', * // 'ancient history', * // 'artwork' * // ] * }); * * //- * // Activate `verbose` mode for a more detailed response. * //- * var opts = { * verbose: true * }; * * vision.detectLabels('image.jpg', opts, function(err, labels, apiResponse) { * // labels = [ * // { * // desc: 'classical sculpture', * // id: '/m/095yjj', * // score: 98.092282 * // }, * // { * // desc: 'statue', * // id: '/m/013_1c', * // score: 90.66112 * // }, * // // ... * // ] * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectLabels('image.jpg').then(function(data) { * var labels = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectLabels = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['labels'] }); this.detect(images, options, callback); }; /** * Detect the landmarks from an image. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @resource [EntityAnnotation JSON representation]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate#EntityAnnotation} * * @example * vision.detectLandmarks('image.jpg', function(err, landmarks, apiResponse) { * // landmarks = [ * // 'Mount Rushmore' * // ] * }); * * //- * // Activate `verbose` mode for a more detailed response. * //- * var image = 'image.jpg'; * * var opts = { * verbose: true * }; * * vision.detectLandmarks(image, opts, function(err, landmarks, apiResponse) { * // landmarks = [ * // { * // desc: 'Mount Rushmore', * // id: '/m/019dvv', * // score: 28.651705, * // bounds: [ * // { * // x: 79, * // y: 130 * // }, * // { * // x: 284, * // y: 130 * // }, * // { * // x: 284, * // y: 226 * // }, * // { * // x: 79, * // y: 226 * // } * // ], * // locations: [ * // { * // latitude: 43.878264, * // longitude: -103.45700740814209 * // } * // ] * // } * // ] * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectLandmarks('image.jpg').then(function(data) { * var landmarks = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectLandmarks = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['landmarks'] }); this.detect(images, options, callback); }; /** * Detect the logos from an image. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @resource [EntityAnnotation JSON representation]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate#EntityAnnotation} * * @example * vision.detectLogos('image.jpg', function(err, logos, apiResponse) { * // logos = [ * // 'Google' * // ] * }); * * //- * // Activate `verbose` mode for a more detailed response. * //- * var options = { * verbose: true * }; * * vision.detectLogos('image.jpg', options, function(err, logos, apiResponse) { * // logos = [ * // { * // desc: 'Google', * // id: '/m/045c7b', * // score: 64.35439, * // bounds: [ * // { * // x: 11, * // y: 11 * // }, * // { * // x: 330, * // y: 11 * // }, * // { * // x: 330, * // y: 72 * // }, * // { * // x: 11, * // y: 72 * // } * // ] * // } * // ] * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectLogos('image.jpg').then(function(data) { * var logos = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectLogos = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['logos'] }); this.detect(images, options, callback); }; /** * Get a set of properties about an image, such as its dominant colors. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @resource [ImageProperties JSON representation]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate#ImageProperties} * * @example * vision.detectProperties('image.jpg', function(err, props, apiResponse) { * // props = { * // colors: [ * // '3b3027', * // '727d81', * // '3f2f22', * // '838e92', * // '482a16', * // '5f4f3c', * // '261b14', * // 'b39b7f', * // '51473f', * // '2c1e12' * // ] * // } * }); * * //- * // Activate `verbose` mode for a more detailed response. * //- * var image = 'image.jpg'; * * var options = { * verbose: true * }; * * vision.detectProperties(image, options, function(err, props, apiResponse) { * // props = { * // colors: [ * // { * // red: 59, * // green: 48, * // blue: 39, * // score: 26.618013, * // coverage: 15.948276, * // hex: '3b3027' * // }, * // { * // red: 114, * // green: 125, * // blue: 129, * // score: 10.319714, * // coverage: 8.3977409, * // hex: '727d81' * // }, * // // ... * // ] * // } * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectProperties('image.jpg').then(function(data) { * var props = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectProperties = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['properties'] }); this.detect(images, options, callback); }; /** * Detect the SafeSearch flags from an image. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @resource [SafeSearch JSON representation]{@link https://cloud.google.com/vision/reference/rest/v1/images/annotate#SafeSearchAnnotation} * * @example * vision.detectSafeSearch('image.jpg', function(err, safeSearch, apiResponse) { * // safeSearch = { * // adult: false, * // medical: false, * // spoof: false, * // violence: true * // } * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectSafeSearch('image.jpg').then(function(data) { * var safeSearch = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectSafeSearch = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['safeSearch'] }); this.detect(images, options, callback); }; /** * Detect the text within an image. * * <h4>Parameters</h4> * * See {module:vision#detect}. * * @example * vision.detectText('image.jpg', function(err, text, apiResponse) { * // text = [ * // 'This was text found in the image' * // ] * }); * * //- * // Activate `verbose` mode for a more detailed response. * //- * var options = { * verbose: true * }; * * vision.detectText('image.jpg', options, function(err, text, apiResponse) { * // text = [ * // { * // desc: 'This was text found in the image', * // bounds: [ * // { * // x: 4, * // y: 5 * // }, * // { * // x: 493, * // y: 5 * // }, * // { * // x: 493, * // y: 89 * // }, * // { * // x: 4, * // y: 89 * // } * // ] * // } * // ] * }); * * //- * // If the callback is omitted, we'll return a Promise. * //- * vision.detectText('image.jpg').then(function(data) { * var text = data[0]; * var apiResponse = data[1]; * }); */ Vision.prototype.detectText = function(images, options, callback) { if (is.fn(options)) { callback = options; options = {}; } options = extend({}, options, { types: ['text'] }); this.detect(images, options, callback); }; /** * Determine the type of image the user is asking to be annotated. If a * {module:storage/file}, convert to its "gs://{bucket}/{file}" URL. If a remote * URL, read the contents and convert to a base64 string. If a file path to a * local file, convert to a base64 string. * * @private */ Vision.findImages_ = function(images, callback) { var MAX_PARALLEL_LIMIT = 5; images = arrify(images); function findImage(image, callback) { if (Buffer.isBuffer(image)) { callback(null, { content: image.toString('base64') }); return; } if (common.util.isCustomType(image, 'storage/file')) { callback(null, { source: { gcsImageUri: format('gs://{bucketName}/{fileName}', { bucketName: image.bucket.name, fileName: image.name }) } }); return; } // File is a URL. if (/^http/.test(image)) { request({ method: 'GET', uri: image, encoding: 'base64' }, function(err, resp, body) { if (err) { callback(err); return; } callback(null, { content: body }); }); return; } // File exists on disk. fs.readFile(image, { encoding: 'base64' }, function(err, contents) { if (err) { callback(err); return; } callback(null, { content: contents }); }); } async.mapLimit(images, MAX_PARALLEL_LIMIT, findImage, callback); }; /** * Format a raw entity annotation response from the API. * * @private */ Vision.formatEntityAnnotation_ = function(entityAnnotation, options) { if (!options.verbose) { return entityAnnotation.description; } var formattedEntityAnnotation = { desc: entityAnnotation.description }; if (entityAnnotation.mid) { formattedEntityAnnotation.mid = entityAnnotation.mid; } if (entityAnnotation.score) { formattedEntityAnnotation.score = entityAnnotation.score * 100; } if (entityAnnotation.boundingPoly) { formattedEntityAnnotation.bounds = entityAnnotation.boundingPoly.vertices; } if (is.defined(entityAnnotation.confidence)) { formattedEntityAnnotation.confidence = entityAnnotation.confidence * 100; } if (entityAnnotation.locations) { var locations = entityAnnotation.locations; formattedEntityAnnotation.locations = locations.map(prop('latLng')); } if (entityAnnotation.properties) { formattedEntityAnnotation.properties = entityAnnotation.properties; } return formattedEntityAnnotation; }; /** * Format a raw error from the API. * * @private */ Vision.formatError_ = function(err) { var httpError = common.GrpcService.GRPC_ERROR_CODE_TO_HTTP[err.code]; if (httpError) { err.code = httpError.code; } return err; }; /** * Format a raw face annotation response from the API. * * @private */ Vision.formatFaceAnnotation_ = function(faceAnnotation) { function findLandmark(type) { var landmarks = faceAnnotation.landmarks; return landmarks.filter(function(landmark) { return landmark.type === type; })[0].position; } var formattedFaceAnnotation = { angles: { pan: faceAnnotation.panAngle, roll: faceAnnotation.rollAngle, tilt: faceAnnotation.tiltAngle }, bounds: { head: faceAnnotation.boundingPoly.vertices, face: faceAnnotation.fdBoundingPoly.vertices }, features: { confidence: faceAnnotation.landmarkingConfidence * 100, chin: { center: findLandmark('CHIN_GNATHION'), left: findLandmark('CHIN_LEFT_GONION'), right: findLandmark('CHIN_RIGHT_GONION') }, ears: { left: findLandmark('LEFT_EAR_TRAGION'), right: findLandmark('RIGHT_EAR_TRAGION'), }, eyebrows: { left: { left: findLandmark('LEFT_OF_LEFT_EYEBROW'), right: findLandmark('RIGHT_OF_LEFT_EYEBROW'), top: findLandmark('LEFT_EYEBROW_UPPER_MIDPOINT') }, right: { left: findLandmark('LEFT_OF_RIGHT_EYEBROW'), right: findLandmark('RIGHT_OF_RIGHT_EYEBROW'), top: findLandmark('RIGHT_EYEBROW_UPPER_MIDPOINT') } }, eyes: { left: { bottom: findLandmark('LEFT_EYE_BOTTOM_BOUNDARY'), center: findLandmark('LEFT_EYE'), left: findLandmark('LEFT_EYE_LEFT_CORNER'), pupil: findLandmark('LEFT_EYE_PUPIL'), right: findLandmark('LEFT_EYE_RIGHT_CORNER'), top: findLandmark('LEFT_EYE_TOP_BOUNDARY') }, right: { bottom: findLandmark('RIGHT_EYE_BOTTOM_BOUNDARY'), center: findLandmark('RIGHT_EYE'), left: findLandmark('RIGHT_EYE_LEFT_CORNER'), pupil: findLandmark('RIGHT_EYE_PUPIL'), right: findLandmark('RIGHT_EYE_RIGHT_CORNER'), top: findLandmark('RIGHT_EYE_TOP_BOUNDARY') } }, forehead: findLandmark('FOREHEAD_GLABELLA'), lips: { bottom: findLandmark('LOWER_LIP'), top: findLandmark('UPPER_LIP') }, mouth: { center: findLandmark('MOUTH_CENTER'), left: findLandmark('MOUTH_LEFT'), right: findLandmark('MOUTH_RIGHT') }, nose: { bottom: { center: findLandmark('NOSE_BOTTOM_CENTER'), left: findLandmark('NOSE_BOTTOM_LEFT'), right: findLandmark('NOSE_BOTTOM_RIGHT') }, tip: findLandmark('NOSE_TIP'), top: findLandmark('MIDPOINT_BETWEEN_EYES') } }, confidence: faceAnnotation.detectionConfidence * 100 }; // Remove the `Likelihood` part from a property name. // input: "joyLikelihood", output: "joy" for (var prop in faceAnnotation) { if (prop.indexOf('Likelihood') > -1) { var shortenedProp = prop.replace('Likelihood', ''); formattedFaceAnnotation[shortenedProp] = Vision.gteLikelihood_(LIKELY, faceAnnotation[prop]); } } return formattedFaceAnnotation; }; /** * Format a raw image properties annotation response from the API. * * @private */ Vision.formatImagePropertiesAnnotation_ = function(imageAnnotation, options) { var formattedImageAnnotation = { colors: imageAnnotation.dominantColors.colors .map(function(colorObject) { var red = colorObject.color.red; var green = colorObject.color.green; var blue = colorObject.color.blue; var hex = rgbHex(red, green, blue); if (!options.verbose) { return hex; } colorObject.hex = hex; colorObject.red = red; colorObject.green = green; colorObject.blue = blue; delete colorObject.color; colorObject.coverage = colorObject.pixelFraction *= 100; delete colorObject.pixelFraction; colorObject.score *= 100; return colorObject; }) }; return formattedImageAnnotation; }; /** * Format a raw SafeSearch annotation response from the API. * * @private */ Vision.formatSafeSearchAnnotation_ = function(ssAnnotation, options) { if (!options.verbose) { for (var prop in ssAnnotation) { var value = ssAnnotation[prop]; ssAnnotation[prop] = Vision.gteLikelihood_(LIKELY, value); } return ssAnnotation; } return ssAnnotation; }; /** * Convert a "likelihood" value to a boolean representation, based on the lowest * likelihood provided. * * @private * * @example * Vision.gteLikelihood_(Vision.likelihood.VERY_LIKELY, 'POSSIBLE'); * // false * * Vision.gteLikelihood_(Vision.likelihood.UNLIKELY, 'POSSIBLE'); * // true */ Vision.gteLikelihood_ = function(baseLikelihood, likelihood) { return Vision.likelihood[likelihood] >= baseLikelihood; }; /*! Developer Documentation * * All async methods (except for streams) will return a Promise in the event * that a callback is omitted. */ common.util.promisifyAll(Vision); module.exports = Vision; module.exports.v1 = require('./v1');