-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fcb2b86
commit b7617bd
Showing
17 changed files
with
757 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
v1.5.4 | ||
v1.5.5 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
136
deployment-examples/SIOOnDemandAnalytics/clients/OnDemandTest.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
|
20 changes: 20 additions & 0 deletions
20
deployment-examples/SIOOnDemandAnalytics/config/analytics/boxFilter.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
] |
96 changes: 96 additions & 0 deletions
96
deployment-examples/SIOOnDemandAnalytics/config/analytics/extension.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
4 changes: 4 additions & 0 deletions
4
deployment-examples/SIOOnDemandAnalytics/config/analytics/extensionConfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"prefix" : "rtsp", | ||
"outputFolder" : "/tmp/runvol/live" | ||
} |
40 changes: 40 additions & 0 deletions
40
deployment-examples/SIOOnDemandAnalytics/config/analytics/pipelines.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
Oops, something went wrong.