-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathapp.py
More file actions
2634 lines (2175 loc) · 89.1 KB
/
Copy pathapp.py
File metadata and controls
2634 lines (2175 loc) · 89.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
print(f"Initializing..")
# Essential imports for basic video streaming
import time
import os
import cv2
import queue
import threading
import sys
import json
import math
from datetime import datetime
import argparse
from collections import deque
# Heavy imports will be loaded in background
# ============================================
# USER CONFIGURABLE VARIABLES
# ============================================
# YOLO Model and Stream Settings
model = "yolo12m" # YOLO model to use (yolo11n, yolo12n, yolo12s, etc.)
# Parse command line arguments
parser = argparse.ArgumentParser(description="MACHINA - Video Stream Processor")
parser.add_argument(
"--stream",
type=str,
default="0",
help="RTSP stream URL or 0 for webcam (default: 0)",
)
args = parser.parse_args()
# Convert '0' string to integer 0 for webcam
if args.stream == "0":
rtsp_stream = 0
else:
rtsp_stream = args.stream
# Processing and Performance Settings
yolo_skip_frames = 2 # Process every Nth frame (2 = every 2nd frame)
buffer = 512 # Frame buffer size
min_confidence = 0.15 # Minimum confidence threshold for detections
min_size = 20 # Minimum size for car detections
stream_timeout_ms = 5000 # Stream timeout detection threshold (5 seconds)
# Object Tracking Settings
point_timeout = 2500 # Time before objects are considered lost (ms)
stationary_val = 16 # Movement threshold for stationary objects
idle_reset = 3000 # Time before frame skip reset (ms)
obj_max = 16 # Maximum number of tracked objects
padding = 6 # Padding around detected objects for cropping
# Display Settings
opsize = (640, 480) # Default processing/display resolution
yolo_input_size = 640 # Square size divisible by 32 for YOLO input
snapshot_directory = "snapshots" # Directory for snapshots
_font = cv2.FONT_HERSHEY_SIMPLEX # Font for text overlay
# Replay and UI Settings
replay_buffer_max_size = 300 # ~10 seconds at 30fps
resolution_display_duration = 2000 # Resolution display time (ms)
# Object Detection Confidence Thresholds
class_confidence = {
"truck": 0.35,
"car": 0.15,
"boat": 0.85,
"bus": 0.5,
"aeroplane": 0.85,
"frisbee": 0.85,
"pottedplant": 0.55,
"train": 0.85,
"chair": 0.5,
"parking meter": 0.9,
"fire hydrant": 0.65,
"traffic light": 0.65,
"backpack": 0.65,
"bicycle": 0.55,
"bench": 0.75,
"zebra": 0.90,
"tvmonitor": 0.80,
}
classlist = [
"person",
"car",
"motorbike",
"bicycle",
"truck",
"traffic light",
"stop sign",
"bench",
"bird",
"cat",
"dog",
"backpack",
"suitcase",
"handbag",
]
# ============================================
# SYSTEM VARIABLES (DO NOT MODIFY)
# ============================================
# Stream and processing state
frames = 0
prev_frames = 0
last_frame = 0
fps = 0
recording = False
out = None
streamsize = (0, 0)
original_opsize = None # Will store the original stream size for Ctrl+4
# Stream timeout detection variables
last_frame_time = 0
stream_timeout_threshold = stream_timeout_ms # Use configurable timeout from settings
max_consecutive_failures = 3 # Max consecutive read failures before restart
# Zoom and pan state
zoom_factor = 1.0
pan_x = 0
pan_y = 0
zoom_mode_active = False
stored_bounding_boxes = []
# UI state variables
hdstream = False
drawing = False
dragging = False
drag_start_x = 0
drag_start_y = 0
draw_start_x = 0
draw_start_y = 0
draw_end_x = 0
draw_end_y = 0
military_mode = False
show_info_overlay = False
show_help_text = False
help_text_start_time = 0
show_frame_skip_display = False
frame_skip_display_start_time = 0
yolo_first_processing_started = False
is_first_run = False
webcam_max_resolution = None # Store webcam's maximum supported resolution
# Audio visualization state
audio_waveform = deque(maxlen=1024)
audio_waveform_lock = threading.Lock()
audio_visualization_enabled = False
audio_playback_enabled = False
audio_thread = None
audio_muted = False
AUDIO_WAVEFORM_HEIGHT = 60
AUDIO_WAVEFORM_TARGET_POINTS = 1024
AUDIO_WAVEFORM_SECONDS = 1.0
audio_waveform_downsample = 1
audio_waveform_max_points = 1024
audio_waveform_sample_rate = 44100
def transform(xmin, ymin, xmax, ymax, pad):
x_scale = streamsize[0] / opsize[0]
y_scale = streamsize[1] / opsize[1]
new_xmin = int(xmin * x_scale) - pad
new_ymin = int(ymin * y_scale) - pad
new_xmax = int(xmax * x_scale) + pad
new_ymax = int(ymax * y_scale) + pad
return (new_xmin, new_ymin, new_xmax, new_ymax)
def resample(frame):
global zoom_factor, pan_x, pan_y, streamsize, opsize
zoomed_width = int(streamsize[0] / zoom_factor)
zoomed_height = int(streamsize[1] / zoom_factor)
center_x = streamsize[0] // 2 + pan_x
center_y = streamsize[1] // 2 + pan_y
start_x = max(0, min(streamsize[0] - zoomed_width, center_x - zoomed_width // 2))
start_y = max(0, min(streamsize[1] - zoomed_height, center_y - zoomed_height // 2))
zoomed_frame = frame[
start_y : start_y + zoomed_height, start_x : start_x + zoomed_width
]
return cv2.resize(zoomed_frame, opsize, interpolation=cv2.INTER_LINEAR_EXACT)
def rest(url, payload):
headers = {"Content-Type": "application/json"}
r = False
try:
data = json.dumps(payload)
response = requests.post(url, data, headers=headers)
if response.status_code == 200:
r = json.loads(response.text)
else:
print(response.text)
return False
except Exception as e:
print(f"-- error {e}")
finally:
return r
def millis():
return round(time.perf_counter() * 1000)
def format_duration(seconds):
"""Convert seconds to human readable format like 1h20m5s, 10m, 5s"""
if seconds < 60:
return f"{seconds}s"
elif seconds < 3600:
minutes = seconds // 60
remaining_seconds = seconds % 60
if remaining_seconds == 0:
return f"{minutes}m"
else:
return f"{minutes}m{remaining_seconds}s"
else:
hours = seconds // 3600
remaining_minutes = (seconds % 3600) // 60
remaining_seconds = seconds % 60
if remaining_minutes == 0 and remaining_seconds == 0:
return f"{hours}h"
elif remaining_seconds == 0:
return f"{hours}h{remaining_minutes}m"
else:
return f"{hours}h{remaining_minutes}m{remaining_seconds}s"
def toggle_fullscreen():
"""Toggle between fullscreen and windowed mode"""
global fullscreen, window
if fullscreen:
# Exit fullscreen
cv2.setWindowProperty(window, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL)
cv2.resizeWindow(window, original_window_size[0], original_window_size[1])
fullscreen = False
print("Exited fullscreen")
else:
# Enter fullscreen
cv2.setWindowProperty(window, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
fullscreen = True
print("Entered fullscreen")
def window_resize_callback(val):
"""Callback for window resize events to maintain aspect ratio"""
global window_aspect_ratio, original_window_size
# Get current window size
try:
# This is a workaround since OpenCV doesn't provide direct window size callbacks
# We'll handle aspect ratio preservation in the main loop
pass
except:
pass
def reset_window_to_stream_resolution():
"""Reset window size to match stream resolution"""
global window, streamsize, original_window_size
reset_size = opsize
cv2.resizeWindow(window, reset_size[0], reset_size[1])
original_window_size = reset_size
print(f"Reset window to {reset_size[0]}x{reset_size[1]}")
def resize_stream_dimensions(new_size):
"""Resize stream processing dimensions and OpenCV window"""
global opsize, window, original_window_size, yolo_input_size, fullscreen, cap, rtsp_stream
global resolution_display_active, resolution_display_text, resolution_display_start_time
opsize = new_size
# If using webcam, check against maximum supported resolution
if rtsp_stream == 0 and cap and webcam_max_resolution:
if (
opsize[0] <= webcam_max_resolution[0]
and opsize[1] <= webcam_max_resolution[1]
):
cap.set(cv2.CAP_PROP_FRAME_WIDTH, opsize[0])
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, opsize[1])
print(f"Set webcam resolution to {opsize[0]}x{opsize[1]}")
# Verify what resolution was actually set
actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
if (actual_width, actual_height) != opsize:
print(
f"Warning: Webcam set to {actual_width}x{actual_height}, not {opsize[0]}x{opsize[1]}"
)
else:
print(
f"Cannot set {opsize[0]}x{opsize[1]} - webcam max is {webcam_max_resolution[0]}x{webcam_max_resolution[1]}"
)
# Don't change the resolution if it exceeds webcam capability
return
# Adjust YOLO input size to be the larger dimension, rounded up to nearest 32
max_dim = max(opsize[0], opsize[1])
yolo_input_size = ((max_dim + 31) // 32) * 32 # Round up to nearest multiple of 32
# If currently in fullscreen, maintain fullscreen after resize
if fullscreen:
cv2.setWindowProperty(window, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
else:
cv2.resizeWindow(window, opsize[0], opsize[1])
original_window_size = opsize
# Activate resolution display
resolution_display_active = True
resolution_display_text = f"{opsize[0]}x{opsize[1]}"
resolution_display_start_time = millis()
print(
f"Resized stream to {opsize[0]}x{opsize[1]}, YOLO input: {yolo_input_size}x{yolo_input_size}"
)
def timestamp():
return int(time.time())
def detect_webcam_max_resolution(cap):
"""Detect the maximum resolution supported by the webcam with fingerprinting"""
global webcam_max_resolution
print("DEBUG: Starting webcam resolution detection with fingerprinting...")
# Get webcam fingerprint
webcam_id, fingerprint = get_webcam_fingerprint(cap)
# Load existing webcam configurations
webcam_configs = load_webcam_config()
# Check if we already know this webcam
if webcam_id in webcam_configs:
print(f"DEBUG: Found existing config for webcam: {webcam_id}")
saved_config = webcam_configs[webcam_id]
max_width = saved_config["max_width"]
max_height = saved_config["max_height"]
print(f"DEBUG: Using saved resolution: {max_width}x{max_height}")
# Set the webcam to the known good resolution
cap.set(cv2.CAP_PROP_FRAME_WIDTH, max_width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, max_height)
webcam_max_resolution = (max_width, max_height)
print(f"Webcam resolution loaded from cache: {max_width}x{max_height}")
return webcam_max_resolution
# New webcam - need to test resolutions
print(f"DEBUG: New webcam detected: {webcam_id}")
print("DEBUG: Testing resolutions for the first time...")
# Test resolutions from highest to lowest - stop at first working one
test_resolutions = [
(1920, 1080), # 1080p
(1280, 720), # 720p
(640, 480), # 480p
]
print(
f"DEBUG: Testing resolutions in order (will stop at first working): {[f'{w}x{h}' for w, h in test_resolutions]}"
)
max_width = 0
max_height = 0
for i, (width, height) in enumerate(test_resolutions):
print(
f"DEBUG: Testing resolution {i+1}/{len(test_resolutions)}: {width}x{height}"
)
# Try to set resolution
print(f"DEBUG: Setting width to {width}...")
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
print(f"DEBUG: Setting height to {height}...")
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# Check what was actually set
print("DEBUG: Getting actual width...")
actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
print("DEBUG: Getting actual height...")
actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"DEBUG: Actual resolution set: {actual_width}x{actual_height}")
# If we got the requested resolution, this is supported
if (
actual_width >= width * 0.9 and actual_height >= height * 0.9
): # Allow 10% tolerance
print(f"DEBUG: Resolution {width}x{height} works! Using this resolution.")
max_width = actual_width
max_height = actual_height
print(f"DEBUG: Selected resolution: {max_width}x{max_height}")
break # Stop testing - use the first working resolution
else:
print(f"DEBUG: Resolution {width}x{height} not supported, trying next...")
print("DEBUG: Resolution detection complete")
webcam_max_resolution = (max_width, max_height)
# Save the results for this webcam
webcam_configs[webcam_id] = {
"fingerprint": fingerprint,
"max_width": max_width,
"max_height": max_height,
"tested_date": datetime.now().isoformat(),
"resolutions_tested": test_resolutions,
}
if save_webcam_config(webcam_configs):
print(f"DEBUG: Saved webcam config for future use: {webcam_id}")
print(f"Webcam maximum resolution detected and saved: {max_width}x{max_height}")
return webcam_max_resolution
def get_available_resolutions():
"""Get available resolutions based on current stream type"""
global webcam_max_resolution, rtsp_stream
# Define resolution presets
all_resolutions = [(640, 480), (800, 600), (1024, 768), (1280, 800), (1920, 1080)]
if rtsp_stream == 0 and webcam_max_resolution:
# Filter resolutions that fit within webcam capability
available = []
for res in all_resolutions:
if (
res[0] <= webcam_max_resolution[0]
and res[1] <= webcam_max_resolution[1]
):
available.append(res)
return available if available else [webcam_max_resolution]
else:
# For RTSP streams, all resolutions are available (will be scaled)
return all_resolutions
def cycle_resolution(direction):
"""Cycle through available resolution presets with + and - keys"""
global opsize
available_resolutions = get_available_resolutions()
# Find current resolution in available list
try:
current_index = available_resolutions.index(opsize)
except ValueError:
# Current resolution not in list, start from first
current_index = 0
if direction > 0: # + key - increase resolution
current_index = (current_index + 1) % len(available_resolutions)
else: # - key - decrease resolution
current_index = (current_index - 1) % len(available_resolutions)
new_resolution = available_resolutions[current_index]
# Show available resolutions for debugging
if rtsp_stream == 0:
print(f"Available webcam resolutions: {available_resolutions}")
resize_stream_dimensions(new_resolution)
print(f"Resolution cycled to: {new_resolution[0]}x{new_resolution[1]}")
def load_config():
"""Load configuration from config.json, create if doesn't exist"""
global yolo_skip_frames, opsize, show_info_overlay, is_first_run
config_file = "config.json"
default_config = {
"processing_nth_frame": 2,
"screen_resolution": [640, 480],
"window_position": [100, 100],
"first_run": True,
}
try:
if os.path.exists(config_file):
with open(config_file, "r") as f:
config = json.load(f)
yolo_skip_frames = config.get("processing_nth_frame", 2)
opsize = tuple(config.get("screen_resolution", [640, 480]))
window_pos = config.get("window_position", [100, 100])
is_first_run = config.get("first_run", False)
print(
f"Config loaded: skip={yolo_skip_frames}, resolution={opsize}, pos={window_pos}"
)
return window_pos
else:
# First run - create config file
with open(config_file, "w") as f:
json.dump(default_config, f, indent=2)
is_first_run = True
print("First run detected - created config.json")
return default_config["window_position"]
except Exception as e:
print(f"Error loading config: {e}")
is_first_run = True
return default_config["window_position"]
def save_config():
"""Save current configuration to config.json"""
global yolo_skip_frames, opsize, window
config_file = "config.json"
try:
# Get current window position (if possible)
window_pos = [100, 100] # Default fallback
try:
# OpenCV doesn't provide direct way to get window position
# We'll use the stored values or defaults
pass
except:
pass
config = {
"processing_nth_frame": yolo_skip_frames,
"screen_resolution": list(opsize),
"window_position": window_pos,
"first_run": False,
}
with open(config_file, "w") as f:
json.dump(config, f, indent=2)
print(f"Configuration saved: skip={yolo_skip_frames}, resolution={opsize}")
except Exception as e:
print(f"Error saving config: {e}")
def get_gpu_info():
"""Get GPU name and VRAM info"""
try:
import torch
if torch.cuda.is_available():
gpu_name = torch.cuda.get_device_name(0)
gpu_memory = torch.cuda.get_device_properties(0).total_memory
gpu_vram_gb = round(gpu_memory / 1024**3, 1)
return gpu_name, f"{gpu_vram_gb}GB"
else:
return "No CUDA GPU", "N/A"
except:
return "Unknown GPU", "N/A"
def get_webcam_fingerprint(cap):
"""Get webcam identifying information for fingerprinting"""
try:
# Try to get webcam properties that might identify it
backend_name = cap.getBackendName()
# Get various properties that might be unique to this webcam
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
brightness = cap.get(cv2.CAP_PROP_BRIGHTNESS)
contrast = cap.get(cv2.CAP_PROP_CONTRAST)
saturation = cap.get(cv2.CAP_PROP_SATURATION)
hue = cap.get(cv2.CAP_PROP_HUE)
# Create a fingerprint from available properties
fingerprint = {
"backend": backend_name,
"default_width": width,
"default_height": height,
"fps": fps,
"brightness": brightness,
"contrast": contrast,
"saturation": saturation,
"hue": hue,
}
# Create a unique ID by combining key properties
unique_id = f"{backend_name}_{width}x{height}_{int(fps)}fps"
print(f"DEBUG: Webcam fingerprint created: {unique_id}")
print(f"DEBUG: Fingerprint details: {fingerprint}")
return unique_id, fingerprint
except Exception as e:
print(f"DEBUG: Error getting webcam fingerprint: {e}")
# Fallback fingerprint
return "unknown_webcam", {"backend": "unknown"}
def load_webcam_config():
"""Load webcam configuration from webcam.json"""
config_file = "webcam.json"
try:
if os.path.exists(config_file):
with open(config_file, "r") as f:
config = json.load(f)
print(f"DEBUG: Loaded webcam config with {len(config)} entries")
return config
else:
print("DEBUG: No webcam.json found, will create new one")
return {}
except Exception as e:
print(f"DEBUG: Error loading webcam config: {e}")
return {}
def save_webcam_config(webcam_configs):
"""Save webcam configuration to webcam.json"""
config_file = "webcam.json"
try:
with open(config_file, "w") as f:
json.dump(webcam_configs, f, indent=2)
print(f"DEBUG: Saved webcam config with {len(webcam_configs)} entries")
return True
except Exception as e:
print(f"DEBUG: Error saving webcam config: {e}")
return False
def get_app_modified_time():
"""Get last modified time of the running script with relative time"""
try:
# Get the actual script filename that's being executed
script_path = os.path.abspath(sys.argv[0])
script_name = os.path.basename(script_path)
modified_time = os.path.getmtime(script_path)
modified_datetime = datetime.fromtimestamp(modified_time)
current_datetime = datetime.now()
# Calculate days difference
time_diff = current_datetime - modified_datetime
days_ago = time_diff.days
# Format the date (handle both Linux and Windows formats)
try:
formatted_date = modified_datetime.strftime("%-d %b %Y") # Linux format
except:
formatted_date = modified_datetime.strftime("%#d %b %Y") # Windows format
if days_ago == 0:
relative_time = "today"
elif days_ago == 1:
relative_time = "1 day ago"
else:
relative_time = f"{days_ago} days ago"
return f"{script_name}: {formatted_date} ({relative_time})"
except:
return "Script: Unknown"
def draw_info_overlay(img):
"""Draw the MACHINA info overlay with reduced brightness"""
overlay = img.copy()
# Reduce brightness by 50%
overlay = cv2.convertScaleAbs(overlay, alpha=0.5, beta=0)
height, width = img.shape[:2]
# Draw MACHINA title at top
title = "MACHINA"
title_font_scale = 2.0
title_thickness = 3
title_size = cv2.getTextSize(title, _font, title_font_scale, title_thickness)[0]
title_x = (width - title_size[0]) // 2
title_y = 60
# Draw title with shadow
cv2.putText(
overlay,
title,
(title_x + 2, title_y + 2),
_font,
title_font_scale,
(0, 0, 0),
title_thickness + 2,
)
cv2.putText(
overlay,
title,
(title_x, title_y),
_font,
title_font_scale,
(255, 255, 255),
title_thickness,
)
# Draw separator line
line_y = title_y + 20
line_start_x = width // 4
line_end_x = 3 * width // 4
cv2.line(overlay, (line_start_x, line_y), (line_end_x, line_y), (255, 255, 255), 2)
# Keyboard commands list
commands = [
"SPACE - Frame skip",
"Q - Quit",
"R - Toggle recording",
"S - Take snapshot",
"F - Reset window size",
"M - Toggle military mode",
"ENTER - Toggle fullscreen",
"ESC - Exit fullscreen",
"TAB - Toggle this info",
"BACKSPACE - Toggle replay mode",
"1-6 - Change resolution",
"+ / - - Adjust frame processing frequency",
"Mouse wheel - Zoom",
"Right click drag - Pan",
"Left click drag - Save selection to elements folder",
]
# Draw commands
cmd_y = line_y + 40
cmd_font_scale = 0.6
cmd_thickness = 1
line_height = 25
for i, cmd in enumerate(commands):
y_pos = cmd_y + (i * line_height)
if y_pos > height - 100: # Don't go too far down
break
cv2.putText(
overlay,
cmd,
(50, y_pos),
_font,
cmd_font_scale,
(255, 255, 255),
cmd_thickness,
)
# Get system info
gpu_name, gpu_vram = get_gpu_info()
app_modified = get_app_modified_time()
# Draw system info at bottom
info_y = height - 60
cv2.putText(
overlay, f"GPU: {gpu_name}", (20, info_y), _font, 0.5, (0, 255, 255), 1
) # Yellow
cv2.putText(
overlay, f"VRAM: {gpu_vram}", (20, info_y + 20), _font, 0.5, (0, 0, 255), 1
) # Red
cv2.putText(
overlay, app_modified, (20, info_y + 40), _font, 0.4, (255, 255, 255), 1
)
return overlay
labels = open("db/coco.names").read().strip().split("\n")
classlist = [labels.index(x) for x in classlist]
object_count = 0
old_count = 0
obj_break = millis()
obj_idle = 0
obj_list = []
obj_max = 16
fskip = False
last_fskip = timestamp()
obj_score = labels
bounding_boxes = []
obj_number = 1
# YOLO processing state
yolo_frame_count = 0
cached_yolo_results = None
zoom_pan_active = False
zoom_pan_pause_time = 0
last_yolo_processing_duration = 0 # Store last YOLO processing time
# Person detection cache for clustering
cached_person_detections = []
last_person_update_frame = 0
# Replay system variables
replay_buffer = []
replay_mode = False
replay_index = 0
replay_last_flash_time = 0
# Window management variables
fullscreen = False
window_aspect_ratio = 4 / 3 # Default aspect ratio
original_window_size = (640, 480)
# Resolution display variables
resolution_display_active = False
resolution_display_text = ""
resolution_display_start_time = 0
# Selection and clipboard variables
selection_complete = False
clipboard_message_active = False
clipboard_message_start_time = 0
clipboard_message_duration = 2000 # Show message for 2 seconds
selection_start_x = 0
selection_start_y = 0
selection_end_x = 0
selection_end_y = 0
clean_processed_img = None # Store clean image before UI overlays
def center(xmin, ymin, xmax, ymax):
center_x = (xmin + xmax) // 2
center_y = (ymin + ymax) // 2
return (center_x, center_y)
def _size(x1, y1, x2, y2):
return abs(x1 - y2)
def mouse_callback(event, x, y, flags, param):
global drawing, draw_start_x, draw_start_y, draw_end_x, draw_end_y, dragging, drag_start_x, drag_start_y, zoom_factor, pan_x, pan_y, zoom_pan_active, zoom_pan_pause_time, cached_yolo_results, zoom_mode_active, stored_bounding_boxes, bounding_boxes, fullscreen, window, selection_complete, clipboard_message_active, clipboard_message_start_time, selection_start_x, selection_start_y, selection_end_x, selection_end_y, audio_muted
if event == cv2.EVENT_RBUTTONDOWN:
dragging = True
zoom_pan_active = True
zoom_pan_pause_time = millis()
cached_yolo_results = None # Clear cache when pan starts
drag_start_x = x
drag_start_y = y
if event == cv2.EVENT_RBUTTONUP:
dragging = False
zoom_pan_active = False
cached_yolo_results = None # Clear cache when pan ends
if event == cv2.EVENT_LBUTTONUP:
if drawing and draw_start_x > 0 and draw_end_x > 0:
# Store selection coordinates before reset
selection_start_x = draw_start_x
selection_start_y = draw_start_y
selection_end_x = draw_end_x
selection_end_y = draw_end_y
# Mark selection as complete for clipboard copying
selection_complete = True
drawing = False
draw_end_x = 0
draw_end_y = 0
draw_start_x = 0
draw_start_y = 0
if event == cv2.EVENT_LBUTTONDOWN:
# Start drawing
drawing = True
draw_end_x = 0
draw_end_y = 0
draw_start_x = x
draw_start_y = y
if event == cv2.EVENT_MOUSEWHEEL:
zoom_pan_active = True
zoom_pan_pause_time = millis()
cached_yolo_results = None # Clear cache on zoom in/out
# Store objects when entering zoom mode
if zoom_factor == 1.0 and not zoom_mode_active:
pan_x = 0
pan_y = 0
old_zoom_factor = zoom_factor
if flags > 0:
zoom_factor = min(6.0, zoom_factor * 1.1)
else:
zoom_factor = max(1.0, zoom_factor / 1.1)
# Entering zoom mode - store current tracking state
if old_zoom_factor == 1.0 and zoom_factor > 1.0 and not zoom_mode_active:
stored_bounding_boxes = copy.deepcopy(bounding_boxes)
zoom_mode_active = True
print(f"Entering zoom mode - stored {len(stored_bounding_boxes)} objects")
# Exiting zoom mode - restore tracking state only when returning to 1.0x
elif zoom_factor == 1.0 and zoom_mode_active:
zoom_mode_active = False
bounding_boxes = copy.deepcopy(stored_bounding_boxes)
stored_bounding_boxes = []
print(f"Exiting zoom mode - restored {len(bounding_boxes)} objects")
if event == cv2.EVENT_MBUTTONDOWN:
audio_muted = not audio_muted
state = "muted" if audio_muted else "unmuted"
print(f"Audio {state} via middle mouse button")
with audio_waveform_lock:
audio_waveform.clear()
if event == cv2.EVENT_MOUSEMOVE:
if drawing:
draw_end_x = x
draw_end_y = y
if dragging:
dx = x - drag_start_x
dy = y - drag_start_y
pan_x -= int(dx * zoom_factor)
pan_y -= int(dy * zoom_factor)
drag_start_x = x
drag_start_y = y
class BoundingBox:
def __init__(self, name, points, size, image):
global obj_number
self.nr = obj_number
obj_number += 1
self.x, self.y = points
self.created = millis()
self.timestamp = self.created
self.size = size
self.name = name
self.checkin = True
self.detections = 0
self.idle = 0
self.image = image
self.desc = False
self.state = 0
self.seen = self.created
self.disappeared_cycles = 0 # Track consecutive disappearance cycles
self.init()
print(
"New object: " + self.name + "#" + str(self.nr) + " size:" + str(self.size)
)
def see(self):
self.seen = millis()
def ping(self):
self.timestamp = millis()
idle = self.timestamp - self.created
if idle >= 1000:
self.idle = idle // 1000
else:
self.idle = 0
return self.idle
def export(self):
_, buffer = cv2.imencode(".png", self.image)
base64_image = base64.b64encode(buffer.tobytes()).decode("utf-8")
return base64_image
def init(self):
self.min_x = self.x - stationary_val
self.max_x = self.x + stationary_val
self.min_y = self.y - (stationary_val)
self.max_y = self.y + (stationary_val)
def contains(self, x, y, time):
return (
((self.checkin == False) and self.min_x <= x <= self.max_x)
and (self.min_y <= y <= self.max_y)
and (time - self.seen < point_timeout)
)
def update(self, time, new_x, new_y):
self.checkin = True
self.timestamp = time
idle = self.timestamp - self.created
if idle >= 1000:
self.idle = idle // 1000
else:
self.idle = 0
self.x = new_x
self.y = new_y
self.detections += 1
self.disappeared_cycles = (
0 # Reset disappearance counter when object is detected
)
self.init()
def resetIteration():
global bounding_boxes
# Reset checkin status and increment disappeared_cycles for objects not seen