argussight 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. argussight/__init__.py +0 -0
  2. argussight/__main__.py +0 -0
  3. argussight/core/config.py +20 -0
  4. argussight/core/configurations/config.yaml +40 -0
  5. argussight/core/configurations/processes/savers/stream_buffer.yaml +15 -0
  6. argussight/core/configurations/processes/savers/video_recorder.yaml +22 -0
  7. argussight/core/configurations/processes/savers/video_saver.yaml +16 -0
  8. argussight/core/configurations/processes/streamer/flow_detection.yaml +21 -0
  9. argussight/core/configurations/processes/streamer/optical_flow_detection.yaml +16 -0
  10. argussight/core/configurations/processes/streamer/streamer.yaml +3 -0
  11. argussight/core/configurations/processes/vprocess.yaml +3 -0
  12. argussight/core/helper_functions.py +35 -0
  13. argussight/core/manager.py +71 -0
  14. argussight/core/spawner.py +321 -0
  15. argussight/core/video_processes/savers/stream_buffer.py +32 -0
  16. argussight/core/video_processes/savers/video_recorder.py +122 -0
  17. argussight/core/video_processes/savers/video_saver.py +164 -0
  18. argussight/core/video_processes/streamer/flow_detection.py +185 -0
  19. argussight/core/video_processes/streamer/optical_flow_detection.py +136 -0
  20. argussight/core/video_processes/streamer/streamer.py +87 -0
  21. argussight/core/video_processes/vprocess.py +245 -0
  22. argussight/grpc/__init__.py +0 -0
  23. argussight/grpc/argus_service.proto +78 -0
  24. argussight/grpc/argus_service_pb2.py +74 -0
  25. argussight/grpc/argus_service_pb2_grpc.py +341 -0
  26. argussight/grpc/helper_functions.py +55 -0
  27. argussight/grpc/server.py +112 -0
  28. argussight/grpc/test_client.py +63 -0
  29. argussight/grpc/test_concurrent_requests.py +48 -0
  30. argussight/main.py +62 -0
  31. argussight/streamsproxy.py +62 -0
  32. argussight-0.1.0.dist-info/LICENSE +21 -0
  33. argussight-0.1.0.dist-info/METADATA +151 -0
  34. argussight-0.1.0.dist-info/RECORD +36 -0
  35. argussight-0.1.0.dist-info/WHEEL +4 -0
  36. argussight-0.1.0.dist-info/entry_points.txt +4 -0
argussight/__init__.py ADDED
File without changes
argussight/__main__.py ADDED
File without changes
@@ -0,0 +1,20 @@
1
+ from typing import Union
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class RedisConfiguration(BaseModel):
7
+ host: str = Field("localhost", description="redis host")
8
+ port: int = Field(6379, description="redis port")
9
+ channel: str = Field(
10
+ "video-streamer", description="redis-channel to publish stream"
11
+ )
12
+
13
+
14
+ class CollectorConfiguration(BaseModel):
15
+ redis: RedisConfiguration
16
+
17
+
18
+ def get_config_from_dict(config_data: dict) -> Union[CollectorConfiguration, None]:
19
+ data = CollectorConfiguration(**config_data)
20
+ return data
@@ -0,0 +1,40 @@
1
+ ---
2
+
3
+ # This indicates the path to the files of the process classes
4
+ modules_path: argussight.core.video_processes
5
+
6
+ # List of available processes and their file locations from modules_path
7
+ # If accessible=true then process can be started via grpc call
8
+ worker_classes:
9
+ test:
10
+ location: vprocess.Test
11
+ accessible: true
12
+ flow_detection:
13
+ location: streamer.optical_flow_detection.OpticalFlowDetection
14
+ accessible: true
15
+ stream_buffer:
16
+ location: savers.stream_buffer.StreamBuffer
17
+ accessible: false
18
+ video_recorder:
19
+ location: savers.video_recorder.Recorder
20
+ accessible: false
21
+
22
+ # This indicates the processes that are started by argussight,
23
+ # if killed due to no response, they are restarted automatically
24
+ processes:
25
+ - name: "Saver"
26
+ type: stream_buffer
27
+ - name: "Recorder"
28
+ type: video_recorder
29
+
30
+ # This indicates the maximal waiting time (s),
31
+ # the spawner waits on responds from processes, before killing them
32
+ wait_time: 5
33
+
34
+ # This is the port for the rerouting of the streams
35
+ streams_layer_port: 7000
36
+
37
+ # Argussight is configured to run streams on different ports
38
+ # It will check for free ports, whenever a new streaming process starts
39
+ # The value below indicates at which port the search for a free port begins,
40
+ streams_starting_port: 9000
@@ -0,0 +1,15 @@
1
+ ---
2
+
3
+ parameters:
4
+ max_queue_len:
5
+ value: 200
6
+ exposed: false
7
+ main_save_folder:
8
+ value: "logs/"
9
+ exposed: false
10
+ save_format:
11
+ value: "both"
12
+ exposed: true
13
+ personnal_folder:
14
+ value: "test"
15
+ exposed: true
@@ -0,0 +1,22 @@
1
+ ---
2
+
3
+ parameters:
4
+ main_save_folder:
5
+ value: "logs/recordings"
6
+ exposed: false
7
+ # pay attention to the setting of the temp folder, as the folder will
8
+ # be cleared when the recorder is initialised,
9
+ # IMPORTANT: do NOT use an existing folder!!!
10
+ temp_folder:
11
+ value: "temp"
12
+ exposed: false
13
+ save_format:
14
+ value: "both"
15
+ exposed: true
16
+ personnal_folder:
17
+ value: "test"
18
+ exposed: true
19
+ max_recording_time:
20
+ # unit: seconds, 0 means not set
21
+ value: 0
22
+ exposed: false
@@ -0,0 +1,16 @@
1
+ ---
2
+
3
+ parameters:
4
+ main_save_folder:
5
+ value: "logs/"
6
+ exposed: false
7
+ save_format:
8
+ value: "both"
9
+ exposed: true
10
+ personnal_folder:
11
+ value: "test"
12
+ exposed: true
13
+ max_recording_time:
14
+ # unit: seconds, 0 means not set
15
+ value: 0
16
+ exposed: false
@@ -0,0 +1,21 @@
1
+ ---
2
+
3
+ parameters:
4
+ feature_params:
5
+ value:
6
+ maxCorners: 5
7
+ qualityLevel: 0.7
8
+ minDistance: 50
9
+ blockSize: 7
10
+ exposed: false
11
+
12
+ lk_params:
13
+ value:
14
+ winSize: [15, 15]
15
+ maxLevel: 2
16
+ criteria: [3, 10, 0.03]
17
+ exposed: false
18
+
19
+ roi:
20
+ value: [550, 450, 200, 500]
21
+ exposed: true
@@ -0,0 +1,16 @@
1
+ ---
2
+
3
+ parameters:
4
+ flow_params:
5
+ value:
6
+ pyr_scale: 0.5
7
+ levels: 3
8
+ winsize: 15
9
+ iterations: 3
10
+ poly_n: 5
11
+ poly_sigma: 1.1
12
+ flags: 0
13
+ exposed: false
14
+ roi:
15
+ value: [550, 450, 200, 500]
16
+ exposed: true
@@ -0,0 +1,3 @@
1
+ ---
2
+
3
+ parameters: {}
@@ -0,0 +1,3 @@
1
+ ---
2
+
3
+ parameters: {}
@@ -0,0 +1,35 @@
1
+ import socket
2
+ from typing import Union
3
+
4
+ import Levenshtein
5
+
6
+
7
+ def find_close_key(d: dict, key: str, max_distance: int = 3) -> Union[str, None]:
8
+ """Return first key in dict whose Levenshtein distance to key is <= max_distance"""
9
+ min_distance = float("inf")
10
+ closest_key = None
11
+
12
+ for dict_key in d:
13
+ distance = Levenshtein.distance(key, dict_key)
14
+ if distance < min_distance:
15
+ min_distance = distance
16
+ closest_key = dict_key
17
+
18
+ if min_distance <= max_distance:
19
+ return closest_key
20
+
21
+ return None
22
+
23
+
24
+ def is_port_free(port):
25
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
26
+ s.settimeout(1) # Optional: set a short timeout for connection attempt
27
+ result = s.connect_ex(("localhost", port))
28
+ return result != 0 # If result is non-zero, port is free
29
+
30
+
31
+ def find_free_port(start_port=9000):
32
+ port = start_port
33
+ while not is_port_free(port):
34
+ port += 1
35
+ return port
@@ -0,0 +1,71 @@
1
+ import queue
2
+ from datetime import datetime
3
+ from multiprocessing import Queue
4
+ from threading import Event
5
+
6
+ from argussight.core.video_processes.vprocess import ProcessError
7
+
8
+
9
+ class Manager:
10
+ def __init__(
11
+ self,
12
+ command_queue: Queue,
13
+ response_queue: Queue,
14
+ finished_event: Event,
15
+ failed_event: Event,
16
+ ) -> None:
17
+ self._commands_list = queue.Queue(
18
+ maxsize=20
19
+ ) # commands that are waiting to be processed by Manager
20
+ self._command_queue = (
21
+ command_queue # commands that are sent to process for being processed
22
+ )
23
+ self._response_queue = response_queue
24
+ self._finished_event = finished_event
25
+ self._failed_event = failed_event
26
+
27
+ def receive_command(
28
+ self,
29
+ command: str,
30
+ wait_time: int,
31
+ processed_event: Event,
32
+ result_queue: queue.Queue,
33
+ args,
34
+ ) -> None:
35
+ if self._commands_list.full():
36
+ raise ProcessError(
37
+ f"Cannot execute command {command}: too many commands in waiting list"
38
+ )
39
+ self._commands_list.put(
40
+ {
41
+ "command": command,
42
+ "max_wait_time": wait_time,
43
+ "time_stamp": datetime.now(),
44
+ "args": args,
45
+ "processed_event": processed_event,
46
+ "result_queue": result_queue,
47
+ }
48
+ )
49
+
50
+ def handle_commands(self) -> None:
51
+ while not self._commands_list.empty():
52
+ command = self._commands_list.get()
53
+
54
+ # Check if command is alive
55
+ if (datetime.now() - command["time_stamp"]).total_seconds() > command[
56
+ "max_wait_time"
57
+ ]:
58
+ continue
59
+
60
+ self._command_queue.put((command["command"], command["args"]))
61
+ command["processed_event"].set()
62
+
63
+ try:
64
+ result = self._response_queue.get(timeout=command["max_wait_time"])
65
+ command["result_queue"].put(result)
66
+
67
+ except queue.Empty:
68
+ self._failed_event.set()
69
+ break
70
+
71
+ self._finished_event.set()
@@ -0,0 +1,321 @@
1
+ import importlib
2
+ import inspect
3
+ import multiprocessing
4
+ import os
5
+ import queue
6
+ import threading
7
+ from typing import Any, Dict, List, Tuple
8
+
9
+ import psutil
10
+ import requests
11
+ import yaml
12
+
13
+ import argussight.streamsproxy as StreamsProxy
14
+ from argussight.core.helper_functions import find_close_key, find_free_port
15
+ from argussight.core.manager import Manager
16
+ from argussight.core.video_processes.streamer.streamer import Streamer
17
+ from argussight.core.video_processes.vprocess import ProcessError, Vprocess
18
+
19
+
20
+ class Spawner:
21
+ def __init__(self, collector_config) -> None:
22
+ self._processes = {}
23
+ self._worker_classes = {}
24
+ self._managers_dict = {}
25
+ self._restricted_classes = []
26
+ self._streamer_types = []
27
+ self.collector_config = collector_config
28
+ self._settings_manager = multiprocessing.Manager()
29
+ self._streams = set([])
30
+
31
+ current_dir = os.path.dirname(os.path.abspath(__file__))
32
+ self.load_config(os.path.join(current_dir, "configurations/config.yaml"))
33
+
34
+ print(f"running stream_layer on port {self.config['streams_layer_port']}")
35
+ streams_layer_process = multiprocessing.Process(
36
+ target=StreamsProxy.run, args=(self.config["streams_layer_port"],)
37
+ )
38
+ streams_layer_process.start()
39
+
40
+ def load_config(self, path_config_file: str) -> None:
41
+ with open(path_config_file, "r") as f:
42
+ self.config = yaml.safe_load(f)
43
+ self.load_worker_classes()
44
+
45
+ for process in self.config["processes"]:
46
+ self.start_process(process["name"], process["type"])
47
+
48
+ def load_worker_classes(self):
49
+ worker_classes_config = self.config["worker_classes"]
50
+ modules_path = self.config["modules_path"]
51
+ for key, worker_class in worker_classes_config.items():
52
+ class_path = worker_class["location"]
53
+ module_name, class_name = class_path.rsplit(".", 1)
54
+ module = importlib.import_module(modules_path + "." + module_name)
55
+ self._worker_classes[key] = getattr(module, class_name)
56
+ if not worker_class["accessible"]:
57
+ self._restricted_classes.append(key)
58
+ if issubclass(self._worker_classes[key], Streamer):
59
+ self._streamer_types.append(key)
60
+
61
+ def create_worker(
62
+ self, worker_type: str, free_port, settings: Dict[str, Any]
63
+ ) -> Vprocess:
64
+ if worker_type in self._streamer_types:
65
+ return self._worker_classes.get(worker_type)(
66
+ self.collector_config, free_port, settings
67
+ )
68
+ return self._worker_classes.get(worker_type)(self.collector_config, settings)
69
+
70
+ def add_process(
71
+ self,
72
+ name: str,
73
+ worker_type: str,
74
+ process: multiprocessing.Process,
75
+ command_queue: multiprocessing.Queue,
76
+ response_queue: multiprocessing.Queue,
77
+ settings: Dict[str, Any],
78
+ ) -> None:
79
+ self._processes[name] = {
80
+ "process_instance": process,
81
+ "command_queue": command_queue,
82
+ "response_queue": response_queue,
83
+ "type": worker_type,
84
+ "settings": settings,
85
+ }
86
+
87
+ # This function checks if worker_type can be accessed
88
+ def check_restricted_access(self, worker_type: str) -> bool:
89
+ # First check if access is restricted
90
+ if worker_type not in self._restricted_classes:
91
+ return True
92
+
93
+ # If access is restricted, check if the caller is the server
94
+ stack = inspect.stack()
95
+
96
+ # The caller's frame will be two levels up in the stack:
97
+ # - The current frame
98
+ # - The frame of the function that called check_restricted_access
99
+ # - The frame of the function that called that function (the original caller)
100
+ caller_frame = stack[2]
101
+
102
+ # Get the calling function's name and module
103
+ caller_module = inspect.getmodule(caller_frame.frame)
104
+
105
+ # Check if the caller is a method in the same class
106
+ if caller_module and caller_module.__name__ == self.__class__.__module__:
107
+ caller_class = caller_frame.frame.f_locals.get("self")
108
+ if isinstance(caller_class, self.__class__):
109
+ return True
110
+
111
+ return False
112
+
113
+ def add_stream(self, name, port, stream_id) -> None:
114
+ requests.post(
115
+ f"http://localhost:{str(self.config['streams_layer_port'])}/add-stream",
116
+ params={
117
+ "path": name,
118
+ "port": port,
119
+ "id": stream_id,
120
+ },
121
+ )
122
+ self._streams.add(name)
123
+
124
+ def start_process(self, name, type) -> None:
125
+ if name in self._processes:
126
+ raise ProcessError(
127
+ f"Process names must be unique. '{name}' already exists. Either terminate '{name}' or choose a different unique name"
128
+ )
129
+ if type not in self._worker_classes:
130
+ raise ProcessError(f"Type {type} does not exist")
131
+ # check if somebody tries to start a restricted worker type from outside this class
132
+ if not self.check_restricted_access(type):
133
+ raise ProcessError(f"Worker of type {type} can only be started by server.")
134
+
135
+ free_port = find_free_port(self.config["streams_starting_port"])
136
+ settings = self._settings_manager.dict()
137
+ worker_instance = self.create_worker(type, free_port, settings)
138
+ command_queue = multiprocessing.Queue()
139
+ response_queue = multiprocessing.Queue()
140
+ p = multiprocessing.Process(
141
+ target=worker_instance.run, args=(command_queue, response_queue)
142
+ )
143
+ print(f"started {name} of type {type}")
144
+ p.start()
145
+ if isinstance(worker_instance, Streamer):
146
+ self.add_stream(name, free_port, worker_instance.get_stream_id())
147
+ self.add_process(name, type, p, command_queue, response_queue, settings)
148
+
149
+ # check if process is running otherwise throw ProcessError
150
+ def check_for_running_process(self, name: str) -> None:
151
+ if name not in self._processes:
152
+ errorMessage = f"{name} is not a running process."
153
+ closest_key = find_close_key(self._processes, name)
154
+ if closest_key:
155
+ errorMessage += f" Did you mean: {closest_key}"
156
+
157
+ raise ProcessError(errorMessage)
158
+
159
+ def find_process_in_config_by_name(self, name: str) -> Dict:
160
+ for process in self.config["processes"]:
161
+ if process["name"] == name:
162
+ return process
163
+ return None
164
+
165
+ def terminate_processes(self, names: List[str]) -> None:
166
+ for name in names:
167
+ self.check_for_running_process(name)
168
+ worker_type = self._processes[name]["type"]
169
+ # check if somebody tries to kill a restricted worker type from outside this class
170
+ if not self.check_restricted_access(worker_type):
171
+ raise ProcessError(
172
+ f"Worker of type {worker_type} can only be terminated by server."
173
+ )
174
+
175
+ for name in names:
176
+ p = self._processes[name]["process_instance"]
177
+ worker_type = self._processes[name]["type"]
178
+ # first kill all possible children
179
+ parent = psutil.Process(p.pid)
180
+ children = parent.children(recursive=True)
181
+ for child in children:
182
+ print(f"Terminating child process: {child.pid}")
183
+ child.terminate()
184
+ # now kill the process itself and clean up
185
+ p.terminate()
186
+ p.join()
187
+ del self._processes[name]["settings"]
188
+ del self._processes[name]
189
+
190
+ if worker_type in self._streamer_types:
191
+ requests.post(
192
+ f"http://localhost:{str(self.config['streams_layer_port'])}/remove-stream",
193
+ params={"path": name},
194
+ )
195
+ self._streams.discard(name)
196
+
197
+ print(f"terminated {name} of type {worker_type}")
198
+ if (
199
+ worker_type in self._restricted_classes
200
+ and self.check_restricted_access(worker_type)
201
+ ):
202
+ process = self.find_process_in_config_by_name(name)
203
+ self.start_process(process["name"], process["type"])
204
+
205
+ def wait_for_manager(
206
+ self,
207
+ finished_event: threading.Event,
208
+ failed_event: threading.Event,
209
+ name: str,
210
+ manager: threading.Thread,
211
+ ) -> None:
212
+ while manager.is_alive():
213
+ finished_event.wait(timeout=1000)
214
+ if finished_event.is_set():
215
+ del self._managers_dict[name]
216
+ if failed_event.is_set():
217
+ self.terminate_processes([name])
218
+ return
219
+
220
+ if name in self._managers_dict:
221
+ del self._managers_dict[name]
222
+ print("manager is not alive anymore but hasn't finished")
223
+
224
+ def send_command_to_manager(
225
+ self, name: str, command: str, args
226
+ ) -> Tuple[threading.Event, queue.Queue]:
227
+ processed_event = threading.Event()
228
+ response_queue = queue.Queue()
229
+
230
+ self._managers_dict[name]["manager"].receive_command(
231
+ command, self.config["wait_time"], processed_event, response_queue, args
232
+ )
233
+
234
+ return processed_event, response_queue
235
+
236
+ # this function should only be called by the spawner service and used as Thread
237
+ def manage_process(self, name: str, command: str, args) -> None:
238
+ self.check_for_running_process(name)
239
+
240
+ # Check if manager already exists
241
+ if name not in self._managers_dict:
242
+ finished_event = threading.Event()
243
+ failed_event = threading.Event()
244
+ self._managers_dict[name] = {
245
+ "manager": Manager(
246
+ self._processes[name]["command_queue"],
247
+ self._processes[name]["response_queue"],
248
+ finished_event,
249
+ failed_event,
250
+ ),
251
+ "failed_event": failed_event,
252
+ }
253
+ processed_event, result_queue = self.send_command_to_manager(
254
+ name, command, args
255
+ )
256
+
257
+ manager_thread = threading.Thread(
258
+ target=self._managers_dict[name]["manager"].handle_commands
259
+ )
260
+ waiter_thread = threading.Thread(
261
+ target=self.wait_for_manager,
262
+ args=(finished_event, failed_event, name, manager_thread),
263
+ )
264
+ manager_thread.start()
265
+ waiter_thread.start()
266
+ else:
267
+ try:
268
+ processed_event, result_queue = self.send_command_to_manager(
269
+ name, command, args
270
+ )
271
+ failed_event = self._managers_dict[name]["failed_event"]
272
+ except ProcessError as e:
273
+ e.message += f" for {name}. Try again later"
274
+
275
+ is_processing = processed_event.wait(timeout=self.config["wait_time"])
276
+ if is_processing:
277
+ try:
278
+ result = result_queue.get(timeout=self.config["wait_time"])
279
+ if isinstance(result, Exception):
280
+ raise result
281
+
282
+ return
283
+ except queue.Empty:
284
+ wait_time = self.config["wait_time"]
285
+ raise ProcessError(
286
+ f"Command {command} could not be executed in time {wait_time}. Hence process {name} is getting terminated"
287
+ )
288
+
289
+ elif failed_event.is_set():
290
+ raise ProcessError(
291
+ f"An error occured in process {name}. Process is no longer alive."
292
+ )
293
+ else:
294
+ raise ProcessError(
295
+ f"Process {name} is busy and could not start command in time. Try again later."
296
+ )
297
+
298
+ def get_processes(self):
299
+ running_processes = {}
300
+ for uname, process in self._processes.items():
301
+ type = process["type"]
302
+ current_class = self._worker_classes[type]
303
+ commands = [
304
+ command
305
+ for command in current_class.create_commands_dict().keys()
306
+ if command != "settings" and command != "default_settings"
307
+ ]
308
+ settings = dict(process["settings"])
309
+ running_processes[uname] = {
310
+ "type": type,
311
+ "commands": commands,
312
+ "settings": settings,
313
+ }
314
+
315
+ available_types = [
316
+ type
317
+ for type in self._worker_classes
318
+ if type not in self._restricted_classes
319
+ ]
320
+
321
+ return running_processes, available_types, self._streams
@@ -0,0 +1,32 @@
1
+ from collections import deque
2
+ from typing import Any, Dict
3
+
4
+ from argussight.core.video_processes.savers.video_saver import VideoSaver
5
+
6
+
7
+ class StreamBuffer(VideoSaver):
8
+ def __init__(self, collector_config, exposed_parameters: Dict[str, Any]) -> None:
9
+ super().__init__(collector_config, exposed_parameters)
10
+ self._queue = deque(maxlen=self._parameters["max_queue_len"])
11
+
12
+ @classmethod
13
+ def create_commands_dict(cls) -> Dict[str, Any]:
14
+ result = super().create_commands_dict()
15
+ result.update({"save": cls.save_queue})
16
+ return result
17
+
18
+ def save_queue(self) -> None:
19
+ queue = self._queue.copy()
20
+ self.executor.submit(
21
+ self.save_iterable,
22
+ queue,
23
+ )
24
+
25
+ def add_to_iterable(self, frame: Dict) -> None:
26
+ self._queue.append(frame)
27
+
28
+ def _max_recording_callback(self) -> None:
29
+ self.save_queue()
30
+ # this should normally not be called but if it is,
31
+ # there is no way to reset recording except by restarting the server
32
+ self._parameters["recording"] = False