Skip to content

Commit 2c26f1e

Browse files
generate video
1 parent 693741d commit 2c26f1e

2 files changed

Lines changed: 206 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ pyllusion\__pycache__\
55
pyllusion.egg
66
try_stuff.py
77
.Rhistory
8+
ebbinghaus_parameters.mp4
9+
.eggs/*
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
"""
2+
Generate an mp4 loop video illustrating how the Ebbinghaus illusion parameters
3+
(illusion_strength and difference) affect the image.
4+
5+
The video follows a continuous smooth sequence:
6+
1. Difference: 0 -> negative max -> positive max
7+
2. Illusion strength: 0 -> positive max -> negative max
8+
3. Difference -> 0
9+
4. Illusion strength -> 0
10+
"""
11+
12+
import os
13+
import sys
14+
15+
# Ensure the project root is on sys.path so pyllusion can be imported
16+
# regardless of the working directory from which the script is invoked.
17+
_HERE = os.path.dirname(os.path.abspath(__file__))
18+
_PROJECT_ROOT = os.path.dirname(os.path.dirname(_HERE))
19+
if _PROJECT_ROOT not in sys.path:
20+
sys.path.insert(0, _PROJECT_ROOT)
21+
22+
import imageio
23+
import numpy as np
24+
import PIL.Image
25+
import PIL.ImageDraw
26+
import PIL.ImageFont
27+
28+
import pyllusion
29+
30+
31+
# ── Video settings ─────────────────────────────────────────────────────────────
32+
FPS = 24
33+
WIDTH = 800
34+
HEIGHT = 544 # divisible by 16 for codec compatibility
35+
OUTPUT = "ebbinghaus_parameters.mp4"
36+
37+
# ── Layout constants ────────────────────────────────────────────────────────
38+
LEFT_BAR = 100 # width of vertical illusion_strength slider strip
39+
TOP_BAR = 50 # height of title strip
40+
BOT_BAR = 90 # height of bottom info strip
41+
42+
43+
# ── Helpers ────────────────────────────────────────────────────────────────────
44+
45+
def _get_font(size, bold=False):
46+
"""Load a TrueType font, falling back to the PIL default."""
47+
names = ["arialbd.ttf", "C:/Windows/Fonts/arialbd.ttf"] if bold \
48+
else ["arial.ttf", "C:/Windows/Fonts/arial.ttf"]
49+
for name in names:
50+
try:
51+
return PIL.ImageFont.truetype(name, size)
52+
except OSError:
53+
pass
54+
return PIL.ImageFont.load_default()
55+
56+
57+
def _add_labels(img: PIL.Image.Image, strength: float, difference: float) -> PIL.Image.Image:
58+
"""Overlay all UI chrome (title, sliders, labels) onto the canvas."""
59+
60+
draw = PIL.ImageDraw.Draw(img)
61+
62+
# ── "Ebbinghaus" centred bold title ─────────────────────────────────────
63+
font_title = _get_font(28, bold=True)
64+
bbox = draw.textbbox((0, 0), "Ebbinghaus", font=font_title)
65+
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
66+
draw.text(((img.width - tw) // 2, (TOP_BAR - th) // 2),
67+
"Ebbinghaus", fill="black", font=font_title)
68+
69+
# ── Shared Slider Properties ─────────────────────────────────────────────
70+
TRACK_W = 6 # half-thickness (12px total thickness)
71+
TRACK_BG = (220, 220, 220) # grey empty track
72+
ILL_TRACK_FG = (100, 150, 200) # blue filled track (strength)
73+
ILL_DOT_FG = (80, 130, 220) # blue dot color (strength)
74+
DIFF_TRACK_FG = (220, 80, 80) # red filled track (difference)
75+
DIFF_DOT_FG = (220, 50, 50) # red dot color (difference)
76+
DOT_R = 8 # dot radius
77+
font_slider_title = _get_font(18, bold=True)
78+
79+
# ── 1. Left Vertical Slider (Illusion Strength) ──────────────────────────
80+
SX = 70
81+
S_TOP = TOP_BAR + 20
82+
S_BOT = img.height - BOT_BAR - 20
83+
track_h = S_BOT - S_TOP
84+
zero_y = (S_TOP + S_BOT) // 2
85+
86+
# track and zero-tick
87+
draw.rectangle([(SX - TRACK_W, S_TOP), (SX + TRACK_W, S_BOT)], fill=TRACK_BG)
88+
draw.line([(SX - TRACK_W - 4, zero_y), (SX + TRACK_W + 4, zero_y)], fill=(160, 160, 160), width=2)
89+
90+
# calculate position and draw fill/dot
91+
norm_str = float(np.clip(strength / 2.0, -1.0, 1.0))
92+
cursor_y = int(S_BOT - (norm_str + 1.0) / 2.0 * track_h)
93+
94+
y0, y1 = min(zero_y, cursor_y), max(zero_y, cursor_y)
95+
draw.rectangle([(SX - TRACK_W, y0), (SX + TRACK_W, y1)], fill=ILL_TRACK_FG)
96+
draw.ellipse([(SX - DOT_R, cursor_y - DOT_R), (SX + DOT_R, cursor_y + DOT_R)], fill=ILL_DOT_FG, outline="white", width=2)
97+
98+
# Vertical Title (Illusion Strength)
99+
lb = draw.textbbox((0, 0), "Illusion Strength", font=font_slider_title)
100+
lw, lh = lb[2] - lb[0], lb[3] - lb[1]
101+
lbl_surf = PIL.Image.new("RGBA", (lw + 4, lh + 4), (255, 255, 255, 0))
102+
PIL.ImageDraw.Draw(lbl_surf).text((2, 2), "Illusion Strength", fill="black", font=font_slider_title)
103+
lbl_rot = lbl_surf.rotate(90, expand=True)
104+
105+
# Paste centered vertically on the left
106+
img.paste(lbl_rot, (20, (S_TOP + S_BOT) // 2 - lbl_rot.height // 2), lbl_rot)
107+
draw = PIL.ImageDraw.Draw(img) # refresh draw context after paste
108+
109+
110+
# ── 2. Bottom Horizontal Slider (Difference Size) ────────────────────────
111+
SY = img.height - 65
112+
S_LEFT = LEFT_BAR + 20
113+
S_RIGHT = img.width - 20
114+
track_w = S_RIGHT - S_LEFT
115+
zero_x = (S_LEFT + S_RIGHT) // 2
116+
117+
# track and zero-tick
118+
draw.rectangle([(S_LEFT, SY - TRACK_W), (S_RIGHT, SY + TRACK_W)], fill=TRACK_BG)
119+
draw.line([(zero_x, SY - TRACK_W - 4), (zero_x, SY + TRACK_W + 4)], fill=(160, 160, 160), width=2)
120+
121+
# calculate position and draw fill/dot (Mapping is reversed here)
122+
norm_diff = float(np.clip(difference, -1.0, 1.0))
123+
cursor_x = int(zero_x - norm_diff * (track_w / 2))
124+
125+
x0, x1 = min(zero_x, cursor_x), max(zero_x, cursor_x)
126+
draw.rectangle([(x0, SY - TRACK_W), (x1, SY + TRACK_W)], fill=DIFF_TRACK_FG)
127+
draw.ellipse([(cursor_x - DOT_R, SY - DOT_R), (cursor_x + DOT_R, SY + DOT_R)], fill=DIFF_DOT_FG, outline="white", width=2)
128+
129+
# Horizontal Title (Difference Size)
130+
db = draw.textbbox((0, 0), "Difference Size", font=font_slider_title)
131+
dw, dh = db[2] - db[0], db[3] - db[1]
132+
# Place it directly below the slider, centered between the left bar and the right edge
133+
title_x = S_LEFT + (track_w // 2) - (dw // 2)
134+
draw.text((title_x, img.height - 35), "Difference Size", fill="black", font=font_slider_title)
135+
136+
return img
137+
138+
139+
def _render_frame(strength: float, difference: float) -> np.ndarray:
140+
"""Render one video frame as an RGB numpy array."""
141+
illusion_w = WIDTH - LEFT_BAR
142+
illusion_h = HEIGHT - TOP_BAR - BOT_BAR
143+
illusion = pyllusion.Ebbinghaus(illusion_strength=strength, difference=difference)
144+
img = illusion.to_image(width=illusion_w, height=illusion_h, background="white")
145+
146+
# Build full canvas
147+
canvas = PIL.Image.new("RGB", (WIDTH, HEIGHT), "white")
148+
canvas.paste(img, (LEFT_BAR, TOP_BAR))
149+
150+
canvas = _add_labels(canvas, strength, difference)
151+
return np.array(canvas)
152+
153+
154+
# ── Sequence Generation ────────────────────────────────────────────────────────
155+
156+
def generate_sequence():
157+
"""Generates the seamless parametric path across the 4 stages."""
158+
diff_vals = []
159+
ill_vals = []
160+
161+
def add_move(d_start, d_end, i_start, i_end, time_units):
162+
frames = int(FPS * time_units)
163+
# Use a smoothstep cosine interpolation for smooth easing
164+
t = np.linspace(0, 1, frames, endpoint=False)
165+
weight = (1 - np.cos(t * np.pi)) / 2
166+
167+
diff_vals.extend(d_start + (d_end - d_start) * weight)
168+
ill_vals.extend(i_start + (i_end - i_start) * weight)
169+
170+
# 1) Difference from 0 to negative max (-1), then to positive max (+1)
171+
add_move(0.0, -1.0, 0.0, 0.0, 1.0)
172+
add_move(-1.0, 1.0, 0.0, 0.0, 2.0)
173+
174+
# 2) Illusion strength from 0 to positive max (+2), then to negative max (-2)
175+
add_move(1.0, 1.0, 0.0, 2.0, 2.0)
176+
add_move(1.0, 1.0, 2.0, -2.0, 4.0)
177+
178+
# 3) Difference goes back to 0
179+
add_move(1.0, 0.0, -2.0, -2.0, 1.0)
180+
181+
# 4) Illusion strength goes back to 0
182+
add_move(0.0, 0.0, -2.0, 0.0, 2.0)
183+
184+
return diff_vals, ill_vals
185+
186+
187+
# ── Main ───────────────────────────────────────────────────────────────────────
188+
189+
if __name__ == "__main__":
190+
191+
diffs, strengths = generate_sequence()
192+
frames = []
193+
total_frames = len(diffs)
194+
195+
print(f"Starting render of {total_frames} frames...")
196+
197+
for i, (d, s) in enumerate(zip(diffs, strengths)):
198+
print(f" Rendering frame {i+1}/{total_frames}", end="\r")
199+
frames.append(_render_frame(float(s), float(d)))
200+
201+
print(f"\nWriting frames → {OUTPUT}")
202+
imageio.mimwrite(OUTPUT, frames, fps=FPS, codec="libx264",
203+
output_params=["-pix_fmt", "yuv420p", "-crf", "18"])
204+
print("Done.")

0 commit comments

Comments
 (0)