diff --git a/.gitignore b/.gitignore index 25751278147..76b065e85ba 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,11 @@ service-account.json .project .classpath .settings + +# vim +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +Session.vim +.netrwhist +*~ +tags diff --git a/pom.xml b/pom.xml index ba2de2f3ab9..790c7629200 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,10 @@ storage/xml-api/serviceaccount-appengine-sample taskqueue/deferred unittests + vision/face-detection + vision/label + vision/landmark-detection + vision/text diff --git a/vision/README.md b/vision/README.md new file mode 100644 index 00000000000..d44cc276d17 --- /dev/null +++ b/vision/README.md @@ -0,0 +1,68 @@ +# Google Cloud Vision API Java examples + +This directory contains [Cloud Vision API](https://cloud.google.com/vision/) Java samples. + +## Prerequisites + +### Download Maven + +This sample uses the [Apache Maven][maven] build system. Before getting started, be +sure to [download][maven-download] and [install][maven-install] it. When you use +Maven as described here, it will automatically download the needed client +libraries. + +[maven]: https://maven.apache.org +[maven-download]: https://maven.apache.org/download.cgi +[maven-install]: https://maven.apache.org/install.html + +### Setup + +* Create a project with the [Google Cloud Console][cloud-console], and enable + the [Vision API][vision-api]. +* Set up your environment with [Application Default Credentials][adc]. For + example, from the Cloud Console, you might create a service account, + download its json credentials file, then set the appropriate environment + variable: + + ```bash + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your-project-credentials.json + ``` + +[cloud-console]: https://console.cloud.google.com +[vision-api]: https://console.cloud.google.com/apis/api/vision.googleapis.com/overview?project=_ +[adc]: https://cloud.google.com/docs/authentication#developer_workflow + +## Samples + +### Label Detection + +This sample annotates an image with labels based on its content. + +- [Java Code](label) + +### Face Detection + +This sample identifies faces within an image. + +- [Quickstart Walkthrough](https://cloud.google.com/vision/docs/face-tutorial) +- [Java Code](face_detection) + +### Landmark Detection Using Google Cloud Storage + +This sample identifies a landmark within an image stored on +Google Cloud Storage. + +- [Documentation and Java Code](landmark_detection) + +### Text Detection Using the Vision API + +This sample uses `TEXT_DETECTION` Vision API requests to build an inverted index +from the stemmed words found in the images, and stores that index in a +[Redis](redis.io) database. The example uses the +[OpenNLP](https://opennlp.apache.org/) library (Open Natural Language +Processing) for finding stopwords and doing stemming. The resulting index can be +queried to find images that match a given set of words, and to list text that +was found in each matching image. + +[Documentation and Java Code](text) + diff --git a/vision/face-detection/.gitignore b/vision/face-detection/.gitignore new file mode 100644 index 00000000000..55d847395a3 --- /dev/null +++ b/vision/face-detection/.gitignore @@ -0,0 +1 @@ +output.jpg diff --git a/vision/face-detection/README.md b/vision/face-detection/README.md new file mode 100644 index 00000000000..25de0a9057b --- /dev/null +++ b/vision/face-detection/README.md @@ -0,0 +1,43 @@ +# Google Cloud Vision API Java Face Detection example + +## Download Maven + +This sample uses the [Apache Maven][maven] build system. Before getting started, be +sure to [download][maven-download] and [install][maven-install] it. When you use +Maven as described here, it will automatically download the needed client +libraries. + +[maven]: https://maven.apache.org +[maven-download]: https://maven.apache.org/download.cgi +[maven-install]: https://maven.apache.org/install.html + +## Setup + +* Create a project with the [Google Cloud Console][cloud-console], and enable + the [Vision API][vision-api]. +* Set up your environment with [Application Default Credentials][adc]. For + example, from the Cloud Console, you might create a service account, + download its json credentials file, then set the appropriate environment + variable: + + ```bash + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your-project-credentials.json + ``` + +[cloud-console]: https://console.cloud.google.com +[vision-api]: https://console.cloud.google.com/apis/api/vision.googleapis.com/overview?project=_ +[adc]: https://cloud.google.com/docs/authentication#developer_workflow + +## Run the sample + +To build and run the sample, run the following from this directory: + +```bash +mvn clean compile assembly:single +java -cp target/vision-face-detection-1.0-SNAPSHOT-jar-with-dependencies.jar com.google.cloud.vision.samples.facedetect.FaceDetectApp data/face.jpg output.jpg +``` + +For more information about face detection see the [Quickstart][quickstart] +guide. + +[quickstart]: https://cloud.google.com/vision/docs/face-tutorial diff --git a/vision/face-detection/data/bad.txt b/vision/face-detection/data/bad.txt new file mode 100644 index 00000000000..d03a5a96367 --- /dev/null +++ b/vision/face-detection/data/bad.txt @@ -0,0 +1 @@ +I am not an image. Labelling shouldn't work on me. diff --git a/vision/face-detection/data/face.jpg b/vision/face-detection/data/face.jpg new file mode 100644 index 00000000000..c0ee5580b37 Binary files /dev/null and b/vision/face-detection/data/face.jpg differ diff --git a/vision/face-detection/pom.xml b/vision/face-detection/pom.xml new file mode 100644 index 00000000000..329b37b6858 --- /dev/null +++ b/vision/face-detection/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + jar + 1.0-SNAPSHOT + com.google.cloud.vision.samples + vision-face-detection + + + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + + + + com.google.apis + google-api-services-vision + v1-rev2-1.21.0 + + + com.google.api-client + google-api-client + 1.21.0 + + + + com.google.guava + guava + 19.0 + + + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 0.28 + test + + + + javax.servlet + javax.servlet-api + 3.1.0 + test + + + + + + org.apache.maven.plugins + 3.3 + maven-compiler-plugin + + 1.7 + 1.7 + + + + maven-assembly-plugin + + + + com.google.cloud.vision.samples.facedetect.FaceDetectApp + + + + jar-with-dependencies + + + + + + diff --git a/vision/face-detection/src/main/java/com/google/cloud/vision/samples/facedetect/FaceDetectApp.java b/vision/face-detection/src/main/java/com/google/cloud/vision/samples/facedetect/FaceDetectApp.java new file mode 100644 index 00000000000..1ab1707c4a9 --- /dev/null +++ b/vision/face-detection/src/main/java/com/google/cloud/vision/samples/facedetect/FaceDetectApp.java @@ -0,0 +1,181 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.facedetect; + +// [BEGIN import_libraries] +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.vision.v1.Vision; +import com.google.api.services.vision.v1.VisionScopes; +import com.google.api.services.vision.v1.model.AnnotateImageRequest; +import com.google.api.services.vision.v1.model.AnnotateImageResponse; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesRequest; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesResponse; +import com.google.api.services.vision.v1.model.FaceAnnotation; +import com.google.api.services.vision.v1.model.Feature; +import com.google.api.services.vision.v1.model.Image; +import com.google.api.services.vision.v1.model.Vertex; +import com.google.common.collect.ImmutableList; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.List; + +import javax.imageio.ImageIO; +// [END import_libraries] + +/** + * A sample application that uses the Vision API to detect faces in an image. + */ +@SuppressWarnings("serial") +public class FaceDetectApp { + /** + * Be sure to specify the name of your application. If the application name is {@code null} or + * blank, the application will log a warning. Suggested format is "MyCompany-ProductName/1.0". + */ + private static final String APPLICATION_NAME = "Google-VisionFaceDetectSample/1.0"; + + private static final int MAX_RESULTS = 4; + + // [START main] + /** + * Annotates an image using the Vision API. + */ + public static void main(String[] args) throws IOException, GeneralSecurityException { + if (args.length != 2) { + System.err.println("Usage:"); + System.err.printf( + "\tjava %s inputImagePath outputImagePath\n", + FaceDetectApp.class.getCanonicalName()); + System.exit(1); + } + Path inputPath = Paths.get(args[0]); + Path outputPath = Paths.get(args[1]); + if (!outputPath.toString().toLowerCase().endsWith(".jpg")) { + System.err.println("outputImagePath must have the file extension 'jpg'."); + System.exit(1); + } + + FaceDetectApp app = new FaceDetectApp(getVisionService()); + List faces = app.detectFaces(inputPath, MAX_RESULTS); + System.out.printf("Found %d face%s\n", faces.size(), faces.size() == 1 ? "" : "s"); + System.out.printf("Writing to file %s\n", outputPath); + app.writeWithFaces(inputPath, outputPath, faces); + } + // [END main] + + // [START get_vision_service] + /** + * Connects to the Vision API using Application Default Credentials. + */ + public static Vision getVisionService() throws IOException, GeneralSecurityException { + GoogleCredential credential = + GoogleCredential.getApplicationDefault().createScoped(VisionScopes.all()); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + return new Vision.Builder(GoogleNetHttpTransport.newTrustedTransport(), jsonFactory, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + // [END get_vision_service] + + private final Vision vision; + + /** + * Constructs a {@link FaceDetectApp} which connects to the Vision API. + */ + public FaceDetectApp(Vision vision) { + this.vision = vision; + } + + // [START detect_face] + /** + * Gets up to {@code maxResults} faces for an image stored at {@code path}. + */ + public List detectFaces(Path path, int maxResults) throws IOException { + byte[] data = Files.readAllBytes(path); + + AnnotateImageRequest request = + new AnnotateImageRequest() + .setImage(new Image().encodeContent(data)) + .setFeatures(ImmutableList.of( + new Feature() + .setType("FACE_DETECTION") + .setMaxResults(maxResults))); + Vision.Images.Annotate annotate = + vision.images() + .annotate(new BatchAnnotateImagesRequest().setRequests(ImmutableList.of(request))); + // Due to a bug: requests to Vision API containing large images fail when GZipped. + annotate.setDisableGZipContent(true); + + BatchAnnotateImagesResponse batchResponse = annotate.execute(); + assert batchResponse.getResponses().size() == 1; + AnnotateImageResponse response = batchResponse.getResponses().get(0); + if (response.getFaceAnnotations() == null) { + throw new IOException( + response.getError() != null + ? response.getError().getMessage() + : "Unknown error getting image annotations"); + } + return response.getFaceAnnotations(); + } + // [END detect_face] + + // [START highlight_faces] + /** + * Reads image {@code inputPath} and writes {@code outputPath} with {@code faces} outlined. + */ + private static void writeWithFaces(Path inputPath, Path outputPath, List faces) + throws IOException { + BufferedImage img = ImageIO.read(inputPath.toFile()); + annotateWithFaces(img, faces); + ImageIO.write(img, "jpg", outputPath.toFile()); + } + + /** + * Annotates an image {@code img} with a polygon around each face in {@code faces}. + */ + public static void annotateWithFaces(BufferedImage img, List faces) { + for (FaceAnnotation face : faces) { + annotateWithFace(img, face); + } + } + + /** + * Annotates an image {@code img} with a polygon defined by {@code face}. + */ + private static void annotateWithFace(BufferedImage img, FaceAnnotation face) { + Graphics2D gfx = img.createGraphics(); + Polygon poly = new Polygon(); + for (Vertex vertex : face.getFdBoundingPoly().getVertices()) { + poly.addPoint(vertex.getX(), vertex.getY()); + } + gfx.setStroke(new BasicStroke(5)); + gfx.setColor(new Color(0x00ff00)); + gfx.draw(poly); + } + // [END highlight_faces] +} diff --git a/vision/face-detection/src/test/java/com/google/cloud/vision/samples/facedetect/FaceDetectAppIT.java b/vision/face-detection/src/test/java/com/google/cloud/vision/samples/facedetect/FaceDetectAppIT.java new file mode 100644 index 00000000000..ce200b7d945 --- /dev/null +++ b/vision/face-detection/src/test/java/com/google/cloud/vision/samples/facedetect/FaceDetectAppIT.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.facedetect; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.services.vision.v1.model.FaceAnnotation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; + +/** + * Integration (system) tests for {@link FaceDetectApp}. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class FaceDetectAppIT { + private static final int MAX_RESULTS = 3; + + private FaceDetectApp appUnderTest; + + @Before public void setUp() throws Exception { + appUnderTest = new FaceDetectApp(FaceDetectApp.getVisionService()); + } + + @Test public void detectFaces_withFace_returnsAtLeastOneFace() throws Exception { + List faces = + appUnderTest.detectFaces(Paths.get("data/face.jpg"), MAX_RESULTS); + + assertThat(faces).named("face.jpg faces").isNotEmpty(); + assertThat(faces.get(0).getFdBoundingPoly().getVertices()) + .named("face.jpg face #0 FdBoundingPoly Vertices") + .isNotEmpty(); + } + + @Test public void detectFaces_badImage_throwsException() throws Exception { + try { + appUnderTest.detectFaces(Paths.get("data/bad.txt"), MAX_RESULTS); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(expected.getMessage().toLowerCase()) + .named("IOException message") + .contains("malformed request"); + } + } +} diff --git a/vision/face-detection/src/test/java/com/google/cloud/vision/samples/facedetect/FaceDetectAppTest.java b/vision/face-detection/src/test/java/com/google/cloud/vision/samples/facedetect/FaceDetectAppTest.java new file mode 100644 index 00000000000..8c843dcad29 --- /dev/null +++ b/vision/face-detection/src/test/java/com/google/cloud/vision/samples/facedetect/FaceDetectAppTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.facedetect; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.services.vision.v1.model.BoundingPoly; +import com.google.api.services.vision.v1.model.FaceAnnotation; +import com.google.api.services.vision.v1.model.Vertex; +import com.google.common.collect.ImmutableList; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.awt.image.BufferedImage; + +/** + * Unit tests for {@link FaceDetectApp}. + */ +@RunWith(JUnit4.class) +public class FaceDetectAppTest { + @Test public void annotateWithFaces_manyFaces_outlinesFaces() throws Exception { + // Arrange + ImmutableList faces = + ImmutableList.of( + new FaceAnnotation() + .setFdBoundingPoly( + new BoundingPoly().setVertices(ImmutableList.of( + new Vertex().setX(10).setY(5), + new Vertex().setX(20).setY(5), + new Vertex().setX(20).setY(25), + new Vertex().setX(10).setY(25)))), + new FaceAnnotation() + .setFdBoundingPoly( + new BoundingPoly().setVertices(ImmutableList.of( + new Vertex().setX(60).setY(50), + new Vertex().setX(70).setY(60), + new Vertex().setX(50).setY(60))))); + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + + // Act + FaceDetectApp.annotateWithFaces(img, faces); + + // Assert + assertThat(img.getRGB(10, 5) & 0x00ff00) + .named("img face #1 vertex (10, 5) green channel") + .isEqualTo(0x00ff00); + assertThat(img.getRGB(20, 5) & 0x00ff00) + .named("img face #1 vertex (20, 5) green channel") + .isEqualTo(0x00ff00); + assertThat(img.getRGB(20, 25) & 0x00ff00) + .named("img face #1 vertex (20, 25) green channel") + .isEqualTo(0x00ff00); + assertThat(img.getRGB(10, 25) & 0x00ff00) + .named("img face #1 vertex (10, 25) green channel") + .isEqualTo(0x00ff00); + assertThat(img.getRGB(60, 50) & 0x00ff00) + .named("img face #2 vertex (60, 50) green channel") + .isEqualTo(0x00ff00); + assertThat(img.getRGB(70, 60) & 0x00ff00) + .named("img face #2 vertex (70, 60) green channel") + .isEqualTo(0x00ff00); + assertThat(img.getRGB(50, 60) & 0x00ff00) + .named("img face #2 vertex (50, 60) green channel") + .isEqualTo(0x00ff00); + } +} diff --git a/vision/label/README.md b/vision/label/README.md new file mode 100644 index 00000000000..c22c399fa44 --- /dev/null +++ b/vision/label/README.md @@ -0,0 +1,38 @@ +# Google Cloud Vision API Java Image Labeling example + +## Download Maven + +This sample uses the [Apache Maven][maven] build system. Before getting started, be +sure to [download][maven-download] and [install][maven-install] it. When you use +Maven as described here, it will automatically download the needed client +libraries. + +[maven]: https://maven.apache.org +[maven-download]: https://maven.apache.org/download.cgi +[maven-install]: https://maven.apache.org/install.html + +## Setup + +* Create a project with the [Google Cloud Console][cloud-console], and enable + the [Vision API][vision-api]. +* Set up your environment with [Application Default Credentials][adc]. For + example, from the Cloud Console, you might create a service account, + download its json credentials file, then set the appropriate environment + variable: + + ```bash + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your-project-credentials.json + ``` + +[cloud-console]: https://console.cloud.google.com +[vision-api]: https://console.cloud.google.com/apis/api/vision.googleapis.com/overview?project=_ +[adc]: https://cloud.google.com/docs/authentication#developer_workflow + +## Run the sample + +To build and run the sample: + +```bash +mvn clean compile assembly:single +java -cp target/vision-label-1.0-SNAPSHOT-jar-with-dependencies.jar com.google.cloud.vision.samples.label.LabelApp data/cat.jpg +``` diff --git a/vision/label/data/bad.txt b/vision/label/data/bad.txt new file mode 100644 index 00000000000..d03a5a96367 --- /dev/null +++ b/vision/label/data/bad.txt @@ -0,0 +1 @@ +I am not an image. Labelling shouldn't work on me. diff --git a/vision/label/data/cat.jpg b/vision/label/data/cat.jpg new file mode 100644 index 00000000000..76af906f0a3 Binary files /dev/null and b/vision/label/data/cat.jpg differ diff --git a/vision/label/data/faulkner.jpg b/vision/label/data/faulkner.jpg new file mode 100644 index 00000000000..93b8ac3ad2f Binary files /dev/null and b/vision/label/data/faulkner.jpg differ diff --git a/vision/label/pom.xml b/vision/label/pom.xml new file mode 100644 index 00000000000..ed2a3e08dff --- /dev/null +++ b/vision/label/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + jar + 1.0-SNAPSHOT + com.google.cloud.vision.samples + vision-label + + + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + + + com.google.apis + google-api-services-vision + v1-rev2-1.21.0 + + + com.google.api-client + google-api-client + 1.21.0 + + + com.google.guava + guava + 19.0 + + + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 0.28 + test + + + + javax.servlet + javax.servlet-api + 3.1.0 + test + + + + + + org.apache.maven.plugins + 3.3 + maven-compiler-plugin + + 1.7 + 1.7 + + + + maven-assembly-plugin + + + + com.google.cloud.vision.samples.label.LabelApp + + + + jar-with-dependencies + + + + + + diff --git a/vision/label/src/main/java/com/google/cloud/vision/samples/label/LabelApp.java b/vision/label/src/main/java/com/google/cloud/vision/samples/label/LabelApp.java new file mode 100644 index 00000000000..2a460a6d310 --- /dev/null +++ b/vision/label/src/main/java/com/google/cloud/vision/samples/label/LabelApp.java @@ -0,0 +1,148 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.label; + +// [BEGIN import_libraries] +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.vision.v1.Vision; +import com.google.api.services.vision.v1.VisionScopes; +import com.google.api.services.vision.v1.model.AnnotateImageRequest; +import com.google.api.services.vision.v1.model.AnnotateImageResponse; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesRequest; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesResponse; +import com.google.api.services.vision.v1.model.EntityAnnotation; +import com.google.api.services.vision.v1.model.Feature; +import com.google.api.services.vision.v1.model.Image; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.List; +// [END import_libraries] + +/** + * A sample application that uses the Vision API to label an image. + */ +@SuppressWarnings("serial") +public class LabelApp { + /** + * Be sure to specify the name of your application. If the application name is {@code null} or + * blank, the application will log a warning. Suggested format is "MyCompany-ProductName/1.0". + */ + private static final String APPLICATION_NAME = "Google-VisionLabelSample/1.0"; + + private static final int MAX_LABELS = 3; + + // [START run_application] + /** + * Annotates an image using the Vision API. + */ + public static void main(String[] args) throws IOException, GeneralSecurityException { + if (args.length != 1) { + System.err.println("Missing imagePath argument."); + System.err.println("Usage:"); + System.err.printf("\tjava %s imagePath\n", LabelApp.class.getCanonicalName()); + System.exit(1); + } + Path imagePath = Paths.get(args[0]); + + LabelApp app = new LabelApp(getVisionService()); + printLabels(System.out, imagePath, app.labelImage(imagePath, MAX_LABELS)); + } + + /** + * Prints the labels received from the Vision API. + */ + public static void printLabels(PrintStream out, Path imagePath, List labels) { + out.printf("Labels for image %s:\n", imagePath); + for (EntityAnnotation label : labels) { + out.printf( + "\t%s (score: %.3f)\n", + label.getDescription(), + label.getScore()); + } + if (labels.isEmpty()) { + out.println("\tNo labels found."); + } + } + // [END run_application] + + // [START authenticate] + /** + * Connects to the Vision API using Application Default Credentials. + */ + public static Vision getVisionService() throws IOException, GeneralSecurityException { + GoogleCredential credential = + GoogleCredential.getApplicationDefault().createScoped(VisionScopes.all()); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + return new Vision.Builder(GoogleNetHttpTransport.newTrustedTransport(), jsonFactory, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + // [END authenticate] + + private final Vision vision; + + /** + * Constructs a {@link LabelApp} which connects to the Vision API. + */ + public LabelApp(Vision vision) { + this.vision = vision; + } + + /** + * Gets up to {@code maxResults} labels for an image stored at {@code path}. + */ + public List labelImage(Path path, int maxResults) throws IOException { + // [START construct_request] + byte[] data = Files.readAllBytes(path); + + AnnotateImageRequest request = + new AnnotateImageRequest() + .setImage(new Image().encodeContent(data)) + .setFeatures(ImmutableList.of( + new Feature() + .setType("LABEL_DETECTION") + .setMaxResults(maxResults))); + Vision.Images.Annotate annotate = + vision.images() + .annotate(new BatchAnnotateImagesRequest().setRequests(ImmutableList.of(request))); + // Due to a bug: requests to Vision API containing large images fail when GZipped. + // annotate.setDisableGZipContent(true); + // [END construct_request] + + // [START parse_response] + BatchAnnotateImagesResponse batchResponse = annotate.execute(); + assert batchResponse.getResponses().size() == 1; + AnnotateImageResponse response = batchResponse.getResponses().get(0); + if (response.getLabelAnnotations() == null) { + throw new IOException( + response.getError() != null + ? response.getError().getMessage() + : "Unknown error getting image annotations"); + } + return response.getLabelAnnotations(); + // [END parse_response] + } +} diff --git a/vision/label/src/test/java/com/google/cloud/vision/samples/label/LabelAppIT.java b/vision/label/src/test/java/com/google/cloud/vision/samples/label/LabelAppIT.java new file mode 100644 index 00000000000..b508dead258 --- /dev/null +++ b/vision/label/src/test/java/com/google/cloud/vision/samples/label/LabelAppIT.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.label; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.services.vision.v1.model.EntityAnnotation; +import com.google.common.collect.ImmutableSet; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; + +/** + * Integration (system) tests for {@link LabelApp}. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class LabelAppIT { + private static final int MAX_LABELS = 3; + + private LabelApp appUnderTest; + + @Before public void setUp() throws Exception { + appUnderTest = new LabelApp(LabelApp.getVisionService()); + } + + @Test public void labelImage_cat_returnsCatDescription() throws Exception { + List labels = + appUnderTest.labelImage(Paths.get("data/cat.jpg"), MAX_LABELS); + + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (EntityAnnotation label : labels) { + builder.add(label.getDescription()); + } + ImmutableSet descriptions = builder.build(); + + assertThat(descriptions).named("cat.jpg labels").contains("cat"); + } + + @Test public void labelImage_badImage_throwsException() throws Exception { + try { + appUnderTest.labelImage(Paths.get("data/bad.txt"), MAX_LABELS); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(expected.getMessage().toLowerCase()) + .named("IOException message") + .contains("malformed request"); + } + } +} diff --git a/vision/label/src/test/java/com/google/cloud/vision/samples/label/LabelAppTest.java b/vision/label/src/test/java/com/google/cloud/vision/samples/label/LabelAppTest.java new file mode 100644 index 00000000000..b6f54653801 --- /dev/null +++ b/vision/label/src/test/java/com/google/cloud/vision/samples/label/LabelAppTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.label; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.services.vision.v1.model.EntityAnnotation; +import com.google.common.collect.ImmutableList; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.file.Paths; + +/** + * Unit tests for {@link LabelApp}. + */ +@RunWith(JUnit4.class) +public class LabelAppTest { + + @Test public void printLabels_emptyList_printsNoLabelsFound() throws Exception { + // Arrange + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + + // Act + LabelApp.printLabels( + out, Paths.get("path/to/some/image.jpg"), ImmutableList.of()); + + // Assert + assertThat(bout.toString()).contains("No labels found."); + } + + @Test public void printLabels_manyLabels_printsLabels() throws Exception { + // Arrange + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + ImmutableList labels = + ImmutableList.of( + new EntityAnnotation().setDescription("dog").setScore(0.7564f), + new EntityAnnotation().setDescription("husky").setScore(0.67891f), + new EntityAnnotation().setDescription("poodle").setScore(0.1233f)); + + // Act + LabelApp.printLabels(out, Paths.get("path/to/some/image.jpg"), labels); + + // Assert + String got = bout.toString(); + assertThat(got).contains("dog (score: 0.756)"); + assertThat(got).contains("husky (score: 0.679)"); + assertThat(got).contains("poodle (score: 0.123)"); + } +} diff --git a/vision/landmark-detection/README.md b/vision/landmark-detection/README.md new file mode 100644 index 00000000000..f04b6d99b26 --- /dev/null +++ b/vision/landmark-detection/README.md @@ -0,0 +1,61 @@ +# Google Cloud Vision API Java Landmark Detection example + +This sample takes in the URI for an object in Google Cloud Storage, and +identifies the landmark pictured in it. + +## Download Maven + +This sample uses the [Apache Maven][maven] build system. Before getting started, be +sure to [download][maven-download] and [install][maven-install] it. When you use +Maven as described here, it will automatically download the needed client +libraries. + +[maven]: https://maven.apache.org +[maven-download]: https://maven.apache.org/download.cgi +[maven-install]: https://maven.apache.org/install.html + +## Setup +* Create a project with the [Google Cloud Console][cloud-console], and enable + the [Vision API][vision-api]. +* Set up your environment with [Application Default Credentials][adc]. For + example, from the Cloud Console, you might create a service account, + download its json credentials file, then set the appropriate environment + variable: + + ```bash + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your-project-credentials.json + ``` + +* Upload an image to [Google Cloud Storage][gcs] (for example, using + [gsutil][gsutil] or the [GUI][gcs-browser]), and make sure the image is + accessible to the default credentials you set up. Note that by default, + Cloud Storage buckets and their associated objects are readable by service + accounts within the same project. For example, if you're using gsutil, you + might do: + + ```bash + gsutil mb gs:// + gsutil cp landmark.jpg gs:///landmark.jpg + # This step is unnecessary with default permissions, but for completeness, + # explicitly give the service account access to the image. This email can + # be found by running: + # `grep client_email /path/to/your-project-credentials.json` + gsutil acl ch -u :R \ + gs:///landmark.jpg + ``` + +[cloud-console]: https://console.cloud.google.com +[vision-api]: https://console.cloud.google.com/apis/api/vision.googleapis.com/overview?project=_ +[adc]: https://cloud.google.com/docs/authentication#developer_workflow +[gcs]: https://cloud.google.com/storage/docs/overview +[gsutil]: https://cloud.google.com/storage/docs/gsutil +[gcs-browser]: https://console.cloud.google.com/storage/browser?project=_ + +## Run the sample + +To build and run the sample, run the following from this directory: + +```bash +mvn clean compile assembly:single +java -cp target/vision-landmark-detection-1.0-SNAPSHOT-jar-with-dependencies.jar com.google.cloud.vision.samples.landmarkdetection.DetectLandmark "gs://your-project-bucket/landmark.jpg" +``` diff --git a/vision/landmark-detection/pom.xml b/vision/landmark-detection/pom.xml new file mode 100644 index 00000000000..1486d0125ed --- /dev/null +++ b/vision/landmark-detection/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + jar + 1.0-SNAPSHOT + com.google.cloud.vision.samples + vision-landmark-detection + + + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + + + com.google.apis + google-api-services-vision + v1-rev2-1.21.0 + + + com.google.api-client + google-api-client + 1.21.0 + + + com.google.guava + guava + 19.0 + + + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 0.28 + test + + + + javax.servlet + javax.servlet-api + 3.1.0 + test + + + + + + org.apache.maven.plugins + 3.3 + maven-compiler-plugin + + 1.7 + 1.7 + + + + maven-assembly-plugin + + + + com.google.cloud.vision.samples.landmarkdetection.DetectLandmark + + + + jar-with-dependencies + + + + + + diff --git a/vision/landmark-detection/src/main/java/com/google/cloud/vision/samples/landmarkdetection/DetectLandmark.java b/vision/landmark-detection/src/main/java/com/google/cloud/vision/samples/landmarkdetection/DetectLandmark.java new file mode 100644 index 00000000000..6d67e296699 --- /dev/null +++ b/vision/landmark-detection/src/main/java/com/google/cloud/vision/samples/landmarkdetection/DetectLandmark.java @@ -0,0 +1,131 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.landmarkdetection; + +// [BEGIN import_libraries] +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.vision.v1.Vision; +import com.google.api.services.vision.v1.VisionScopes; +import com.google.api.services.vision.v1.model.AnnotateImageRequest; +import com.google.api.services.vision.v1.model.AnnotateImageResponse; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesRequest; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesResponse; +import com.google.api.services.vision.v1.model.EntityAnnotation; +import com.google.api.services.vision.v1.model.Feature; +import com.google.api.services.vision.v1.model.Image; +import com.google.api.services.vision.v1.model.ImageSource; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; +// [END import_libraries] + +/** + * A sample application that uses the Vision API to detect landmarks in an image that is hosted on + * Google Cloud Storage. + */ +@SuppressWarnings("serial") +public class DetectLandmark { + /** + * Be sure to specify the name of your application. If the application name is {@code null} or + * blank, the application will log a warning. Suggested format is "MyCompany-ProductName/1.0". + */ + private static final String APPLICATION_NAME = "Google-VisionDetectLandmark/1.0"; + + private static final int MAX_RESULTS = 4; + + // [START run_application] + /** + * Annotates an image using the Vision API. + */ + public static void main(String[] args) throws IOException, GeneralSecurityException { + if (args.length != 1) { + System.err.println("Usage:"); + System.err.printf("\tjava %s gs:///\n", + DetectLandmark.class.getCanonicalName()); + System.exit(1); + } else if (!args[0].toLowerCase().startsWith("gs://")) { + System.err.println("Google Cloud Storage url must start with 'gs://'."); + System.exit(1); + } + + DetectLandmark app = new DetectLandmark(getVisionService()); + List landmarks = app.identifyLandmark(args[0], MAX_RESULTS); + System.out.printf("Found %d landmark%s\n", landmarks.size(), landmarks.size() == 1 ? "" : "s"); + for (EntityAnnotation annotation : landmarks) { + System.out.printf("\t%s\n", annotation.getDescription()); + } + } + // [END run_application] + + // [START authenticate] + /** + * Connects to the Vision API using Application Default Credentials. + */ + public static Vision getVisionService() throws IOException, GeneralSecurityException { + GoogleCredential credential = + GoogleCredential.getApplicationDefault().createScoped(VisionScopes.all()); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + return new Vision.Builder(GoogleNetHttpTransport.newTrustedTransport(), jsonFactory, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + // [END authenticate] + + // [START detect_gcs_object] + private final Vision vision; + + /** + * Constructs a {@link DetectLandmark} which connects to the Vision API. + */ + public DetectLandmark(Vision vision) { + this.vision = vision; + } + + /** + * Gets up to {@code maxResults} landmarks for an image stored at {@code uri}. + */ + public List identifyLandmark(String uri, int maxResults) throws IOException { + AnnotateImageRequest request = + new AnnotateImageRequest() + .setImage(new Image().setSource( + new ImageSource().setGcsImageUri(uri))) + .setFeatures(ImmutableList.of( + new Feature() + .setType("LANDMARK_DETECTION") + .setMaxResults(maxResults))); + Vision.Images.Annotate annotate = + vision.images() + .annotate(new BatchAnnotateImagesRequest().setRequests(ImmutableList.of(request))); + + BatchAnnotateImagesResponse batchResponse = annotate.execute(); + assert batchResponse.getResponses().size() == 1; + AnnotateImageResponse response = batchResponse.getResponses().get(0); + if (response.getLandmarkAnnotations() == null) { + throw new IOException( + response.getError() != null + ? response.getError().getMessage() + : "Unknown error getting image annotations"); + } + return response.getLandmarkAnnotations(); + } + // [END detect_gcs_object] +} diff --git a/vision/landmark-detection/src/test/java/com/google/cloud/vision/samples/landmarkdetection/DetectLandmarkIT.java b/vision/landmark-detection/src/test/java/com/google/cloud/vision/samples/landmarkdetection/DetectLandmarkIT.java new file mode 100644 index 00000000000..df73a40a3f6 --- /dev/null +++ b/vision/landmark-detection/src/test/java/com/google/cloud/vision/samples/landmarkdetection/DetectLandmarkIT.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.landmarkdetection; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.services.vision.v1.model.EntityAnnotation; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.util.List; + +/** + * Integration (system) tests for {@link DetectLandmark}. + **/ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class DetectLandmarkIT { + private static final int MAX_RESULTS = 3; + private static final String LANDMARK_URI = "gs://cloud-samples-tests/vision/water.jpg"; + private static final String PRIVATE_LANDMARK_URI = + "gs://cloud-samples-tests/vision/water-private.jpg"; + + private DetectLandmark appUnderTest; + + @Before public void setUp() throws Exception { + appUnderTest = new DetectLandmark(DetectLandmark.getVisionService()); + } + + @Test public void identifyLandmark_withLandmark_returnsKnownLandmark() throws Exception { + List landmarks = appUnderTest.identifyLandmark(LANDMARK_URI, MAX_RESULTS); + + assertThat(landmarks).named("water.jpg landmarks").isNotEmpty(); + assertThat(landmarks.get(0).getDescription()) + .named("water.jpg landmark #0 description") + .isEqualTo("Taitung, Famous Places \"up the water flow\" marker"); + } + + @Test public void identifyLandmark_noImage_throwsNotFound() throws Exception { + try { + appUnderTest.identifyLandmark(LANDMARK_URI + "/nonexistent.jpg", MAX_RESULTS); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(expected.getMessage()).named("IOException message").contains("OBJECT_NOT_FOUND"); + } + } + + @Test public void identifyLandmark_noImage_throwsForbidden() throws Exception { + try { + appUnderTest.identifyLandmark(PRIVATE_LANDMARK_URI, MAX_RESULTS); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(expected.getMessage()).named("IOException message").contains("ACCESS_DENIED"); + } + } +} diff --git a/vision/text/.gitignore b/vision/text/.gitignore new file mode 100644 index 00000000000..6507e175abc --- /dev/null +++ b/vision/text/.gitignore @@ -0,0 +1 @@ +en-token.bin diff --git a/vision/text/README.md b/vision/text/README.md new file mode 100644 index 00000000000..8e7cb5550e3 --- /dev/null +++ b/vision/text/README.md @@ -0,0 +1,55 @@ +# Text Detection using the Vision API + +This sample requires Java 8. + +## Download Maven + +This sample uses the [Apache Maven][maven] build system. Before getting started, be +sure to [download][maven-download] and [install][maven-install] it. When you use +Maven as described here, it will automatically download the needed client +libraries. + +[maven]: https://maven.apache.org +[maven-download]: https://maven.apache.org/download.cgi +[maven-install]: https://maven.apache.org/install.html + +## Introduction + +This example uses the [Cloud Vision API](https://cloud.google.com/vision/) to +detect text within images, stores this text in an index, and then lets you +query this index. + +## Initial Setup + +### Install and Start Up a Redis server + +This example uses a [redis](http://redis.io/) server, which must be up and +running before you start the indexing. To install Redis, follow the +instructions on the [download page](http://redis.io/download), or install via a +package manager like [homebrew](http://brew.sh/) or `apt-get` as appropriate +for your OS. + +The example assumes that the server is running on `localhost`, on the default +port, and it uses [redis +dbs](http://www.rediscookbook.org/multiple_databases.html) 0 and 1 for its data. +Edit the example code before you start if your redis settings are different. + +### Set up OpenNLP + +Download Tokenizer data and save it to this directory. + + wget http://opennlp.sourceforge.net/models-1.5/en-token.bin + +## Run the sample + +To build and run the sample, run the jar from this directory. You can provide a +directory to index the text in all the images it contains. + + mvn clean compile assembly:single + java -cp target/vision-text-1.0-SNAPSHOT-jar-with-dependencies.jar com.google.cloud.vision.samples.text.TextApp data/ + +Once this builds the index, you can run the same command without the input path +to query the index. + + java -cp target/vision-text-1.0-SNAPSHOT-jar-with-dependencies.jar com.google.cloud.vision.samples.text.TextApp + diff --git a/vision/text/data/bonito.gif b/vision/text/data/bonito.gif new file mode 100644 index 00000000000..bea0b6ebd45 Binary files /dev/null and b/vision/text/data/bonito.gif differ diff --git a/vision/text/data/mountain.jpg b/vision/text/data/mountain.jpg new file mode 100644 index 00000000000..f9505df38fe Binary files /dev/null and b/vision/text/data/mountain.jpg differ diff --git a/vision/text/data/no-text.jpg b/vision/text/data/no-text.jpg new file mode 100644 index 00000000000..8b77575de75 Binary files /dev/null and b/vision/text/data/no-text.jpg differ diff --git a/vision/text/data/not-a-meme.txt b/vision/text/data/not-a-meme.txt new file mode 100644 index 00000000000..b78a6eeb9cf --- /dev/null +++ b/vision/text/data/not-a-meme.txt @@ -0,0 +1,2 @@ +I am not a meme. Don't fail if you accidently include me in your Vision API +request, please. diff --git a/vision/text/data/sabertooth.gif b/vision/text/data/sabertooth.gif new file mode 100644 index 00000000000..2cee28eeb13 Binary files /dev/null and b/vision/text/data/sabertooth.gif differ diff --git a/vision/text/data/succulents.jpg b/vision/text/data/succulents.jpg new file mode 100644 index 00000000000..197fe6ac16a Binary files /dev/null and b/vision/text/data/succulents.jpg differ diff --git a/vision/text/data/sunbeamkitties.jpg b/vision/text/data/sunbeamkitties.jpg new file mode 100644 index 00000000000..b9a584ef554 Binary files /dev/null and b/vision/text/data/sunbeamkitties.jpg differ diff --git a/vision/text/data/wakeupcat.jpg b/vision/text/data/wakeupcat.jpg new file mode 100644 index 00000000000..139cf461eca Binary files /dev/null and b/vision/text/data/wakeupcat.jpg differ diff --git a/vision/text/pom.xml b/vision/text/pom.xml new file mode 100644 index 00000000000..ed1198e13d5 --- /dev/null +++ b/vision/text/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + jar + 1.0-SNAPSHOT + com.google.cloud.vision.samples + vision-text + + + + com.google.cloud + doc-samples + 1.0.0 + ../.. + + + + + com.google.apis + google-api-services-vision + v1-rev2-1.21.0 + + + com.google.api-client + google-api-client + 1.21.0 + + + com.google.guava + guava + 19.0 + + + com.google.auto.value + auto-value + 1.1 + provided + + + org.apache.opennlp + opennlp-tools + 1.6.0 + + + redis.clients + jedis + 2.8.0 + + + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 0.28 + test + + + + javax.servlet + javax.servlet-api + 3.1.0 + test + + + + + + org.apache.maven.plugins + 3.3 + maven-compiler-plugin + + 1.8 + 1.8 + + + + maven-assembly-plugin + + + + com.google.cloud.vision.samples.text.TextApp + + + + jar-with-dependencies + + + + + + diff --git a/vision/text/src/main/java/com/google/cloud/vision/samples/text/ImageText.java b/vision/text/src/main/java/com/google/cloud/vision/samples/text/ImageText.java new file mode 100644 index 00000000000..0b4adb4d85e --- /dev/null +++ b/vision/text/src/main/java/com/google/cloud/vision/samples/text/ImageText.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.text; + +import com.google.api.services.vision.v1.model.EntityAnnotation; +import com.google.api.services.vision.v1.model.Status; +import com.google.auto.value.AutoValue; + +import java.nio.file.Path; +import java.util.List; + +import javax.annotation.Nullable; + +/** + * A data object for mapping text to file paths. + */ +@AutoValue +abstract class ImageText { + + public static Builder builder() { + return new AutoValue_ImageText.Builder(); + } + + public abstract Path path(); + + public abstract List textAnnotations(); + + @Nullable + public abstract Status error(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder path(Path path); + + public abstract Builder textAnnotations(List ts); + + public abstract Builder error(@Nullable Status err); + + public abstract ImageText build(); + } +} diff --git a/vision/text/src/main/java/com/google/cloud/vision/samples/text/Index.java b/vision/text/src/main/java/com/google/cloud/vision/samples/text/Index.java new file mode 100644 index 00000000000..4c739a66dfd --- /dev/null +++ b/vision/text/src/main/java/com/google/cloud/vision/samples/text/Index.java @@ -0,0 +1,181 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.text; + +import com.google.common.collect.ImmutableSet; + +import opennlp.tools.stemmer.Stemmer; +import opennlp.tools.tokenize.Tokenizer; +import opennlp.tools.tokenize.TokenizerModel; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.stream.Stream; + +/** + * An inverted index using Redis. + * + *

The {@code Index} indexes the files in which each keyword stem was found and supports queries + * on the index. + */ +public class Index { + private static final int TOKEN_DB = 0; + private static final int DOCS_DB = 1; + + /** + * Parses tokenizer data and creates a tokenizer. + */ + public static TokenizerModel getEnglishTokenizerMeModel() throws IOException { + try (InputStream modelIn = new FileInputStream("en-token.bin")) { + return new TokenizerModel(modelIn); + } + } + + /** + * Creates a Redis connection pool. + */ + public static JedisPool getJedisPool() { + return new JedisPool(new JedisPoolConfig(), "localhost"); + } + + private final Tokenizer tokenizer; + private final Stemmer stemmer; + private final JedisPool pool; + + /** + * Constructs a connection to the index. + */ + public Index(Tokenizer tokenizer, Stemmer stemmer, JedisPool pool) { + this.tokenizer = tokenizer; + this.stemmer = stemmer; + this.pool = pool; + } + + /** + * Prints {@code words} information from the index. + */ + public void printLookup(Iterable words) { + ImmutableSet hits = lookup(words); + if (hits.size() == 0) { + System.out.print("No hits found.\n\n"); + } + for (String document : hits) { + String text = ""; + try (Jedis jedis = pool.getResource()) { + jedis.select(DOCS_DB); + text = jedis.get(document); + } + System.out.printf("***Image %s has text:\n%s\n", document, text); + } + } + + /** + * Looks up the set of documents containing each word. Returns the intersection of these. + */ + public ImmutableSet lookup(Iterable words) { + HashSet documents = null; + try (Jedis jedis = pool.getResource()) { + jedis.select(TOKEN_DB); + for (String word : words) { + word = stemmer.stem(word.toLowerCase()).toString(); + if (documents == null) { + documents = new HashSet(); + documents.addAll(jedis.smembers(word)); + } else { + documents.retainAll(jedis.smembers(word)); + } + } + } + if (documents == null) { + return ImmutableSet.of(); + } + return ImmutableSet.copyOf(documents); + } + + /** + * Checks if the document at {@code path} needs to be processed. + */ + public boolean isDocumentUnprocessed(Path path) { + try (Jedis jedis = pool.getResource()) { + jedis.select(DOCS_DB); + String result = jedis.get(path.toString()); + if (result == null) { + return true; + } + if (result.equals("")) { + System.out.printf("File %s was already checked, and contains no text.\n", path); + return false; + } + System.out.printf("%s already added to index.\n", path); + return false; + } + } + + /** + * Extracts all tokens from a {@code document} as a stream. + */ + public Stream extractTokens(Word document) { + Stream.Builder output = Stream.builder(); + String[] words = tokenizer.tokenize(document.word()); + // Ensure we track empty documents throughout so that they are not reprocessed. + if (words.length == 0) { + output.add(Word.builder().path(document.path()).word("").build()); + return output.build(); + } + for (int i = 0; i < words.length; i++) { + output.add(Word.builder().path(document.path()).word(words[i]).build()); + } + return output.build(); + } + + /** + * Extracts the stem from a {@code word}. + */ + public Word stem(Word word) { + return Word.builder().path(word.path()).word(stemmer.stem(word.word()).toString()).build(); + } + + /** + * Adds a {@code document} to the index. + */ + public void addDocument(Word document) { + try (Jedis jedis = pool.getResource()) { + jedis.select(DOCS_DB); + jedis.set(document.path().toString(), document.word()); + } + extractTokens(document) + .map(this::stem) + .forEach(this::add); + } + + /** + * Adds a {@code word} to the index. + */ + public void add(Word word) { + try (Jedis jedis = pool.getResource()) { + jedis.select(TOKEN_DB); + jedis.sadd(word.word().toLowerCase(), word.path().toString()); + } + } +} diff --git a/vision/text/src/main/java/com/google/cloud/vision/samples/text/TextApp.java b/vision/text/src/main/java/com/google/cloud/vision/samples/text/TextApp.java new file mode 100644 index 00000000000..1ca841069ef --- /dev/null +++ b/vision/text/src/main/java/com/google/cloud/vision/samples/text/TextApp.java @@ -0,0 +1,250 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.text; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.vision.v1.Vision; +import com.google.api.services.vision.v1.VisionScopes; +import com.google.api.services.vision.v1.model.AnnotateImageRequest; +import com.google.api.services.vision.v1.model.AnnotateImageResponse; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesRequest; +import com.google.api.services.vision.v1.model.BatchAnnotateImagesResponse; +import com.google.api.services.vision.v1.model.EntityAnnotation; +import com.google.api.services.vision.v1.model.Feature; +import com.google.api.services.vision.v1.model.Image; +import com.google.api.services.vision.v1.model.Status; +import com.google.common.base.MoreObjects; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import opennlp.tools.stemmer.snowball.SnowballStemmer; +import opennlp.tools.tokenize.TokenizerME; + +import redis.clients.jedis.JedisPool; + +import java.io.Console; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * A sample application that uses the Vision API to OCR text in an image. + */ +@SuppressWarnings("serial") +public class TextApp { + private static final int MAX_RESULTS = 6; + private static final int BATCH_SIZE = 10; + + /** + * Be sure to specify the name of your application. If the application name is {@code null} or + * blank, the application will log a warning. Suggested format is "MyCompany-ProductName/1.0". + */ + private static final String APPLICATION_NAME = "Google-VisionTextSample/1.0"; + + /** + * Connects to the Vision API using Application Default Credentials. + */ + public static Vision getVisionService() throws IOException, GeneralSecurityException { + GoogleCredential credential = + GoogleCredential.getApplicationDefault().createScoped(VisionScopes.all()); + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + return new Vision.Builder(GoogleNetHttpTransport.newTrustedTransport(), jsonFactory, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + + /** + * Annotates an image using the Vision API. + */ + public static void main(String[] args) throws IOException, GeneralSecurityException { + if (args.length > 1) { + System.err.println("Usage:"); + System.err.printf( + "\tjava %s inputDirectory\n", + TextApp.class.getCanonicalName()); + System.exit(1); + } + + JedisPool pool = Index.getJedisPool(); + try { + Index index = + new Index( + new TokenizerME(Index.getEnglishTokenizerMeModel()), + new SnowballStemmer(SnowballStemmer.ALGORITHM.ENGLISH), + pool); + TextApp app = new TextApp(TextApp.getVisionService(), index); + + if (args.length == 0) { + app.lookupWords(); + return; + } + Path inputPath = Paths.get(args[0]); + app.indexDirectory(inputPath); + } finally { + if (pool != null) { + pool.destroy(); + } + } + } + + private final Vision vision; + private final Index index; + + /** + * Constructs a {@code TextApp} using the {@link Vision} service. + */ + public TextApp(Vision vision, Index index) { + this.vision = vision; + this.index = index; + } + + /** + * Looks up words in the index that the user enters into the console. + */ + public void lookupWords() { + System.out.println("Entering word lookup mode."); + System.out + .println("To index a directory, add an input path argument when you run this command."); + System.out.println(); + + Console console = System.console(); + if (console == null) { + System.err.println("No console."); + System.exit(1); + } + + while (true) { + String words = + console.readLine("Enter word(s) (comma-separated, leave blank to exit): ").trim(); + if (words.equals("")) { + break; + } + index.printLookup(Splitter.on(',').split(words)); + } + } + + /** + * Indexes all the images in the {@code inputPath} directory for text. + */ + public void indexDirectory(Path inputPath) throws IOException { + List unprocessedImages = + Files.walk(inputPath) + .filter(Files::isRegularFile) + .filter(index::isDocumentUnprocessed) + .collect(Collectors.toList()); + Lists.partition(unprocessedImages, BATCH_SIZE) + .stream() + .map(this::detectText) + .flatMap(l -> l.stream()) + .filter(this::successfullyDetectedText) + .map(this::extractDescriptions) + .forEach(index::addDocument); + } + + /** + * Gets up to {@code maxResults} text annotations for images stored at {@code paths}. + */ + public ImmutableList detectText(List paths) { + ImmutableList.Builder requests = ImmutableList.builder(); + try { + for (Path path : paths) { + byte[] data; + data = Files.readAllBytes(path); + requests.add( + new AnnotateImageRequest() + .setImage(new Image().encodeContent(data)) + .setFeatures(ImmutableList.of( + new Feature() + .setType("TEXT_DETECTION") + .setMaxResults(MAX_RESULTS)))); + } + + Vision.Images.Annotate annotate = + vision.images() + .annotate(new BatchAnnotateImagesRequest().setRequests(requests.build())); + // Due to a bug: requests to Vision API containing large images fail when GZipped. + annotate.setDisableGZipContent(true); + BatchAnnotateImagesResponse batchResponse = annotate.execute(); + assert batchResponse.getResponses().size() == paths.size(); + + ImmutableList.Builder output = ImmutableList.builder(); + for (int i = 0; i < paths.size(); i++) { + Path path = paths.get(i); + AnnotateImageResponse response = batchResponse.getResponses().get(i); + output.add( + ImageText.builder() + .path(path) + .textAnnotations( + MoreObjects.firstNonNull( + response.getTextAnnotations(), + ImmutableList.of())) + .error(response.getError()) + .build()); + } + return output.build(); + } catch (IOException ex) { + // Got an exception, which means the whole batch had an error. + ImmutableList.Builder output = ImmutableList.builder(); + for (Path path : paths) { + output.add( + ImageText.builder() + .path(path) + .textAnnotations(ImmutableList.of()) + .error(new Status().setMessage(ex.getMessage())) + .build()); + } + return output.build(); + } + } + + /** + * Checks that there was not an error processing an {@code image}. + */ + public boolean successfullyDetectedText(ImageText image) { + if (image.error() != null) { + System.out.printf("Error reading %s:\n%s\n", image.path(), image.error().getMessage()); + return false; + } + return true; + } + + /** + * Extracts as a combinded string, all the descriptions from text annotations on an {@code image}. + */ + public Word extractDescriptions(ImageText image) { + String document = ""; + for (EntityAnnotation text : image.textAnnotations()) { + document += text.getDescription(); + } + if (document.equals("")) { + System.out.printf("%s had no discernible text.\n", image.path()); + } + // Output a progress indicator. + System.out.print('.'); + System.out.flush(); + return Word.builder().path(image.path()).word(document).build(); + } +} diff --git a/vision/text/src/main/java/com/google/cloud/vision/samples/text/Word.java b/vision/text/src/main/java/com/google/cloud/vision/samples/text/Word.java new file mode 100644 index 00000000000..0dfb4f24ed0 --- /dev/null +++ b/vision/text/src/main/java/com/google/cloud/vision/samples/text/Word.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.text; + +import com.google.auto.value.AutoValue; + +import java.nio.file.Path; + +/** + * A data object for mapping words to file paths. + */ +@AutoValue +abstract class Word { + + public static Builder builder() { + return new AutoValue_Word.Builder(); + } + + public abstract Path path(); + + public abstract String word(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder path(Path path); + + public abstract Builder word(String word); + + public abstract Word build(); + } +} diff --git a/vision/text/src/test/java/com/google/cloud/vision/samples/text/TextAppIT.java b/vision/text/src/test/java/com/google/cloud/vision/samples/text/TextAppIT.java new file mode 100644 index 00000000000..2e1867ebe7e --- /dev/null +++ b/vision/text/src/test/java/com/google/cloud/vision/samples/text/TextAppIT.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.text; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Integration (system) tests for {@link TextApp}. + **/ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:abbreviationaswordinname") +public class TextAppIT { + private TextApp appUnderTest; + + @Before public void setUp() throws Exception { + appUnderTest = new TextApp(TextApp.getVisionService(), null /* index */); + } + + @Test public void extractDescriptions_withImage_returnsText() throws Exception { + // Arrange + List image = + appUnderTest.detectText(ImmutableList.of(Paths.get("data/wakeupcat.jpg"))); + + // Act + Word word = appUnderTest.extractDescriptions(image.get(0)); + + // Assert + assertThat(word.path().toString()) + .named("wakeupcat.jpg path") + .isEqualTo("data/wakeupcat.jpg"); + assertThat(word.word().toLowerCase()).named("wakeupcat.jpg word").contains("wake"); + assertThat(word.word().toLowerCase()).named("wakeupcat.jpg word").contains("up"); + assertThat(word.word().toLowerCase()).named("wakeupcat.jpg word").contains("human"); + } +} diff --git a/vision/text/src/test/java/com/google/cloud/vision/samples/text/TextAppTest.java b/vision/text/src/test/java/com/google/cloud/vision/samples/text/TextAppTest.java new file mode 100644 index 00000000000..9ada8e0ad7f --- /dev/null +++ b/vision/text/src/test/java/com/google/cloud/vision/samples/text/TextAppTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 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. + */ + +package com.google.cloud.vision.samples.text; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.Json; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.services.vision.v1.Vision; +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Unit tests for {@link TextApp}. + */ +@RunWith(JUnit4.class) +public class TextAppTest { + private TextApp appUnderTest; + + @Before public void setUp() throws Exception { + // Mock out the vision service for unit tests. + JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); + HttpTransport transport = new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() throws IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + response.setContentType(Json.MEDIA_TYPE); + response.setContent("{\"responses\": [{\"textAnnotations\": []}]}"); + return response; + } + }; + } + }; + Vision vision = new Vision(transport, jsonFactory, null); + + appUnderTest = new TextApp(vision, null /* index */); + } + + @Test public void detectText_withImage_returnsPath() throws Exception { + List image = + appUnderTest.detectText(ImmutableList.of(Paths.get("data/wakeupcat.jpg"))); + + assertThat(image.get(0).path().toString()) + .named("wakeupcat.jpg path") + .isEqualTo("data/wakeupcat.jpg"); + } +}