-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommon.py
More file actions
91 lines (72 loc) · 3.11 KB
/
Copy pathcommon.py
File metadata and controls
91 lines (72 loc) · 3.11 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
import numpy as np
# Robot36 Constants (Base frequencies)
SAMPLE_RATE = 48000
# Frequencies
SYNC_FREQ = 1200.0
PORCH_FREQ = 1500.0
VIS_BIT_1_FREQ = 1100.0
VIS_BIT_0_FREQ = 1300.0
BLACK_FREQ = 1500.0
WHITE_FREQ = 2300.0
EVEN_SEPARATOR_FREQ = 1500.0
ODD_SEPARATOR_FREQ = 2300.0
PORCH_SEPARATOR_FREQ = 1900.0
LEADER_TONE_FREQ = 1900.0
BREAK_FREQ = 1200.0
# Base Durations (ms) for Robot36 Standard (320x240)
SYNC_DURATION_MS = 9.0
SYNC_PORCH_DURATION_MS = 3.0
SEPARATOR_DURATION_MS = 4.5
PORCH_DURATION_MS = 1.5
LEADER_TONE_DURATION_MS = 300.0
BREAK_DURATION_MS = 10.0
VIS_BIT_DURATION_MS = 30.0
def ms_to_samples(ms, sample_rate=SAMPLE_RATE):
return int(round(ms * sample_rate / 1000.0))
class ModeConfig:
def __init__(self, width=320, height=240, sample_rate=SAMPLE_RATE):
self.width = width
self.height = height
self.sample_rate = sample_rate
# Calculate scan durations based on width
# Standard Robot36 (320px) takes 88ms for Y and 44ms for UV
# We scale this linearly
self.y_scan_duration_ms = 88.0 * (width / 320.0)
self.uv_scan_duration_ms = 44.0 * (width / 320.0)
# Sample counts
self.sync_samples = ms_to_samples(SYNC_DURATION_MS, sample_rate)
self.sync_porch_samples = ms_to_samples(SYNC_PORCH_DURATION_MS, sample_rate)
self.separator_samples = ms_to_samples(SEPARATOR_DURATION_MS, sample_rate)
self.porch_samples = ms_to_samples(PORCH_DURATION_MS, sample_rate)
self.y_scan_samples = ms_to_samples(self.y_scan_duration_ms, sample_rate)
self.uv_scan_samples = ms_to_samples(self.uv_scan_duration_ms, sample_rate)
# Header samples (fixed)
self.leader_tone_samples = ms_to_samples(LEADER_TONE_DURATION_MS, sample_rate)
self.break_samples = ms_to_samples(BREAK_DURATION_MS, sample_rate)
self.vis_bit_samples = ms_to_samples(VIS_BIT_DURATION_MS, sample_rate)
def get_mode_config(width, height, sample_rate=SAMPLE_RATE):
return ModeConfig(width, height, sample_rate)
def rgb_to_yuv(r, g, b):
# Rec. 601 limited range as per Java implementation
# Y = 16 + (65.738*R + 129.057*G + 25.064*B) / 256
# U = 128 + (-37.945*R - 74.494*G + 112.439*B) / 256
# V = 128 + (112.439*R - 94.154*G - 18.285*B) / 256
y = 16.0 + (65.738 * r + 129.057 * g + 25.064 * b) / 256.0
u = 128.0 + (-37.945 * r - 74.494 * g + 112.439 * b) / 256.0
v = 128.0 + (112.439 * r - 94.154 * g - 18.285 * b) / 256.0
return np.clip(y, 16, 235), np.clip(u, 16, 240), np.clip(v, 16, 240)
def yuv_to_rgb(y, u, v):
# Inverse of the above
# Y -= 16
# U -= 128
# V -= 128
# R = (298 * Y + 409 * V + 128) >> 8
# G = (298 * Y - 100 * U - 208 * V + 128) >> 8
# B = (298 * Y + 516 * U + 128) >> 8
y_shifted = y - 16.0
u_shifted = u - 128.0
v_shifted = v - 128.0
r = (298.082 * y_shifted + 408.583 * v_shifted) / 256.0
g = (298.082 * y_shifted - 100.291 * u_shifted - 208.120 * v_shifted) / 256.0
b = (298.082 * y_shifted + 516.412 * u_shifted) / 256.0
return np.clip(r, 0, 255), np.clip(g, 0, 255), np.clip(b, 0, 255)