{ "cells": [ { "cell_type": "markdown", "id": "cffd59b9", "metadata": {}, "source": [ "# Multi object tracking(MOT) analysis Sample Application\n", "\n", "This notebook shows how to create an analysis app for Panorama using a pretrained PyTorch model." ] }, { "cell_type": "markdown", "id": "47498764", "metadata": {}, "source": [ "--- \n", "\n", "1. [Prerequisites](#Prerequisites)\n", "1. [Set up](#Set-up)\n", "1. [Import model](#Import-model)\n", "1. [Write and test app code](#Write-and-test-app-code-in-notebook)\n", "1. [Package app](#Package-app)\n", "1. [Deploy app to device](#Deploy-app-to-device)" ] }, { "cell_type": "markdown", "id": "8a2a3ae2", "metadata": {}, "source": [ "# Prerequisites" ] }, { "cell_type": "markdown", "id": "9aaddd01", "metadata": {}, "source": [ "1. In a terminal session on this Jupyter notebook server, run `aws configure`. This allows this notebook server to access Panorama resources and deploy applications on your behalf." ] }, { "cell_type": "markdown", "id": "b1c2f6f8", "metadata": {}, "source": [ "# Set up" ] }, { "cell_type": "markdown", "id": "352b4749", "metadata": {}, "source": [ "Import libraries for use with this notebook environment, you do not need these libraries when you write your application code." ] }, { "cell_type": "code", "execution_count": null, "id": "1183276e", "metadata": {}, "outputs": [], "source": [ "import sys\n", "import os\n", "import time\n", "import json\n", "\n", "import boto3\n", "import sagemaker\n", "\n", "import matplotlib.pyplot as plt\n", "from IPython.core.magic import register_cell_magic\n", "\n", "# configure matplotlib\n", "%matplotlib inline\n", "plt.rcParams[\"figure.figsize\"] = (20,20)\n", "\n", "# register custom magic command\n", "@register_cell_magic\n", "def save_cell(line, cell):\n", " 'Save python code block to a file'\n", " with open(line, 'wt') as fd:\n", " fd.write(cell)" ] }, { "cell_type": "markdown", "id": "00f12da5", "metadata": { "tags": [] }, "source": [ "## Notebook parameters\n", "Global constants that help the notebook create Panorama resources on your behalf." ] }, { "cell_type": "code", "execution_count": null, "id": "e548fe3a-937b-4a3b-a3fe-bc94accbb6d6", "metadata": {}, "outputs": [], "source": [ "# Device ID, should look like: device-oc66nax4cgzwhyuaeyifrqowue\n", "DEVICE_ID = input( 'DEVICE_ID (format: device-*)' ).strip()\n", "\n", "# Enter your S3 bucket info here\n", "S3_BUCKET = input( 'S3_BUCKET' ).strip()\n", "\n", "# Enter your desired AWS Panorama region\n", "AWS_REGION = input( 'AWS_REGION (e.g. us-east-1)' ).strip()\n", "\n", "# Enter application role to be deployed in panorama device (S3 and kinesis firehose required, secretsmanager and kinesis video are optional if want remote view)\n", "APPLICATION_ROLE = input( 'Application ROLE ARN' ).strip()\n", "\n", "# Precompiled sagemaker neo compatible yolox models are yolox_s_neo and yolox_m_neo\n", "ML_MODEL_FNAME = 'yolox_m_neo' " ] }, { "cell_type": "code", "execution_count": null, "id": "82721929", "metadata": { "tags": [] }, "outputs": [], "source": [ "model_package_name = 'YOLOXM_MODEL'\n", "model_data_shape = '{\"input0\":[1,3,640,640]}'\n", "\n", "# application name\n", "app_name = 'mot_analysis_app'\n", "\n", "## package names and node names\n", "code_package_name = 'MOT_ANALYSIS_CODE'\n", "camera_node_name = 'abstract_rtsp_media_source'\n", "\n", "container_asset_name = 'code_asset'\n", "\n", "# model node name, raw model path (without platform dependent suffics), and input data shape\n", "model_node_name = \"model_node\"\n", "model_file_basename = \"./models/\" + ML_MODEL_FNAME\n", "\n", "# video file path to simulate camera stream\n", "videoname = '../common/test_utility/videos/TownCentreXVID.avi'\n", "\n", "sys.path.insert( 0, os.path.abspath( \"../common/test_utility\" ) )\n", "import panorama_test_utility\n", "\n", "# instantiate boto3 clients\n", "s3_client = boto3.client('s3')\n", "panorama_client = boto3.client('panorama', region_name=AWS_REGION)\n", "\n", "# AWS account ID\n", "account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]" ] }, { "cell_type": "markdown", "id": "d1034c1e", "metadata": {}, "source": [ "## Set up application\n", "\n", "Every application uses the creator's AWS Account ID as the prefix to uniquely identifies the application resources. Running `panorama-cli import-application` replaces the generic account Id with your account Id." ] }, { "cell_type": "code", "execution_count": null, "id": "96ef4328", "metadata": {}, "outputs": [], "source": [ "!cd ./{app_name} && panorama-cli import-application" ] }, { "cell_type": "markdown", "id": "000e62b8", "metadata": { "tags": [] }, "source": [ "# Import model" ] }, { "cell_type": "markdown", "id": "b80a83d3", "metadata": {}, "source": [ "We need to compile and import the model twice. Once for testing with this notebook server and once for deploying to the Panorama device.\n", "\n", "While working with the Panorama sample code, we provide pretrained models for you to use. Locally, models are stored in `./models`. This step downloads the model artifacts from our Amazon S3 bucket to the local folder. If you want to use your own models, put your tar.gz file into the `./models` folder." ] }, { "cell_type": "markdown", "id": "8d57d4ea", "metadata": { "tags": [] }, "source": [ "### Prepare model for testing with notebook server" ] }, { "cell_type": "code", "execution_count": null, "id": "151ae7c1-d08a-4c7c-97ed-5ac3e4cac458", "metadata": {}, "outputs": [], "source": [ "# Downloads pretrained model for this sample.\n", "# This step takes some time, depending on your network environment.\n", "panorama_test_utility.download_sample_model( ML_MODEL_FNAME, \"./models\" )" ] }, { "cell_type": "code", "execution_count": null, "id": "4d35111d-32d1-4aa7-91b9-7dec8cd697c7", "metadata": {}, "outputs": [], "source": [ "# Compile the model to run with test-utility.\n", "# This step takes 7 mins ~ 10 mins.\n", "%run ../common/test_utility/panorama_test_utility_compile.py \\\n", "\\\n", "--s3-model-location s3://{S3_BUCKET}/{app_name}/ \\\n", "\\\n", "--model-node-name model_node \\\n", "--model-file-basename ./models/{ML_MODEL_FNAME} \\\n", "--model-data-shape '{model_data_shape}' \\\n", "--model-framework pytorch" ] }, { "cell_type": "markdown", "id": "803e360e", "metadata": { "tags": [] }, "source": [ "### Prepare model for deploying to Panorama device" ] }, { "cell_type": "code", "execution_count": null, "id": "3c2eb9dc", "metadata": {}, "outputs": [], "source": [ "model_asset_name = 'model_asset'\n", "model_package_path = f'packages/{account_id}-{model_package_name}-1.0'\n", "model_descriptor_path = f'packages/{account_id}-{model_package_name}-1.0/descriptor.json'" ] }, { "cell_type": "code", "execution_count": null, "id": "44e21ded", "metadata": {}, "outputs": [], "source": [ "!cd ./{app_name} && panorama-cli add-raw-model \\\n", " --model-asset-name {model_asset_name} \\\n", " --model-local-path ../models/{ML_MODEL_FNAME}.tar.gz \\\n", " --descriptor-path {model_descriptor_path} \\\n", " --packages-path {model_package_path}" ] }, { "cell_type": "markdown", "id": "1c2101a7", "metadata": {}, "source": [ "# Write and test app code in notebook" ] }, { "cell_type": "markdown", "id": "8c8d548c", "metadata": {}, "source": [ "Every app has an entry point script, written in Python that pulls the frames from camera streams, performs inference, and send the results to the desired location. This file can be found in `your_app/packages/code_node/src/app.py`. Below, you will iterate on the code from within the notebook environment. The entry point file will be updated everytime you run the next notebook cell thanks to the `%%save_cell`. This is a magic command to update the contents of the entry point script. \n", "\n", "After updating the entry point script, use the Test Utility Run command (panorama_test_utility_run.py) command to simulate the application.\n", "\n", "### Iterating on Code Changes\n", "\n", "To iterate on the code:\n", "1. Interrupt the kernel if application is still running.\n", "2. Make changes in the next cell, and run the cell to update the entry point script. \n", "3. Run the panorama_test_utility_run.py again.\n", "\n", "**CHANGE VIDEO** : For you to change video, please set the file path to the --video-file argument of the panorama_test_utility_run.py command." ] }, { "cell_type": "code", "execution_count": null, "id": "85df18b5-031a-4740-a9c6-262a90b18bb6", "metadata": {}, "outputs": [], "source": [ "%%save_cell ./{app_name}/packages/{account_id}-{code_package_name}-1.0/src/app.py\n", "\n", "import json\n", "import logging\n", "import time\n", "from logging.handlers import RotatingFileHandler\n", "\n", "import boto3\n", "import cv2\n", "import numpy as np\n", "import panoramasdk\n", "\n", "import os\n", "os.environ[\"GST_DEBUG\"] = \"2\"\n", "os.environ[\"GST_PLUGIN_PATH\"] = \"$GST_PLUGIN_PATH:/usr/local/lib/gstreamer-1.0/:/amazon-kinesis-video-streams-producer-sdk-cpp/build\"\n", "\n", "from types import SimpleNamespace\n", "import torch\n", "from bytetracker.byte_tracker import BYTETracker\n", "from yolox_postprocess import demo_postprocess, multiclass_nms\n", "\n", "from datetime import datetime, timezone\n", "\n", "class Application(panoramasdk.node):\n", " def __init__(self):\n", " \"\"\"Initializes the application's attributes with parameters from the interface, and default values.\"\"\"\n", " self.VIDEO_RECORDING = False\n", " self.STREAM_ID = 0\n", " self.MODEL_NODE = \"model_node\"\n", " self.MODEL_INPUT = (640, 640) #YOLOX\n", " self.source_fnum = 0\n", " self.target_fnum = 0\n", " \n", " #for uploading still-shot each start day\n", " self.refresh = True\n", " self.lastday = datetime.now(timezone.utc).strftime('%Y-%m-%d')\n", " self.today = self.lastday\n", " \n", " #Origin size\n", " streams = self.inputs.video_in.get()\n", " stream = streams[0]\n", " width = stream.image.shape[1]\n", " height = stream.image.shape[0]\n", " \n", " self.CAMERA_INPUT = (height, width)\n", " \n", " # Parameters\n", " logger.info('Getting parameters')\n", " self.service_region = self.inputs.service_region.get()\n", " self.bucket_name = self.inputs.bucket_name.get()\n", " self.kinesis_name = self.inputs.kinesis_name.get()\n", " self.kinesis_video_name = self.inputs.kinesis_video_name.get()\n", " \n", " session = boto3.Session(region_name=self.service_region)\n", " self.s3 = session.client('s3')\n", " self.firehose = session.client('firehose')\n", " \n", " self.SOURCE_FPS = self.inputs.source_fps.get() #30\n", " self.TARGET_FPS = self.inputs.target_fps.get() #10\n", " self.CATEGORY = compile(self.inputs.yolox_category.get(), '', 'eval') #[0,1,2]\n", " self.VERTICAL_RATIO = round(self.inputs.vertical_ratio.get(), 2) #1.6\n", " \n", " self.args = SimpleNamespace(**{\n", " 'nms': round(self.inputs.nms.get(),2), #0.45\n", " 'track_thresh': round(self.inputs.track_thresh.get(), 2), #0.65\n", " 'track_buffer': self.inputs.track_buffer.get(), #30\n", " 'match_thresh': round(self.inputs.match_thresh.get(), 2), #0.9\n", " 'min_box_area': self.inputs.min_box_area.get(), #100 w*h\n", " 'mot20': False})\n", " \n", " self.trackers = [BYTETracker(self.args, frame_rate=self.TARGET_FPS) for _ in range(len(streams))]\n", " \n", " gst_out = self.inputs.gstreamer_encoder.get()\n", " if len(gst_out) > 0:\n", " self.VIDEO_RECORDING = True\n", " kvssecret = session.client('secretsmanager') \n", " aksk = json.loads(kvssecret.get_secret_value(SecretId='KVSSecret')['SecretString'])\n", " gst_out += f\" ! kvssink log-config=/amazon-kinesis-video-streams-producer-sdk-cpp stream-name={self.kinesis_video_name} framerate={self.TARGET_FPS} access-key={aksk['accesskey']} secret-key={aksk['secretkey']} aws-region={self.service_region} \"\n", " self.videowriter = cv2.VideoWriter(gst_out, cv2.CAP_GSTREAMER, 0, float(self.TARGET_FPS), (width, height))\n", " \n", " logger.info('Initialiation complete.')\n", " logger.info('Args: {}'.format(self.args))\n", "\n", " def stop(self):\n", " if self.VIDEO_RECORDING == True:\n", " self.videowriter.release()\n", " logger.info('Terminated.')\n", " \n", " def resetstate(self):\n", " streams = self.inputs.video_in.get()\n", " for stream, tracker in zip(streams, self.trackers):\n", " image = cv2.imencode('.png', stream.image)[1].tostring()\n", " self.s3.put_object(Body=image, Bucket=self.bucket_name, \n", " Key=f\"dailycapture/{stream.stream_id}/{self.today}.png\", ContentType='image/PNG')\n", " #Refresh byte_track\n", " tracker.reset()\n", "\n", " def process_streams(self):\n", " \"\"\"Processes one frame of video from one or more video streams.\"\"\"\n", " self.source_fnum += 1\n", " \n", " #Check every minute to refresh check, 30 * 60 is 60 seconds\n", " if self.source_fnum % 1800 == 0:\n", " self.today = datetime.now(timezone.utc).strftime('%Y-%m-%d')\n", " if self.lastday != self.today:\n", " self.lastday = self.today\n", " self.refresh = True\n", " \n", " if self.refresh == True:\n", " self.refresh = False\n", " self.resetstate()\n", " \n", " #For processing partial frames to improve performance\n", " if self.source_fnum % (self.SOURCE_FPS / self.TARGET_FPS) != 0:\n", " return\n", " \n", " self.target_fnum += 1\n", "\n", " # Loop through attached video streams\n", " streams = self.inputs.video_in.get()\n", " for stream, tracker in zip(streams, self.trackers):\n", " self.process_media(stream, tracker)\n", "\n", " #TODO: Currently send only stream 0 to KVS, additional implemendation required to switch stream id by using iot channel\n", " if self.VIDEO_RECORDING == True:\n", " self.videowriter.write(streams[self.STREAM_ID].image)\n", " \n", " self.outputs.video_out.put(streams)\n", " \n", " def preproc(self, img, input_size, swap=(2, 0, 1)):\n", " if len(img.shape) == 3:\n", " padded_img = np.ones((input_size[0], input_size[1], 3)) * 114.0\n", " else:\n", " padded_img = np.ones(input_size) * 114.0\n", " \n", " r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1])\n", " resized_img = cv2.resize(\n", " img,\n", " (int(img.shape[1] * r), int(img.shape[0] * r)),\n", " interpolation=cv2.INTER_LINEAR,\n", " ).astype(np.uint8)\n", " padded_img[: int(img.shape[0] * r), : int(img.shape[1] * r)] = resized_img\n", "\n", " padded_img = padded_img.transpose(swap)\n", " padded_img = np.ascontiguousarray(padded_img, dtype=np.float32)\n", " return padded_img, r\n", " \n", " def process_media(self, stream, tracker):\n", " \"\"\"Runs inference on a frame of video.\"\"\"\n", " image_data, ratio = self.preproc(stream.image, self.MODEL_INPUT)\n", "\n", " inference_results = self.call({\"input0\":image_data}, self.MODEL_NODE)[0]\n", " \n", " # Process results (object deteciton)\n", " num_people = 0\n", " if len(inference_results) > 0:\n", " num_people = self.process_results(inference_results, stream, tracker, ratio) \n", " \n", " add_label(stream.image, f\"{stream.stream_id} / # People {num_people} / {datetime.utcnow().strftime('%H:%M:%S.%f')[:-5]}\", 30, 50)\n", " \n", " def process_results(self, inference_results, stream, tracker, ratio):\n", " boxes, scores, class_indices = self.postprocess(inference_results, self.MODEL_INPUT, ratio) \n", " if boxes is None:\n", " return 0\n", " \n", " media_height, media_width, _ = stream.image.shape\n", " media_scale = np.asarray([media_width, media_height, media_width, media_height])\n", " \n", " candidates = []\n", " for box, score, category_id in zip(boxes, scores, class_indices):\n", " if category_id in eval(self.CATEGORY):\n", " w = box[2] - box[0]\n", " h = box[3] - box[1]\n", " if w * h < self.args.min_box_area:\n", " continue\n", " horizontal = w / h > self.VERTICAL_RATIO\n", " if category_id == 0 and horizontal:\n", " continue\n", " candidates.append([box[0], box[1], box[2], box[3], score, category_id])\n", " \n", " num_people = len(candidates)\n", " if num_people == 0:\n", " return 0\n", " \n", " online_targets = tracker.update(self.target_fnum, torch.tensor(candidates))\n", " jsonlist = []\n", " ts = stream.time_stamp\n", " for t in online_targets:\n", " tlwh = t.tlwh\n", " tid = t.track_id\n", " tcid = t.category_id\n", " tscore = t.score\n", " age = round((self.target_fnum - t.start_frame)/self.TARGET_FPS, 1)\n", " jsonlist.append({\"Data\":f'{{\"sid\":\"{stream.stream_id}\",\"ts\":{ts[0] + (0.1) * ts[1]},\"fnum\":{self.target_fnum},\"cid\":{tcid},\"tid\":{tid},\"age\":{age},\"left\":{tlwh[0]/self.CAMERA_INPUT[1]},\"top\":{tlwh[1]/self.CAMERA_INPUT[0]},\"w\":{tlwh[2]/self.CAMERA_INPUT[1]},\"h\":{tlwh[3]/self.CAMERA_INPUT[0]}}}'})\n", " add_rect(stream.image, tlwh[0], tlwh[1], tlwh[2], tlwh[3])\n", " add_label(stream.image, f'{tid}/{tcid}/{age}', tlwh[0], tlwh[1] - 10)\n", "\n", " num_people = len(jsonlist)\n", " if num_people == 0:\n", " return 0\n", " \n", " self.firehose.put_record_batch(DeliveryStreamName=self.kinesis_name, Records=jsonlist)\n", " \n", " return num_people\n", " \n", " def postprocess(self, result, input_shape, ratio): \n", " # source: https://github.com/Megvii-BaseDetection/YOLOX/blob/2c2dd1397ab090b553c6e6ecfca8184fe83800e1/demo/ONNXRuntime/onnx_inference.py#L73\n", " input_size = input_shape[-2:]\n", " predictions = demo_postprocess(result, input_size)\n", " predictions = predictions[0] # TODO: iterate through eventual batches\n", " \n", " boxes = predictions[:, :4]\n", " scores = predictions[:, 4:5] * predictions[:, 5:]\n", "\n", " boxes_xyxy = np.ones_like(boxes)\n", " boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2]/2.\n", " boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3]/2.\n", " boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2]/2.\n", " boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3]/2.\n", " boxes_xyxy /= ratio\n", " \n", " dets = multiclass_nms(boxes_xyxy, scores, nms_thr=self.args.nms, score_thr=0.1)\n", " if dets is None:\n", " return None, None, None\n", " \n", " final_boxes, final_scores, final_cls_inds = dets[:, :4], dets[:, 4], dets[:, 5]\n", " boxes = final_boxes\n", " scores = final_scores\n", " class_indices = final_cls_inds.astype(int)\n", " return boxes, scores, class_indices\n", " \n", "def add_label(image, text, x1, y1):\n", " # White in BGR\n", " color = (255, 255, 255)\n", " # Using cv2.putText() method\n", " return cv2.putText(image, text, (int(x1), int(y1)), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2, cv2.LINE_AA)\n", "\n", "def add_rect(image, x1, y1, x2, y2):\n", " # Red in BGR\n", " color = (0, 0, 255) \n", " return cv2.rectangle(image, (int(x1), int(y1)), (int(x1 + x2), int(y1 + y2)), color, 2)\n", "\n", "def get_logger(name=__name__,level=logging.INFO):\n", " logger = logging.getLogger(name)\n", " logger.setLevel(level)\n", " handler = RotatingFileHandler(\"/opt/aws/panorama/logs/app.log\", maxBytes=100000000, backupCount=2)\n", " formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')\n", " handler.setFormatter(formatter)\n", " logger.addHandler(handler)\n", " return logger\n", "\n", "def main():\n", " while True:\n", " try:\n", " logger.info(\"INITIALIZING APPLICATION\")\n", " app = Application()\n", " logger.info(\"PROCESSING STREAMS\")\n", " while True:\n", " app.process_streams()\n", " except:\n", " logger.exception('Exception during processing loop.')\n", " finally:\n", " app.stop()\n", " \n", " #TODO: What about the failover?\n", " break\n", " #time.sleep(10)\n", "\n", "logger = get_logger(level=logging.INFO)\n", "main()" ] }, { "cell_type": "code", "execution_count": null, "id": "bf93017c-2ae4-4bee-ab59-996de5173579", "metadata": { "tags": [] }, "outputs": [], "source": [ "# Run the application with test-utility.\n", "#\n", "# As '--output-pyplot' option is specified, this command simulates HDMI output with pyplot rendering in the output cell.\n", "# In order to see console output (stdout/stderr) from the application, please remove the --output-pyplot option.\n", "#\n", "%run ../common/test_utility/panorama_test_utility_run.py \\\n", "\\\n", "--app-name {app_name} \\\n", "--code-package-name {code_package_name} \\\n", "--model-package-name {model_package_name} \\\n", "--camera-node-name {camera_node_name} \\\n", "--model-node-name {model_node_name} \\\n", "--model-file-basename {model_file_basename} \\\n", "--video-file {videoname} \\\n", "--video-stop 30 \\\n", "--py-file ./{app_name}/packages/{account_id}-{code_package_name}-1.0/src/app.py \\\n", "--output-pyplot" ] }, { "cell_type": "markdown", "id": "a714082b", "metadata": {}, "source": [ "# Package app" ] }, { "cell_type": "markdown", "id": "36c7a060", "metadata": {}, "source": [ "Updates the app to be deployed with the recent code" ] }, { "cell_type": "code", "execution_count": null, "id": "7dd93954", "metadata": {}, "outputs": [], "source": [ "py_file_name = 'app.py'\n", "panorama_test_utility.update_package_descriptor( app_name, account_id, code_package_name, py_file_name )" ] }, { "cell_type": "markdown", "id": "4df1a26a-e84b-48f3-a2c6-b289a5a0b4d1", "metadata": { "tags": [] }, "source": [ "## Update camera streams\n", "\n", "In the AWS Panorama console, you can select the camera streams, but programmatically, you need to define the camera stream info for the cameras you are using with the app.\n", "\n", "We used an ```abstract data source``` here, usually this lets you select the pre-created camera source from the console. But programatically, we have to do the following steps\n", "\n", "\n", "- Create Camera\n", "- Create Override json file\n", "- Include the Override json file while are deploying the application" ] }, { "cell_type": "markdown", "id": "c217bb6c-a15f-4ec8-b00b-b91fca369180", "metadata": {}, "source": [ "### Create New Camera\n", "\n", "Because we are using an ```abstract_rtsp_media_source```, we have to create a camera before we can use the ```abstract_rtsp_media_source```\n", "\n", "**NOTE** : Update your RTSP Info in the next cell, Username, Password and RTSP Stream URL" ] }, { "cell_type": "code", "execution_count": null, "id": "b6f1b76e-b20c-46f2-b4d9-8a477fb65a6a", "metadata": {}, "outputs": [], "source": [ "#For testing purpose, use https://github.com/aler9/rtsp-simple-server to generate infitite live video using static file\n", "CAMERA_NAME = \"StreetSample\"\n", "CAMERA_CREDS = '{\"StreamUrl\": \"rtsp://192.168.35.44:8554/mystream\"}'" ] }, { "cell_type": "code", "execution_count": null, "id": "53af9488-5e7c-4bb9-ba65-feaf06be8a4e", "metadata": {}, "outputs": [], "source": [ "res = !aws panorama create-node-from-template-job --template-type RTSP_CAMERA_STREAM \\\n", " --output-package-name {CAMERA_NAME} \\\n", " --output-package-version '1.0' \\\n", " --node-name {CAMERA_NAME} \\\n", " --template-parameters '{CAMERA_CREDS}'\n", "\n", "# FIXME : camera node creation fails if it already exists.\n", "# Should either ignore the already-exist error, or delete the node at the end of this notebook\n", "\n", "res = ''.join(res)\n", "print(res)\n", "res_json = json.loads(res)" ] }, { "cell_type": "code", "execution_count": null, "id": "b94dc937-f62a-49a1-ae86-315b9a0c3266", "metadata": {}, "outputs": [], "source": [ "!aws panorama describe-node-from-template-job --job-id {res_json['JobId']}" ] }, { "cell_type": "markdown", "id": "62921b7b-0669-45cb-b0b3-18087fc9be82", "metadata": {}, "source": [ "## Overriding camera node" ] }, { "cell_type": "markdown", "id": "672873ea", "metadata": {}, "source": [ "If you want to override the camera configuration at deployment (for ex. deploy to another site) you can provide a deployment time override. Go to `mot_analysis_app/deployment_overrides/override_camera.json` file and replace YOUR_AWS_ACCOUNT_ID with your ACCOUNT_ID and YOUR_CAMERA_NAME with your camera name." ] }, { "cell_type": "code", "execution_count": null, "id": "36e9c66f-3f32-4984-b5d8-be69251f022b", "metadata": {}, "outputs": [], "source": [ "# Update Account ID\n", "with open( f\"./{app_name}/deployment_overrides/override_camera.json\", \"r\" ) as fd:\n", " override_json = json.load(fd)\n", "\n", "override_json['nodeGraphOverrides']['packages'][0]['name'] = '{}::{}'.format(account_id, CAMERA_NAME)\n", "override_json['nodeGraphOverrides']['nodes'][0]['name'] = CAMERA_NAME\n", "override_json['nodeGraphOverrides']['nodes'][0]['interface'] = '{}::{}.{}'.format(account_id, CAMERA_NAME, CAMERA_NAME) \n", "override_json['nodeGraphOverrides']['nodeOverrides'][0]['with'][0]['name'] = CAMERA_NAME \n", "\n", "with open( f\"./{app_name}/deployment_overrides/override_camera.json\", \"w\") as fd:\n", " json.dump(override_json, fd)" ] }, { "cell_type": "markdown", "id": "337238ff", "metadata": {}, "source": [ "### Build app with container" ] }, { "cell_type": "code", "execution_count": null, "id": "25f5b4b1", "metadata": {}, "outputs": [], "source": [ "%%capture captured_output\n", "\n", "# Building container image.This process takes time (5min ~ 10min)\n", "# FIXME : without %%capture, browser tab crashes because of too much output from the command.\n", "\n", "!cd ./{app_name} && panorama-cli build \\\n", " --container-asset-name {container_asset_name} \\\n", " --package-path packages/{account_id}-{code_package_name}-1.0" ] }, { "cell_type": "markdown", "id": "f017081e", "metadata": {}, "source": [ "### Upload application to Panorama for deploying to devices" ] }, { "cell_type": "code", "execution_count": null, "id": "e8bc1f3b", "metadata": {}, "outputs": [], "source": [ "# This step takes some time, depending on your network environment.\n", "!cd ./{app_name} && panorama-cli package-application" ] }, { "cell_type": "markdown", "id": "4a6794d7", "metadata": {}, "source": [ "### Ready for deploying to a device\n", "\n", "Congrats! Your app is now ready to deploy to a device. Next, you can continue in this notebook to deploy the app programmatically or you can go to the Panorama console and deploying using the AWS Console. The console makes it easier to select camera streams and select the devices you want to deploy to. Programmatic deployment is faster to complete and easier to automate." ] }, { "cell_type": "markdown", "id": "e8e572a8", "metadata": {}, "source": [ "# Deploy app to device" ] }, { "cell_type": "markdown", "id": "b59f04c5", "metadata": {}, "source": [ "Let's make sure the device we are deploying to is available." ] }, { "cell_type": "code", "execution_count": null, "id": "c4cd42dc", "metadata": {}, "outputs": [], "source": [ "response = panorama_client.describe_device(\n", " DeviceId= DEVICE_ID\n", ")\n", "\n", "print('You are deploying to Device: {}'.format(response['Name']))" ] }, { "cell_type": "markdown", "id": "a09b151f", "metadata": {}, "source": [ "## Deploy app\n", "\n", "You are ready to deploy your app. Below, you can see an example of how to use the AWS CLI to deploy the app. Alternatively, you can use the boto3 SDK as you did above for getting the device information." ] }, { "cell_type": "code", "execution_count": null, "id": "63cd44d3-cc80-4145-896f-eaefaed3370d", "metadata": {}, "outputs": [], "source": [ "with open(f\"./{app_name}/graphs/{app_name}/graph.json\") as fd:\n", " manifest_payload = \"'%s'\" % json.dumps({\"PayloadData\":json.dumps(json.load(fd))})\n", " \n", "with open(f\"./{app_name}/deployment_overrides/override_camera.json\") as fd:\n", " override_payload = \"'%s'\" % json.dumps({\"PayloadData\":json.dumps(json.load(fd))})" ] }, { "cell_type": "code", "execution_count": null, "id": "025e3640-a7f8-460d-b39e-6216ed46e719", "metadata": {}, "outputs": [], "source": [ "res = !aws panorama create-application-instance \\\n", " --name {app_name} \\\n", " --default-runtime-context-device {DEVICE_ID} \\\n", " --manifest-payload {manifest_payload} \\\n", " --manifest-overrides-payload {override_payload} \\\n", " --runtime-role-arn {APPLICATION_ROLE}\n", "\n", "res = ''.join(res)\n", "print(res)\n", "res_json = json.loads(res)" ] }, { "cell_type": "markdown", "id": "08db290f", "metadata": {}, "source": [ "### Check Application Status" ] }, { "cell_type": "code", "execution_count": null, "id": "03691551-7dbb-451c-b3a3-a0a9b1fedfd1", "metadata": {}, "outputs": [], "source": [ "app_id = res_json['ApplicationInstanceId']\n", "print( \"Application Instance Id :\", app_id )\n", "\n", "progress_dots = panorama_test_utility.ProgressDots()\n", "while True:\n", " response = panorama_client.describe_application_instance( ApplicationInstanceId = app_id )\n", " status = response['Status']\n", " progress_dots.update_status( f'{status} ({response[\"StatusDescription\"]})' )\n", " if status in ('DEPLOYMENT_SUCCEEDED','DEPLOYMENT_FAILED'):\n", " break\n", " time.sleep(60)" ] }, { "cell_type": "markdown", "id": "ad70b33a", "metadata": {}, "source": [ "# Clean up" ] }, { "cell_type": "code", "execution_count": null, "id": "994b97f5-942e-4a95-8471-df627f4d272c", "metadata": {}, "outputs": [], "source": [ "panorama_test_utility.remove_application( DEVICE_ID, app_id )" ] }, { "cell_type": "code", "execution_count": null, "id": "2ed106ae-bde0-434a-88b4-d3ddc27ea923", "metadata": {}, "outputs": [], "source": [ "nodes = panorama_client.list_nodes()\n", "for node in nodes['Nodes']:\n", " print(f'Deleting {node[\"PackageId\"]}')\n", " panorama_client.delete_package(ForceDelete=True, PackageId=node['PackageId'])" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.9" } }, "nbformat": 4, "nbformat_minor": 5 }