mirror of
https://github.com/bagusindrayana/ews-concept-new.git
synced 2026-06-08 09:45:34 +00:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
083cf23dbf | ||
|
|
91f96952e3 |
4 changed files with 292 additions and 0 deletions
131
convert_bad_apple.py
Normal file
131
convert_bad_apple.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
import json
|
||||
import argparse
|
||||
import sys
|
||||
import math
|
||||
|
||||
def distance(p1, p2):
|
||||
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])
|
||||
|
||||
def sort_points_nearest_neighbor(points):
|
||||
"""
|
||||
Mengurutkan titik-titik agar menjadi satu garis yang menyambung (TSP approximation).
|
||||
Sangat krusial untuk rendering tipe oscilloscope/XY agar garis tidak melompat-lompat berantakan.
|
||||
"""
|
||||
if not points:
|
||||
return []
|
||||
|
||||
unvisited = points.copy()
|
||||
current = unvisited.pop(0)
|
||||
sorted_points = [current]
|
||||
|
||||
while unvisited:
|
||||
nearest_idx = 0
|
||||
min_dist = distance(current, unvisited[0])
|
||||
for i in range(1, len(unvisited)):
|
||||
dist = distance(current, unvisited[i])
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
nearest_idx = i
|
||||
|
||||
current = unvisited.pop(nearest_idx)
|
||||
sorted_points.append(current)
|
||||
|
||||
return sorted_points
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Convert black & white video to XY JSON for Waveform Chart")
|
||||
parser.add_argument("input", help="Path to input video (e.g. bad_apple.mp4)")
|
||||
parser.add_argument("output", help="Path to output JSON (e.g. bad_apple_frames.json)")
|
||||
parser.add_argument("--fps", type=int, default=15, help="Target FPS to extract (lower is easier on browser memory)")
|
||||
parser.add_argument("--max-points", type=int, default=400, help="Max points per frame (lower = faster render, higher = detailed)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
cap = cv2.VideoCapture(args.input)
|
||||
if not cap.isOpened():
|
||||
print(f"Error: Could not open video {args.input}")
|
||||
sys.exit(1)
|
||||
|
||||
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
if video_fps <= 0:
|
||||
video_fps = 30
|
||||
|
||||
frame_skip = max(1, int(video_fps / args.fps))
|
||||
|
||||
frames_data = []
|
||||
frame_count = 0
|
||||
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
print(f"Processing video: {width}x{height} @ {video_fps}fps")
|
||||
print(f"Targeting: {args.fps}fps, Max {args.max_points} points/frame")
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
if frame_count % frame_skip == 0:
|
||||
# 1. Convert to graysale
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# 2. Thresholding (Pastikan benar-benar hitam/putih murni)
|
||||
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# 3. Cari garis tepi (Contours)
|
||||
# Menggunakan RETR_LIST agar lubang (seperti apel hitam di background putih) ikut terbaca
|
||||
contours, _ = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
frame_points = []
|
||||
screen_area = width * height
|
||||
for contour in contours:
|
||||
# Abaikan batas kotak frame layar penuh saat warnanya terbalik
|
||||
if cv2.contourArea(contour) < screen_area * 0.95:
|
||||
for point in contour:
|
||||
x, y = point[0]
|
||||
frame_points.append((x, y))
|
||||
|
||||
# 4. Batasi jumlah titik agar web tidak hang (Downsampling)
|
||||
if len(frame_points) > args.max_points:
|
||||
step = len(frame_points) / args.max_points
|
||||
frame_points = [frame_points[int(i * step)] for i in range(args.max_points)]
|
||||
|
||||
# 5. Sortir titik-titik (Algoritma Jalur Terpendek)
|
||||
# supaya "benang" yang digambar di Svelte tidak ruwet/melompat jauh
|
||||
if len(frame_points) > 0:
|
||||
frame_points = sort_points_nearest_neighbor(frame_points)
|
||||
|
||||
# 6. Normalisasi ke rentang nx, ny (-1.0 sampai 1.0)
|
||||
normalized_points = []
|
||||
for (x, y) in frame_points:
|
||||
nx = (x / width) * 2.0 - 1.0
|
||||
|
||||
# Koordinat Canvas Y itu dari Atas(0) ke Bawah(Height).
|
||||
# Sumbu Math/Grafik Bawah(-1) ke Atas(1). Di versi terbaru chart,
|
||||
# Y positif ny sudah menuju ke bawah, jadi tidak perlu di-invert lagi.
|
||||
ny = (y / height) * 2.0 - 1.0
|
||||
|
||||
normalized_points.append({"nx": round(nx, 4), "ny": round(ny, 4)})
|
||||
|
||||
# Hanya simpan frame jika ada point-nya
|
||||
frames_data.append(normalized_points)
|
||||
|
||||
if len(frames_data) % 100 == 0:
|
||||
print(f"Processed {len(frames_data)} target frames...")
|
||||
|
||||
frame_count += 1
|
||||
|
||||
cap.release()
|
||||
|
||||
print(f"Saving {len(frames_data)} frames to {args.output}...")
|
||||
with open(args.output, 'w') as f:
|
||||
# Tulis JSON serapat mungkin (tanpa indent) agar file lebih kecil
|
||||
json.dump(frames_data, f, separators=(',', ':'))
|
||||
|
||||
print(f"Done! The file size is ready for consumption by WaveformChart.svelte.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
159
src/routes/bad-apple/+page.svelte
Normal file
159
src/routes/bad-apple/+page.svelte
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import WaveformChart from "$lib/components/WaveformChart.svelte";
|
||||
|
||||
let frames: any[] = [];
|
||||
let currentFrameIndex = 0;
|
||||
let isPlaying = false;
|
||||
let isLoading = true;
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
// Props for WaveformChart
|
||||
let waveformChart: any;
|
||||
let psychoPoints: { nx: number; ny: number }[] = [];
|
||||
|
||||
// Target FPS used during extraction
|
||||
const TARGET_FPS = 15;
|
||||
const MS_PER_FRAME = 1000 / TARGET_FPS;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch("/bad_apple_frames_v4.json");
|
||||
if (response.ok) {
|
||||
frames = await response.json();
|
||||
isLoading = false;
|
||||
|
||||
// Set initial frame
|
||||
if (frames.length > 0) {
|
||||
psychoPoints = frames[0];
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch frames:", response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load Bad Apple frames:", error);
|
||||
}
|
||||
});
|
||||
|
||||
function togglePlay() {
|
||||
if (isPlaying) {
|
||||
isPlaying = false;
|
||||
if (interval) clearInterval(interval);
|
||||
} else {
|
||||
if (frames.length === 0) return;
|
||||
isPlaying = true;
|
||||
|
||||
// Restart if at end
|
||||
if (currentFrameIndex >= frames.length - 1) {
|
||||
currentFrameIndex = 0;
|
||||
}
|
||||
|
||||
interval = setInterval(() => {
|
||||
if (currentFrameIndex < frames.length) {
|
||||
psychoPoints = frames[currentFrameIndex];
|
||||
currentFrameIndex++;
|
||||
} else {
|
||||
isPlaying = false;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, MS_PER_FRAME);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Bad Apple!! - Oscilloscope Mode</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="w-full min-h-screen bg-black flex flex-col items-center justify-center p-4 lg:p-8 font-mono relative"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-6 w-full">
|
||||
<div class="w-full max-w-7xl flex flex-col bordered p-1 grow gap-3">
|
||||
<div
|
||||
class="relative w-full h-[75vh] border-b-4 border-l-4 bg-black overflow-hidden flex flex-col items-center"
|
||||
style="border-bottom-color: #fa0; border-left-color: #fa0;"
|
||||
>
|
||||
<!-- Top Left -->
|
||||
<div
|
||||
class="absolute right-2 lg:right-0 lg:top-6 left-4 lg:left-24 pointer-events-none z-5 max-w-100 flex flex-col justify-center items-end lg:items-start gap-1"
|
||||
>
|
||||
<div
|
||||
class="rounded-sm bordered text-3xl bg-black/60 shadow-lg h-10 text-center flex justify-center items-center px-1"
|
||||
>
|
||||
<div class="font-bold md:text-3xl uppercase ews-title">
|
||||
PSYCHOGRAPHIC DISPLAY
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="font-bold mt-1 tracking-widest text-sm md:text-xl ews-title"
|
||||
>
|
||||
Phase 4 Link
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Right -->
|
||||
<div
|
||||
class="absolute bottom-16 lg:bottom-auto top-auto lg:top-18 md:top-4 right-2 lg:right-16 pointer-events-none flex flex-col items-end z-5"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg p-1 inline-block bg-black/60 shadow-lg"
|
||||
>
|
||||
<div class="bordered px-3 py-1 flex flex-col text-3xl">
|
||||
<div
|
||||
class="font-bold text-[10px] md:text-sm tracking-widest leading-none mb-1 ews-title"
|
||||
>
|
||||
STATION:
|
||||
</div>
|
||||
<div
|
||||
class="font-bold text-2xl md:text-4xl tracking-widest leading-none text-right ews-title"
|
||||
>
|
||||
BAD APPLE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WaveformChart
|
||||
bind:this={waveformChart}
|
||||
waveformData={[]}
|
||||
timeWindowMs={20000}
|
||||
selectedTimezone={0}
|
||||
isDemoPsychoMode={true}
|
||||
{psychoPoints}
|
||||
backgroundColor="#000000"
|
||||
gridColor="#33cc55"
|
||||
axesColor="#fa0"
|
||||
waveformColor="#ff9900"
|
||||
psychoWaveformColor="#ff9900"
|
||||
emptyTextColor="#fa0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-2">
|
||||
<button
|
||||
class="px-10 py-3 bg-white text-black font-bold uppercase tracking-widest text-lg rounded hover:bg-gray-200 transition-all shadow-[0_0_15px_rgba(255,255,255,0.3)] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={togglePlay}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isPlaying ? "PAUSE" : "PLAY VIDEO"}
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-3 bg-transparent border-2 border-white/50 text-white font-bold uppercase tracking-widest text-lg rounded hover:bg-white hover:text-black transition-all cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
on:click={() => {
|
||||
currentFrameIndex = 0;
|
||||
if (frames.length > 0) psychoPoints = frames[0];
|
||||
if (isPlaying) togglePlay();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
RESTART
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
1
static/bad_apple_frames_v3.json
Normal file
1
static/bad_apple_frames_v3.json
Normal file
File diff suppressed because one or more lines are too long
1
static/bad_apple_frames_v4.json
Normal file
1
static/bad_apple_frames_v4.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue