Vehicle Analytics REST API - Docker¶
The Sighthound Vehicle Analytics REST API returns detection coordinates and annotations for all vehicles and license plates detected in an image. For vehicles, the annotations include make, model, color, and generation. For license plates, the annotations include the characters from the plate and region (e.g., Florida, Germany, etc.).
This deployment is comprised of 3 services: a REST gateway, SIO's Vehicle Analytics, and a browser-based UI demo. The gateway works alongside the SIO Analytics service container, using its folder watcher pipeline for processing and a folder shared between the two containers for data exchange. The UI demo can be used to quickly and easily upload images to the API and visualize the results.
This guide contains instructions on how to run the Sighthound Vehicle Analytics REST API using Docker Compose. If you prefer to use your own docker run
command or other orchestration methods, please refer to the docker-compose.yml
file for the necessary environment variables, configuration files, and volume mounting examples. More fine-tuning is possible both through docker configuration and SIO parameter modifications.
Contact support@sighthound.com with any questions.
Initial Setup¶
Project Files¶
Choose one of the following options to obtain the project files:
-
Download the SighthoundRestApiGateway.zip file and extract it to your preferred location. This action will create a directory named
./SighthoundRestApiGateway
. -
Clone the Sighthound Services GitHub repository. You will find the necessary project files in the
./deployment-examples/SighthoundRestApiGateway
directory.
Throughout this document, we'll refer to the directory containing the project files as ./SighthoundRestApiGateway
.
Additional Required Files
Before you can run the services, you will also need the sighthound-keyfile.json
and sighthound-license
files. These will be provided by Sighthound in sighthound-secrets.zip
.
- Extract these files and move them into the
./SighthoundRestApiGateway/config/
directory.
Docker¶
Ensure your system has a recent version of Docker Engine and Docker Compose. If you need to install Docker, you can follow the directions on Docker's site, or you can run the convenience script that Docker provides:
NVIDIA GPU¶
Running Vehicle Analytics on a computer with an NVIDIA GPU is optional but highly recommended for increased performance. Depending on the complexity of the scene and the number of objects detected, a system with an NVIDIA GPU can result in a 2x-15x speed increase.
If you are running on a computer without an NVIDIA GPU, you can skip this section and proceed to the Running the API section.
If your system has an NVIDIA GPU installed, install the latest drivers and install the NVIDIA Container Toolkit to give the Docker access to the GPU. You can confirm that the system is configured correctly by running the following command:
Running the API¶
Note
Depending on your machine's configuration, you may need to append sudo before each docker command in this document. Additionally, you may need to run docker-compose
instead of docker compose
if you have an older version.
Pull the Docker Image¶
To pull the Docker image from our private repository, you must log in to Docker using the sighthound-keyfile.json
.
Open a terminal, cd
into the ./SighthoundRestApiGateway/config/
folder, and run the following command. You should only need to do this once per machine where the API services will be running.
Run this command to pull the Docker image:
Start the REST API Services¶
Open a terminal, cd
into the ./SighthoundRestApiGateway/
folder, and run the following command to start the services:
If you have an NVIDIA GPU installed and properly configured, you can run the following command instead to enable GPU acceleration:
View Service Logs¶
Run the following command from within the ./SighthoundRestApiGateway/
folder to follow the service logs:
Upon first launch, the application will perform an initial health check and download the necessary models. The service is ready to accept requests when you see the initial health test passed, server ready
message:
23/05/19-15:14:59.952 INFO [main] running initial health test ...
23/05/19-15:15:06.436 INFO [main] initial health test passed, server ready
Note
If you have an NVIDIA GPU, the first launch will start a process that builds the NVIDIA GPU model cache as part of the initial health test. This may take several minutes to complete depending on your specific hardware configuration. During this time, API requests will not be served. Once it finishes, the initial health test will continue and log initial health test passed, server ready
when complete. This process should only happen once, and subsequent service restarts will not need to rebuild the cache unless your hardware changes or Sighthound's models are updated in future releases.
UI Demo¶
The default deployment contains a browser-based UI demo which allows you to quickly and easily submit images to the API by using HTTP/HTTPS links or by uploading files from your local computer. Once the containers are running, the UI demo will be available at http://localhost:8484. The browser's developer tools allow you to inspect the requests and responses to see how it's communicating with the Sighthound REST API Gateway.
We recommend using the UI Demo for development purposes only, and it should be removed from the docker-compose.yml before deploying to production.
Request Format¶
Headers¶
Body¶
There are two ways to send images to the Cloud API: by using a URI or base64-encoded content. See the image
object in the examples below.
Only JPG and PNG images are supported at this time. If your use case requires a different format, please contact us.
Image URI
{
"requests": [
{
"image": {
"source": {
"imageUri": "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
}
},
"features": [
{
"type": "VEHICLE_DETECTION"
},
{
"type": "LICENSE_PLATE_DETECTION"
}
]
}
]
}
Base64 Content
{
"requests": [
{
"image": {
"content": "/9j/4QAYR...truncatedBase64String...ZUcQP/2Q=="
},
"features": [
{
"type": "VEHICLE_DETECTION"
},
{
"type": "LICENSE_PLATE_DETECTION"
}
]
}
]
}
Examples¶
Note
The examples below assume the REST API gateway service is running on http://localhost:8383
. If the service runs on a different machine or you previously changed the port in the docker-compose.yml file, you must update the examples accordingly.
Request¶
curl --location --request POST 'http://localhost:8383/v11/images:annotate' \
--header 'Content-Type: application/json' \
--data-raw '{
"requests": [
{
"image": {
"source": {
"imageUri": "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
}
},
"features": [
{
"type": "VEHICLE_DETECTION"
},
{
"type": "LICENSE_PLATE_DETECTION"
}
]}
]}'
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
namespace Sighthound {
public class Feature {
public string type { get; set; }
}
public class Source {
public string imageUri { get; set; }
}
public class Image {
public Source source { get; set; }
}
public class Request {
public Image image { get; set; }
public List<Feature> features { get; set; }
}
public class BatchRequest {
public List<Request> requests { get; set; }
}
public class Example {
public static void Main() {
var apiUrl = "http://localhost:8383/v11/images:annotate";
var batchRequest = new BatchRequest {
requests = new List<Request>(
new Request[] {
new Request {
image = new Image {
source = new Source {
imageUri = "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
}
},
features = new List<Feature>(
new Feature[] {
new Feature {
type = "VEHICLE_DETECTION"
},
new Feature {
type = "LICENSE_PLATE_DETECTION"
}
}
)
}
}
)
};
string requestJson = JsonSerializer.Serialize(batchRequest);
byte[] requestData = Encoding.UTF8.GetBytes(requestJson);
var webRequest = (HttpWebRequest)WebRequest.Create(apiUrl);
webRequest.Method = "POST";
webRequest.Accept = "application/json";
webRequest.ContentType = "application/json";
webRequest.ContentLength = requestData.Length;
webRequest.GetRequestStream().Write(requestData, 0, requestData.Length);
using var webResponse = webRequest.GetResponse();
using var webStream = webResponse.GetResponseStream();
using var responseReader = new StreamReader(webStream);
var responseJson = responseReader.ReadToEnd();
var responseData = JsonDocument.Parse(responseJson);
var response = responseData.RootElement.GetProperty("responses")[0];
Console.WriteLine("detected " +
response.GetProperty("vehicleAnnotations").GetArrayLength() + " vehicles and " +
response.GetProperty("licenseplateAnnotations").GetArrayLength() + " license plates");
}
}
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type Source struct {
ImageUri string `json:"imageUri"`
}
type Image struct {
Source Source `json:"source"`
}
type Feature struct {
Type string `json:"type"`
}
type Request struct {
Image Image `json:"image"`
Features []Feature `json:"features"`
}
type BatchRequest struct {
Requests []Request `json:"requests"`
}
type ExampleResponse struct {
VehicleAnnotations []interface{} `json:"vehicleAnnotations"`
LicenseplateAnnotations []interface{} `json:"licenseplateAnnotations"`
}
type ExampleBatchResponse struct {
Responses []ExampleResponse `json:"responses"`
}
var apiUrl = "http://localhost:8383/v11/images:annotate"
func main() {
batchRequest := BatchRequest{
Requests: []Request{
{
Image: Image{
Source: Source{
ImageUri: "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg",
},
},
Features: []Feature{
{
Type: "VEHICLE_DETECTION",
},
{
Type: "LICENSE_PLATE_DETECTION",
},
},
},
},
}
httpRequestBody, _ := json.Marshal(batchRequest)
httpRequest, _ := http.NewRequest("POST", apiUrl, bytes.NewBuffer(httpRequestBody))
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("Accept", "application/json")
httpClient := &http.Client{}
httpResponse, error := httpClient.Do(httpRequest)
if error != nil {
fmt.Println("request failed:", error)
return
}
defer httpResponse.Body.Close()
if httpResponse.Status != "200 OK" {
fmt.Println("response error:", httpResponse.Status)
return
}
responseBody, _ := ioutil.ReadAll(httpResponse.Body)
var batchResponse ExampleBatchResponse
err := json.Unmarshal(responseBody, &batchResponse)
if err != nil {
fmt.Println("parse error:", err)
return
}
fmt.Println("detected",
len(batchResponse.Responses[0].VehicleAnnotations), "vehicles and",
len(batchResponse.Responses[0].LicenseplateAnnotations), "license plates")
}
import java.net.*;
import java.net.http.*;
import org.json.*;
class Example {
public static void main(String[] args) throws Exception {
System.setProperty("java.net.preferIPv6Addresses","true"); // for correct "localhost" resolution
var imageSource = new JSONObject();
imageSource.put("imageUri", "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg");
var image = new JSONObject();
image.put("source", imageSource);
var featureVehicleDetection = new JSONObject();
featureVehicleDetection.put("type", "VEHICLE_DETECTION");
var featureLicenseplateDetection = new JSONObject();
featureLicenseplateDetection.put("type", "LICENSE_PLATE_DETECTION");
var features = new JSONArray();
features.put(featureVehicleDetection);
features.put(featureLicenseplateDetection);
var request = new JSONObject();
request.put("image", image);
request.put("features", features);
var requests = new JSONArray();
requests.put(request);
var batchRequest = new JSONObject();
batchRequest.put("requests", requests);
var apiUrl = "http://localhost:8383/v11/images:annotate";
var httpRequest = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(batchRequest.toString()))
.build();
var httpClient = HttpClient.newHttpClient();
var httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
if (httpResponse.statusCode() == 200) {
var batchResponse = new JSONObject(httpResponse.body());
var response = batchResponse.getJSONArray("responses").getJSONObject(0);
System.out.printf("detected %d vehicles and %d license plates%n",
response.getJSONArray("vehicleAnnotations").length(),
response.getJSONArray("licenseplateAnnotations").length());
}
}
}
const apiUrl = "http://localhost:8383/v11/images:annotate";
const batchRequest = {
requests: [
{
image: {
source: {
imageUri: "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
}
},
features: [
{
type: "VEHICLE_DETECTION"
},
{
type: "LICENSE_PLATE_DETECTION"
}
]
}
]
};
const fetchResponse = await fetch(apiUrl, {
body: JSON.stringify(batchRequest),
headers: { "Content-Type": "application/json" },
method: "POST"
});
const batchResponse = await fetchResponse.json();
const response = batchResponse.responses[0];
console.log("detected",
response.vehicleAnnotations.length, "vehicles and",
response.licenseplateAnnotations.length, "license plates");
<?php
$apiUrl = "http://localhost:8383/v11/images:annotate";
$batchRequest = [
"requests" => [
[
"image" => [
"source" => [
"imageUri" => "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
]
],
"features" => [
[
"type" => "VEHICLE_DETECTION"
],
[
"type" => "LICENSE_PLATE_DETECTION"
]
]
]
]
];
$post = array(
'http' => array(
'method' => 'POST',
'content' => json_encode($batchRequest),
'header'=> "Content-Type: application/json\r\n" .
"Accept: application/json\r\n"
)
);
$context = stream_context_create($post);
$result = file_get_contents($apiUrl, false, $context);
$batchResponse = json_decode($result);
print 'detected ' .
count($batchResponse->{'responses'}[0]->{'vehicleAnnotations'}) . ' vehicles and ' .
count($batchResponse->{'responses'}[0]->{'licenseplateAnnotations'}) . ' license plates' .
PHP_EOL;
?>
import requests
apiUrl = "http://localhost:8383/v11/images:annotate"
batchRequest = {
"requests": [
{
"image": {
"source": {
"imageUri": "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
}
},
"features": [
{
"type": "VEHICLE_DETECTION"
},
{
"type": "LICENSE_PLATE_DETECTION"
}
]
}
]
}
batchResponse = requests.post(apiUrl, json = batchRequest).json()
response = batchResponse['responses'][0]
print('detected %r vehicles and %r license plates' %(
len(response['vehicleAnnotations']),
len(response['licenseplateAnnotations'])
))
require 'json'
require 'net/http'
apiUrl = 'http://localhost:8383/v11/images:annotate'
batchRequest = {
requests: [
{
image: {
source: {
imageUri: 'https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg'
}
},
features: [
{
type: 'VEHICLE_DETECTION'
},
{
type: 'LICENSE_PLATE_DETECTION'
}
]
}
]
}
response = Net::HTTP.post URI(apiUrl), batchRequest.to_json, 'Content-Type' => 'application/json'
batchResponse = JSON.parse(response.body)
vehicleCount = batchResponse['responses'][0]['vehicleAnnotations'].length()
licenseplateCount = batchResponse['responses'][0]['licenseplateAnnotations'].length()
puts "detected #{vehicleCount} vehicles and #{licenseplateCount} license plates"
import Foundation
let done = DispatchSemaphore(value: 0)
let apiUrl = URL(string: "http://localhost:8383/v11/images:annotate")!
let batchRequest: [String: Any] = [
"requests": [
[
"image": [
"source": [
"imageUri": "https://d1mfcqjbhp6mmy.cloudfront.net/vehicles/audi-sports-car-r8-marlene.jpeg"
]
],
"features": [
[
"type": "VEHICLE_DETECTION"
],
[
"type": "LICENSE_PLATE_DETECTION"
]
]
]
]
]
let batchRequestJson = try? JSONSerialization.data(withJSONObject: batchRequest)
var httpRequest = URLRequest(url: apiUrl)
httpRequest.httpMethod = "POST"
httpRequest.httpBody = batchRequestJson;
httpRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
httpRequest.setValue("application/json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: httpRequest) { (data: Data?, httpResponse, error) in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "unknown error")
return
}
let batchResponseJson = try? JSONSerialization.jsonObject(with: data, options: [])
let batchResponse = batchResponseJson as? [String: Any]
let responses = batchResponse!["responses"] as? [[String: [[String: Any]]]]
let response = responses![0]
print("detected \(response["vehicleAnnotations"]!.count) vehicles and \(response["licenseplateAnnotations"]!.count) license plates")
done.signal()
}
task.resume()
done.wait()
Response¶
{
"responses": [
{
"licenseplateAnnotations": [
{
"string": {
"value": "RTBB221",
"score": 0.86
},
"region": {
"value": "Germany",
"score": 0.86
},
"boundingPoly": {
"vertices": [
{
"x": 620,
"y": 450
},
{
"x": 738,
"y": 450
},
{
"x": 738,
"y": 478
},
{
"x": 620,
"y": 478
}
],
"normalizedVertices": [
{
"x": 0.6888888888888889,
"y": 0.7258064516129032
},
{
"x": 0.82,
"y": 0.7258064516129032
},
{
"x": 0.82,
"y": 0.7709677419354839
},
{
"x": 0.6888888888888889,
"y": 0.7709677419354839
}
]
},
"locale": "en-US",
"score": 0.86,
"mid": "/m/01jfm_",
"description": "License plate"
}
],
"vehicleAnnotations": [
{
"make": {
"value": "audi",
"score": 0.94
},
"model": {
"value": "r8",
"score": 0.94
},
"generation": {
"value": {
"start": 2007,
"end": 2014
},
"score": 0.94
},
"color": {
"value": "white",
"score": 0.94
},
"licenseplate": {
"$ref": "#/responses/0/licenseplateAnnotations/0"
},
"boundingPoly": {
"vertices": [
{
"x": 428,
"y": 287
},
{
"x": 833,
"y": 287
},
{
"x": 833,
"y": 556
},
{
"x": 428,
"y": 556
}
],
"normalizedVertices": [
{
"x": 0.47555555555555556,
"y": 0.4629032258064516
},
{
"x": 0.9255555555555556,
"y": 0.4629032258064516
},
{
"x": 0.9255555555555556,
"y": 0.896774193548387
},
{
"x": 0.47555555555555556,
"y": 0.896774193548387
}
]
},
"locale": "en-US",
"score": 0.94,
"mid": "/m/0k4j",
"description": "Car"
}
]
}
]
}