Skip to content

Commit

Permalink
v1.5.5
Browse files Browse the repository at this point in the history
  • Loading branch information
actions-user committed Feb 2, 2024
1 parent fcb2b86 commit b7617bd
Show file tree
Hide file tree
Showing 17 changed files with 757 additions and 5 deletions.
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release Notes

## v1.5.5
- Initial version of on-demand analytics sample
- Sighthound REST API Gateway - Docker Compose Updates
- Fix MCPEvents logic
- Update SIO version
- sh-services: bug fix: ensure folders are created before interacting with them

## v1.5.4
- Add more sio examples and rename them
- Fix MCPEvents example
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.5.4
v1.5.5
24 changes: 24 additions & 0 deletions deployment-examples/SIOOnDemandAnalytics/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM ubuntu:20.04

ARG PYTHONVER=3

RUN apt-get update &&\
apt-get install -y python${PYTHONVER} &&\
apt-get install -y python${PYTHONVER}-dev &&\
apt-get install -y python${PYTHONVER}-distutils &&\
apt-get install -y python${PYTHONVER}-pip &&\
python${PYTHONVER} -m pip install --upgrade pip

WORKDIR /rest

COPY ./requirements.txt /rest


RUN pip${PYTHONVER} --no-cache-dir install -r requirements.txt

COPY ./restApi.py /rest/

EXPOSE 8080

CMD ["python3", "restApi.py"]

18 changes: 18 additions & 0 deletions deployment-examples/SIOOnDemandAnalytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# On-Demand Analytics REST API Sample

## Summary

This sample implements a REST API based analytics, allowing monitoring of an RTSP stream, and retrieving a most-recent image with associated analytics, as well as providing an inline image for analytics.

## Implementation

This sample is backed by two separate pipelines, executing within the same process. One pipeline is responsible for monitoring an RTSP stream, and emitting it's output to a shared storage volume, as well as maintaining the lifecycle of that output. The output is generated and managed with an extension module.

The other pipeline is a vanilla Folder Watch pipeline.

REST API Flask module is bringing it all together, providing a front-end implementation of the API. Upon request for an analytics, it finds the most recent file generated by the live pipeline, and serves JSON containing the image and relevant analytics. Another flavor of that API will serve an image/jpg, which is useful for debugging.

## Client

A sample client is implemented in `SIOOnDemandAnalytics/clients/OnDemandTest.py`.
The client can be ran using `python3 ./clients/OnDemandTest.py [-i inputImage] [-o outputFolder]`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions deployment-examples/SIOOnDemandAnalytics/clients/OnDemandTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import os
import requests
import base64
import json
import time
import argparse
import copy
import traceback

def send_image_and_get_result(api_url, image_path, output_path):
try:
# Read the image binary
with open(image_path, 'rb') as file:
image_data = file.read()

# Prepare the POST request with the image data
jsonReq = {
"id": 1,
"imageData": base64.b64encode(image_data).decode('utf-8')
}

send_request_and_get_result(api_url, jsonReq, output_path)
except Exception as e:
print(f"Error: {str(e)}\n{traceback.format_exc()}")



def send_request_and_get_result(api_url, json_req, save_path):
result = {}
try:
print(f"Sending request to {api_url}")
start = time.time()
if json_req:
response = requests.post(api_url, json=json_req)
else:
response = requests.get(api_url, json=json_req)

# Check if request was successful (status code 200)
if response.status_code == 200:
# Parse JSON response
result = response.json()
duration = time.time()-start

print(f"JSON response in {duration}s:")
if save_path:
print(f"Saving response to {save_path}.json")
with open(save_path+".json", 'w') as file:
file.write(json.dumps(result, indent=4))
imgData = result.get("imageData", None)
if not imgData:
imgData = result.get("streamDetail", [{}])[0].get("image", [{}])[0].get("imageData", None)
if imgData:
print(f"Saving image to {save_path}.jpg")
with open(save_path+".jpg", 'wb') as file:
file.write(base64.b64decode(imgData))
else:
resultCopy = copy.deepcopy(result)
imgData = resultCopy.get("imageData", None)
if imgData:
resultCopy["imageData"] = "removedForBrevity"
else:
imgData = resultCopy.get("streamDetail", [{}])[0].get("image", [{}])[0].get("imageData", None)
if imgData:
resultCopy["streamDetail"][0]["image"][0]["imageData"] = "removedForBrevity"
print(json.dumps(resultCopy, indent=4))
else:
print(f"Error:{response.status_code}")
except Exception as e:
print(f"Error: {str(e)}\n{traceback.format_exc()}")
return result


def main():
module_folder = os.path.dirname(os.path.abspath(__file__))

# Create ArgumentParser object
parser = argparse.ArgumentParser(description='Description of your program')
parser.add_argument('-i', '--input', type=str, help='Path to the input file')
parser.add_argument('-o', '--output', type=str, help='Path to the output file')
args = parser.parse_args()

# Access the values of the arguments
image_path = args.input
if not image_path:
image_path = os.path.join( module_folder, "2lps.jpg" )


api_url = 'http://127.0.0.1:8080/alpr'
result = send_request_and_get_result(api_url, None, None)
versions = result.get("version", None)
if versions is None or len(versions) < 1:
print(f"Unexpected result for {api_url}: {str(result)}")
return
version = versions[0]

api_url = f'http://127.0.0.1:8080/alpr/{version}'
result = send_request_and_get_result(api_url, None, None)
resource = result.get("resource", [])
if (not "locations" in resource) or \
(not "analyzeImage" in resource) or \
(not "annotateLive" in resource):
print(f"Unexpected result for {api_url}: {str(result)} res={str(resource)}")
return

api_url = f'http://127.0.0.1:8080/alpr/{version}/analyzeImage'
path = None if args.output is None else os.path.join (args.output, "analyzeImage")
send_image_and_get_result(api_url, image_path, path)

api_url = f'http://127.0.0.1:8080/alpr/{version}/locations'
result = send_request_and_get_result(api_url, None, None)
locations = result.get("location", [])
location = locations[0].get('id', None) if locations and len(locations) >= 1 else None
if not location:
print(f"Unexpected result for {api_url}: {str(result)}")
return

api_url = f'http://127.0.0.1:8080/alpr/{version}/locations/{location}/streams'
result = send_request_and_get_result(api_url, None, None)
streams = result.get("stream", [])
stream = streams[0].get('id', None) if streams and len(streams) >= 1 else None
if stream is None:
print(f"Unexpected result for {api_url}: {str(result)}")
return

api_url = f'http://127.0.0.1:8080/alpr/{version}/locations/{location}'
path = None if args.output is None else os.path.join (args.output, "liveImage")
result = send_request_and_get_result(api_url, None, path)

api_url = f'http://127.0.0.1:8080/alpr/{version}/locations/{location}/annotateLive'
path = None if args.output is None else os.path.join (args.output, "liveAnnotatedImage")
result = send_request_and_get_result(api_url, None, path)


if __name__ == "__main__":
main()

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"name": "plateBike_SizeFilter",
"type": "size",
"subtype": "dimension",
"max": 0,
"min": 10,
"classes": ["licenseplate", "motorbike"],
"debug": false
},
{
"name": "vehicle_SizeFilter",
"type": "size",
"subtype": "dimension",
"max": 0,
"min": 15,
"classes": ["car", "bus", "truck"],
"debug": false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#
# Sample SIO pipeline extension
#

import sys
import os
import json
import time
import glob
from PIL import Image


class SIOPlugin:
# ===========================================================
def __init__(self):
print(f"Creating SIO extension")
self.prefix = "unknown"
self.outputFolder = None
self.generatedFiles = []
self.maxOutput = 10

# ===========================================================
def clearFolder(self, folderPath, ext):
# Construct the path pattern to match all files in the folder
filesPattern = os.path.join(folderPath, '*.'+ext)

# Use glob to get a list of all file paths matching the pattern
toDelete = glob.glob(filesPattern)

# Iterate through the list and delete each file
for fp in toDelete:
try:
os.remove(fp)
print(f"Deleted: {fp}")
except Exception as e:
print(f"Error deleting {fp}: {e}")

# ===========================================================
# Remove old output
def trimOutput(self):
while len(self.generatedFiles) > self.maxOutput:
toDelete = self.generatedFiles.pop(0)
try:
name = os.path.join(self.outputFolder, toDelete)
print(f"Removing {name}")
os.remove(name+".jpg")
os.remove(name+".json")
except:
print(f"Failed to remove {name} jpg/json")

# ===========================================================
# Prepare the module using provided configuration
def configure(self, configJsonPath):
try:
with open(configJsonPath) as configJsonFile:
config = json.load(configJsonFile)
self.prefix = config["prefix"]
self.outputFolder = config["outputFolder"]
except:
print(f"Failed to load extension module configuration from {configJsonPath}")
raise
# Ensure a clean slate
os.makedirs(self.outputFolder, exist_ok=True)
self.clearFolder(self.outputFolder, "jpg")
self.clearFolder(self.outputFolder, "json")
print(f"Loaded extension {__name__}")

# ===========================================================
# Process the output - save the json and the image
def process(self, tick, frameDataStr, frame):
frameData = json.loads(frameDataStr)
t = int(time.time()*1000)
name = os.path.join(self.outputFolder, f'{t}-{tick}')

w = int(frameData.get("frameDimensions", {}).get("w", None))
h = int(frameData.get("frameDimensions", {}).get("h", None))
if w and h:
img = Image.frombuffer("RGB", (w, h), frame, 'raw')
print(f"Saving data in {name}")
img.save(name+".jpg")
with open(name+".json", 'w') as file:
file.write(frameDataStr)

self.generatedFiles.append(name)
self.trimOutput()

# always return the (potentially filtered or modified) output
return frameDataStr

# ===========================================================
# Finalize the module
def finalize(self):
print(f"{self.prefix} - Pipeline completed")
# Ensure a clean slate
self.clearFolder(self.outputFolder, "jpg")
self.clearFolder(self.outputFolder, "json")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"prefix" : "rtsp",
"outputFolder" : "/tmp/runvol/live"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"rtsp" : {
"pipeline" : "./share/pipelines/VehicleAnalytics/VehicleAnalyticsRTSP.yaml",
"restartPolicy" : "restart",
"parameters" : {
"VIDEO_IN" : "rtsp://live555_svc:554/Turn-01.mkv",
"boxFilterConfig" : "/config/analytics/boxFilter.json",
"detectionModel" : "gen7es",
"lptModel" : "gen7es",
"lptFilter" : "['eu', 'us']",
"lptMinConfidence" : "0.7",
"sourceId" : "rtsp-stream-1",
"lptPreferAccuracyToSpeed" : "false",
"fpsLimit" : "2",
"updateOnlyOnChange" : "false",
"splitMakeModel" : "true",
"extensionModules" : "/config/analytics/extension.py",
"extensionConfigurations" : "/config/analytics/extensionConfig.json"
}
},
"folderWatch" : {
"pipeline" : "./share/pipelines/VehicleAnalytics/VehicleAnalyticsFolderWatch.yaml",
"restartPolicy" : "restart",
"parameters" : {
"boxFilterConfig" : "/config/analytics/boxFilter.json",
"detectionModel" : "gen7es",
"lptModel" : "gen7es",
"lptFilter" : "['eu', 'us']",
"lptMinConfidence" : "0.7",
"sourceId" : "fw-stream-1",
"lptPreferAccuracyToSpeed" : "false",
"fpsLimit" : "2",
"updateOnlyOnChange" : "false",
"folderPath" : "/tmp/runvol/fw",
"folderPollInterval" : "100",
"folderRemoveSourceFiles" : "true",
"splitMakeModel": "true"
}
}
}
Loading

0 comments on commit b7617bd

Please sign in to comment.