Skip to content

Commit 01ec5ea

Browse files
committed
WIP: video detecting plugin fixes
1 parent 8f2437f commit 01ec5ea

File tree

11 files changed

+188
-67
lines changed

11 files changed

+188
-67
lines changed

external_plugins/image_detecting_plugin/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ RUN find / -path /proc -prune -o -path /sys -prune -o -path /dev -prune -o -writ
77
RUN apt update && apt install -y motion vim
88
RUN pip install watchdog
99
RUN pip install PILLOW
10+
RUN pip install PyYaml
1011

1112
# Config files
1213
ADD motion.default /etc/default/motion

external_plugins/image_detecting_plugin/image_detecting_plugin.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,41 @@
55
import time
66
import uuid
77
import zmq
8+
import yaml
9+
import logging
810
from subprocess import Popen
911

1012
from watchdog.observers import Observer
1113
from watchdog.events import FileSystemEventHandler
1214

1315
from ctevents import ctevents
14-
from pyevents.events import get_plugin_socket
16+
from pyevents.events import get_plugin_socket, send_quit_command
17+
from ctevents.ctevents import send_terminate_plugin_fb_event
1518

1619
# Path to a directory that this plugin "watches" for new image files.
1720
# By default, we set this directory to `/var/lib/motion` in the container, assuming
1821
# that the Linux Motion package will also be configured and running in the same container.
22+
log_level = os.environ.get("IMAGE_GENERATING_LOG_LEVEL", "INFO")
1923
DATA_MONITORING_PATH = os.environ.get("DATA_MONITORING_PATH", "/var/lib/motion")
2024
MIN_SECONDS_BETWEEN_IMAGES = float(os.environ.get("MIN_SECONDS_BETWEEN_IMAGES", "2.0"))
2125
MODE = os.environ.get("MODE", "demo")
2226

27+
logger = logging.getLogger("Image Generating Plugin")
28+
if log_level == "DEBUG":
29+
logger.setLevel(logging.DEBUG)
30+
elif log_level == "INFO":
31+
logger.setLevel(logging.INFO)
32+
elif log_level == "WARN":
33+
logger.setLevel(logging.WARN)
34+
elif log_level == "ERROR":
35+
logger.setLevel(logging.ERROR)
36+
if not logger.handlers:
37+
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s '
38+
'[in %(pathname)s:%(lineno)d]')
39+
handler = logging.StreamHandler()
40+
handler.setFormatter(formatter)
41+
logger.addHandler(handler)
42+
2343
def get_socket():
2444
"""
2545
This function creates the zmq socket object and generates the event-engine plugin socket
@@ -166,23 +186,22 @@ def process_file(self, file_path):
166186
logging.error(f"Error processing {file_path}: {e}")
167187

168188
def get_duration():
169-
if MODE == 'video_simuation':
170-
video_info_file = os.environ.get('TRAPS_VIDEO_INFO_PATH', '/video_info/video_info.yaml')
171-
while True:
172-
if os.path.exists(video_info_file):
173-
try:
174-
with open(video_info_file, 'r') as f:
175-
video_info = yaml.safe_load(f)
176-
except Exception as e:
177-
logging.error(f'Error processing {video_info_file}: {e}')
178-
else:
179-
sleep(1)
189+
if MODE == 'simulation':
190+
video_info_file = os.environ.get('TRAPS_VIDEO_INFO_PATH', '/video_info.yaml')
191+
192+
while not os.path.exists(video_info_file):
193+
time.sleep(1)
194+
195+
try:
196+
with open(video_info_file, 'r') as f:
197+
video_info = yaml.safe_load(f)
198+
except Exception as e:
199+
logging.error(f'Error processing {video_info_file}: {e}')
180200

181201
if 'duration' not in video_info.keys():
182202
logging.error(f'duration value not set in {video_info_file}')
183203
else:
184-
duration = video_info['duration']
185-
204+
return video_info['duration']
186205

187206
if __name__ == "__main__":
188207
logging.basicConfig(level=logging.INFO,
@@ -205,6 +224,8 @@ def get_duration():
205224
log_handler = LogFileHandler(log_observer, '/var/log/motion/motion.log')
206225
log_observer.schedule(log_handler, '/var/log/motion', recursive=False)
207226
log_observer.start()
227+
log_observer.join()
228+
logger.info('motion has connected to camera')
208229

209230
# instantiate and start the event handler
210231
event_handler = NewFileHandler()
@@ -215,11 +236,17 @@ def get_duration():
215236
# run for specified video duration, or if undefined until interrupted
216237
try:
217238
if duration:
239+
logger.info(f'Running motion for {duration} seconds')
218240
time.sleep(duration)
241+
observer.stop()
219242
else:
243+
logger.info('No duration specified. Running motion indefinitely')
220244
while True:
221245
time.sleep(1)
222246
except KeyboardInterrupt:
223247
observer.stop()
224248
observer.join()
225249
motion_proc.kill()
250+
logger.info('Sending quit command')
251+
send_terminate_plugin_fb_event(socket, "*", "35f20cdd-a404-4436-8df9-d80a9de91147")
252+
send_quit_command(socket)

external_plugins/video_generating_plugin/Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ FROM tapis/camera_traps_py_3.13:$REL
33

44
# Update apt packages and install dependencies
55
RUN apt-get update && apt-get install -y ffmpeg v4l-utils libv4l-dev
6+
RUN pip install PyYaml requests
67

78
ADD video_generating_plugin.py /video_generating_plugin.py
8-
ADD example_video.mp4 /example_video.mp4
9-
ADD ground_truth.yml /ground_truth.yml
109

1110
RUN chmod -R 0777 /video_generating_plugin.py || true
1211

external_plugins/video_generating_plugin/video_generating_plugin.py

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import os
22
import zmq
3-
from ctevents import ctevents
4-
from ctevents.ctevents import send_terminating_plugin_fb_event
3+
from ctevents import MonitorPowerStartEvent, MonitorPowerStopEvent, PluginTerminateEvent
4+
from ctevents.ctevents import socket_message_to_typed_event, send_terminate_plugin_fb_event, send_monitor_power_start_fb_event
55
from pyevents.events import get_plugin_socket, get_next_msg, send_quit_command
66
import logging
7-
from subprocess import run, PIPE, STDOUT
7+
from subprocess import Popen, run, PIPE, STDOUT
8+
from math import ceil
9+
import yaml
10+
import requests
811

912
log_level = os.environ.get("VIDEO_GENERATING_LOG_LEVEL", "INFO")
1013
input_video_path = os.environ.get("INPUT_VIDEO_PATH", "/example_video.mp4")
14+
use_ground_truth_url = os.environ.get("USE_CUSTOM_GROUND_TRUTH_FILE_URL", False)
15+
ground_truth_url = os.environ.get("CUSTOM_GROUND_TRUTH_URL")
1116
ground_truth_file = os.environ.get("GROUND_TRUTH_FILE", "/ground_truth.yml")
12-
device = os.environ.get("DEVICE", "http://0.0.0.0/8090")
17+
device = os.environ.get("DEVICE", "/dev/video0")
18+
mode = os.environ.get("MODE", "device")
1319

1420
logger = logging.getLogger("Image Generating Plugin")
1521
if log_level == "DEBUG":
@@ -33,7 +39,7 @@ def get_socket():
3339
for the port configured for this plugin.
3440
"""
3541
# get the port assigned to the Image Generating plugin
36-
PORT = os.environ.get('VIDEO_GENERATING_PLUGIN_PORT', 6000)
42+
PORT = os.environ.get('VIDEO_GENERATING_PLUGIN_PORT', 6003)
3743
# create the zmq context object
3844
context = zmq.Context()
3945
socket = get_plugin_socket(context, PORT)
@@ -42,18 +48,33 @@ def get_socket():
4248

4349
def get_video_duration():
4450
result = run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", input_video_path], stdout=PIPE, stderr=STDOUT)
45-
return ceil(float(result.stdout))
51+
duration = ceil(float(result.stdout))
52+
logger.info(f'{input_video_path} has a duration of {duration} seconds')
53+
return duration
4654

4755

4856
def load_ground_truth():
49-
return None
50-
with open(ground_truth_file, 'r') as f:
51-
video_info = yaml.safe_load(f)
57+
video_info = {}
58+
try:
59+
if use_ground_truth_url:
60+
logger.info(f"Retrieving custom ground truth file: {ground_truth_url}")
61+
response = requests.get(ground_truth_url)
62+
if response.status_code == 200:
63+
yml_content = response.content.decode('utf-8').splitlines()
64+
video_info = yaml.safe_load(yml_content)
65+
else:
66+
with open(ground_truth_file, 'r') as f:
67+
video_info = yaml.safe_load(f)
68+
except FileNotFoundError:
69+
logger.error(f"File not found: {ground_truth_file}")
70+
except Exception as e:
71+
logger.error(f'An error occurred: {e}')
5272
video_info['duration'] = get_video_duration()
53-
OUTPUT_DIR = os.environ.get('TRAPS_VIDEO_OUTPUT_PATH', '/output')
73+
OUTPUT_DIR = os.environ.get('TRAPS_VIDEO_OUTPUT_PATH', '/video_info')
5474
video_info_file = os.path.join(OUTPUT_DIR, 'video_info.yaml')
5575
with open(video_info_file, 'w') as f:
5676
yaml.dump(video_info, f)
77+
logger.info(f'Updating {OUTPUT_DIR}/video_info.yaml')
5778

5879
def monitor_generating_power():
5980
"""
@@ -64,7 +85,7 @@ def monitor_generating_power():
6485
monitor_type = [1]
6586
monitor_seconds = 0
6687
if monitor_flag:
67-
ctevents.send_monitor_power_start_fb_event(socket, pid, monitor_type, monitor_seconds)
88+
send_monitor_power_start_fb_event(socket, pid, monitor_type, monitor_seconds)
6889
logger.info(f"Monitoring image generating power")
6990

7091
def is_v4l2loopback_available():
@@ -76,42 +97,50 @@ def is_v4l2loopback_available():
7697

7798
def stream_file_to_device(input_video_path):
7899
logger.info(f'starting video device stream to {device}')
79-
run(['ffmpeg', '-re', '-stream_loop', '-1', '-i', input_video_path, '-f', 'v4l2', '-pix_fmt', 'yuv420p', device])
80-
logger.info('Video stream finished')
81-
82-
def stream_file_to_netcam(input_video_path, device):
83-
logger.info(f'starting netcam stream at {netcam_url}')
84-
run(['ffmpeg', '-re', '-i', input_video_path, '-f', 'mjpeg', '-pix_fmt', 'yuv420p', '-listen', '1', netcam_url])
85-
logger.info('Video stream finished')
100+
return Popen(['ffmpeg', '-re', '-stream_loop', '-1', '-i', input_video_path, '-f', 'v4l2', '-pix_fmt', 'yuv420p', device])
86101

87-
def get_device_type():
88-
if '/dev/video' in device:
89-
return 'video_device'
90-
elif 'http://' in device:
91-
return 'netcam'
92102

93103
def process_video(input_video_path, ground_truth):
94104
"""
95-
Main function that starts a video stream, either on /dev/video or as a netcam
105+
Main function that starts a video stream on a /dev/video device
96106
"""
97107
logger.info(f"The input video path specified by the user:{input_video_path}")
98-
device_type = get_device_type()
99-
if device_type == 'video_device':
100-
if is_v4l2loopback_available():
101-
stream_file_to_device(input_video_path)
102-
else:
103-
logger.info('v4l2loopback not available for {device}. Falling back to netcam stream.')
104-
stream_file_to_netcam(input_video_path, DEFAULT_NETCAM)
108+
if is_v4l2loopback_available():
109+
return stream_file_to_device(input_video_path)
105110
else:
106-
stream_file_to_netcam(input_video_path)
111+
logger.warning('v4l2loopback not available for {device}. Shutting down')
107112

108113
def main():
109114
global socket
110-
socket = get_socket()
111115
ground_truth = load_ground_truth()
112-
monitor_generating_power()
113-
#motion_ready_signal(socket)
114-
process_video(input_video_path, ground_truth)
116+
stream_proc = None
117+
if mode == 'device':
118+
monitor_generating_power()
119+
stream_proc = process_video(input_video_path, ground_truth)
120+
done = False
121+
while not done:
122+
socket = get_socket()
123+
try:
124+
message = get_next_msg(socket)
125+
except zmq.error.Again:
126+
logger.debug(f"Got a zmq.error.Again; i.e., waited {SOCKET_TIMEOUT} ms without getting a message")
127+
continue
128+
except Exception as e:
129+
logger.debug(f"Got exception from get_next_msg; type(e): {type(e)}; e: {e}")
130+
done = True
131+
logger.info("Video generating plugin stopping due to timeout limit...")
132+
continue
133+
if not message:
134+
logger.info("No message found in get_next_msg")
135+
136+
event = socket_message_to_typed_event(message)
137+
logger.info(f"Got a message from the event socket of type: {type(event)}")
138+
if isinstance(event, PluginTerminateEvent):
139+
logging.info('received PluginTerminateEvent')
140+
done = True
141+
142+
if stream_proc:
143+
stream_proc.kill()
115144
send_quit_command(socket)
116145

117146
if __name__ == '__main__':

installer/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ADD example_input.yml /host/input.yml
1313
RUN mkdir /defaults
1414
ADD defaults.yml /defaults/defaults.yml
1515
ADD example_images /defaults/example_images
16+
ADD example_video /defaults/example_video
1617
ADD ground_truth.csv /defaults/ground_truth.csv
1718

1819
# The installer app itself
@@ -23,4 +24,4 @@ ADD compile.py /compile.py
2324
RUN chmod 0777 -R /installer
2425
RUN chmod 0777 /compile.py
2526

26-
CMD [ "python", "/compile.py" ]
27+
CMD [ "python", "/compile.py" ]

installer/compile.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,10 @@ def get_vars(input_data, default_data):
9898
'deploy_video_generating': True,
9999
'deploy_image_detecting': True,
100100
'deploy_reporter': False,
101-
'deploy_ckn': True,
101+
'deploy_ckn': False,
102102
'deploy_ckn_mqtt': False,
103-
'deploy_oracle': True,
104-
'motion_video_device': 'http://video_generating:8090',
105-
'generating_video_device': 'http://0.0.0.0:8090',
103+
'deploy_oracle': False,
104+
'use_bundled_example_images': False,
106105
'inference_server': False}
107106

108107
if vars.get("mode") == 'demo':
@@ -157,11 +156,16 @@ def get_vars(input_data, default_data):
157156
vars['model_id'] = '41d3ed40-b836-4a62-b3fb-67cee79f33d9-model'
158157

159158
# for video simulations, determine if motion is using device or netcam
160-
if vars.get('motion_video_device'):
161-
if '/dev' in vars.get('motion_video_device'):
159+
if vars.get('mode') == 'video_simulation':
160+
if vars.get('motion_video_device'):
162161
vars['motion_video_type'] = 'device'
163-
elif '://' in vars.get('motion_video_device'):
164-
vars['motion_video_type'] = 'netcam'
162+
else:
163+
vars['motion_video_type'] = 'file'
164+
if vars.get('use_example_video'):
165+
vars['local_video_path'] = './video.mp4'
166+
else:
167+
if not vars.get('local_video_path'):
168+
vars['local_video_path'] = './video.mp4'
165169

166170
# Add the installer's UID and GID
167171
vars["uid"] = uid
@@ -329,6 +333,30 @@ def generate_additional_directories(vars, full_install_dir):
329333
sys.exit(1)
330334
print(f"Using URL for images: {vars['source_image_url']}")
331335

336+
if vars['use_example_video'] == True:
337+
try:
338+
shutil.copy("/defaults/example_video/example_video.mp4", os.path.join(full_install_dir, "video.mp4"))
339+
shutil.copy("/defaults/example_video/ground_truth.yml", os.path.join(full_install_dir, "ground_truth.yml"))
340+
except Exception as e:
341+
print(f"ERROR: Could not copy bundled example video; error: {e}")
342+
print("Exiting...")
343+
sys.exit(1)
344+
else:
345+
if vars['source_video_url']:
346+
try:
347+
rsp = requests.get(vars['source_video_url'])
348+
rsp.raise_for_status()
349+
except Exception as e:
350+
print(f'Error: could not download video at URL {source_video_url}; details: {e}')
351+
sys.exit(1)
352+
video_install_path = os.path.join(full_install_dir, 'video.mp4')
353+
with open(video_install_path, 'wb') as f:
354+
f.write(rsp.content)
355+
elif vars['local_video_path']:
356+
full_video_path = os.path.join(full_install_dir, vars.get('local_video_path'))
357+
if not os.path.exists(full_video_path):
358+
print(f'ERROR: local_video_path must be a relative path to the install directory and must already exist; the computed path ({full_video_dir}) does not exist.\nExiting...')
359+
sys.exit(1)
332360

333361
# create output directories if they do not exist
334362
images_output_dir = os.path.join(full_install_dir, vars["images_output_dir"])
@@ -347,6 +375,11 @@ def generate_additional_directories(vars, full_install_dir):
347375
detection_output_dir = os.path.join(full_install_dir, vars["detection_reporter_plugin_output_dir"])
348376
if not os.path.exists(detection_output_dir):
349377
os.makedirs(detection_output_dir)
378+
379+
if vars['deploy_video_generating']:
380+
video_output_dir = os.path.join(full_install_dir, vars["video_output_dir"])
381+
if not os.path.exists(video_output_dir):
382+
os.makedirs(video_output_dir)
350383

351384

352385
def main():

installer/defaults.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ motion_event_gap: 1
3333
motion_threshold: 100
3434
motion_width: 640
3535
motion_height: 480
36+
image_detecting_log_level: DEBUG
3637

3738
# video generating plugin
3839
deploy_video_generating: false
3940
video_generating_plugin_image: tapis/video_generating_plugin
4041
video_generating_log_level: DEBUG
42+
video_output_dir: video_output_dir
43+
use_example_video: true
4144
source_video_url:
42-
generating_video_device:
45+
local_video_path:
4346

4447

4548
# Image Scoring Plugin
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)