diff --git a/surveillance-system/services.pkl b/surveillance-system/services.pkl index 3ddc844..dbd1bd9 100644 --- a/surveillance-system/services.pkl +++ b/surveillance-system/services.pkl @@ -5,45 +5,45 @@ bindings { name = "cameraCapture" `local` = true scheme = "http" - host = "services" - port = 8000 - endPoint = "/bell/on" + host = "localhost" + port = 8001 + endPoint = "/capture" method = "POST" } new HttpServiceImplementationBinding { name = "detectPersons" `local` = false scheme = "http" - host = "services" - port = 8000 - endPoint = "/bell/off" + host = "localhost" + port = 8002 + endPoint = "/detect" method = "POST" } new HttpServiceImplementationBinding { name = "alarmOn" `local` = false scheme = "http" - host = "services" - port = 8000 - endPoint = "/gate/up" + host = "localhost" + port = 8001 + endPoint = "/alarm/on" method = "POST" } new HttpServiceImplementationBinding { name = "alarmOff" `local` = false scheme = "http" - host = "services" - port = 8000 - endPoint = "/gate/down" + host = "localhost" + port = 8001 + endPoint = "/alarm/off" method = "POST" } new HttpServiceImplementationBinding { name = "analyze" `local` = false scheme = "http" - host = "services" - port = 8000 - endPoint = "/light/on" + host = "localhost" + port = 8003 + endPoint = "/analyze" method = "POST" } } \ No newline at end of file diff --git a/surveillance-system/services/README.md b/surveillance-system/services/README.md new file mode 100644 index 0000000..8b4bd80 --- /dev/null +++ b/surveillance-system/services/README.md @@ -0,0 +1,28 @@ +# Services + +All services required by the video surveillance use case are found in [services](services) sub folder. +- `iotservices`
+ Camera service which simulates image capturing. Adds random noise to each image to ensure uniqueness. +- `edgeservices`
+ Person detection service which checks whether the provided image contains any persons. +- `cloudservices`
+ Service which simulates face analysis. Marks persons as threats (20% chance) or no threat (otherwise). + +Used by both the Cirrina and Sonataflow version of the smart factory use case. + +## Usage with Docker Compose + +To run all services locally on a single device, you can use Docker Compose: + +``` +docker compose up +``` + +By default, the services will use protobuf to serialize/deserialize response/request data. This can be disabled by +setting an environment variable beforehand: + +``` +PROTO=false docker compose up +``` + +Individual Dockerfiles for separate manual builds can be found in the respective service folders. \ No newline at end of file diff --git a/surveillance-system/services/cloudservices/ContextVariable_pb2.py b/surveillance-system/services/cloudservices/ContextVariable_pb2.py new file mode 100644 index 0000000..0f2e815 --- /dev/null +++ b/surveillance-system/services/cloudservices/ContextVariable_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ContextVariable.proto +# Protobuf Python Version: 4.25.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x43ontextVariable.proto\x12\x08\x65xchange\"?\n\x0f\x43ontextVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.exchange.Value\";\n\x10\x43ontextVariables\x12\'\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x19.exchange.ContextVariable\"\x89\x01\n\x05Value\x12\x11\n\x07integer\x18\x01 \x01(\x05H\x00\x12\x0f\n\x05\x66loat\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04long\x18\x03 \x01(\x03H\x00\x12\x10\n\x06\x64ouble\x18\x04 \x01(\x01H\x00\x12\x10\n\x06string\x18\x05 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x06 \x01(\x08H\x00\x12\x0f\n\x05\x62ytes\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05valueBK\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x15\x43ontextVariableProtosP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ContextVariable_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\025ContextVariableProtosP\000' + _globals['_CONTEXTVARIABLE']._serialized_start=35 + _globals['_CONTEXTVARIABLE']._serialized_end=98 + _globals['_CONTEXTVARIABLES']._serialized_start=100 + _globals['_CONTEXTVARIABLES']._serialized_end=159 + _globals['_VALUE']._serialized_start=162 + _globals['_VALUE']._serialized_end=299 +# @@protoc_insertion_point(module_scope) diff --git a/surveillance-system/services/cloudservices/Dockerfile b/surveillance-system/services/cloudservices/Dockerfile new file mode 100644 index 0000000..775e462 --- /dev/null +++ b/surveillance-system/services/cloudservices/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11 + +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +WORKDIR /app +COPY ContextVariable_pb2.py . +COPY Event_pb2.py . +COPY services.py . + +ENV PROTO="true" + +CMD ["uvicorn", "services:app", "--host", "0.0.0.0", "--workers", "33", "--port", "8000"] \ No newline at end of file diff --git a/surveillance-system/services/cloudservices/Event_pb2.py b/surveillance-system/services/cloudservices/Event_pb2.py new file mode 100644 index 0000000..9f4a29a --- /dev/null +++ b/surveillance-system/services/cloudservices/Event_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: Event.proto +# Protobuf Python Version: 4.25.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import ContextVariable_pb2 as ContextVariable__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x45vent.proto\x12\x08\x65xchange\x1a\x15\x43ontextVariable.proto\"\xb7\x01\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x07\x63hannel\x18\x03 \x01(\x0e\x32\x17.exchange.Event.Channel\x12\'\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x19.exchange.ContextVariable\"A\n\x07\x43hannel\x12\x0c\n\x08INTERNAL\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01\x12\n\n\x06GLOBAL\x10\x02\x12\x0e\n\nPERIPHERAL\x10\x03\x42\x41\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x0b\x45ventProtosP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Event_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\013EventProtosP\000' + _globals['_EVENT']._serialized_start=49 + _globals['_EVENT']._serialized_end=232 + _globals['_EVENT_CHANNEL']._serialized_start=167 + _globals['_EVENT_CHANNEL']._serialized_end=232 +# @@protoc_insertion_point(module_scope) diff --git a/surveillance-system/services/cloudservices/requirements.txt b/surveillance-system/services/cloudservices/requirements.txt new file mode 100644 index 0000000..1b099f6 --- /dev/null +++ b/surveillance-system/services/cloudservices/requirements.txt @@ -0,0 +1,6 @@ +numpy<2 +fastapi==0.104.1 +opencv-python-headless==4.8.1.78 +opencv-contrib-python-headless==4.8.1.78 +uvicorn +protobuf \ No newline at end of file diff --git a/surveillance-system/services/cloudservices/services.py b/surveillance-system/services/cloudservices/services.py new file mode 100644 index 0000000..893c2c6 --- /dev/null +++ b/surveillance-system/services/cloudservices/services.py @@ -0,0 +1,109 @@ +import ContextVariable_pb2 + +import cv2 +import uvicorn + +import numpy as np + +from fastapi import FastAPI, Request, HTTPException, Response + +import json +import os +import random +import base64 +import time +import hashlib + +local_random = random.Random(os.getpid()) + +app = FastAPI() + +# Decide whether protobuf should be used (optional environment variable) +proto = "PROTO" not in os.environ or os.environ["PROTO"].lower() in ["true", "t", "1"] + + +# For logging detection times +def log_hash(data: bytes): + # Compute a consistent hash + sha256 = hashlib.sha256() + sha256.update(data) + hash = sha256.hexdigest() + + # Acquire the current timestamp in milliseconds + timestamp = time.time_ns() / 1_000_000.0 + log_entry = f"{hash},{timestamp}\n" + + # Append to log file + with open("/tmp/log_analysis.csv", "a") as log_file: + log_file.write(log_entry) + + +@app.post("/analyze") +async def detect(request: Request): + time_start = time.time_ns() / 1_000_000.0 + + if proto: + # Read the raw request body + body = await request.body() + + # Parse the protobuf message + context_variables = ContextVariable_pb2.ContextVariables() + context_variables.ParseFromString(body) + + # Extract the image from the protobuf message + image_bytes = None + for context_variable in context_variables.data: + if context_variable.name == "image": + image_bytes = context_variable.value.bytes + break + else: + # Parse the request variables + context_variables = await request.json() + + # Get the image from the request json + if "image" in context_variables: + image_bytes = base64.b64decode(context_variables["image"].encode("utf-8")) + + if image_bytes is None: + raise HTTPException(status_code=400, detail="No image provided in the request") + + # Read the image + np_arr = np.frombuffer(image_bytes, np.uint8) + image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) + + if image is None: + raise HTTPException(status_code=400, detail="Failed to decode the image") + + is_threat = local_random.random() < (1.0 / 5.0) + + # Prepare output data + if proto: + # Create response protobuf message + response_context_variables = ContextVariable_pb2.ContextVariables() + threats_context_variable = ContextVariable_pb2.ContextVariable( + name="hasThreat", + value=ContextVariable_pb2.Value(bool=is_threat), + ) + response_context_variables.data.append(threats_context_variable) + + # Serialize the response to protobuf format + response = response_context_variables.SerializeToString() + media_type = "application/x-protobuf" + else: + response = json.dumps({"hasThreat": is_threat}) + media_type = "application/json" + + time_end = time.time_ns() / 1_000_000.0 + + with open("/tmp/time_analysis.csv", "a") as log_file: + log_file.write(f"{time_end - time_start}\n") + + # Log data + log_hash(image_bytes) + + # Return the protobuf response + return Response(content=response, media_type=media_type) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/surveillance-system/services/compose.yaml b/surveillance-system/services/compose.yaml new file mode 100644 index 0000000..a35d452 --- /dev/null +++ b/surveillance-system/services/compose.yaml @@ -0,0 +1,25 @@ +services: + iotservices: + build: + context: ./iotservices + dockerfile: Dockerfile + ports: + - "8001:8000" + environment: + - "PROTO=${PROTO:-true}" + edgeservices: + build: + context: ./edgeservices + dockerfile: Dockerfile + ports: + - "8002:8000" + environment: + - "PROTO=${PROTO:-true}" + cloudservices: + build: + context: ./cloudservices + dockerfile: Dockerfile + ports: + - "8003:8000" + environment: + - "PROTO=${PROTO:-true}" \ No newline at end of file diff --git a/surveillance-system/services/edgeservices/ContextVariable_pb2.py b/surveillance-system/services/edgeservices/ContextVariable_pb2.py new file mode 100644 index 0000000..0f2e815 --- /dev/null +++ b/surveillance-system/services/edgeservices/ContextVariable_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ContextVariable.proto +# Protobuf Python Version: 4.25.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x43ontextVariable.proto\x12\x08\x65xchange\"?\n\x0f\x43ontextVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.exchange.Value\";\n\x10\x43ontextVariables\x12\'\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x19.exchange.ContextVariable\"\x89\x01\n\x05Value\x12\x11\n\x07integer\x18\x01 \x01(\x05H\x00\x12\x0f\n\x05\x66loat\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04long\x18\x03 \x01(\x03H\x00\x12\x10\n\x06\x64ouble\x18\x04 \x01(\x01H\x00\x12\x10\n\x06string\x18\x05 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x06 \x01(\x08H\x00\x12\x0f\n\x05\x62ytes\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05valueBK\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x15\x43ontextVariableProtosP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ContextVariable_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\025ContextVariableProtosP\000' + _globals['_CONTEXTVARIABLE']._serialized_start=35 + _globals['_CONTEXTVARIABLE']._serialized_end=98 + _globals['_CONTEXTVARIABLES']._serialized_start=100 + _globals['_CONTEXTVARIABLES']._serialized_end=159 + _globals['_VALUE']._serialized_start=162 + _globals['_VALUE']._serialized_end=299 +# @@protoc_insertion_point(module_scope) diff --git a/surveillance-system/services/edgeservices/Dockerfile b/surveillance-system/services/edgeservices/Dockerfile new file mode 100644 index 0000000..775e462 --- /dev/null +++ b/surveillance-system/services/edgeservices/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11 + +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +WORKDIR /app +COPY ContextVariable_pb2.py . +COPY Event_pb2.py . +COPY services.py . + +ENV PROTO="true" + +CMD ["uvicorn", "services:app", "--host", "0.0.0.0", "--workers", "33", "--port", "8000"] \ No newline at end of file diff --git a/surveillance-system/services/edgeservices/Event_pb2.py b/surveillance-system/services/edgeservices/Event_pb2.py new file mode 100644 index 0000000..9f4a29a --- /dev/null +++ b/surveillance-system/services/edgeservices/Event_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: Event.proto +# Protobuf Python Version: 4.25.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import ContextVariable_pb2 as ContextVariable__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x45vent.proto\x12\x08\x65xchange\x1a\x15\x43ontextVariable.proto\"\xb7\x01\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x07\x63hannel\x18\x03 \x01(\x0e\x32\x17.exchange.Event.Channel\x12\'\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x19.exchange.ContextVariable\"A\n\x07\x43hannel\x12\x0c\n\x08INTERNAL\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01\x12\n\n\x06GLOBAL\x10\x02\x12\x0e\n\nPERIPHERAL\x10\x03\x42\x41\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x0b\x45ventProtosP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Event_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\013EventProtosP\000' + _globals['_EVENT']._serialized_start=49 + _globals['_EVENT']._serialized_end=232 + _globals['_EVENT_CHANNEL']._serialized_start=167 + _globals['_EVENT_CHANNEL']._serialized_end=232 +# @@protoc_insertion_point(module_scope) diff --git a/surveillance-system/services/edgeservices/requirements.txt b/surveillance-system/services/edgeservices/requirements.txt new file mode 100644 index 0000000..1b099f6 --- /dev/null +++ b/surveillance-system/services/edgeservices/requirements.txt @@ -0,0 +1,6 @@ +numpy<2 +fastapi==0.104.1 +opencv-python-headless==4.8.1.78 +opencv-contrib-python-headless==4.8.1.78 +uvicorn +protobuf \ No newline at end of file diff --git a/surveillance-system/services/edgeservices/services.py b/surveillance-system/services/edgeservices/services.py new file mode 100644 index 0000000..7b9574b --- /dev/null +++ b/surveillance-system/services/edgeservices/services.py @@ -0,0 +1,111 @@ +import ContextVariable_pb2 + +import cv2 +import uvicorn + +import numpy as np + +from fastapi import FastAPI, Request, HTTPException, Response + +import json +import os +import random +import base64 +import time + +app = FastAPI() + +# Decide whether protobuf should be used (optional environment variable) +proto = "PROTO" not in os.environ or os.environ["PROTO"].lower() in ["true", "t", "1"] + +# Create a global HOG descriptor +hog = cv2.HOGDescriptor() +hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) + + +# Utility function for debugging +# def generate_random_colors(n: int): +# random.seed(0) +# +# return [ +# (random.randint(128, 255), random.randint(128, 255), random.randint(128, 255)) +# for _ in range(n) +# ] + + +@app.post("/detect") +async def detect(request: Request): + time_start = time.time_ns() / 1_000_000.0 + + if proto: + # Read the raw request body + body = await request.body() + + # Parse the protobuf message + context_variables = ContextVariable_pb2.ContextVariables() + context_variables.ParseFromString(body) + + # Extract the image from the protobuf message + image_bytes = None + for context_variable in context_variables.data: + if context_variable.name == "image": + image_bytes = context_variable.value.bytes + break + else: + # Parse the request variables + context_variables = await request.json() + + # Get the image from the request json + if "image" in context_variables: + image_bytes = base64.b64decode(context_variables["image"].encode("utf-8")) + + if image_bytes is None: + raise HTTPException(status_code=400, detail="No image provided in the request") + + # Read the image + np_arr = np.frombuffer(image_bytes, np.uint8) + image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) + + if image is None: + raise HTTPException(status_code=400, detail="Failed to decode the image") + + # Perform detection + (regions, _) = hog.detectMultiScale( + image, winStride=(4, 4), padding=(4, 4), scale=1.05 + ) + + # For drawing detections: + # image_dbg = image.copy() + # colors = generate_random_colors(len(regions)) + # for color, (x, y, w, h) in zip(colors, regions): + # cv2.rectangle(image_dbg, (x, y), (x + w, y + h), color, 2) + # cv2.imwrite("image_detected.jpg", image_dbg) + + # Prepare output data + if proto: + # Create response protobuf message + response_context_variables = ContextVariable_pb2.ContextVariables() + detections_context_variable = ContextVariable_pb2.ContextVariable( + name="hasDetectedPersons", + value=ContextVariable_pb2.Value(bool=len(regions) > 0), + ) + response_context_variables.data.append(detections_context_variable) + + # Serialize the response to protobuf format + response = response_context_variables.SerializeToString() + media_type = "application/x-protobuf" + else: + response = json.dumps({"hasDetectedPersons": len(regions) > 0}) + media_type = "application/json" + + time_end = time.time_ns() / 1_000_000.0 + + with open("/tmp/time_detection.csv", "a") as log_file: + log_file.write(f"{time_end - time_start}\n") + + # Return the protobuf response + return Response(content=response, media_type=media_type) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/surveillance-system/services/iotservices/ContextVariable_pb2.py b/surveillance-system/services/iotservices/ContextVariable_pb2.py new file mode 100644 index 0000000..a4ba5b8 --- /dev/null +++ b/surveillance-system/services/iotservices/ContextVariable_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ContextVariable.proto +# Protobuf Python Version: 4.25.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x15\x43ontextVariable.proto\x12\x08\x65xchange"?\n\x0f\x43ontextVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.exchange.Value";\n\x10\x43ontextVariables\x12\'\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x19.exchange.ContextVariable"\x89\x01\n\x05Value\x12\x11\n\x07integer\x18\x01 \x01(\x05H\x00\x12\x0f\n\x05\x66loat\x18\x02 \x01(\x02H\x00\x12\x0e\n\x04long\x18\x03 \x01(\x03H\x00\x12\x10\n\x06\x64ouble\x18\x04 \x01(\x01H\x00\x12\x10\n\x06string\x18\x05 \x01(\tH\x00\x12\x0e\n\x04\x62ool\x18\x06 \x01(\x08H\x00\x12\x0f\n\x05\x62ytes\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05valueBK\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x15\x43ontextVariableProtosP\x00\x62\x06proto3' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "ContextVariable_pb2", _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals["DESCRIPTOR"]._options = None + _globals["DESCRIPTOR"]._serialized_options = ( + b"\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\025ContextVariableProtosP\000" + ) + _globals["_CONTEXTVARIABLE"]._serialized_start = 35 + _globals["_CONTEXTVARIABLE"]._serialized_end = 98 + _globals["_CONTEXTVARIABLES"]._serialized_start = 100 + _globals["_CONTEXTVARIABLES"]._serialized_end = 159 + _globals["_VALUE"]._serialized_start = 162 + _globals["_VALUE"]._serialized_end = 299 +# @@protoc_insertion_point(module_scope) diff --git a/surveillance-system/services/iotservices/Dockerfile b/surveillance-system/services/iotservices/Dockerfile new file mode 100644 index 0000000..9c3b770 --- /dev/null +++ b/surveillance-system/services/iotservices/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11 + +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +WORKDIR /app +COPY ContextVariable_pb2.py . +COPY Event_pb2.py . +COPY services.py . +COPY resources resources + +ENV PROTO="true" + +CMD ["uvicorn", "services:app", "--host", "0.0.0.0", "--workers", "33", "--port", "8000"] \ No newline at end of file diff --git a/surveillance-system/services/iotservices/Event_pb2.py b/surveillance-system/services/iotservices/Event_pb2.py new file mode 100644 index 0000000..9f4a29a --- /dev/null +++ b/surveillance-system/services/iotservices/Event_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: Event.proto +# Protobuf Python Version: 4.25.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import ContextVariable_pb2 as ContextVariable__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x45vent.proto\x12\x08\x65xchange\x1a\x15\x43ontextVariable.proto\"\xb7\x01\n\x05\x45vent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12(\n\x07\x63hannel\x18\x03 \x01(\x0e\x32\x17.exchange.Event.Channel\x12\'\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x19.exchange.ContextVariable\"A\n\x07\x43hannel\x12\x0c\n\x08INTERNAL\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01\x12\n\n\x06GLOBAL\x10\x02\x12\x0e\n\nPERIPHERAL\x10\x03\x42\x41\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\x0b\x45ventProtosP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'Event_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n0at.ac.uibk.dps.cirrina.execution.object.exchangeB\013EventProtosP\000' + _globals['_EVENT']._serialized_start=49 + _globals['_EVENT']._serialized_end=232 + _globals['_EVENT_CHANNEL']._serialized_start=167 + _globals['_EVENT_CHANNEL']._serialized_end=232 +# @@protoc_insertion_point(module_scope) diff --git a/surveillance-system/services/iotservices/requirements.txt b/surveillance-system/services/iotservices/requirements.txt new file mode 100644 index 0000000..1b099f6 --- /dev/null +++ b/surveillance-system/services/iotservices/requirements.txt @@ -0,0 +1,6 @@ +numpy<2 +fastapi==0.104.1 +opencv-python-headless==4.8.1.78 +opencv-contrib-python-headless==4.8.1.78 +uvicorn +protobuf \ No newline at end of file diff --git a/surveillance-system/services/iotservices/resources/1.avi b/surveillance-system/services/iotservices/resources/1.avi new file mode 100644 index 0000000..bc95938 Binary files /dev/null and b/surveillance-system/services/iotservices/resources/1.avi differ diff --git a/surveillance-system/services/iotservices/resources/2.avi b/surveillance-system/services/iotservices/resources/2.avi new file mode 100644 index 0000000..911e3b4 Binary files /dev/null and b/surveillance-system/services/iotservices/resources/2.avi differ diff --git a/surveillance-system/services/iotservices/resources/3.avi b/surveillance-system/services/iotservices/resources/3.avi new file mode 100644 index 0000000..6b10c47 Binary files /dev/null and b/surveillance-system/services/iotservices/resources/3.avi differ diff --git a/surveillance-system/services/iotservices/resources/4.avi b/surveillance-system/services/iotservices/resources/4.avi new file mode 100644 index 0000000..efe5f60 Binary files /dev/null and b/surveillance-system/services/iotservices/resources/4.avi differ diff --git a/surveillance-system/services/iotservices/resources/5.avi b/surveillance-system/services/iotservices/resources/5.avi new file mode 100644 index 0000000..c33a9d0 Binary files /dev/null and b/surveillance-system/services/iotservices/resources/5.avi differ diff --git a/surveillance-system/services/iotservices/services.py b/surveillance-system/services/iotservices/services.py new file mode 100644 index 0000000..e85a18e --- /dev/null +++ b/surveillance-system/services/iotservices/services.py @@ -0,0 +1,158 @@ +import ContextVariable_pb2 + +import cv2 +import uvicorn + +import numpy as np + +from fastapi import FastAPI, HTTPException, Request, Response +from google.protobuf.json_format import MessageToDict +from starlette.status import HTTP_200_OK + +import base64 +import json +import hashlib +import os +import time + +FPS = 30 + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + +VIDEOS_CAPTURES = { + 0: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "1.avi")), + 1: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "2.avi")), + 2: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "3.avi")), + 3: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "4.avi")), + 4: cv2.VideoCapture(os.path.join(ROOT_DIR, "resources", "5.avi")), +} + +app = FastAPI() + +# Decide whether protobuf should be used (optional environment variable) +proto = "PROTO" not in os.environ or os.environ["PROTO"].lower() in ["true", "t", "1"] + + +# Get the current frame number +def get_frame_number() -> int: + return round(int(time.time()) * FPS) + + +# For logging detection times +def log_hash(data: bytes): + # Compute a consistent hash + sha256 = hashlib.sha256() + sha256.update(data) + hash = sha256.hexdigest() + + # Acquire the current timestamp in milliseconds + timestamp = time.time_ns() / 1_000_000.0 + log_entry = f"{hash},{timestamp}\n" + + # Append to log file + with open("/tmp/log_send.csv", "a") as log_file: + log_file.write(log_entry) + + +@app.post("/alarm/on") +async def alarm_on(): + return Response(status_code=HTTP_200_OK) + + +@app.post("/alarm/off") +async def alarm_on(): + return Response(status_code=HTTP_200_OK) + + +@app.post("/capture") +async def capture(request: Request): + video_number = None + + # Attempt to read the video number + if proto: + # Read the raw request body + body = await request.body() + + # Parse the protobuf message + context_variables = ContextVariable_pb2.ContextVariables() + context_variables.ParseFromString(body) + + # Extract specific values + for context_variable in context_variables.data: + context_variable_dict = MessageToDict(context_variable) + + # Check for cameraId and delay + if context_variable_dict["name"] == "cameraId": + video_number = context_variable_dict["value"].get("integer") + else: + # Parse the request variables + context_variables = await request.json() + + # Get context variables from the request json + if "cameraId" in context_variables: + video_number = int(context_variables["cameraId"]) + + if video_number not in VIDEOS_CAPTURES: + raise HTTPException( + status_code=400, detail=f"Invalid video_number: {video_number}" + ) + + # Acquire the video frame + cap = VIDEOS_CAPTURES[video_number] + + if not cap.isOpened(): + print(f"Failed to open video {video_number}") + raise HTTPException( + status_code=400, detail=f"Failed to open video {video_number}" + ) + + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + frame_number = get_frame_number() % total_frames + cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) + + ret, frame = cap.read() + if not ret: + raise HTTPException(status_code=400, detail="Unable to capture frame") + + frame = cv2.resize(frame, (640, 480)) + + # Introduce entropy to make every frame unique + random_values = (np.random.rand(1, frame.shape[1], frame.shape[2]) * 256).astype( + np.uint8 + ) + + frame[:1, :, :] = random_values + + # JPEG compression + jpeg_params = [int(cv2.IMWRITE_JPEG_QUALITY), 80] + + _, buffer = cv2.imencode(".jpg", frame, jpeg_params) + + buffer_bytes = buffer.tobytes() + + # Prepare output data + if proto: + # Create response protobuf message + response_context_variables = ContextVariable_pb2.ContextVariables() + image_context_variable = ContextVariable_pb2.ContextVariable( + name="image", value=ContextVariable_pb2.Value(bytes=buffer_bytes) + ) + response_context_variables.data.append(image_context_variable) + + # Serialize the response to protobuf format + response = response_context_variables.SerializeToString() + media_type = "application/x-protobuf" + else: + buffer_base64 = base64.b64encode(buffer_bytes).decode("utf-8") + + response = json.dumps({"image": buffer_base64}) + media_type = "application/json" + + log_hash(buffer_bytes) + + # Return the protobuf response + return Response(content=response, media_type=media_type) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000)