primocode 10.13.0 → 10.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +48 -1
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/cota.js +150 -0
- package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
- package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
- package/lib/estudio/dados/projetos/clube-agent.json +120 -0
- package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
- package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
- package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
- package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
- package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
- package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
- package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
- package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
- package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
- package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
- package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
- package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
- package/lib/estudio/dados/projetos/clube-clube.json +108 -0
- package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
- package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
- package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
- package/lib/estudio/dados/projetos/clube-dub.json +119 -0
- package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
- package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
- package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
- package/lib/estudio/dados/projetos/clube-frame.json +108 -0
- package/lib/estudio/dados/projetos/clube-glass.json +118 -0
- package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
- package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
- package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
- package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
- package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
- package/lib/estudio/dados/projetos/clube-morph.json +119 -0
- package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
- package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
- package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
- package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
- package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
- package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
- package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
- package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
- package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
- package/lib/estudio/dados/projetos/clube-promo.json +114 -0
- package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
- package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
- package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
- package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
- package/lib/estudio/dados/projetos/clube-robo.json +125 -0
- package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
- package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
- package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
- package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
- package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
- package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
- package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
- package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
- package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
- package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
- package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
- package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
- package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
- package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
- package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
- package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
- package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
- package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
- package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
- package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
- package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
- package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
- package/lib/estudio/editor.js +285 -0
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/index.js +90 -2
- package/lib/estudio/kit-ae/base.jsx +1146 -0
- package/lib/estudio/kit-ae/cenas.jsx +395 -0
- package/lib/estudio/kit-ae/primitivas.jsx +911 -0
- package/lib/estudio/maquina-diretor.js +393 -0
- package/lib/estudio/maquina-elementos.js +134 -0
- package/lib/estudio/maquina-layout.js +198 -0
- package/lib/estudio/maquina-som.js +478 -0
- package/lib/estudio/maquina-trilha.js +160 -0
- package/lib/estudio/maquina-voz.js +429 -0
- package/lib/estudio/maquina.js +516 -0
- package/lib/estudio/projetos.js +60 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +309 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +47 -19
- package/lib/preparar.js +111 -0
- package/lib/retomar.js +55 -0
- package/lib/sandbox.js +197 -0
- package/lib/skills.js +3 -1
- package/lib/tools.js +13 -4
- package/lib/ui.js +1 -1
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Ajusta a câmera medida de cada cena a uma SOMA DE MOVIMENTOS com ease do AE (o rig do autor:
|
|
2
|
+
um null por movimento, 2 keyframes com velocidade zero nas pontas e influências de saída/entrada).
|
|
3
|
+
Cada movimento k: início a, duração D, influência de saída u (1º keyframe), de entrada w (2º keyframe),
|
|
4
|
+
e deltas em zoom (log2), mira x, mira y, giro. Canal(t) = valor_inicial + soma_k delta_k * ease_k(t).
|
|
5
|
+
Saída: camera/movimentos.json (por cena) + estatística das durações e influências."""
|
|
6
|
+
import json, sys, numpy as np
|
|
7
|
+
from scipy.optimize import least_squares
|
|
8
|
+
from scipy.signal import find_peaks, savgol_filter
|
|
9
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
10
|
+
FPS = 24.0
|
|
11
|
+
def ease(tau, u, w):
|
|
12
|
+
"""Progresso 0..1 da curva temporal do AE com velocidade 0 nas duas pontas (bezier 0,u | 1-w,1)."""
|
|
13
|
+
tau = np.clip(tau, 0, 1); s = tau.copy()
|
|
14
|
+
for _ in range(30): # Newton em x(s) = tau
|
|
15
|
+
x = 3 * (1 - s) ** 2 * s * u + 3 * (1 - s) * s ** 2 * (1 - w) + s ** 3
|
|
16
|
+
dx = 3 * (1 - s) ** 2 * u + 6 * (1 - s) * s * (1 - w - u) + 3 * s ** 2 * w
|
|
17
|
+
s = np.clip(s - (x - tau) / np.maximum(dx, 1e-6), 0, 1)
|
|
18
|
+
return 3 * (1 - s) * s ** 2 + s ** 3
|
|
19
|
+
def modelo(p, t, K, base):
|
|
20
|
+
out = np.tile(base[:, None], (1, len(t))).astype(float)
|
|
21
|
+
for k in range(K):
|
|
22
|
+
a, D, u, w, dz, dx, dy, dg = p[8 * k: 8 * k + 8]
|
|
23
|
+
e = ease((t - a) / max(D, 1 / FPS), u, w)
|
|
24
|
+
out += np.array([dz, dx, dy, dg])[:, None] * e[None, :]
|
|
25
|
+
return out
|
|
26
|
+
d = json.load(open("camera/camera-ancorada.json")); res = {}; todos = []
|
|
27
|
+
for cena, v in d.items():
|
|
28
|
+
q = v["quadros"]; t = np.array([x["t"] for x in q])
|
|
29
|
+
Y = np.array([np.log2([x["s"] for x in q]), [x["wx"] for x in q], [x["wy"] for x in q], [x["rot"] for x in q]])
|
|
30
|
+
if cena in ("A-agenda", "B-docs", "C-alttab", "D-spotify", "E-notas"): Y[3] = 0
|
|
31
|
+
esc = np.array([1.0, 1 / 200, 1 / 200, 1 / 10]) # pesos: 1 oitava de zoom ~ 200 px de mira ~ 10° de giro
|
|
32
|
+
vel = np.sqrt((np.gradient(Y, axis=1) * esc[:, None] * FPS) ** 2).sum(0)
|
|
33
|
+
vel = savgol_filter(vel, 5, 2) if len(vel) > 7 else vel
|
|
34
|
+
pk, _ = find_peaks(vel, distance=6, prominence=max(0.15, 0.08 * vel.max()))
|
|
35
|
+
K = max(1, len(pk)); p0, lo, hi = [], [], []
|
|
36
|
+
for i in pk if len(pk) else [int(np.argmax(vel))]:
|
|
37
|
+
a0 = t[i] - 0.2; D0 = 0.8
|
|
38
|
+
j0, j1 = max(0, int(i - 5)), min(len(t) - 1, int(i + 14))
|
|
39
|
+
dl = Y[:, j1] - Y[:, j0]
|
|
40
|
+
p0 += [a0, D0, 0.25, 0.75, *dl]; lo += [t[0] - 1.5, 4 / FPS, 0.01, 0.01, -12, -5000, -5000, -90]; hi += [t[-1] + 0.5, 3.0, 1.0, 1.0, 12, 5000, 5000, 90]
|
|
41
|
+
base = Y[:, 0].copy()
|
|
42
|
+
def resid(p):
|
|
43
|
+
return ((modelo(p, t, K, base) - Y) * esc[:, None]).ravel()
|
|
44
|
+
r = least_squares(resid, np.clip(p0, lo, hi), bounds=(lo, hi), max_nfev=4000)
|
|
45
|
+
erro = np.sqrt(np.mean(resid(r.x) ** 2)); amp = np.sqrt(np.mean(((Y - Y.mean(1, keepdims=True)) * esc[:, None]) ** 2))
|
|
46
|
+
movs = []
|
|
47
|
+
for k in range(K):
|
|
48
|
+
a, D, u, w, dz, dx, dy, dg = r.x[8 * k: 8 * k + 8]
|
|
49
|
+
movs.append({"ini": round(a, 3), "dur": round(D, 3), "infl_saida": round(u * 100, 1), "infl_entrada": round(w * 100, 1),
|
|
50
|
+
"zoom_log2": round(dz, 3), "mira_x": round(dx, 1), "mira_y": round(dy, 1), "giro": round(dg, 2)})
|
|
51
|
+
if abs(dz) > 0.1 or np.hypot(dx, dy) > 30: todos.append((D, u, w))
|
|
52
|
+
movs.sort(key=lambda m: m["ini"])
|
|
53
|
+
res[cena] = {"base": {"zoom_log2": round(base[0], 3), "mira_x": round(base[1], 1), "mira_y": round(base[2], 1), "giro": round(base[3], 2)},
|
|
54
|
+
"movimentos": movs, "erro_rel": round(erro / max(amp, 1e-6), 3)}
|
|
55
|
+
print(f"{cena:10s} {K:2d} movimentos | erro relativo {erro / max(amp, 1e-6):.3f}")
|
|
56
|
+
json.dump(res, open("camera/movimentos.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False)
|
|
57
|
+
A = np.array(todos)
|
|
58
|
+
if len(A):
|
|
59
|
+
print(f"\n{len(A)} movimentos relevantes | duração mediana {np.median(A[:,0]):.2f} s ({np.median(A[:,0])*FPS:.0f} quadros), quartis {np.percentile(A[:,0],25):.2f}–{np.percentile(A[:,0],75):.2f} s")
|
|
60
|
+
print(f"influência de saída (1º key) mediana {np.median(A[:,1])*100:.0f}% (quartis {np.percentile(A[:,1],25)*100:.0f}–{np.percentile(A[:,1],75)*100:.0f}%)")
|
|
61
|
+
print(f"influência de entrada (2º key) mediana {np.median(A[:,2])*100:.0f}% (quartis {np.percentile(A[:,2],25)*100:.0f}–{np.percentile(A[:,2],75)*100:.0f}%)")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Corrige a deriva da câmera integrada: casa cada quadro direto com o quadro de referência da cena (SIFT)
|
|
2
|
+
e usa essas medidas absolutas como âncoras; entre âncoras, a correção é interpolada no tempo.
|
|
3
|
+
Entrada: vídeo original, camera-medida.json. Saída: camera-ancorada.json (mesmo formato) + relatório."""
|
|
4
|
+
import json, sys, cv2, numpy as np
|
|
5
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
6
|
+
VID, MED, OUT = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
7
|
+
M = json.load(open(MED)); FPS = 24.0
|
|
8
|
+
cap = cv2.VideoCapture(VID); quadros = []
|
|
9
|
+
while True:
|
|
10
|
+
ok, q = cap.read()
|
|
11
|
+
if not ok: break
|
|
12
|
+
quadros.append(cv2.cvtColor(q, cv2.COLOR_BGR2GRAY))
|
|
13
|
+
sift = cv2.SIFT_create(nfeatures=4000)
|
|
14
|
+
cache = {}
|
|
15
|
+
def kp(f):
|
|
16
|
+
if f not in cache: cache[f] = sift.detectAndCompute(quadros[f], None)
|
|
17
|
+
return cache[f]
|
|
18
|
+
bf = cv2.BFMatcher()
|
|
19
|
+
def casar(fr, f):
|
|
20
|
+
"""Similaridade que leva a tela do quadro de referência fr para a tela do quadro f (3x3) ou None."""
|
|
21
|
+
k1, d1 = kp(fr); k2, d2 = kp(f)
|
|
22
|
+
if d1 is None or d2 is None or len(k1) < 10 or len(k2) < 10: return None, 0
|
|
23
|
+
mm = bf.knnMatch(d1, d2, k=2)
|
|
24
|
+
bons = [m for m, n in (x for x in mm if len(x) == 2) if m.distance < 0.72 * n.distance]
|
|
25
|
+
if len(bons) < 15: return None, len(bons)
|
|
26
|
+
p1 = np.float32([k1[m.queryIdx].pt for m in bons]); p2 = np.float32([k2[m.trainIdx].pt for m in bons])
|
|
27
|
+
A, inl = cv2.estimateAffinePartial2D(p1, p2, method=cv2.RANSAC, ransacReprojThreshold=4.0)
|
|
28
|
+
if A is None: return None, 0
|
|
29
|
+
n = int(inl.sum())
|
|
30
|
+
if n < 20 or n / len(bons) < 0.35: return None, n
|
|
31
|
+
T = np.eye(3); T[:2] = A
|
|
32
|
+
return T, n
|
|
33
|
+
def S_de(q):
|
|
34
|
+
"""Matriz mundo->tela a partir do estado (s, rot, wx, wy)."""
|
|
35
|
+
s, r = q["s"], np.radians(q["rot"]); R = s * np.array([[np.cos(r), -np.sin(r)], [np.sin(r), np.cos(r)]])
|
|
36
|
+
T = np.eye(3); T[:2, :2] = R; T[:2, 2] = np.array([540, 540]) - R @ np.array([q["wx"], q["wy"]]); return T
|
|
37
|
+
def estado_de(S):
|
|
38
|
+
A = S[:2, :2]; s = float(np.sqrt(abs(np.linalg.det(A)))); r = float(np.degrees(np.arctan2(A[1, 0], A[0, 0])))
|
|
39
|
+
w = np.linalg.inv(S) @ np.array([540, 540, 1.0]); return s, r, float(w[0]), float(w[1])
|
|
40
|
+
saida = {}
|
|
41
|
+
for k, v in M.items():
|
|
42
|
+
fr = int(round(v["ref"] * FPS)); q = v["quadros"]
|
|
43
|
+
anc = {}
|
|
44
|
+
for i, x in enumerate(q):
|
|
45
|
+
f = x["f"]
|
|
46
|
+
if f >= len(quadros): continue
|
|
47
|
+
if f == fr: anc[i] = np.eye(3); continue
|
|
48
|
+
if i % 2: continue
|
|
49
|
+
T, n = casar(fr, f)
|
|
50
|
+
if T is None: continue
|
|
51
|
+
Sabs = T # mundo (= tela em fr) -> tela em f
|
|
52
|
+
if abs(np.linalg.det(Sabs[:2, :2])) < 1e-6: continue
|
|
53
|
+
sa, ra, _, _ = estado_de(Sabs); si = q[i]["s"]
|
|
54
|
+
if not (0.2 < sa / si < 5): continue # casamento absurdo
|
|
55
|
+
anc[i] = Sabs
|
|
56
|
+
# correção C = Sabs * Sint^-1 nas âncoras, parametrizada (log s, rot, tx, ty) e interpolada
|
|
57
|
+
idx = sorted(anc)
|
|
58
|
+
par = []
|
|
59
|
+
for i in idx:
|
|
60
|
+
C = anc[i] @ np.linalg.inv(S_de(q[i])); A = C[:2, :2]
|
|
61
|
+
par.append([np.log(np.sqrt(abs(np.linalg.det(A)))), np.arctan2(A[1, 0], A[0, 0]), C[0, 2], C[1, 2]])
|
|
62
|
+
par = np.array(par) if par else np.zeros((0, 4))
|
|
63
|
+
novos = []
|
|
64
|
+
for i, x in enumerate(q):
|
|
65
|
+
if len(idx) == 0: c = [0, 0, 0, 0]
|
|
66
|
+
else: c = [np.interp(i, idx, par[:, j]) for j in range(4)]
|
|
67
|
+
Cs = np.exp(c[0]); Cr = c[1]; C = np.eye(3); C[:2, :2] = Cs * np.array([[np.cos(Cr), -np.sin(Cr)], [np.sin(Cr), np.cos(Cr)]]); C[:2, 2] = c[2:]
|
|
68
|
+
s, r, wx, wy = estado_de(C @ S_de(x))
|
|
69
|
+
novos.append({"f": x["f"], "t": x["t"], "s": round(s, 5), "rot": round(r, 3), "wx": round(wx, 2), "wy": round(wy, 2)})
|
|
70
|
+
cobertura = len(idx) / max(1, len(q) / 2)
|
|
71
|
+
saida[k] = dict(v, quadros=novos, ancoras=[q[i]["t"] for i in idx])
|
|
72
|
+
d0 = abs(np.log(novos[0]["s"] / q[0]["s"])); dw = np.hypot(novos[0]["wx"] - q[0]["wx"], novos[0]["wy"] - q[0]["wy"])
|
|
73
|
+
print(f"{k:10s} âncoras {len(idx):3d} ({cobertura*100:4.0f}% dos quadros pares) | início: zoom {q[0]['s']:.2f}->{novos[0]['s']:.2f}, mira desloca {dw:.0f} px | faixa das âncoras {min(saida[k]['ancoras']) if idx else '-'}–{max(saida[k]['ancoras']) if idx else '-'}")
|
|
74
|
+
json.dump(saida, open(OUT, "w"), indent=0)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Transforma a câmera medida (camera-medida.json) em keyframes por cena + geometria dos encaixes.
|
|
2
|
+
|
|
3
|
+
Limpeza (decidida olhando os quadros, ver README):
|
|
4
|
+
- giro zerado nas cenas planas (A, B, C, D, E): o rastreio pegou giro falso em trocas de conteúdo;
|
|
5
|
+
- trechos em que um ELEMENTO se mexe sozinho (não a câmera) ficam com a câmera segurada:
|
|
6
|
+
B 4,46–4,83 s (globo entrando) e C 5,80–6,04 s (janela do Spotify abrindo);
|
|
7
|
+
- K: o giro do comecinho é do jornal girado dentro do pino; giro começa igual ao de 28,40 s.
|
|
8
|
+
Suavização: Savitzky-Golay de 5 quadros (tira tremida do rastreio sem matar os picos).
|
|
9
|
+
Saída: camera/cena-<id>.json {fps, ini, fim, chaves: {zoom: [[t, log2 s]], mx, my, giro}} (tempo = tempo do vídeo)
|
|
10
|
+
camera/encaixes.json onde a cena anterior/seguinte precisa estar no mundo desta cena no instante da troca.
|
|
11
|
+
"""
|
|
12
|
+
import json, math, numpy as np
|
|
13
|
+
from scipy.signal import savgol_filter
|
|
14
|
+
M = json.load(open("camera/camera-ancorada.json"))
|
|
15
|
+
PLANAS = {"A-agenda", "B-docs", "C-alttab", "D-spotify", "E-notas"}
|
|
16
|
+
SEGURA = {"B-docs": [(4.46, 4.83)], "C-alttab": [(5.80, 6.04)]}
|
|
17
|
+
cenas = {}
|
|
18
|
+
for k, v in M.items():
|
|
19
|
+
q = v["quadros"]; t = np.array([x["t"] for x in q])
|
|
20
|
+
z = np.log2([x["s"] for x in q]); mx = np.array([x["wx"] for x in q]); my = np.array([x["wy"] for x in q]); g = np.array([x["rot"] for x in q])
|
|
21
|
+
if k in PLANAS: g[:] = 0
|
|
22
|
+
if k == "K-maps":
|
|
23
|
+
i0 = int(np.argmin(abs(t - 28.40))); g[:i0] = g[i0]
|
|
24
|
+
for a, b in SEGURA.get(k, []):
|
|
25
|
+
m = (t >= a) & (t <= b); i = int(np.argmax(m)) - 1
|
|
26
|
+
for arr in (z, mx, my, g): arr[m] = arr[i]
|
|
27
|
+
if len(t) >= 7:
|
|
28
|
+
z, mx, my, g = [savgol_filter(a, 5, 2) for a in (z, mx, my, g)]
|
|
29
|
+
ch = {"zoom": z, "mx": mx, "my": my, "giro": g}
|
|
30
|
+
cenas[k] = {"fps": 24, "ini": v["ini"], "fim": v["fim"], "ref": v["ref"], "pegada": v["pegada"],
|
|
31
|
+
"chaves": {n: [[round(float(tt), 4), round(float(a[i]), 4)] for i, tt in enumerate(t)] for n, a in ch.items()}}
|
|
32
|
+
json.dump(cenas[k], open(f"camera/cena-{k}.json", "w"))
|
|
33
|
+
ids = list(cenas)
|
|
34
|
+
enc = []
|
|
35
|
+
def estado(k, tt):
|
|
36
|
+
ch = cenas[k]["chaves"]; i = min(range(len(ch["zoom"])), key=lambda j: abs(ch["zoom"][j][0] - tt))
|
|
37
|
+
return 2 ** ch["zoom"][i][1], ch["mx"][i][1], ch["my"][i][1], ch["giro"][i][1]
|
|
38
|
+
for a, b in zip(ids, ids[1:]):
|
|
39
|
+
ts = cenas[b]["ini"]
|
|
40
|
+
if b == "D-spotify": # empurra: a cena seguinte (Spotify) mora numa janela do alt-tab
|
|
41
|
+
s, x, y, r = estado(a, ts); onde, quem = a, b
|
|
42
|
+
else: # recua: a cena anterior mora num elemento da seguinte
|
|
43
|
+
s, x, y, r = estado(b, ts); onde, quem = b, a
|
|
44
|
+
enc.append({"troca_s": ts, "mundo_de": onde, "cena_encaixada": quem, "centro": [round(x, 2), round(y, 2)],
|
|
45
|
+
"lado": round(1080 / s, 3), "escala_pct": round(100 / s, 4), "giro": round(-r, 3)})
|
|
46
|
+
print(f"{ts:6.2f}s {quem:10s} dentro de {onde:10s}: centro ({x:7.1f},{y:7.1f}) lado {1080/s:7.1f} px ({100/s:6.2f}%) giro {-r:5.2f}")
|
|
47
|
+
json.dump(enc, open("camera/encaixes.json", "w"), indent=1)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Câmera 3D das cenas do jornal (F-jornal1, J-jornal2) a partir da homografia medida no original.
|
|
2
|
+
|
|
3
|
+
Achado (17/10): com a página "plana" bem definida, o original bate com a câmera padrão do AE para comp 1080
|
|
4
|
+
(zoom 1500 px, centro óptico no centro) e o jornal girando em 3D: erro médio < 1 px. É o rig do autor: camadas
|
|
5
|
+
3D, sem camada de câmera.
|
|
6
|
+
|
|
7
|
+
Passos:
|
|
8
|
+
1) homografia-<X>.json leva a tela do quadro-base para a tela de cada quadro (homografia_jornal.py);
|
|
9
|
+
2) o plano verdadeiro = Hr · (tela do quadro-base - 540), com Hr = retificação de 4 parâmetros (a página pode
|
|
10
|
+
não estar 100% de frente no quadro-base); Hr é escolhida para minimizar o erro de todos os quadros;
|
|
11
|
+
3) por quadro: pose (R, t) do plano para a câmera fixa f = 1500 (solvePnP + mínimos quadrados);
|
|
12
|
+
4) no começo da cena a pose é misturada com a pose "de frente" (para o encaixe encher a tela sem quina);
|
|
13
|
+
5) saída para o AE: null 3D "PLANO" (ponto de ancoragem 0) com Posição = t + [540, 540, -1500] e
|
|
14
|
+
Rotação X/Y/Z tais que Rx·Ry·Rz = R (ordem do AE medida no próprio AE).
|
|
15
|
+
Saída: camera/camera3d-<X>.json {f, quadros: [{f, t, pos, rot:[x,y,z], erro_px}], Hr, encaixe}
|
|
16
|
+
Uso: python -I camera3d_jornal.py F|J
|
|
17
|
+
"""
|
|
18
|
+
import json, sys, numpy as np, cv2
|
|
19
|
+
from scipy.optimize import least_squares, minimize
|
|
20
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
21
|
+
CENA = sys.argv[1]
|
|
22
|
+
FOC, C = 1500.0, 540.0
|
|
23
|
+
K = np.array([[FOC, 0, C], [0, FOC, C], [0, 0, 1.0]])
|
|
24
|
+
d = json.load(open(f"homografia-{CENA}.json"))
|
|
25
|
+
H = {int(k): np.array(v) for k, v in d["H"].items()}
|
|
26
|
+
fs = sorted(H)
|
|
27
|
+
# troca no 1º quadro inteiro da cena (10,80 s e 24,62 s caem entre quadros: 1º quadro visível = 260 e 591)
|
|
28
|
+
TROCA = {"F": 260 / 24, "J": 591 / 24}[CENA]
|
|
29
|
+
MISTURA = {"F": (260 / 24, 11.00), "J": (591 / 24, 24.80)}[CENA]
|
|
30
|
+
gx = np.linspace(60, 1020, 7)
|
|
31
|
+
GRADE = np.array([[x, y] for y in gx for x in gx], float)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def Hr_de(g):
|
|
35
|
+
a, b, p, q = g
|
|
36
|
+
return np.array([[1 + a, b, 0], [0, 1, 0], [p / 1000, q / 1000, 1.0]])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def plano_de_tela(f, g, uv):
|
|
40
|
+
Hi = np.linalg.inv(H[f]); P = (Hi @ np.c_[uv, np.ones(len(uv))].T).T; x = P[:, :2] / P[:, 2:3] - C
|
|
41
|
+
Q = (Hr_de(g) @ np.c_[x, np.ones(len(x))].T).T
|
|
42
|
+
return Q[:, :2] / Q[:, 2:3]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def proj(p, X):
|
|
46
|
+
R, _ = cv2.Rodrigues(p[:3]); Xc = (R @ np.c_[X, np.zeros(len(X))].T).T + p[3:6]
|
|
47
|
+
return np.c_[FOC * Xc[:, 0] / Xc[:, 2] + C, FOC * Xc[:, 1] / Xc[:, 2] + C]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def pose(f, g, p0=None):
|
|
51
|
+
X = plano_de_tela(f, g, GRADE); u = GRADE
|
|
52
|
+
ok, rv, tv = cv2.solvePnP(np.c_[X, np.zeros(len(X))], u, K, None, flags=cv2.SOLVEPNP_IPPE)
|
|
53
|
+
inits = [np.r_[rv.ravel(), tv.ravel()]]
|
|
54
|
+
if p0 is not None: inits.append(p0)
|
|
55
|
+
best = None
|
|
56
|
+
for q0 in inits:
|
|
57
|
+
s = least_squares(lambda p: (proj(p, X) - u).ravel(), q0, method="lm", max_nfev=200)
|
|
58
|
+
e = float(np.sqrt(((proj(s.x, X) - u) ** 2).sum(1)).mean())
|
|
59
|
+
if best is None or e < best[1]: best = (s.x, e)
|
|
60
|
+
return best
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
cache = {}
|
|
64
|
+
def custo(g):
|
|
65
|
+
tot = []
|
|
66
|
+
for f in fs[::2]:
|
|
67
|
+
p, e = pose(f, g, cache.get(f)); cache[f] = p; tot.append(e)
|
|
68
|
+
return float(np.mean(np.array(tot) ** 2))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
print("sem retificação: rms", custo(np.zeros(4)) ** .5)
|
|
72
|
+
r = minimize(custo, np.zeros(4), method="Nelder-Mead",
|
|
73
|
+
options={"xatol": 1e-6, "fatol": 1e-4, "maxiter": 300,
|
|
74
|
+
"initial_simplex": np.array([[0, 0, 0, 0], [.02, 0, 0, 0], [0, .02, 0, 0], [0, 0, .05, 0], [0, 0, 0, .05]])})
|
|
75
|
+
g = r.x
|
|
76
|
+
print("retificação (Nelder-Mead)", np.round(g, 6), "rms", r.fun ** .5)
|
|
77
|
+
# ajuste conjunto (retificação + todas as poses) por mínimos quadrados esparsos
|
|
78
|
+
from scipy.sparse import lil_matrix
|
|
79
|
+
N = len(fs); NP = 4 + 6 * N
|
|
80
|
+
x0 = np.r_[g, np.concatenate([pose(f, g, cache.get(f))[0] for f in fs])]
|
|
81
|
+
def res_all(x):
|
|
82
|
+
gg = x[:4]; out = []
|
|
83
|
+
for i, f in enumerate(fs):
|
|
84
|
+
X = plano_de_tela(f, gg, GRADE); out.append((proj(x[4 + 6 * i: 10 + 6 * i], X) - GRADE).ravel())
|
|
85
|
+
return np.concatenate(out)
|
|
86
|
+
M = len(GRADE) * 2
|
|
87
|
+
J = lil_matrix((M * N, NP), dtype=int)
|
|
88
|
+
for i in range(N):
|
|
89
|
+
J[i * M:(i + 1) * M, :4] = 1; J[i * M:(i + 1) * M, 4 + 6 * i: 10 + 6 * i] = 1
|
|
90
|
+
sol = least_squares(res_all, x0, jac_sparsity=J, x_scale="jac", max_nfev=200, verbose=0)
|
|
91
|
+
g = sol.x[:4]
|
|
92
|
+
for i, f in enumerate(fs): cache[f] = sol.x[4 + 6 * i: 10 + 6 * i]
|
|
93
|
+
rr = sol.fun.reshape(N, -1, 2); print("retificação (conjunto)", np.round(g, 6), "rms", float(np.sqrt((rr ** 2).sum(2)).mean()))
|
|
94
|
+
poses = {}
|
|
95
|
+
for f in fs:
|
|
96
|
+
poses[f] = pose(f, g, cache.get(f))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def frontal(p):
|
|
100
|
+
"""Mesma pose, sem inclinação: a normal do plano alinhada ao eixo da câmera (mantém o giro no plano e t)."""
|
|
101
|
+
R, _ = cv2.Rodrigues(p[:3])
|
|
102
|
+
n = R[:, 2] # normal do plano em coordenadas da câmera
|
|
103
|
+
# rotação mínima que leva n para (0,0,1)
|
|
104
|
+
z = np.array([0, 0, 1.0]); v = np.cross(n, z); s = np.linalg.norm(v); c = float(n @ z)
|
|
105
|
+
if s < 1e-9: return p.copy()
|
|
106
|
+
vx = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
|
|
107
|
+
Rm = np.eye(3) + vx + vx @ vx * ((1 - c) / s ** 2)
|
|
108
|
+
R2 = Rm @ R
|
|
109
|
+
# mantém o ponto do plano que está no centro da tela no mesmo lugar e à mesma distância
|
|
110
|
+
t = p[3:6]
|
|
111
|
+
Xc0 = np.array([0, 0, 1.0]) * 1.0
|
|
112
|
+
# ponto do plano no eixo óptico: R X + t = lambda * z
|
|
113
|
+
A = np.c_[R[:, 0], R[:, 1], -z]; sol = np.linalg.solve(A, -t); X0 = np.array([sol[0], sol[1], 0]); lam = sol[2]
|
|
114
|
+
t2 = lam * z - R2 @ X0
|
|
115
|
+
return np.r_[cv2.Rodrigues(R2)[0].ravel(), t2]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def slerp_pose(pa, pb, w):
|
|
119
|
+
Ra, _ = cv2.Rodrigues(pa[:3]); Rb, _ = cv2.Rodrigues(pb[:3])
|
|
120
|
+
dr, _ = cv2.Rodrigues(Rb @ Ra.T); Rw = cv2.Rodrigues(dr * w)[0] @ Ra
|
|
121
|
+
return np.r_[cv2.Rodrigues(Rw)[0].ravel(), (1 - w) * pa[3:6] + w * pb[3:6]]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
a0, a1 = MISTURA
|
|
125
|
+
final = {}
|
|
126
|
+
for f in fs:
|
|
127
|
+
p, e = poses[f]
|
|
128
|
+
t = f / 24
|
|
129
|
+
if t < a1:
|
|
130
|
+
w = 0.0 if t <= a0 else (t - a0) / (a1 - a0)
|
|
131
|
+
w = w * w * (3 - 2 * w)
|
|
132
|
+
p = slerp_pose(frontal(p), p, w)
|
|
133
|
+
X = plano_de_tela(f, g, GRADE); e = float(np.sqrt(((proj(p, X) - GRADE) ** 2).sum(1)).mean())
|
|
134
|
+
final[f] = (p, e)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def euler_ae(R):
|
|
138
|
+
"""R = Rx(a)·Ry(b)·Rz(c) (ordem do AE) -> [a, b, c] em graus."""
|
|
139
|
+
b = np.arcsin(np.clip(R[0, 2], -1, 1)); c = np.arctan2(-R[0, 1], R[0, 0]); a = np.arctan2(-R[1, 2], R[2, 2])
|
|
140
|
+
return np.degrees([a, b, c])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
out = []
|
|
144
|
+
prev = None
|
|
145
|
+
for f in fs:
|
|
146
|
+
p, e = final[f]
|
|
147
|
+
R, _ = cv2.Rodrigues(p[:3]); t = p[3:6]
|
|
148
|
+
rot = euler_ae(R)
|
|
149
|
+
if prev is not None: # sem saltos de 360°
|
|
150
|
+
rot = prev + ((rot - prev + 180) % 360 - 180)
|
|
151
|
+
prev = rot
|
|
152
|
+
out.append({"f": f, "t": round(f / 24, 5), "pos": (t + np.array([C, C, -FOC])).tolist(), "rot": rot.tolist(),
|
|
153
|
+
"R": R.tolist(), "tc": t.tolist(), "erro_px": round(e, 2)})
|
|
154
|
+
print(f, f"{f/24:7.3f}", "pos", np.round(t + [C, C, -FOC], 1), "rot", np.round(rot, 2), f"erro {e:.2f}")
|
|
155
|
+
|
|
156
|
+
# encaixe: quadrado na tela no instante da troca -> quadrilátero no plano (com a pose interpolada na troca)
|
|
157
|
+
f0 = max(f for f in fs if f / 24 <= TROCA); f1 = f0 + 1
|
|
158
|
+
w = (TROCA - f0 / 24) * 24
|
|
159
|
+
pt = slerp_pose(final[f0][0], final[f1][0], w)
|
|
160
|
+
R, _ = cv2.Rodrigues(pt[:3]); tt = pt[3:6]
|
|
161
|
+
cantos = []
|
|
162
|
+
for u, v in ((0, 0), (1080, 0), (1080, 1080), (0, 1080)):
|
|
163
|
+
ray = np.array([(u - C) / FOC, (v - C) / FOC, 1.0])
|
|
164
|
+
# R X + t = s ray, X=(x,y,0)
|
|
165
|
+
A = np.c_[R[:, 0], R[:, 1], -ray]; sol = np.linalg.solve(A, -tt); cantos.append([sol[0], sol[1]])
|
|
166
|
+
cantos = np.array(cantos)
|
|
167
|
+
cen = cantos.mean(0); lado = float(np.linalg.norm(cantos[1] - cantos[0]))
|
|
168
|
+
giro = float(np.degrees(np.arctan2(cantos[1][1] - cantos[0][1], cantos[1][0] - cantos[0][0])))
|
|
169
|
+
print("encaixe no plano: cantos", np.round(cantos, 2).tolist(), "centro", np.round(cen, 2), "lado", round(lado, 3), "giro", round(giro, 3))
|
|
170
|
+
json.dump({"foco": FOC, "Hr": Hr_de(g).tolist(), "g": g.tolist(), "troca": TROCA, "mistura": MISTURA,
|
|
171
|
+
"encaixe": {"cantos": cantos.tolist(), "centro": cen.tolist(), "lado": lado, "giro": giro},
|
|
172
|
+
"quadros": out}, open(f"camera3d-{CENA}.json", "w"), indent=0)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Compara a câmera do original com a da recriação (zoom, dx, dy, giro por quadro), cena a cena.
|
|
2
|
+
Uso: python -P comparar.py original.csv recriacao.csv cenas.json saida_prefixo
|
|
3
|
+
(os CSVs saem do medir_quadro_a_quadro.py rodado em cada vídeo)."""
|
|
4
|
+
import sys, csv, json, numpy as np
|
|
5
|
+
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
|
|
6
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
7
|
+
def ler(p):
|
|
8
|
+
R = list(csv.DictReader(open(p)))
|
|
9
|
+
f = lambda k: np.array([float(r[k]) if r[k] not in ("nan", "") else np.nan for r in R])
|
|
10
|
+
return f("t"), (f("descala") - 1) * 100, f("dx_px1080"), f("dy_px1080"), f("drot_graus")
|
|
11
|
+
to, *o = ler(sys.argv[1]); tr, *r = ler(sys.argv[2]); cenas = json.load(open(sys.argv[3])); out = sys.argv[4]
|
|
12
|
+
n = min(len(to), len(tr)); to = to[:n]; o = [x[:n] for x in o]; r = [x[:n] for x in r]
|
|
13
|
+
nomes = ["zoom %/quadro", "dx px/quadro", "dy px/quadro", "giro °/quadro"]
|
|
14
|
+
linhas = ["| cena | zoom corr | dx corr | dy corr | dif. média zoom (%/q) | dif. média trans. (px/q) |", "|---|---|---|---|---|---|"]
|
|
15
|
+
for c in cenas:
|
|
16
|
+
m = (to >= c["ini"]) & (to < c["fim"])
|
|
17
|
+
def corr(a, b):
|
|
18
|
+
ok = m & np.isfinite(a) & np.isfinite(b)
|
|
19
|
+
return np.corrcoef(a[ok], b[ok])[0, 1] if ok.sum() > 5 and np.std(a[ok]) > 0 and np.std(b[ok]) > 0 else np.nan
|
|
20
|
+
dz = np.nanmean(abs(o[0][m] - r[0][m])); dt = np.nanmean(np.hypot(o[1][m] - r[1][m], o[2][m] - r[2][m]))
|
|
21
|
+
linhas.append(f"| {c['id']} | {corr(o[0], r[0]):.2f} | {corr(o[1], r[1]):.2f} | {corr(o[2], r[2]):.2f} | {dz:.2f} | {dt:.1f} |")
|
|
22
|
+
print("\n".join(linhas)); open(out + ".md", "w", encoding="utf-8").write("\n".join(linhas) + "\n")
|
|
23
|
+
for a0, a1 in ((0, 12), (11, 23), (22, 34)):
|
|
24
|
+
m = (to >= a0) & (to <= a1)
|
|
25
|
+
fig, ax = plt.subplots(4, 1, figsize=(22, 12), sharex=True)
|
|
26
|
+
for k in range(4):
|
|
27
|
+
ax[k].plot(to[m], o[k][m], color="0.2", lw=1.4, label="original"); ax[k].plot(to[m], r[k][m], color="tab:green", lw=1.2, label="recriação")
|
|
28
|
+
ax[k].set_ylabel(nomes[k]); ax[k].grid(alpha=.3); ax[k].axhline(0, color="k", lw=.5)
|
|
29
|
+
for c in cenas:
|
|
30
|
+
if a0 <= c["ini"] <= a1: ax[k].axvline(c["ini"], color="tab:red", alpha=.3)
|
|
31
|
+
ax[0].legend(loc="upper right"); ax[3].set_xticks(np.arange(a0, a1 + .01, 1))
|
|
32
|
+
plt.tight_layout(); plt.savefig(f"{out}-{a0:02d}-{a1:02d}.png", dpi=65); plt.close()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Junta a câmera 3D (PLANO) e a mesa 2D de uma cena do jornal num JSON compacto para o gerador do AE.
|
|
2
|
+
Mesa: semelhança medida na madeira (mesa2d-<X>.json) onde existe; antes disso, a câmera 2D medida
|
|
3
|
+
(cena-<id>.json) e, no vão sem madeira à vista, interpolação suave (log zoom, giro, mira).
|
|
4
|
+
Uso: python -I dados_ae_jornal.py F|J -> ../ae/<id>-dados.json"""
|
|
5
|
+
import json, sys, numpy as np
|
|
6
|
+
C = sys.argv[1]
|
|
7
|
+
ID = {"F": "F-jornal1", "J": "J-jornal2"}[C]
|
|
8
|
+
cam3 = json.load(open(f"camera3d-{C}.json")); mesa = json.load(open(f"mesa2d-{C}.json")); c2 = json.load(open(f"cena-{ID}.json"))["chaves"]
|
|
9
|
+
fs = [q["f"] for q in cam3["quadros"]]
|
|
10
|
+
# --- mesa: semelhança S (tela no ref -> tela no quadro) por quadro
|
|
11
|
+
def par_de_S(S):
|
|
12
|
+
S = np.array(S); s = float(np.sqrt(abs(np.linalg.det(S[:2, :2])))); th = float(np.degrees(np.arctan2(S[1, 0], S[0, 0])))
|
|
13
|
+
M = np.linalg.inv(S) @ np.array([540, 540, 1.0]); return [np.log2(s), th, M[0], M[1]]
|
|
14
|
+
def S_de_par(p):
|
|
15
|
+
s = 2 ** p[0]; th = np.radians(p[1]); R = s * np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
|
|
16
|
+
S = np.eye(3); S[:2, :2] = R; S[:2, 2] = np.array([540, 540]) - R @ np.array([p[2], p[3]]); return S
|
|
17
|
+
med = {q["f"]: par_de_S(q["S"]) for q in mesa["quadros"] if q["S"] is not None}
|
|
18
|
+
cam2 = {}
|
|
19
|
+
for (t, z), (_, mx), (_, my), (_, g) in zip(c2["zoom"], c2["mx"], c2["my"], c2["giro"]):
|
|
20
|
+
cam2[int(round(t * 24))] = [z, g, mx, my]
|
|
21
|
+
f_med0 = min(med) # primeiro quadro medido na madeira (encadeado a partir do ref)
|
|
22
|
+
CORTE = {"F": 288, "J": 616}[C] # até aqui vale a câmera 2D (madeira coerente com ela); depois interpola até f_med0
|
|
23
|
+
par = {}
|
|
24
|
+
for f in fs:
|
|
25
|
+
if f in med: par[f] = med[f]
|
|
26
|
+
elif f <= CORTE: par[f] = cam2.get(f, cam2[min(cam2, key=lambda k: abs(k - f))])
|
|
27
|
+
for f in fs:
|
|
28
|
+
if f not in par:
|
|
29
|
+
a, b = CORTE, f_med0; w = (f - a) / (b - a); w = w * w * (3 - 2 * w)
|
|
30
|
+
pa, pb = np.array(par[a]), np.array(par[b]); par[f] = list((1 - w) * pa + w * pb)
|
|
31
|
+
# F: nos últimos quadros a borda da mesa aparece; S vem das bordas medidas (encolhe em torno de ~(282,282), sem giro)
|
|
32
|
+
if C == "F":
|
|
33
|
+
RET = (6, 7, 1059, 1050)
|
|
34
|
+
bordas = {395: (10, 1048), 396: (15, 1032), 397: (23, 1011), 398: (33, 983)}
|
|
35
|
+
for f, (L, R) in bordas.items():
|
|
36
|
+
s = (R - L) / (RET[2] - RET[0]); Tx = L - s * RET[0]; cx = Tx / (1 - s); Ty = (1 - s) * 282
|
|
37
|
+
S = np.array([[s, 0, Tx], [0, s, Ty], [0, 0, 1.0]]); par[f] = par_de_S(S)
|
|
38
|
+
pa, pb = np.array(par[397]), np.array(par[398]); par[399] = list(pb + (pb - pa))
|
|
39
|
+
mesa_k = []
|
|
40
|
+
for f in fs:
|
|
41
|
+
S = S_de_par(par[f]); s = 2 ** par[f][0]
|
|
42
|
+
mesa_k.append([round(f / 24, 5), [round(x, 5) for x in S[:2].ravel().tolist()]])
|
|
43
|
+
out = {"id": ID, "foco": 1500, "plano": [[q["t"]] + [round(v, 4) for v in q["pos"]] + [round(v, 5) for v in q["rot"]] for q in cam3["quadros"]],
|
|
44
|
+
"mesa": mesa_k, "encaixe": cam3["encaixe"], "troca": cam3["troca"]}
|
|
45
|
+
json.dump(out, open(f"../ae/{ID}-dados.json", "w"))
|
|
46
|
+
print("ok", ID, len(out["plano"]), "quadros; mesa medida a partir de", f_med0 / 24)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Folha de contato com carimbo de tempo: N quadros por segundo, grade 6×4 por folha.
|
|
2
|
+
Uso: python -P camera/folha_contato.py video.mp4 prefixo_saida QPS [largura_do_quadro] → prefixo-01.jpg, -02.jpg…"""
|
|
3
|
+
import sys, os, cv2
|
|
4
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
5
|
+
src, out, fps_s = sys.argv[1], sys.argv[2], float(sys.argv[3])
|
|
6
|
+
w = int(sys.argv[4]) if len(sys.argv)>4 else 270
|
|
7
|
+
cols, rows = 6, 4
|
|
8
|
+
os.makedirs(os.path.dirname(out) or '.', exist_ok=True)
|
|
9
|
+
cap = cv2.VideoCapture(src); fps = cap.get(cv2.CAP_PROP_FPS); n = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
10
|
+
step = fps / fps_s
|
|
11
|
+
idxs = []; f = 0.0
|
|
12
|
+
while f < n: idxs.append(int(round(f))); f += step
|
|
13
|
+
font = ImageFont.truetype(r"C:\Windows\Fonts\arialbd.ttf", 18)
|
|
14
|
+
frames = []
|
|
15
|
+
for i in idxs:
|
|
16
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, i); ok, q = cap.read()
|
|
17
|
+
if not ok: continue
|
|
18
|
+
h = int(q.shape[0] * w / q.shape[1]); q = cv2.resize(q, (w, h), interpolation=cv2.INTER_AREA)
|
|
19
|
+
im = Image.fromarray(cv2.cvtColor(q, cv2.COLOR_BGR2RGB)); d = ImageDraw.Draw(im)
|
|
20
|
+
lab = f"{i/fps:05.2f}s f{i}"; d.rectangle([0,0,140,22], fill=(0,0,0)); d.text((4,1), lab, fill=(255,255,0), font=font)
|
|
21
|
+
frames.append(im)
|
|
22
|
+
per = cols*rows
|
|
23
|
+
for s in range(0, len(frames), per):
|
|
24
|
+
chunk = frames[s:s+per]; W, H = chunk[0].size
|
|
25
|
+
sheet = Image.new('RGB', (cols*W, rows*H), (40,40,40))
|
|
26
|
+
for k, im in enumerate(chunk): sheet.paste(im, ((k%cols)*W, (k//cols)*H))
|
|
27
|
+
sheet.save(f"{out}-{s//per+1:02d}.jpg", quality=85)
|
|
28
|
+
print(len(frames), 'frames', fps, 'fps', n, 'total')
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Mede a homografia do plano do jornal quadro a quadro (cenas F-jornal1 e J-jornal2).
|
|
2
|
+
|
|
3
|
+
O jornal + a mesa formam um plano só. Cada quadro do original é o mesmo plano visto por outra câmera,
|
|
4
|
+
então tela(t) = H(t) · plano. Aqui o "plano" é a tela de um quadro-base em que a página está de frente
|
|
5
|
+
(F: 10,833 s; J: 25,25 s), e H(t) leva a tela do quadro-base para a tela do quadro t.
|
|
6
|
+
|
|
7
|
+
Método: SIFT + RANSAC (findHomography) entre quadros vizinhos (encadeado) e, sempre que dá, direto contra
|
|
8
|
+
alguns quadros-âncora já medidos (corrige deriva). Saída: camera/homografia-<id>.json {f: [3x3...]}.
|
|
9
|
+
Uso: python -I homografia_jornal.py ORIGINAL F|J
|
|
10
|
+
"""
|
|
11
|
+
import json, sys, cv2, numpy as np
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
13
|
+
VID, CENA = sys.argv[1], sys.argv[2]
|
|
14
|
+
if CENA == "F":
|
|
15
|
+
F0, F1, BASE = 258, 399, 260 # 10,75 s .. 16,625 s ; base 10,833 s
|
|
16
|
+
else:
|
|
17
|
+
F0, F1, BASE = 589, 673, 591 # 24,54 s .. 28,04 s ; base 24,625 s
|
|
18
|
+
cap = cv2.VideoCapture(VID)
|
|
19
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, F0)
|
|
20
|
+
Q = {}
|
|
21
|
+
for f in range(F0, F1 + 1):
|
|
22
|
+
ok, q = cap.read()
|
|
23
|
+
if not ok: break
|
|
24
|
+
Q[f] = cv2.cvtColor(q, cv2.COLOR_BGR2GRAY)
|
|
25
|
+
sift = cv2.SIFT_create(nfeatures=6000, contrastThreshold=0.02)
|
|
26
|
+
KP = {f: sift.detectAndCompute(Q[f], None) for f in Q}
|
|
27
|
+
bf = cv2.BFMatcher()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def casar(a, b, thr=3.0):
|
|
31
|
+
"""Homografia que leva a tela do quadro a para a tela do quadro b."""
|
|
32
|
+
k1, d1 = KP[a]; k2, d2 = KP[b]
|
|
33
|
+
if d1 is None or d2 is None or len(k1) < 12 or len(k2) < 12: return None, 0
|
|
34
|
+
mm = bf.knnMatch(d1, d2, k=2)
|
|
35
|
+
bons = [m for m, n in (x for x in mm if len(x) == 2) if m.distance < 0.75 * n.distance]
|
|
36
|
+
if len(bons) < 20: return None, len(bons)
|
|
37
|
+
p1 = np.float32([k1[m.queryIdx].pt for m in bons]); p2 = np.float32([k2[m.trainIdx].pt for m in bons])
|
|
38
|
+
H, inl = cv2.findHomography(p1, p2, cv2.USAC_MAGSAC, thr, maxIters=20000, confidence=0.999)
|
|
39
|
+
if H is None: return None, 0
|
|
40
|
+
n = int(inl.sum())
|
|
41
|
+
if n < 25: return None, n
|
|
42
|
+
return H / H[2, 2], n
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# encadeado a partir da base, para frente e para trás; a cada passo tenta casar direto com a base
|
|
46
|
+
# ou com o quadro já medido mais distante que ainda casa bem (menos deriva)
|
|
47
|
+
H = {BASE: np.eye(3)}
|
|
48
|
+
info = {BASE: "base"}
|
|
49
|
+
for direc in (1, -1):
|
|
50
|
+
f = BASE
|
|
51
|
+
while F0 <= f + direc <= F1 and (f + direc) in Q:
|
|
52
|
+
g = f + direc
|
|
53
|
+
cand = []
|
|
54
|
+
# vizinho imediato
|
|
55
|
+
Hn, n = casar(f, g)
|
|
56
|
+
if Hn is not None: cand.append((n, H[f], Hn, f))
|
|
57
|
+
# saltos maiores para trás (quadros já medidos) para segurar a deriva
|
|
58
|
+
for salto in (3, 6, 12, 24):
|
|
59
|
+
h = g - direc * salto
|
|
60
|
+
if h in H and h != f:
|
|
61
|
+
Hj, nj = casar(h, g)
|
|
62
|
+
if Hj is not None and nj > 60: cand.append((nj * 1.15, H[h], Hj, h))
|
|
63
|
+
if not cand:
|
|
64
|
+
print("sem casamento em", g); H[g] = H[f]; info[g] = "repetido"; f = g; continue
|
|
65
|
+
cand.sort(key=lambda c: -c[0])
|
|
66
|
+
n, Hb, Hj, h = cand[0]
|
|
67
|
+
H[g] = Hj @ Hb; H[g] /= H[g][2, 2]; info[g] = f"de {h} ({int(n)} pts)"
|
|
68
|
+
f = g
|
|
69
|
+
out = {str(f): H[f].tolist() for f in sorted(H)}
|
|
70
|
+
json.dump({"base": BASE, "fps": 24, "H": out, "info": {str(k): v for k, v in info.items()}},
|
|
71
|
+
open(f"homografia-{CENA}.json", "w"), indent=0)
|
|
72
|
+
for f in sorted(H):
|
|
73
|
+
c = H[f] @ np.array([540, 540, 1.0]); c /= c[2]
|
|
74
|
+
print(f, f"{f/24:7.3f}", info[f], np.round(c[:2], 1))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Integra o movimento global medido quadro a quadro (05-quadro-a-quadro.csv) em câmera absoluta por cena.
|
|
2
|
+
|
|
3
|
+
Mundo da cena N = tela do original no instante de referência r_N (câmera identidade em r_N).
|
|
4
|
+
Saída por quadro: zoom s, giro θ (graus), ponto de mira w (coordenadas do mundo, px de 1080),
|
|
5
|
+
e a pegada da tela no mundo (para saber até onde a interface precisa existir).
|
|
6
|
+
"""
|
|
7
|
+
import csv, json, sys, numpy as np
|
|
8
|
+
|
|
9
|
+
CSV = sys.argv[1]; CENAS = json.load(open(sys.argv[2], encoding="utf-8")); SAIDA = sys.argv[3]
|
|
10
|
+
R = list(csv.DictReader(open(CSV)))
|
|
11
|
+
FPS = 24.0
|
|
12
|
+
c = np.array([540.0, 540.0])
|
|
13
|
+
|
|
14
|
+
def M_quadro(i):
|
|
15
|
+
"""Transformação da tela do quadro i-1 para a do quadro i (similaridade em torno do centro)."""
|
|
16
|
+
r = R[i - 1] # linha i-1 do csv descreve quadro i (o csv começa no quadro 1)
|
|
17
|
+
try:
|
|
18
|
+
ds, dr, dx, dy = float(r["descala"]), np.radians(float(r["drot_graus"])), float(r["dx_px1080"]), float(r["dy_px1080"])
|
|
19
|
+
if not np.isfinite(ds): raise ValueError
|
|
20
|
+
except ValueError:
|
|
21
|
+
ds, dr, dx, dy = 1.0, 0.0, 0.0, 0.0
|
|
22
|
+
A = ds * np.array([[np.cos(dr), -np.sin(dr)], [np.sin(dr), np.cos(dr)]])
|
|
23
|
+
t = c + np.array([dx, dy]) - A @ c
|
|
24
|
+
M = np.eye(3); M[:2, :2] = A; M[:2, 2] = t
|
|
25
|
+
return M
|
|
26
|
+
|
|
27
|
+
out = {}
|
|
28
|
+
for cena in CENAS:
|
|
29
|
+
nome, ini, fim, ref = cena["id"], cena["ini"], cena["fim"], cena["ref"]
|
|
30
|
+
fi, ff, fr = int(round(ini * FPS)), int(round(fim * FPS)), int(round(ref * FPS))
|
|
31
|
+
S = {fr: np.eye(3)}
|
|
32
|
+
for f in range(fr + 1, ff + 1): S[f] = M_quadro(f) @ S[f - 1]
|
|
33
|
+
for f in range(fr - 1, fi - 1, -1): S[f] = np.linalg.inv(M_quadro(f + 1)) @ S[f + 1]
|
|
34
|
+
linhas = []; xs = []; ys = []
|
|
35
|
+
for f in range(fi, ff + 1):
|
|
36
|
+
A = S[f][:2, :2]; s = float(np.sqrt(abs(np.linalg.det(A)))); th = float(np.degrees(np.arctan2(A[1, 0], A[0, 0])))
|
|
37
|
+
Si = np.linalg.inv(S[f]); w = (Si @ np.array([540, 540, 1.0]))[:2]
|
|
38
|
+
cant = [(Si @ np.array([x, y, 1.0]))[:2] for x, y in ((0, 0), (1080, 0), (1080, 1080), (0, 1080))]
|
|
39
|
+
xs += [p[0] for p in cant]; ys += [p[1] for p in cant]
|
|
40
|
+
linhas.append({"f": f, "t": round(f / FPS, 4), "s": round(s, 5), "rot": round(th, 3), "wx": round(float(w[0]), 2), "wy": round(float(w[1]), 2)})
|
|
41
|
+
out[nome] = {"ini": ini, "fim": fim, "ref": ref, "pegada": [round(min(xs)), round(min(ys)), round(max(xs)), round(max(ys))], "quadros": linhas}
|
|
42
|
+
s = [l["s"] for l in linhas]
|
|
43
|
+
print(f"{nome}: {ini:5.2f}-{fim:5.2f} ref {ref:5.2f} | zoom {s[0]:.2f} -> {s[-1]:.2f} (min {min(s):.2f} max {max(s):.2f}) | pegada x[{min(xs):.0f},{max(xs):.0f}] y[{min(ys):.0f},{max(ys):.0f}]")
|
|
44
|
+
json.dump(out, open(SAIDA, "w"), indent=0)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sys, csv, importlib.util, numpy as np
|
|
2
|
+
spec = importlib.util.spec_from_file_location("olho", r"<SUA_CENTRAL>\07-videos\scripts\olho_quadro_a_quadro.py")
|
|
3
|
+
olho = importlib.util.module_from_spec(spec); spec.loader.exec_module(olho)
|
|
4
|
+
import cv2
|
|
5
|
+
from PIL import Image, ImageDraw
|
|
6
|
+
video, saida = sys.argv[1], sys.argv[2]
|
|
7
|
+
larg = 540
|
|
8
|
+
q, t, fps = olho.ler_quadros(video, 0, None, None, larg)
|
|
9
|
+
k = 1080 / larg
|
|
10
|
+
rows = []
|
|
11
|
+
g0 = cv2.cvtColor(q[0], cv2.COLOR_BGR2GRAY); h, w = g0.shape; c = np.array([w/2, h/2, 1.0])
|
|
12
|
+
for i in range(1, len(q)):
|
|
13
|
+
g1 = cv2.cvtColor(q[i], cv2.COLOR_BGR2GRAY)
|
|
14
|
+
p0 = cv2.goodFeaturesToTrack(g0, 600, 0.01, 6)
|
|
15
|
+
M = None; inl = 0; npts = 0
|
|
16
|
+
if p0 is not None and len(p0) >= 6:
|
|
17
|
+
p1, st, _ = cv2.calcOpticalFlowPyrLK(g0, g1, p0, None, winSize=(25, 25), maxLevel=4)
|
|
18
|
+
b = st.ravel() == 1; npts = int(b.sum())
|
|
19
|
+
if npts >= 6:
|
|
20
|
+
M, m = cv2.estimateAffinePartial2D(p0[b], p1[b], method=cv2.RANSAC, ransacReprojThreshold=2.0)
|
|
21
|
+
inl = int(m.sum()) if m is not None else 0
|
|
22
|
+
diff = float(np.abs(g1.astype(np.float32) - g0.astype(np.float32)).mean())
|
|
23
|
+
if M is None:
|
|
24
|
+
rows.append([t[i], i, np.nan, np.nan, np.nan, np.nan, npts, inl, diff])
|
|
25
|
+
else:
|
|
26
|
+
d = M @ c - c[:2]
|
|
27
|
+
rows.append([t[i], i, d[0]*k, d[1]*k, np.degrees(np.arctan2(M[1,0], M[0,0])), float(np.hypot(M[0,0], M[1,0])), npts, inl, diff])
|
|
28
|
+
g0 = g1
|
|
29
|
+
with open(saida + ".csv", "w", newline="") as fh:
|
|
30
|
+
wr = csv.writer(fh); wr.writerow(["t","quadro","dx_px1080","dy_px1080","drot_graus","descala","pontos","inliers","dif_media"])
|
|
31
|
+
for r in rows: wr.writerow([round(float(v),4) if isinstance(v,(float,np.floating)) else v for v in r])
|
|
32
|
+
print(len(rows), "quadros", fps)
|