diff --git a/.gitignore b/.gitignore index d49cec1..614cbff 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ preview/**/*.jpeg preview/**/*.mp4 preview/**/*.webm preview/**/*.wav +preview/**/scan_assets/ diff --git a/AGENTS.md b/AGENTS.md index 9ccc9a3..82dcc54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,3 +105,9 @@ Versorgung und Sicherheit im Plan. - Kamera bei tatsächlicher Dunkelheit, Beleuchtung und Verkleidungen testen. - Sicheren Zustand bei Netzwerkausfall, Neustart und Stopp verifizieren. - Dokumentationsänderungen erfordern keine zusätzlichen Softwaretests. + +## Fiktive Videoeinblendungen + +Scan-/Datenbankdarstellungen in Vorschauvideos verwenden erfundene +Avatare und IDs, mit sichtbarer Simulationskennzeichnung. Externe +Bildsequenzen eines Blender-Schnittprojekts bei Weitergabe mitnehmen. diff --git a/PLAN.md b/PLAN.md index ab4f722..301f1c7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -409,3 +409,44 @@ Der vollständige Render erfolgte mit freigegebenem Grafikkartenzugriff. - [x] Bestehende Videobilder mit neuer Tonspur exportiert; 20 Sekunden, 720p/24 fps und AAC-Spur geprüft. Ergebnis: `preview/v4/halloween_dark_groan.mp4`. - [x] Separate bearbeitbare Blender-Datei mit gepackter Tonspur gespeichert. + + +### Fiktive Scan-/Datenbankvisualisierung im Video + +- [x] Simulierte Scanrahmen und bewegte Scanlinie auf den 3D-Figuren ergänzt. +- [x] Anschließend vier erfundene Datensätze als gespeichert visualisiert. +- [x] Beide Einblendungen sichtbar als 3D-Demo/Simulation gekennzeichnet. +- [x] Standbilder visuell sowie 20 Sekunden, 720p/24 fps und Tonspur geprüft. + +Ergebnis: `preview/v4/halloween_scan_demo.mp4`; Schnittprojekt: +`preview/v4/halloween_scan_demo.blend`. Keine tatsächliche Erkennung, +biometrische Verarbeitung oder Verbindung zur SQLite-Datenbank der Pi-WebUI. + + +### Scan-Kegel mit fiktiven Gesichtern und IDs + +- [x] Kameraperspektivisch projizierten Lichtkegel aus dem Grabstein ergänzt. +- [x] Wandernde Scanfläche und vier gezeichnete Gesichts-Avatare ergänzt. +- [x] Erfundenen IDs DEMO-001 bis DEMO-004 und Simulationskennzeichnung angezeigt. +- [x] Beispielbilder visuell und Videoeigenschaften geprüft: 20 Sekunden, + 720p/24 fps, AAC-Tonspur. Ergebnis: `preview/v4/halloween_cone_demo.mp4`. + +### Überarbeiteter Stöhneinsatz + +- [x] Alte modulierende Bassanteile aus beiden Stöhnlagen entfernt. +- [x] Weichen, hauchigeren Stimmeinsatz beim Handimpuls ergänzt. +- [x] Neu gemischt und mit vollständigen Scan-Kegel-Bildern exportiert; + keine digitale Übersteuerung, 20 Sekunden, 720p/24 fps und AAC geprüft. + Ergebnis: `preview/v4/halloween_cone_demo_audio_v2.mp4`. + +### Gruppenfoto am Videoende + +- [x] Drei Sekunden Fotoanzeige mit Blitz, Auslösegeräusch und Speicherbestätigung. +- [x] Bild der vier fiktiven Besucher auch separat gespeichert. +- [x] Schlussbild visuell und Export geprüft: 23 Sekunden, 720p/24 fps, AAC. + Ergebnis: `preview/v4/halloween_group_photo.mp4`. + +- [x] Gruppenfoto auf tatsächliche 3D-Perspektive des simulierten Scanners + im Grabstein korrigiert, separat gerendert und im Schlussbild ersetzt. + Ergebnis: `preview/v4/halloween_scanner_group_photo.mp4`; + Foto: `preview/v4/gruppenfoto_scanner.png`. diff --git a/preview/instructions.md b/preview/instructions.md index c96007d..aaa3976 100644 --- a/preview/instructions.md +++ b/preview/instructions.md @@ -13,3 +13,7 @@ echte Mechanik oder Personenerkennung. README-Dateien aktuell halten. - Bildprüfung, vollständigen Videorender und reale Hardwareprüfung klar getrennt ausweisen. Ein Standbild belegt keine Bewegung im fertigen Video. + +Scan-Einblendungen sind sichtbar gekennzeichnete Simulationen. +Beim Weitergeben eines Schnittprojekts dessen externe Bildsequenzen und +Avatarbilder mitnehmen; nicht mit echter Gesichtsauswertung verwechseln. diff --git a/preview/v4/README.md b/preview/v4/README.md index 4ba4ee6..71d2ed8 100644 --- a/preview/v4/README.md +++ b/preview/v4/README.md @@ -41,3 +41,69 @@ bestehenden Video übernommen und erneut codiert; kein neuer 3D-Render. `halloween_dark_groan.blend` enthält die neue gepackte Tonspur, `effects_with_groan.wav` die Mischung als separate Audiodatei. Die Mischung bleibt ohne digitale Übersteuerung. + +## Fiktive Scan-Visualisierung + +`halloween_scan_demo.mp4` zeigt von ca. 8,2–11,4 Sekunden einen simulierten +Scan der vier 3D-Figuren und ab ca. 16,8 Sekunden das Schreiben erfundener +Demo-Datensätze. Kennzeichnung als Simulation ist im Video sichtbar. +Es gibt keine tatsächliche Gesichtsauswertung. Die Einblendung ist nicht mit +der SQLite-Datenbank der Pi-WebUI verbunden. Das zugehörige Schnittprojekt +heißt `halloween_scan_demo.blend`. + +## Kegelförmiger Demo-Scan + +`halloween_cone_demo.mp4` erweitert die Scan-Visualisierung um einen +kegelförmigen Strahl vom Grabstein und eine wandernde Scanfläche. +Vier vollständig gezeichnete Gesichts-Avatare erscheinen anschließend +neben den erfundenen IDs DEMO-001 bis DEMO-004. Die Simulation bleibt +als solche beschriftet; keine Verarbeitung realer biometrischer Daten. + +Der Kegel wird aus 3D-Koordinaten in die vorhandene Kameraperspektive +projiziert und als transparente Bildsequenz über das Video gelegt. +Reproduktion: zuerst `tools/project_scan_cone.py` in Blender ausführen, +dann `tools/draw_scan_assets.py` mit Python/Pillow und zuletzt +`tools/add_cone_visualization.py` in Blender. Das Schnittprojekt heißt +`halloween_cone_demo.blend`; seine Bildsequenz liegt in `scan_assets/` +und muss beim Weitergeben mitgenommen werden. Git enthält diese generierten +Bilddateien nicht; vor dem Öffnen des Schnittprojekts die oben genannten +Skripte ausführen. + +## Weicherer Stimmeinsatz + +`halloween_cone_demo_audio_v2.mp4` enthält die vollständigen bisherigen +Bilder mit einer überarbeiteten Tonspur. Beide alten Bass-Stöhnanteile +wurden ersetzt: langsamer Einsatz über 0,65 Sekunden, hauchiger Vokal, +keine Subbasskomponente und keine schnelle Amplitudenmodulation. +Das Stöhnen beginnt weiterhin beim Handimpuls bei 13,04 Sekunden. +Reproduktion: `blender -b -t 12 --python tools/add_dark_groan_v2.py`. +Bearbeitbarer Schnitt: `halloween_cone_demo_audio_v2.blend`, Mischung: +`effects_with_groan_v2.wav`. Audio ohne digitale Übersteuerung geprüft; +Videometadaten bestätigen 20 Sekunden, 720p/24 fps und AAC-Ton. +Die klangliche Wirkung bleibt vom Abspielgerät abhängig. + +## Gruppenfoto am Schluss + +`halloween_group_photo.mp4` ergänzt drei Sekunden mit Kamerablitz, +Auslösegeräusch und der Anzeige „Gruppenfoto gespeichert“. Das Bild zeigt +die vier fiktiven Besucher aus der bestehenden Animation und liegt separat +als `gruppenfoto_demo_001.png` vor. Es illustriert eine Foto-Speicherfunktion. +Schnittprojekt: `halloween_group_photo.blend`; Reproduktion mit +`blender -b -t 12 --python tools/add_group_photo.py` (Python/Pillow benötigt). +Schlussbild visuell geprüft; 23 Sekunden, 720p/24 fps und AAC bestätigt mit +`python3 tools/check_preview.py preview/v4/halloween_group_photo.mp4 23`. + +### Korrigierte Scannerperspektive + +`halloween_scanner_group_photo.mp4` ersetzt das Schlussfoto durch einen +neuen 3D-Render direkt vom Scan-Ursprung `(0, 1.48, 1.02)` am Grabstein. +Die Blickrichtung entspricht dem Kegelziel `(3.25, -0.65, 1.2)`; +20-mm-Objektiv und weiches Licht am Scanner machen die Gruppe sichtbar. +Die Positionen der vier Figuren stammen aus Frame 480 der Animation; +gegenseitige Verdeckungen entsprechen dieser räumlichen Anordnung. +Foto: `gruppenfoto_scanner.png`, 3D-Quelle: `scanner_photo_scene.blend`, +Schnittprojekt: `halloween_scanner_group_photo.blend`. +Reproduktion: zuerst `tools/render_scanner_photo.py`, anschließend +`tools/add_scanner_group_photo.py` jeweils in Blender ausführen. +Scannerfoto und Schlussbild visuell geprüft; Video: 23 Sekunden, +1280 × 720, 24 fps, AAC-Tonspur. diff --git a/preview/v4/halloween_cone_demo.blend b/preview/v4/halloween_cone_demo.blend new file mode 100644 index 0000000..38d84e3 Binary files /dev/null and b/preview/v4/halloween_cone_demo.blend differ diff --git a/preview/v4/halloween_cone_demo_audio_v2.blend b/preview/v4/halloween_cone_demo_audio_v2.blend new file mode 100644 index 0000000..58eda95 Binary files /dev/null and b/preview/v4/halloween_cone_demo_audio_v2.blend differ diff --git a/preview/v4/halloween_group_photo.blend b/preview/v4/halloween_group_photo.blend new file mode 100644 index 0000000..88e940f Binary files /dev/null and b/preview/v4/halloween_group_photo.blend differ diff --git a/preview/v4/halloween_scan_demo.blend b/preview/v4/halloween_scan_demo.blend new file mode 100644 index 0000000..ad6f19a Binary files /dev/null and b/preview/v4/halloween_scan_demo.blend differ diff --git a/preview/v4/halloween_scanner_group_photo.blend b/preview/v4/halloween_scanner_group_photo.blend new file mode 100644 index 0000000..647937c Binary files /dev/null and b/preview/v4/halloween_scanner_group_photo.blend differ diff --git a/preview/v4/scanner_photo_scene.blend b/preview/v4/scanner_photo_scene.blend new file mode 100644 index 0000000..7bf9ea1 Binary files /dev/null and b/preview/v4/scanner_photo_scene.blend differ diff --git a/tools/add_cone_visualization.py b/tools/add_cone_visualization.py new file mode 100644 index 0000000..398d7f6 --- /dev/null +++ b/tools/add_cone_visualization.py @@ -0,0 +1,51 @@ +"""Fictional scan/database HUD for synthetic 3D visitors; no biometric processing.""" +import bpy, os +ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +OUT=os.path.join(ROOT,'preview','v4') +bpy.ops.wm.read_factory_settings(use_empty=True);s=bpy.context.scene +s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100;s.render.fps=24;s.frame_start=1;s.frame_end=480 +s.view_settings.view_transform='Standard';s.sequence_editor_create();seq=s.sequence_editor.sequences +seq.new_movie('Existing animation',os.path.join(OUT,'halloween_dark_groan.mp4'),channel=1,frame_start=1) +seq.new_sound('Existing moan and atmosphere',os.path.join(OUT,'effects_with_groan.wav'),channel=2,frame_start=1) +assets=os.path.join(OUT,'scan_assets') +beam=seq.new_image('Grave headstone scan cone',os.path.join(assets,'cone_198.png'),channel=3,frame_start=198) +for f in range(199,274):beam.elements.append('cone_%03d.png'%f) +beam.frame_final_duration=76 +channel=4 +def text(name,body,start,end,x,y,size=24,color=(.4,1,.8,1)): + global channel + o=seq.new_effect(name,type='TEXT',channel=channel,frame_start=start,frame_end=end);channel+=1 + o.text=body;o.font_size=size;o.color=color;o.location=(x,y);o.align_x='LEFT';o.align_y='TOP';o.use_shadow=True;o.shadow_color=(0,0,0,1) + return o +def bar(name,start,end,x,y,w,h,color): + global channel + o=seq.new_effect(name,type='COLOR',channel=channel,frame_start=start,frame_end=end);channel+=1;o.color=color + o.transform.scale_x=w/1280;o.transform.scale_y=h/720;o.transform.offset_x=x-640;o.transform.offset_y=y-360 + return o +# Scan overlay on fictional figures: static stylized boxes, intentionally not a real detector. +bar('Scan header',198,274,640,665,1280,90,(.006,.018,.025)) +text('Scan title','SIMULIERTER GESICHTSSCAN',198,274,.035,.96,30) +text('Permanent simulation label','3D-DEMO / KEINE REALEN GESICHTSDATEN',198,274,.035,.90,18,(.9,.9,.9,1)) +for i,(x,y,w,h) in enumerate([(765,345,68,78),(819,334,68,78),(969,425,90,100),(1140,417,90,100)]): + for xx,yy,ww,hh in [(x-w/2,y,2,h),(x+w/2,y,2,h),(x,y-h/2,w,2),(x,y+h/2,w,2)]:bar('Synthetic head frame',198,274,xx,yy,ww,hh,(.08,.6,.38)) + text('Synthetic figure label','DEMO %02d'%(i+1),198,274,(x-w/2)/1280,(y+h/2+22)/720,15) +text('Scan progress','DEMO-BILDAUSSCHNITTE WERDEN VORBEREITET ...',198,246,.04,.13,22) +text('Scan completed','4 FIKTIVE 3D-FIGUREN / DEMO-SCAN ABGESCHLOSSEN',246,274,.04,.13,22) +# Later simulated database write, using invented records only. +bar('Database panel',405,481,365,365,690,540,(.007,.019,.025)) +text('Database title','DEMO-DATENBANK',405,481,.045,.84,32) +text('Database subtitle','Fiktive Scan-Datensaetze der 3D-Figuren',405,481,.045,.77,20,(.8,.85,.9,1)) +for i in range(4): + f=410+i*10 + portrait=seq.new_image('Invented face '+str(i),os.path.join(assets,'face_%02d.png'%i),channel=channel,frame_start=f);channel+=1;portrait.frame_final_duration=481-f + text('Record '+str(i),'ID: DEMO-%03d / GESPEICHERT'%(i+1),f,481,.11,.70-i*.11,23) + text('Fictional portrait '+str(i),'FIKTIVES GESICHT | 3D-DEMO',f,481,.11,.66-i*.11,15,(.75,.85,.85,1)) +text('Write confirmation','SCHREIBVORGANG ABGESCHLOSSEN',451,481,.045,.22,22) +text('Database simulation notice','SIMULATION: erfundene Gesichter und IDs.',405,481,.045,.16,17,(.85,.85,.85,1)) +s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='HIGH';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192 +s.render.filepath=os.path.join(OUT,'halloween_cone_demo.mp4') +bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_cone_demo.blend')) +bpy.ops.render.render(animation=True) +s.render.image_settings.file_format='PNG' +for f in [225,465]: + s.frame_set(f);s.render.filepath=os.path.join(OUT,'cone_demo_%03d.png'%f);bpy.ops.render.render(write_still=True) diff --git a/tools/add_dark_groan_v2.py b/tools/add_dark_groan_v2.py new file mode 100644 index 0000000..72520e2 --- /dev/null +++ b/tools/add_dark_groan_v2.py @@ -0,0 +1,99 @@ +"""Replace both old bass growls with a softly entering, breathy vocal moan.""" +import array +import math +import random +import wave +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / 'preview' / 'v4' +rate = 24000 +random.seed(72) +ambience = [] +lp = 0.0 +for i in range(rate * 20): + t = i / rate + lp = .90 * lp + .10 * random.uniform(-1, 1) + env = (.5 + .5 * math.sin(t * 2.1)) ** 3 + value = .07 * lp * env if t < 10 or t > 17 else .006 * lp + if 7.1 < t < 8.3: + value += .13 * lp * (.5 + .5 * math.sin(t * 90)) + # The old 62 Hz modulated growl and rapid onset scratching are omitted. + ambience.append(value) + +random.seed(149) +moan = [0.0] * len(ambience) +phase = breath_low = breath_high = 0.0 +start, length = 13.04, 3.45 +for i in range(int(length * rate)): + t = i / rate + progress = t / length + pitch = 108 - 26 * progress + 1.4 * math.sin(t * 8) + phase += math.tau * pitch / rate + voice = 0.0 + for harmonic in range(2, 23): + frequency = harmonic * pitch + resonance = (.8 * math.exp(-((frequency - 460) / 150) ** 2) + + .4 * math.exp(-((frequency - 950) / 220) ** 2) + + .18 * math.exp(-((frequency - 1450) / 260) ** 2)) + voice += resonance * math.sin(harmonic * phase + harmonic * .17) / math.sqrt(harmonic) + noise = random.uniform(-1, 1) + breath_low += .12 * (noise - breath_low) + breath_high += .015 * (noise - breath_high) + attack = min(1.0, t / .65) + attack = attack * attack * (3 - 2 * attack) + release = min(1.0, (length - t) / .9) + release = release * release * (3 - 2 * release) + envelope = attack * release * (.85 + .15 * math.sin(math.pi * progress)) + moan[round(start * rate) + i] = envelope * (.37 * voice + .13 * (breath_low - breath_high)) + +mixed = [] +peak = 0.0 +for i, ambient in enumerate(ambience): + value = moan[i] + for delay, gain in [(.17, .16), (.33, .08)]: + index = i - round(delay * rate) + if index >= 0: + value += gain * moan[index] + value += ambient + peak = max(peak, abs(value)) + mixed.append(round(value * 32767)) +assert peak < .98, peak +path = OUT / 'effects_with_groan_v2.wav' +with wave.open(str(path), 'wb') as handle: + handle.setparams((1, 2, rate, len(mixed), 'NONE', 'not compressed')) + handle.writeframes(array.array('h', mixed).tobytes()) +print(f'Soft vocal onset at {start}s; audio peak {peak:.3f}, no clipping') + +import bpy +bpy.ops.wm.open_mainfile(filepath=str(OUT / 'halloween_cone_demo.blend')) +scene = bpy.context.scene +for strip in list(scene.sequence_editor.sequences): + if strip.type == 'SOUND': + scene.sequence_editor.sequences.remove(strip) +scene.sequence_editor.sequences.new_sound('Soft breathy moan and ambience', str(path), channel=2, frame_start=1) +bpy.ops.file.pack_all() +bpy.ops.wm.save_as_mainfile(filepath=str(OUT / 'halloween_cone_demo_audio_v2.blend')) + +# Reuse the complete latest picture, including the cone and fictional IDs. +bpy.ops.wm.read_factory_settings(use_empty=True) +scene = bpy.context.scene +scene.render.resolution_x = 1280 +scene.render.resolution_y = 720 +scene.render.resolution_percentage = 100 +scene.render.fps = 24 +scene.frame_start = 1 +scene.frame_end = 480 +scene.view_settings.view_transform = 'Standard' +scene.view_settings.look = 'None' +scene.sequence_editor_create() +scene.sequence_editor.sequences.new_movie('Latest complete picture', str(OUT / 'halloween_cone_demo.mp4'), channel=1, frame_start=1) +scene.sequence_editor.sequences.new_sound('Revised audio', str(path), channel=2, frame_start=1) +scene.render.image_settings.file_format = 'FFMPEG' +scene.render.ffmpeg.format = 'MPEG4' +scene.render.ffmpeg.codec = 'H264' +scene.render.ffmpeg.constant_rate_factor = 'HIGH' +scene.render.ffmpeg.audio_codec = 'AAC' +scene.render.ffmpeg.audio_bitrate = 192 +scene.render.filepath = str(OUT / 'halloween_cone_demo_audio_v2.mp4') +bpy.ops.render.render(animation=True) diff --git a/tools/add_group_photo.py b/tools/add_group_photo.py new file mode 100644 index 0000000..29f5c70 --- /dev/null +++ b/tools/add_group_photo.py @@ -0,0 +1,42 @@ +"""Append a simulated saved group snapshot of the four synthetic visitors.""" +from pathlib import Path +import array, math, random, wave +from PIL import Image, ImageEnhance +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / 'preview/v4' +image = Image.open(OUT/'frames/review_185.png').convert('RGB') +image = ImageEnhance.Brightness(image).enhance(1.7) +image.save(OUT/'group_photo.png') +image.save(OUT/'gruppenfoto_demo_001.png') +import bpy +bpy.ops.wm.open_mainfile(filepath=str(OUT/'halloween_cone_demo_audio_v2.blend')) +s=bpy.context.scene +seq=s.sequence_editor.sequences +photo=seq.new_image('Group snapshot - four fictional visitors',str(OUT/'group_photo.png'),channel=30,frame_start=481) +photo.frame_final_duration=72 +photo.transform.scale_x=.88;photo.transform.scale_y=.88 +for name,body,y,size,color in [ + ('Photo title','GRUPPENFOTO GESPEICHERT',.96,30,(.4,1,.8,1)), + ('Photo file','gruppenfoto_demo_001.png | 4 fiktive Besucher',.085,20,(.9,.9,.9,1)), + ('Simulation','SIMULATION / 3D-FIGUREN',.045,17,(.65,.8,.8,1))]: + strip=seq.new_effect(name,type='TEXT',channel=31+len([x for x in seq if x.name.startswith('Photo')]),frame_start=481,frame_end=553) + strip.text=body;strip.font_size=size;strip.color=color;strip.location=(.5,y);strip.align_x='CENTER';strip.use_shadow=True +flash=seq.new_effect('Camera flash',type='COLOR',channel=40,frame_start=481,frame_end=486) +flash.color=(1,1,1);flash.blend_type='ALPHA_OVER' +flash.blend_alpha=.75;flash.keyframe_insert('blend_alpha',frame=481) +flash.blend_alpha=0;flash.keyframe_insert('blend_alpha',frame=485) +rate=24000;random.seed(51);sound=[] +for i in range(rate*3): + t=i/rate + click=.16*random.uniform(-1,1)*math.exp(-t*75) + click+=.08*random.uniform(-1,1)*math.exp(-max(0,t-.11)*90) if t>=.11 else 0 + sound.append(round(click*32767)) +with wave.open(str(OUT/'group_photo_shutter.wav'),'wb') as w: + w.setparams((1,2,rate,len(sound),'NONE','not compressed'));w.writeframes(array.array('h',sound).tobytes()) +seq.new_sound('Camera shutter',str(OUT/'group_photo_shutter.wav'),channel=41,frame_start=481) +s.frame_end=552 +s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='HIGH';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192 +s.render.filepath=str(OUT/'halloween_group_photo.mp4') +bpy.ops.file.pack_all();bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'halloween_group_photo.blend')) +bpy.ops.render.render(animation=True) +s.render.image_settings.file_format='PNG';s.frame_set(510);s.render.filepath=str(OUT/'group_photo_end.png');bpy.ops.render.render(write_still=True) diff --git a/tools/add_scan_visualization.py b/tools/add_scan_visualization.py new file mode 100644 index 0000000..27c551d --- /dev/null +++ b/tools/add_scan_visualization.py @@ -0,0 +1,48 @@ +"""Fictional scan/database HUD for synthetic 3D visitors; no biometric processing.""" +import bpy, os +ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +OUT=os.path.join(ROOT,'preview','v4') +bpy.ops.wm.read_factory_settings(use_empty=True);s=bpy.context.scene +s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100;s.render.fps=24;s.frame_start=1;s.frame_end=480 +s.view_settings.view_transform='Standard';s.sequence_editor_create();seq=s.sequence_editor.sequences +seq.new_movie('Existing animation',os.path.join(OUT,'halloween_dark_groan.mp4'),channel=1,frame_start=1) +seq.new_sound('Existing moan and atmosphere',os.path.join(OUT,'effects_with_groan.wav'),channel=2,frame_start=1) +channel=3 +def text(name,body,start,end,x,y,size=24,color=(.4,1,.8,1)): + global channel + o=seq.new_effect(name,type='TEXT',channel=channel,frame_start=start,frame_end=end);channel+=1 + o.text=body;o.font_size=size;o.color=color;o.location=(x,y);o.align_x='LEFT';o.align_y='TOP';o.use_shadow=True;o.shadow_color=(0,0,0,1) + return o +def bar(name,start,end,x,y,w,h,color): + global channel + o=seq.new_effect(name,type='COLOR',channel=channel,frame_start=start,frame_end=end);channel+=1;o.color=color + o.transform.scale_x=w/1280;o.transform.scale_y=h/720;o.transform.offset_x=x-640;o.transform.offset_y=y-360 + return o +# Scan overlay on fictional figures: static stylized boxes, intentionally not a real detector. +bar('Scan header',198,274,640,665,1280,90,(.006,.018,.025)) +text('Scan title','SIMULIERTER GESICHTSSCAN',198,274,.035,.96,30) +text('Permanent simulation label','3D-DEMO / KEINE REALEN GESICHTSDATEN',198,274,.035,.90,18,(.9,.9,.9,1)) +for i,(x,y,w,h) in enumerate([(765,345,68,78),(819,334,68,78),(969,425,90,100),(1140,417,90,100)]): + for xx,yy,ww,hh in [(x-w/2,y,2,h),(x+w/2,y,2,h),(x,y-h/2,w,2),(x,y+h/2,w,2)]:bar('Synthetic head frame',198,274,xx,yy,ww,hh,(.08,.6,.38)) + text('Synthetic figure label','DEMO %02d'%(i+1),198,274,(x-w/2)/1280,(y+h/2+22)/720,15) +line=bar('Scan sweep',198,246,640,475,1100,2,(.05,.6,.35)) +line.transform.offset_y=110;line.transform.keyframe_insert('offset_y',frame=198) +line.transform.offset_y=-155;line.transform.keyframe_insert('offset_y',frame=245) +text('Scan progress','DEMO-BILDAUSSCHNITTE WERDEN VORBEREITET ...',198,246,.04,.13,22) +text('Scan completed','4 FIKTIVE 3D-FIGUREN / DEMO-SCAN ABGESCHLOSSEN',246,274,.04,.13,22) +# Later simulated database write, using invented records only. +bar('Database panel',405,481,365,365,690,540,(.007,.019,.025)) +text('Database title','DEMO-DATENBANK',405,481,.045,.84,32) +text('Database subtitle','Fiktive Scan-Datensaetze der 3D-Figuren',405,481,.045,.77,20,(.8,.85,.9,1)) +for i in range(4): + f=410+i*10 + text('Record '+str(i),'DEMO-%03d | 3D-Gesichtsausschnitt | GESPEICHERT'%(i+1),f,481,.045,.66-i*.07,19) +text('Write confirmation','SCHREIBVORGANG ABGESCHLOSSEN',451,481,.045,.32,24) +text('Database simulation notice','SIMULATION: keine Erkennung, keine echte Datenbank.',405,481,.045,.23,18,(.85,.85,.85,1)) +s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='HIGH';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192 +s.render.filepath=os.path.join(OUT,'halloween_scan_demo.mp4') +bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_scan_demo.blend')) +bpy.ops.render.render(animation=True) +s.render.image_settings.file_format='PNG' +for f in [225,465]: + s.frame_set(f);s.render.filepath=os.path.join(OUT,'scan_demo_%03d.png'%f);bpy.ops.render.render(write_still=True) diff --git a/tools/add_scanner_group_photo.py b/tools/add_scanner_group_photo.py new file mode 100644 index 0000000..de0d4ce --- /dev/null +++ b/tools/add_scanner_group_photo.py @@ -0,0 +1,20 @@ +"""Replace the concluding photo with the rendered scanner viewpoint.""" +import bpy +from pathlib import Path +OUT=Path(__file__).resolve().parents[1]/'preview/v4' +bpy.ops.wm.open_mainfile(filepath=str(OUT/'halloween_group_photo.blend')) +s=bpy.context.scene +for strip in s.sequence_editor.sequences: + if strip.name.startswith('Group snapshot'): + strip.directory=str(OUT)+'/' + strip.elements[0].filename='gruppenfoto_scanner.png' + if strip.name=='Photo file': + strip.text='gruppenfoto_scanner.png | SCANNER IM GRABSTEIN' +s.render.image_settings.file_format='FFMPEG' +s.render.filepath=str(OUT/'halloween_scanner_group_photo.mp4') +bpy.ops.file.pack_all() +bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'halloween_scanner_group_photo.blend')) +bpy.ops.render.render(animation=True) +s.render.image_settings.file_format='PNG';s.frame_set(510) +s.render.filepath=str(OUT/'scanner_group_photo_end.png') +bpy.ops.render.render(write_still=True) diff --git a/tools/check_preview.py b/tools/check_preview.py index eaa10b2..8d3bbd1 100644 --- a/tools/check_preview.py +++ b/tools/check_preview.py @@ -14,7 +14,7 @@ def boxes(data): def child(data,name):return next(body for kind,body in boxes(data) if kind==name) -def inspect(path): +def inspect(path, expected_seconds=20): moov=child(Path(path).read_bytes(),'moov');result=[] for kind,trak in boxes(moov): if kind!='trak':continue @@ -27,9 +27,9 @@ def inspect(path): item={'type':kind,'codec':entry[0],'duration_seconds':seconds,'samples':count} if kind=='vide': width,height=struct.unpack_from('>HH',entry[1],24);item.update(width=width,height=height,fps=count/seconds) - assert (width,height,count)==(1280,720,480),item - assert abs(seconds-20)<.01,item + assert (width,height,count)==(1280,720,round(expected_seconds*24)),item + assert abs(seconds-expected_seconds)<.01,item result.append(item) assert any(t['type']=='soun' for t in result),'Missing audio track' return result -if __name__=='__main__':print(json.dumps(inspect(sys.argv[1]),indent=2)) +if __name__=='__main__':print(json.dumps(inspect(sys.argv[1],float(sys.argv[2]) if len(sys.argv)>2 else 20),indent=2)) diff --git a/tools/draw_scan_assets.py b/tools/draw_scan_assets.py new file mode 100644 index 0000000..3a213e5 --- /dev/null +++ b/tools/draw_scan_assets.py @@ -0,0 +1,32 @@ +"""Draw projected cone and four invented synthetic face avatars.""" +import json, math +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter +root=Path(__file__).resolve().parents[1];out=root/'preview/v4/scan_assets' +for record in json.loads((out/'projection.json').read_text()): + layer=Image.new('RGBA',(1280,720));d=ImageDraw.Draw(layer) + origin=tuple(record['origin']);ring=[tuple(p) for p in record['ring']] + # Transparent beam facets and a softly luminous emitter; geometry follows the camera. + for i in range(len(ring)): + d.polygon([origin,ring[i],ring[(i+1)%len(ring)]],fill=(20,220,170,13)) + d.line(ring+[ring[0]],fill=(80,255,215,110),width=2) + for i in [0,10,20,30]:d.line([origin,ring[i]],fill=(50,240,190,80),width=1) + # A travelling cross-section makes the cone visibly sweep through the group. + progress=((record['frame']-198)%32)/31 + scan=[(origin[0]+(p[0]-origin[0])*progress,origin[1]+(p[1]-origin[1])*progress) for p in ring] + d.line(scan+[scan[0]],fill=(110,255,225,160),width=2) + x,y=origin;d.ellipse((x-4,y-4,x+4,y+4),fill=(190,255,235,235)) + glow=layer.filter(ImageFilter.GaussianBlur(5));Image.alpha_composite(glow,layer).save(out/('cone_%03d.png'%record['frame'])) +for i in range(4): + layer=Image.new('RGBA',(1280,720));d=ImageDraw.Draw(layer);x=56;y=205+i*79 + d.rounded_rectangle((x,y,x+64,y+66),radius=5,fill=(8,31,37,255),outline=(75,245,200,240),width=1) + cx=x+32;cy=y+31;skin=[(158,107,79),(200,147,111),(129,90,68),(220,175,140)][i] + d.ellipse((cx-17,cy-23,cx+17,cy+23),fill=(*skin,255),outline=(95,240,190,255)) + hair=[(31,24,21),(83,42,23),(18,19,25),(133,79,33)][i] + d.pieslice((cx-18,cy-25,cx+18,cy+6),180,360,fill=(*hair,255)) + for sign in [-1,1]: + ex=cx+sign*7;d.line((ex-3,cy-4,ex+3,cy-4),fill=(20,25,24,255),width=2) + d.line((cx,cy-2,cx-2,cy+7,cx+2,cy+7),fill=(67,53,42,255),width=1) + d.arc((cx-6,cy+8,cx+6,cy+16),0,180,fill=(54,32,25,255),width=1) + for px,py in [(cx-8,cy-5),(cx+8,cy-5),(cx,cy+5),(cx-5,cy+15),(cx+5,cy+15)]:d.ellipse((px-1,py-1,px+1,py+1),fill=(80,255,190,255)) + layer.save(out/('face_%02d.png'%i)) diff --git a/tools/instructions.md b/tools/instructions.md index b78913e..aa5a866 100644 --- a/tools/instructions.md +++ b/tools/instructions.md @@ -19,3 +19,7 @@ Hier liegen Pi-Transfer/Einrichtung und die reproduzierbaren Blender-Werkzeuge. Änderungen an Upload- und Installationsbefehlen in `UPLOAD.md` und `SOFTWARE.md` nachführen; Renderabläufe in den jeweiligen README-Dateien unter `preview/` dokumentieren. + +Fiktive Scan-Videos nutzen nur synthetische Avatare und Demo-IDs. +Kegelprojektion, Zeichnen der Bildsequenz und Videoschnitt sind getrennte +Schritte; Reihenfolge im zugehörigen Vorschau-README dokumentieren. diff --git a/tools/project_scan_cone.py b/tools/project_scan_cone.py new file mode 100644 index 0000000..138a9c8 --- /dev/null +++ b/tools/project_scan_cone.py @@ -0,0 +1,15 @@ +"""Project fictional grave emitter into the existing animated camera.""" +import bpy, os, json, math +from mathutils import Vector +from bpy_extras.object_utils import world_to_camera_view +root=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +bpy.ops.wm.open_mainfile(filepath=os.path.join(root,'preview/v4/halloween_dark_groan.blend')) +s=bpy.context.scene;origin=Vector((0,1.48,1.02));end=Vector((3.25,-.65,1.2));direction=(end-origin).normalized() +u=direction.cross(Vector((0,0,1))).normalized();v=direction.cross(u).normalized();records=[] +def project(p): + c=world_to_camera_view(s,s.camera,p);return [c.x*1280,(1-c.y)*720] +for f in range(198,274): + s.frame_set(f);ring=[project(end+.72*(u*math.cos(i*math.tau/40)+v*math.sin(i*math.tau/40))) for i in range(40)] + records.append({'frame':f,'origin':project(origin),'ring':ring}) +path=os.path.join(root,'preview/v4/scan_assets');os.makedirs(path,exist_ok=True) +with open(os.path.join(path,'projection.json'),'w') as out:json.dump(records,out) diff --git a/tools/render_scanner_photo.py b/tools/render_scanner_photo.py new file mode 100644 index 0000000..81a0c41 --- /dev/null +++ b/tools/render_scanner_photo.py @@ -0,0 +1,20 @@ +"""Render a genuine view from the same origin/direction as the demo scan cone.""" +import bpy +from pathlib import Path +from mathutils import Vector +ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'preview/v4' +bpy.ops.wm.open_mainfile(filepath=str(OUT/'halloween_dark.blend')) +s=bpy.context.scene;s.frame_set(480) +bpy.ops.object.camera_add(location=(0,1.48,1.02)) +cam=bpy.context.object;cam.name='Scanner camera at headstone emitter' +cam.rotation_euler=(Vector((3.25,-.65,1.2))-cam.location).to_track_quat('-Z','Y').to_euler() +cam.data.lens=20;cam.data.clip_start=.03;s.camera=cam +# Soft illumination alongside the scanner represents its photo capture light. +light=bpy.data.lights.new('Scanner photo fill','AREA');light.energy=110;light.color=(.72,.82,1);light.size=.55 +ob=bpy.data.objects.new('Scanner photo fill',light);s.collection.objects.link(ob) +ob.location=cam.location+Vector((.05,-.05,.06));ob.rotation_euler=cam.rotation_euler +s.render.use_sequencer=False;s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100 +s.eevee.taa_render_samples=32;s.eevee.volumetric_samples=16 +s.render.image_settings.file_format='PNG';s.render.filepath=str(OUT/'gruppenfoto_scanner.png') +bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'scanner_photo_scene.blend')) +bpy.ops.render.render(write_still=True)