Skip to content

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:

  1. Download the SighthoundRestApiGateway.zip file and extract it to your preferred location. This action will create a directory named ./SighthoundRestApiGateway.

  2. 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:

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

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:

sudo docker run --rm --runtime=nvidia --gpus all nvidia/cuda:11.6.2-base-ubuntu20.04 nvidia-smi

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.

cat sighthound-keyfile.json | docker login -u _json_key --password-stdin us-central1-docker.pkg.dev

Run this command to pull the Docker image:

docker compose pull

Start the REST API Services

Open a terminal, cd into the ./SighthoundRestApiGateway/ folder, and run the following command to start the services:

docker compose up -d

If you have an NVIDIA GPU installed and properly configured, you can run the following command instead to enable GPU acceleration:

SIO_DOCKER_RUNTIME=nvidia docker compose up -d

View Service Logs

Run the following command from within the ./SighthoundRestApiGateway/ folder to follow the service logs:

docker compose logs -f

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.

REST API Gateway UI Demo

Request Format

POST http://localhost:8383/v11/images:annotate

Headers

Content-Type: application/json

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"
        }
      ]
    }
  ]
}