primocode 10.12.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 +74 -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/assinaturas/gerativo.json +1 -1
- package/lib/estudio/camadas.js +4 -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/diretor.js +2 -1
- package/lib/estudio/editor.js +285 -0
- package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/gerativo.js +44 -15
- package/lib/estudio/index.js +126 -5
- package/lib/estudio/kit/gerativo.jsx +15 -0
- 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/kokoro.js +3 -1
- 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/estudio/remotion.js +6 -4
- package/lib/estudio/revisor.js +61 -0
- package/lib/estudio/voicestudio.js +81 -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/lib/video/auto.js +18 -3
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
- package/skills/motion-de-primeira/SKILL.md +33 -51
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Mede o movimento 2D (semelhança: zoom, giro, translação) da MESA nas cenas do jornal.
|
|
2
|
+
Achado: a mesa do autor é uma camada 2D (retângulo alinhado à tela em 16,54 s), filha dos nulls de câmera;
|
|
3
|
+
só o jornal gira em 3D. Fluxo óptico (DIS) entre quadros vizinhos só na madeira (fora do quadrilátero das
|
|
4
|
+
páginas, que vem da câmera 3D), semelhança por RANSAC, encadeada a partir do quadro de referência.
|
|
5
|
+
Saída: camera/mesa2d-<X>.json {ref, quadros: [{f, t, S: 3x3 (tela no ref -> tela no quadro), area}]}
|
|
6
|
+
Uso: python -I mesa_2d.py F|J '<json do quadrilátero das páginas no plano>'"""
|
|
7
|
+
import json, sys, cv2, numpy as np
|
|
8
|
+
CENA = sys.argv[1]; PAG = np.array(json.loads(sys.argv[2]), float)
|
|
9
|
+
VID = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
|
|
10
|
+
d = json.load(open(f"camera3d-{CENA}.json")); Q = {q["f"]: q for q in d["quadros"]}
|
|
11
|
+
REF = {"F": 394, "J": 670}[CENA]
|
|
12
|
+
K = np.array([[1500, 0, 540], [0, 1500, 540], [0, 0, 1.0]])
|
|
13
|
+
fs = sorted(Q)
|
|
14
|
+
cap = cv2.VideoCapture(VID); cap.set(cv2.CAP_PROP_POS_FRAMES, fs[0]); G = {}
|
|
15
|
+
for f in fs:
|
|
16
|
+
ok, im = cap.read(); G[f] = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
|
|
17
|
+
def mask(f):
|
|
18
|
+
R = np.array(Q[f]["R"]); t = np.array(Q[f]["tc"]); Hp = K @ np.c_[R[:, 0], R[:, 1], t]
|
|
19
|
+
P = np.c_[PAG, np.ones(4)] @ Hp.T; ok = (P[:, 2] > 0).all()
|
|
20
|
+
m = np.full((1080, 1080), 255, np.uint8)
|
|
21
|
+
if ok:
|
|
22
|
+
P = P[:, :2] / P[:, 2:3]; cv2.fillPoly(m, [np.clip(P, -1e5, 1e5).astype(np.int32)], 0)
|
|
23
|
+
else:
|
|
24
|
+
m[:] = 0
|
|
25
|
+
m = cv2.erode(m, np.ones((31, 31), np.uint8))
|
|
26
|
+
m[:30] = 0; m[-30:] = 0; m[:, :30] = 0; m[:, -30:] = 0
|
|
27
|
+
# fora da mesa (fundo escuro do fim) também sai
|
|
28
|
+
m[G[f] < 25] = 0
|
|
29
|
+
return m
|
|
30
|
+
dis = cv2.DISOpticalFlow_create(cv2.DISOPTICAL_FLOW_PRESET_MEDIUM)
|
|
31
|
+
def sim(a, b):
|
|
32
|
+
"""semelhança tela a -> tela b medida na madeira visível nos dois"""
|
|
33
|
+
ma = mask(a); mb = mask(b)
|
|
34
|
+
fl = dis.calc(G[a], G[b], None)
|
|
35
|
+
ys, xs = np.nonzero(ma[::6, ::6]); ys *= 6; xs *= 6
|
|
36
|
+
if len(xs) < 300: return None, len(xs)
|
|
37
|
+
p1 = np.c_[xs, ys].astype(np.float32); p2 = p1 + fl[ys, xs]
|
|
38
|
+
ok = (p2[:, 0] > 0) & (p2[:, 0] < 1079) & (p2[:, 1] > 0) & (p2[:, 1] < 1079)
|
|
39
|
+
p1, p2 = p1[ok], p2[ok]
|
|
40
|
+
keep = mb[p2[:, 1].astype(int), p2[:, 0].astype(int)] > 0
|
|
41
|
+
p1, p2 = p1[keep], p2[keep]
|
|
42
|
+
if len(p1) < 300: return None, len(p1)
|
|
43
|
+
A, inl = cv2.estimateAffinePartial2D(p1, p2, method=cv2.RANSAC, ransacReprojThreshold=2.0)
|
|
44
|
+
if A is None or inl.sum() < 200: return None, int(len(p1))
|
|
45
|
+
S = np.eye(3); S[:2] = A; return S, int(inl.sum())
|
|
46
|
+
S = {REF: np.eye(3)}; area = {REF: 1}
|
|
47
|
+
for direc in (-1, 1):
|
|
48
|
+
f = REF
|
|
49
|
+
while (f + direc) in Q:
|
|
50
|
+
g = f + direc
|
|
51
|
+
T, n = sim(f, g)
|
|
52
|
+
if T is None: S[g] = None; area[g] = n; f = g; continue
|
|
53
|
+
base = f
|
|
54
|
+
while S.get(base) is None: base -= direc
|
|
55
|
+
S[g] = T @ S[f] if S[f] is not None else None; area[g] = n
|
|
56
|
+
f = g
|
|
57
|
+
out = []
|
|
58
|
+
for f in fs:
|
|
59
|
+
s = S.get(f)
|
|
60
|
+
if s is not None:
|
|
61
|
+
sc = float(np.sqrt(abs(np.linalg.det(s[:2, :2])))); rot = float(np.degrees(np.arctan2(s[1, 0], s[0, 0])))
|
|
62
|
+
print(f, round(f / 24, 3), area.get(f), None if s is None else (round(sc, 3), round(rot, 2), np.round(s[:2, 2], 1).tolist()))
|
|
63
|
+
out.append({"f": f, "t": round(f / 24, 5), "S": None if s is None else s.tolist(), "n": area.get(f)})
|
|
64
|
+
json.dump({"ref": REF, "quadros": out}, open(f"mesa2d-{CENA}.json", "w"))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import json, sys, numpy as np
|
|
2
|
+
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
|
|
3
|
+
d = json.load(open(sys.argv[1])); out = sys.argv[2]
|
|
4
|
+
fig, ax = plt.subplots(4, 1, figsize=(22, 12), sharex=True)
|
|
5
|
+
for k, v in d.items():
|
|
6
|
+
q = v["quadros"]; t = [x["t"] for x in q]
|
|
7
|
+
ax[0].plot(t, np.log2([x["s"] for x in q]), label=k); ax[1].plot(t, [x["wx"] for x in q]); ax[2].plot(t, [x["wy"] for x in q]); ax[3].plot(t, [x["rot"] for x in q])
|
|
8
|
+
for a in ax: a.axvline(v["ini"], color="k", alpha=.3)
|
|
9
|
+
ax[0].text(v["ini"] + .05, ax[0].get_ylim()[1] if False else 5, k, fontsize=9)
|
|
10
|
+
for a, l in zip(ax, ["log2 zoom (0 = ref)", "mira x (mundo)", "mira y (mundo)", "giro (graus)"]): a.set_ylabel(l); a.grid(alpha=.3)
|
|
11
|
+
ax[3].set_xticks(np.arange(0, 34.1, 1)); plt.tight_layout(); plt.savefig(out, dpi=65)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Desentorta quadros do original para o plano do jornal (coordenadas do mundo da cena) e monta um mosaico,
|
|
2
|
+
ficando em cada ponto o quadro que tem mais resolução ali. Serve para decalcar a diagramação da página.
|
|
3
|
+
Uso: python -I retificar_quadros.py F|J escala saida.png quadro1 quadro2 ... (escala = px por unidade do plano)"""
|
|
4
|
+
import json, sys, cv2, numpy as np
|
|
5
|
+
CENA, ESC, OUT = sys.argv[1], float(sys.argv[2]), sys.argv[3]
|
|
6
|
+
QS = [int(x) for x in sys.argv[4:]]
|
|
7
|
+
VID = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
|
|
8
|
+
d = json.load(open(f"camera3d-{CENA}.json")); Q = {q["f"]: q for q in d["quadros"]}
|
|
9
|
+
K = np.array([[1500, 0, 540], [0, 1500, 540], [0, 0, 1.0]])
|
|
10
|
+
def Hps(f):
|
|
11
|
+
R = np.array(Q[f]["R"]); t = np.array(Q[f]["tc"]); return K @ np.c_[R[:, 0], R[:, 1], t]
|
|
12
|
+
# extensão: o maior quadro (mais recuado) define a área
|
|
13
|
+
fl = max(QS, key=lambda f: Q[f]["tc"][2])
|
|
14
|
+
Hi = np.linalg.inv(Hps(fl)); cs = np.array([[0, 0, 1], [1080, 0, 1], [1080, 1080, 1], [0, 1080, 1.0]]) @ Hi.T
|
|
15
|
+
cs = cs[:, :2] / cs[:, 2:3]; x0, y0 = cs.min(0); x1, y1 = cs.max(0)
|
|
16
|
+
if len(sys.argv) > 4 and "--" in sys.argv: pass
|
|
17
|
+
W, Hh = int((x1 - x0) * ESC), int((y1 - y0) * ESC)
|
|
18
|
+
T = np.array([[ESC, 0, -x0 * ESC], [0, ESC, -y0 * ESC], [0, 0, 1.0]])
|
|
19
|
+
cap = cv2.VideoCapture(VID)
|
|
20
|
+
acc = np.zeros((Hh, W, 3), np.float32); best = np.zeros((Hh, W), np.float32)
|
|
21
|
+
for f in sorted(QS):
|
|
22
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, im = cap.read()
|
|
23
|
+
M = Hps(f) @ np.linalg.inv(T) # saída -> tela
|
|
24
|
+
w = cv2.warpPerspective(im, M, (W, Hh), flags=cv2.INTER_LINEAR | cv2.WARP_INVERSE_MAP)
|
|
25
|
+
# resolução local: px de tela por unidade do plano ~ f / z_cam
|
|
26
|
+
yy, xx = np.mgrid[0:Hh, 0:W].astype(np.float32)
|
|
27
|
+
X = xx / ESC + x0; Y = yy / ESC + y0
|
|
28
|
+
R = np.array(Q[f]["R"]); t = np.array(Q[f]["tc"])
|
|
29
|
+
zc = R[2, 0] * X + R[2, 1] * Y + t[2]
|
|
30
|
+
P = np.stack([X, Y, np.ones_like(X)], -1) @ Hps(f).T; u = P[..., 0] / P[..., 2]; v = P[..., 1] / P[..., 2]
|
|
31
|
+
dentro = (u > 2) & (u < 1078) & (v > 2) & (v < 1078) & (zc > 0)
|
|
32
|
+
res = np.where(dentro, 1500 / np.maximum(zc, 1), 0)
|
|
33
|
+
m = res > best
|
|
34
|
+
acc[m] = w[m]; best[m] = res[m]
|
|
35
|
+
print(f, "z", round(t[2]), "px/unidade", round(1500 / t[2], 3))
|
|
36
|
+
cv2.imencode(".png", acc.astype(np.uint8))[1].tofile(OUT)
|
|
37
|
+
json.dump({"x0": float(x0), "y0": float(y0), "escala": ESC, "W": W, "H": Hh}, open(OUT + ".json", "w"))
|
|
38
|
+
print("extensão do plano", round(x0), round(y0), round(x1), round(y1), "->", W, Hh)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Câmera final das cenas A e B (agente w-agenda, 17/10).
|
|
2
|
+
A: cadeia SIFT de medir_ab.py com 6 passos refeitos por geometria, porque o SIFT falhou neles:
|
|
3
|
+
- quadros 0->3: anel do checkbox (centro e diâmetro medidos na tela; anel = 18,7 px de mundo);
|
|
4
|
+
- quadros 28->31: o evento escurece e ganha risco; escala pela altura do chip (69 px de mundo) e
|
|
5
|
+
translação pelo início do "w" e pelo topo do chip.
|
|
6
|
+
B: cadeia SIFT de medir_ab.py como está (inclusive 4,46–4,83: a câmera NÃO segura, continua recuando).
|
|
7
|
+
Saída: cena-A-agenda.json e cena-B-docs.json no formato do ae/camera.jsx."""
|
|
8
|
+
import json, numpy as np, os
|
|
9
|
+
D = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
M = json.load(open(os.path.join(D, "medida-ab.json")))
|
|
11
|
+
def S_de(z, mx, my):
|
|
12
|
+
s = 2 ** z; T = np.eye(3); T[0, 0] = T[1, 1] = s; T[:2, 2] = [540 - s * mx, 540 - s * my]; return T
|
|
13
|
+
def est(T):
|
|
14
|
+
s = T[0, 0]; w = np.linalg.inv(T) @ np.array([540, 540, 1.0]); return float(np.log2(s)), float(w[0]), float(w[1])
|
|
15
|
+
def passo(r, tx, ty):
|
|
16
|
+
T = np.eye(3); T[0, 0] = T[1, 1] = r; T[0, 2] = tx; T[1, 2] = ty; return T
|
|
17
|
+
A = M["A-agenda"]; S = {round(l[0] * 24): S_de(l[1], l[2], l[3]) for l in A}
|
|
18
|
+
# passos medidos por geometria: tela f -> tela f+1
|
|
19
|
+
anel = {0: ((531.5, 548.5), 1158.0), 1: ((449.5, 498.5), 874.0), 2: ((316.0, 414.5), 417.5), 3: ((275.5, 389.0), 274.5)}
|
|
20
|
+
P = {}
|
|
21
|
+
for f in (0, 1, 2):
|
|
22
|
+
(c0, d0), (c1, d1) = anel[f], anel[f + 1]; r = d1 / d0
|
|
23
|
+
P[f] = passo(r, c1[0] - r * c0[0], c1[1] - r * c0[1])
|
|
24
|
+
P[28] = passo(382 / 406, 85 - 382 / 406 * 35, 246 - 382 / 406 * 244)
|
|
25
|
+
P[29] = passo(353 / 382, 150 - 353 / 382 * 85, 246 - 353 / 382 * 246)
|
|
26
|
+
P[30] = passo(321 / 353, 225 - 321 / 353 * 150, 246 - 321 / 353 * 246)
|
|
27
|
+
# refaz a cadeia de trás para frente a partir de f31 (bom) e de f3 (refeito)
|
|
28
|
+
for f in (30, 29, 28):
|
|
29
|
+
S[f] = np.linalg.inv(P[f]) @ S[f + 1]
|
|
30
|
+
# quadros 4..27 dependem da cadeia SIFT ligada ao f28 antigo: aplica a mesma correção relativa
|
|
31
|
+
# (a cadeia SIFT entre 4 e 28 está boa; só o elo 28->31 estava errado)
|
|
32
|
+
Sant28 = S_de(*[l for l in A if round(l[0] * 24) == 28][0][1:4])
|
|
33
|
+
C = np.linalg.inv(Sant28) @ S[28] # correção no espaço do mundo (as transformações relativas entre quadros ficam iguais)
|
|
34
|
+
for f in range(3, 28):
|
|
35
|
+
S[f] = S[f] @ C
|
|
36
|
+
for f in (2, 1, 0):
|
|
37
|
+
S[f] = np.linalg.inv(P[f]) @ S[f + 1]
|
|
38
|
+
def salvar(nome, quadros, ini, fim, ref, pegada):
|
|
39
|
+
ch = {"zoom": [], "mx": [], "my": [], "giro": []}
|
|
40
|
+
for f in sorted(quadros):
|
|
41
|
+
z, mx, my = est(quadros[f]); t = round(f / 24, 4)
|
|
42
|
+
ch["zoom"].append([t, round(z, 4)]); ch["mx"].append([t, round(mx, 2)]); ch["my"].append([t, round(my, 2)]); ch["giro"].append([t, 0.0])
|
|
43
|
+
json.dump({"fps": 24, "ini": ini, "fim": fim, "ref": ref, "pegada": pegada, "fonte": "w-agenda/assar_ab.py", "chaves": ch},
|
|
44
|
+
open(os.path.join(D, "cena-%s.json" % nome), "w"))
|
|
45
|
+
return ch
|
|
46
|
+
chA = salvar("A-agenda", S, 0.0, 2.62, 2.58, None)
|
|
47
|
+
SB = {round(l[0] * 24): S_de(l[1], l[2], l[3]) for l in M["B-docs"]}
|
|
48
|
+
chB = salvar("B-docs", SB, 2.62, 4.95, 4.875, None)
|
|
49
|
+
for n, ch in (("A", chA), ("B", chB)):
|
|
50
|
+
print(n)
|
|
51
|
+
for i in range(0, len(ch["zoom"]), 3):
|
|
52
|
+
print(" t=%.3f z=%.3f mx=%.1f my=%.1f" % (ch["zoom"][i][0], ch["zoom"][i][1], ch["mx"][i][1], ch["my"][i][1]))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Remedição da câmera das cenas A e B (agente w-agenda, 17/10).
|
|
2
|
+
Casa cada quadro com o anterior (SIFT + RANSAC, similaridade sem giro), com máscara que tira elementos que se mexem
|
|
3
|
+
sozinhos (cartão voando, globo), e integra a partir do quadro de referência de cada cena.
|
|
4
|
+
Saída: json {cena: [[t, log2 s, mx, my, n_inliers], ...]} no mesmo sentido do camera.jsx (mundo = tela no ref)."""
|
|
5
|
+
import json, sys, cv2, numpy as np
|
|
6
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
7
|
+
VID = sys.argv[1]; OUT = sys.argv[2]
|
|
8
|
+
cap = cv2.VideoCapture(VID); Q = []
|
|
9
|
+
while True:
|
|
10
|
+
ok, q = cap.read()
|
|
11
|
+
if not ok or len(Q) > 125: break
|
|
12
|
+
Q.append(cv2.cvtColor(q, cv2.COLOR_BGR2GRAY))
|
|
13
|
+
sift = cv2.SIFT_create(nfeatures=6000, contrastThreshold=0.02)
|
|
14
|
+
bf = cv2.BFMatcher()
|
|
15
|
+
def mascara(f):
|
|
16
|
+
m = np.full((1080, 1080), 255, np.uint8)
|
|
17
|
+
t = f / 24.0
|
|
18
|
+
if 2.19 <= t <= 2.56: # cartão de detalhes voando (ocupa a metade de baixo): usa só a faixa de cima e a esquerda
|
|
19
|
+
m[:, :] = 0; m[0:300, :] = 255
|
|
20
|
+
if 4.40 <= t <= 4.80: # globo entrando: usa só a faixa do menu/barra
|
|
21
|
+
m[:, :] = 0; m[0:260, :] = 255
|
|
22
|
+
return m
|
|
23
|
+
cache = {}
|
|
24
|
+
def kp(f, mf):
|
|
25
|
+
key = (f, mf)
|
|
26
|
+
if key not in cache: cache[key] = sift.detectAndCompute(Q[f], mascara(mf))
|
|
27
|
+
return cache[key]
|
|
28
|
+
def casar(a, b):
|
|
29
|
+
"""transformação tela a -> tela b (similaridade sem giro), usando a máscara do quadro mais crítico"""
|
|
30
|
+
mf = a if mascara(a).mean() < mascara(b).mean() else b
|
|
31
|
+
k1, d1 = kp(a, mf); k2, d2 = kp(b, mf)
|
|
32
|
+
if d1 is None or d2 is None or len(k1) < 8 or len(k2) < 8: return None, 0
|
|
33
|
+
mm = bf.knnMatch(d1, d2, k=2)
|
|
34
|
+
bons = [m for m, n in (x for x in mm if len(x) == 2) if m.distance < 0.75 * n.distance]
|
|
35
|
+
if len(bons) < 8: return None, len(bons)
|
|
36
|
+
p1 = np.float32([k1[m.queryIdx].pt for m in bons]); p2 = np.float32([k2[m.trainIdx].pt for m in bons])
|
|
37
|
+
A, inl = cv2.estimateAffinePartial2D(p1, p2, method=cv2.RANSAC, ransacReprojThreshold=3.0, maxIters=5000)
|
|
38
|
+
if A is None: return None, 0
|
|
39
|
+
s = float(np.hypot(A[0, 0], A[1, 0])); T = np.eye(3); T[0, 0] = T[1, 1] = s; T[:2, 2] = A[:, 2]
|
|
40
|
+
# refaz a translação com escala sem giro (média dos inliers)
|
|
41
|
+
i = inl.ravel().astype(bool)
|
|
42
|
+
T[:2, 2] = (p2[i] - s * p1[i]).mean(0)
|
|
43
|
+
return T, int(i.sum())
|
|
44
|
+
res = {}
|
|
45
|
+
for nome, ini, fim, ref in (("A-agenda", 0, 63, 62), ("B-docs", 63, 119, 117)):
|
|
46
|
+
S = {ref: np.eye(3)}; N = {ref: 999}
|
|
47
|
+
for f in range(ref + 1, fim + 1):
|
|
48
|
+
T, n = casar(f - 1, f); S[f] = (T if T is not None else np.eye(3)) @ S[f - 1]; N[f] = n
|
|
49
|
+
for f in range(ref - 1, ini - 1, -1):
|
|
50
|
+
T, n = casar(f, f + 1); S[f] = np.linalg.inv(T if T is not None else np.eye(3)) @ S[f + 1]; N[f] = n
|
|
51
|
+
lin = []
|
|
52
|
+
for f in range(ini, fim + 1):
|
|
53
|
+
s = S[f][0, 0]; w = np.linalg.inv(S[f]) @ np.array([540, 540, 1.0])
|
|
54
|
+
lin.append([round(f / 24, 4), round(float(np.log2(s)), 4), round(float(w[0]), 2), round(float(w[1]), 2), N[f]])
|
|
55
|
+
res[nome] = lin
|
|
56
|
+
json.dump(res, open(OUT, "w"))
|
|
57
|
+
for k, v in res.items():
|
|
58
|
+
print(k)
|
|
59
|
+
for l in v: print(" t=%.3f z=%.3f mx=%.1f my=%.1f n=%s" % tuple(l))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Câmera por homografia: para cada quadro, H (mundo -> tela) casando com quadros âncora (SIFT), direto ou em cadeia.
|
|
2
|
+
# Decompõe H = P ∘ S: S = similaridade local no centro da tela (zoom, mira, giro -> controles do CAM)
|
|
3
|
+
# P = resto projetivo em tela (vira Corner Pin), guardado como deslocamento dos 4 cantos
|
|
4
|
+
# do quadro ampliado [-M, 1080+M] (M = margem do comp interno).
|
|
5
|
+
# uso: python ajustar_h.py <id> <saida.json> --ancora t [t ...] [--de t0] [--ate t1] [--margem 270] [--sem-ref]
|
|
6
|
+
import sys, json, math, cv2, numpy as np
|
|
7
|
+
P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
|
|
8
|
+
ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
|
|
9
|
+
a = sys.argv; cid = a[1]; out = a[2]
|
|
10
|
+
d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8")); ch = d["chaves"]
|
|
11
|
+
def lista(flag):
|
|
12
|
+
if flag not in a: return []
|
|
13
|
+
i = a.index(flag) + 1; r = []
|
|
14
|
+
while i < len(a) and not a[i].startswith("--"): r.append(float(a[i])); i += 1
|
|
15
|
+
return r
|
|
16
|
+
ancoras = lista("--ancora") or [d["ref"]]
|
|
17
|
+
t0 = lista("--de")[0] if "--de" in a else d["ini"]; t1 = lista("--ate")[0] if "--ate" in a else d["fim"]
|
|
18
|
+
MG = lista("--margem")[0] if "--margem" in a else 270.0
|
|
19
|
+
def interp(lst, t):
|
|
20
|
+
ts = [x for x, y in lst]
|
|
21
|
+
if t <= ts[0]: return lst[0][1]
|
|
22
|
+
if t >= ts[-1]: return lst[-1][1]
|
|
23
|
+
for i in range(1, len(ts)):
|
|
24
|
+
if ts[i] >= t:
|
|
25
|
+
u = (t - ts[i-1]) / (ts[i] - ts[i-1]); return lst[i-1][1] * (1-u) + lst[i][1] * u
|
|
26
|
+
def json_cam(t): return [interp(ch[n], t) for n in ("zoom", "mx", "my", "giro")]
|
|
27
|
+
def S_de(z, mx, my, g):
|
|
28
|
+
s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
|
|
29
|
+
T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
|
|
30
|
+
CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
|
|
31
|
+
def decompor(H):
|
|
32
|
+
H = H / H[2, 2]
|
|
33
|
+
c = np.linalg.inv(H) @ np.array([540, 540, 1.0]); w = c[:2] / c[2]
|
|
34
|
+
def Hp(x, y):
|
|
35
|
+
v = H @ np.array([x, y, 1.0]); return v[:2] / v[2]
|
|
36
|
+
e = 0.5; J = np.column_stack([(Hp(w[0] + e, w[1]) - Hp(w[0] - e, w[1])) / (2 * e), (Hp(w[0], w[1] + e) - Hp(w[0], w[1] - e)) / (2 * e)])
|
|
37
|
+
s = math.sqrt(abs(np.linalg.det(J))); g = math.degrees(math.atan2(J[1, 0] - J[0, 1], J[0, 0] + J[1, 1]))
|
|
38
|
+
S = S_de(math.log2(s), w[0], w[1], g)
|
|
39
|
+
Pm = H @ np.linalg.inv(S); Pm /= Pm[2, 2]
|
|
40
|
+
cp = cv2.perspectiveTransform(CANT[None], Pm)[0] - CANT
|
|
41
|
+
return [math.log2(s), float(w[0]), float(w[1]), g], cp.ravel().tolist()
|
|
42
|
+
def compor(cam, cp):
|
|
43
|
+
S = S_de(*cam); dst = (CANT + np.float32(cp).reshape(4, 2)).astype(np.float32)
|
|
44
|
+
Pm = cv2.getPerspectiveTransform(CANT, dst); return Pm @ S
|
|
45
|
+
cap = cv2.VideoCapture(ORIG); cache = {}
|
|
46
|
+
sift = cv2.SIFT_create(6000); bf = cv2.BFMatcher()
|
|
47
|
+
def quadro(f):
|
|
48
|
+
if f not in cache:
|
|
49
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, q = cap.read(); g = cv2.cvtColor(q, cv2.COLOR_BGR2GRAY)
|
|
50
|
+
cache[f] = sift.detectAndCompute(g, None)
|
|
51
|
+
return cache[f]
|
|
52
|
+
def homog(fa, fb):
|
|
53
|
+
k1, d1 = quadro(fa); k2, d2 = quadro(fb)
|
|
54
|
+
if d1 is None or d2 is None or len(k1) < 15 or len(k2) < 15: return None, 0
|
|
55
|
+
mm = bf.knnMatch(d1, d2, k=2); b = [m for m, n in (x for x in mm if len(x) == 2) if m.distance < 0.75 * n.distance]
|
|
56
|
+
if len(b) < 20: return None, 0
|
|
57
|
+
p1 = np.float32([k1[m.queryIdx].pt for m in b]); p2 = np.float32([k2[m.trainIdx].pt for m in b])
|
|
58
|
+
H, inl = cv2.findHomography(p1, p2, cv2.RANSAC, 3.0)
|
|
59
|
+
if H is None: return None, 0
|
|
60
|
+
n = int(inl.sum())
|
|
61
|
+
if n < 25 or n / len(b) < 0.3: return None, n
|
|
62
|
+
q = p2[inl.ravel() > 0]
|
|
63
|
+
if q[:, 0].std() < 50 or q[:, 1].std() < 50: return None, n
|
|
64
|
+
return H, n
|
|
65
|
+
f0 = int(math.ceil(t0 * 24 - 1e-6)); f1 = int(math.floor(t1 * 24 + 1e-6))
|
|
66
|
+
fa = [int(round(t * 24)) for t in ancoras]
|
|
67
|
+
sol = {}
|
|
68
|
+
for f in fa: sol[f] = (S_de(*json_cam(f / 24.0)), "ancora", 9999)
|
|
69
|
+
def plausivel(H, prev):
|
|
70
|
+
cam, cp = decompor(H)
|
|
71
|
+
if prev is None: return True
|
|
72
|
+
pc, pp = decompor(prev)
|
|
73
|
+
if abs(cam[0] - pc[0]) > 0.6 or math.hypot(cam[1] - pc[1], cam[2] - pc[2]) * 2 ** cam[0] > 500: return False
|
|
74
|
+
if max(abs(v) for v in cp) > 1500: return False
|
|
75
|
+
return True
|
|
76
|
+
ordem = sorted(fa)
|
|
77
|
+
for i, A in enumerate(ordem):
|
|
78
|
+
esq = (ordem[i-1] + A) // 2 if i > 0 else f0 - 1
|
|
79
|
+
dir_ = (A + ordem[i+1]) // 2 if i + 1 < len(ordem) else f1 + 1
|
|
80
|
+
for passo, lim in ((-1, esq), (1, dir_)):
|
|
81
|
+
viz = A; f = A + passo
|
|
82
|
+
while (f > lim if passo < 0 else f < lim):
|
|
83
|
+
melhor = None
|
|
84
|
+
H, n = homog(A, f)
|
|
85
|
+
if H is not None and plausivel(H @ sol[A][0], sol[viz][0]):
|
|
86
|
+
melhor = (H @ sol[A][0], "direto %.2f" % (A / 24.0), n)
|
|
87
|
+
else:
|
|
88
|
+
H2, n2 = homog(viz, f)
|
|
89
|
+
if H2 is not None and plausivel(H2 @ sol[viz][0], sol[viz][0]):
|
|
90
|
+
melhor = (H2 @ sol[viz][0], "cadeia", n2)
|
|
91
|
+
if melhor:
|
|
92
|
+
sol[f] = melhor; viz = f
|
|
93
|
+
f += passo
|
|
94
|
+
ok = sorted(k for k in sol if f0 <= k <= f1)
|
|
95
|
+
dec = {k: decompor(sol[k][0]) for k in ok}
|
|
96
|
+
corr = {k: [dec[k][0][j] - json_cam(k / 24.0)[j] for j in range(4)] for k in ok}
|
|
97
|
+
linhas = []
|
|
98
|
+
for f in range(f0, f1 + 1):
|
|
99
|
+
jc = json_cam(f / 24.0)
|
|
100
|
+
if f in dec:
|
|
101
|
+
cam, cp = dec[f]; met = sol[f][1]
|
|
102
|
+
elif ok:
|
|
103
|
+
c = [float(np.interp(f, ok, [corr[k][j] for k in ok])) for j in range(4)]
|
|
104
|
+
cam = [jc[j] + c[j] for j in range(4)]
|
|
105
|
+
cp = [float(np.interp(f, ok, [dec[k][1][j] for k in ok])) for j in range(8)]; met = "buraco"
|
|
106
|
+
else:
|
|
107
|
+
cam, cp, met = jc, [0.0] * 8, "json"
|
|
108
|
+
linhas.append({"t": round(f / 24.0, 4), "zoom": cam[0], "mx": cam[1], "my": cam[2], "giro": cam[3], "cp": cp, "met": met})
|
|
109
|
+
print("%.3f %-12s z%6.3f M(%7.1f,%7.1f) g%6.2f | pin max %5.1f | json z%6.3f M(%7.1f,%7.1f) g%6.2f" % (
|
|
110
|
+
f / 24.0, met, cam[0], cam[1], cam[2], cam[3], max(abs(v) for v in cp), jc[0], jc[1], jc[2], jc[3]))
|
|
111
|
+
json.dump({"cena": cid, "ancoras": ancoras, "margem": MG, "quadros": linhas}, open(out, "w"), indent=0)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Correção manual (conferida no quadro do original) somada a um canal da câmera ajustada.
|
|
2
|
+
uso: python camera/wg_corrige_cam.py <id> <canal: zoom|mx|my|giro> t:delta [t:delta ...]
|
|
3
|
+
Os deltas são interpolados linearmente entre os tempos dados e valem 0 fora do intervalo.
|
|
4
|
+
Grava um histórico em "correcoes" dentro do JSON (para saber o que foi mexido à mão)."""
|
|
5
|
+
import sys, json, numpy as np
|
|
6
|
+
P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
|
|
7
|
+
cid, canal = sys.argv[1], sys.argv[2]
|
|
8
|
+
pts = sorted([tuple(float(v) for v in a.split(":")) for a in sys.argv[3:]])
|
|
9
|
+
arq = P + "camera/cena-%s-ajustada.json" % cid
|
|
10
|
+
d = json.load(open(arq))
|
|
11
|
+
ts = [p[0] for p in pts]; vs = [p[1] for p in pts]
|
|
12
|
+
for k in d["chaves"][canal]:
|
|
13
|
+
t = k[0]
|
|
14
|
+
if ts[0] <= t <= ts[-1]: k[1] = round(k[1] + float(np.interp(t, ts, vs)), 4)
|
|
15
|
+
d.setdefault("correcoes", []).append({"canal": canal, "pontos": pts})
|
|
16
|
+
json.dump(d, open(arq, "w"))
|
|
17
|
+
print("ok", cid, canal, pts)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Câmera por cadeia ECC quadro a quadro (para cenas de pouca textura, ex.: K-maps).
|
|
2
|
+
# uso: python ecc_cadeia.py <id> <saida.json> <t_ancora> [--de t0] [--ate t1] [--margem 270] [--modelo affine|homog]
|
|
3
|
+
# A âncora usa a câmera json; cada passo alinha o quadro f com f±1 (ECC multiescala) começando pelo movimento do json.
|
|
4
|
+
import sys, json, math, cv2, numpy as np
|
|
5
|
+
P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
|
|
6
|
+
ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
|
|
7
|
+
a = sys.argv; cid = a[1]; out = a[2]; tanc = float(a[3])
|
|
8
|
+
d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8")); ch = d["chaves"]
|
|
9
|
+
def opt(flag, dflt):
|
|
10
|
+
return a[a.index(flag) + 1] if flag in a else dflt
|
|
11
|
+
t0 = float(opt("--de", d["ini"])); t1 = float(opt("--ate", d["fim"])); MG = float(opt("--margem", 270)); modelo = opt("--modelo", "homog")
|
|
12
|
+
def interp(lst, t):
|
|
13
|
+
ts = [x for x, y in lst]
|
|
14
|
+
if t <= ts[0]: return lst[0][1]
|
|
15
|
+
if t >= ts[-1]: return lst[-1][1]
|
|
16
|
+
for i in range(1, len(ts)):
|
|
17
|
+
if ts[i] >= t:
|
|
18
|
+
u = (t - ts[i-1]) / (ts[i] - ts[i-1]); return lst[i-1][1] * (1-u) + lst[i][1] * u
|
|
19
|
+
def json_cam(t): return [interp(ch[n], t) for n in ("zoom", "mx", "my", "giro")]
|
|
20
|
+
def S_de(z, mx, my, g):
|
|
21
|
+
s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
|
|
22
|
+
T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
|
|
23
|
+
CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
|
|
24
|
+
def decompor(H):
|
|
25
|
+
H = H / H[2, 2]; c = np.linalg.inv(H) @ np.array([540, 540, 1.0]); w = c[:2] / c[2]
|
|
26
|
+
def Hp(x, y):
|
|
27
|
+
v = H @ np.array([x, y, 1.0]); return v[:2] / v[2]
|
|
28
|
+
e = 0.5; J = np.column_stack([(Hp(w[0] + e, w[1]) - Hp(w[0] - e, w[1])) / (2 * e), (Hp(w[0], w[1] + e) - Hp(w[0], w[1] - e)) / (2 * e)])
|
|
29
|
+
s = math.sqrt(abs(np.linalg.det(J))); g = math.degrees(math.atan2(J[1, 0] - J[0, 1], J[0, 0] + J[1, 1]))
|
|
30
|
+
S = S_de(math.log2(s), w[0], w[1], g); Pm = H @ np.linalg.inv(S); Pm /= Pm[2, 2]
|
|
31
|
+
cp = cv2.perspectiveTransform(CANT[None], Pm)[0] - CANT
|
|
32
|
+
return [math.log2(s), float(w[0]), float(w[1]), g], cp.ravel().tolist()
|
|
33
|
+
cap = cv2.VideoCapture(ORIG); cache = {}
|
|
34
|
+
def quadro(f):
|
|
35
|
+
if f not in cache:
|
|
36
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, q = cap.read(); cache[f] = cv2.cvtColor(q, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
|
|
37
|
+
return cache[f]
|
|
38
|
+
def ecc(fa, fb, H0):
|
|
39
|
+
"""H tela(fa)->tela(fb). ECC acha W com I_b(W x) ~ I_a(x)? Usamos template = fb, input = fa: W leva coords de fb -> fa."""
|
|
40
|
+
A = quadro(fa); B = quadro(fb)
|
|
41
|
+
W = np.linalg.inv(H0) # fb -> fa
|
|
42
|
+
W = W / W[2, 2]
|
|
43
|
+
cc = -1
|
|
44
|
+
for sc, bl in ((0.125, 5), (0.25, 5), (0.5, 3)):
|
|
45
|
+
As = cv2.GaussianBlur(cv2.resize(A, None, fx=sc, fy=sc, interpolation=cv2.INTER_AREA), (bl, bl), 0)
|
|
46
|
+
Bs = cv2.GaussianBlur(cv2.resize(B, None, fx=sc, fy=sc, interpolation=cv2.INTER_AREA), (bl, bl), 0)
|
|
47
|
+
Ssc = np.diag([sc, sc, 1.0]); Ws = Ssc @ W @ np.linalg.inv(Ssc)
|
|
48
|
+
try:
|
|
49
|
+
if modelo == "homog":
|
|
50
|
+
cc, Ws = cv2.findTransformECC(Bs, As, Ws.astype(np.float32), cv2.MOTION_HOMOGRAPHY, (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 150, 1e-6), None, 1)
|
|
51
|
+
else:
|
|
52
|
+
cc, W2 = cv2.findTransformECC(Bs, As, Ws[:2].astype(np.float32), cv2.MOTION_AFFINE, (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 150, 1e-6), None, 1)
|
|
53
|
+
Ws = np.vstack([W2, [0, 0, 1]])
|
|
54
|
+
except cv2.error:
|
|
55
|
+
return None, -1
|
|
56
|
+
W = np.linalg.inv(Ssc) @ Ws @ Ssc
|
|
57
|
+
return np.linalg.inv(W), cc
|
|
58
|
+
f0 = int(math.ceil(t0 * 24 - 1e-6)); f1 = int(math.floor(t1 * 24 + 1e-6)); fa = int(round(tanc * 24))
|
|
59
|
+
sol = {fa: (S_de(*json_cam(fa / 24.0)), 1.0)}
|
|
60
|
+
for passo, lim in ((1, f1), (-1, f0)):
|
|
61
|
+
f = fa
|
|
62
|
+
while (f < lim if passo > 0 else f > lim):
|
|
63
|
+
g = f + passo
|
|
64
|
+
H0 = S_de(*json_cam(g / 24.0)) @ np.linalg.inv(S_de(*json_cam(f / 24.0))) # movimento do json
|
|
65
|
+
H, cc = ecc(f, g, H0)
|
|
66
|
+
if H is None or cc < 0.5:
|
|
67
|
+
H, cc = H0, -1 # sem alinhamento: segue o json neste passo
|
|
68
|
+
elif "--sim" in a:
|
|
69
|
+
# projeta o passo numa similaridade (sem cisalhamento acumulado), preservando o centro da tela
|
|
70
|
+
H = H / H[2, 2]; A2 = H[:2, :2]; sc = math.sqrt(abs(np.linalg.det(A2))); rr = math.atan2(A2[1, 0] - A2[0, 1], A2[0, 0] + A2[1, 1])
|
|
71
|
+
c = H @ np.array([540, 540, 1.0]); c = c[:2] / c[2]
|
|
72
|
+
R2 = sc * np.array([[math.cos(rr), -math.sin(rr)], [math.sin(rr), math.cos(rr)]])
|
|
73
|
+
Hs = np.eye(3); Hs[:2, :2] = R2; Hs[:2, 2] = c - R2 @ np.array([540, 540]); H = Hs
|
|
74
|
+
sol[g] = (H @ sol[f][0], cc); f = g
|
|
75
|
+
linhas = []
|
|
76
|
+
for f in range(f0, f1 + 1):
|
|
77
|
+
cam, cp = decompor(sol[f][0]); jc = json_cam(f / 24.0)
|
|
78
|
+
linhas.append({"t": round(f / 24.0, 4), "zoom": cam[0], "mx": cam[1], "my": cam[2], "giro": cam[3], "cp": cp, "met": "ecc %.2f" % sol[f][1]})
|
|
79
|
+
print("%.3f ecc%5.2f z%6.3f M(%7.1f,%7.1f) g%6.2f pin %5.1f | json z%6.3f M(%7.1f,%7.1f) g%6.2f" % (f / 24.0, sol[f][1], cam[0], cam[1], cam[2], cam[3], max(abs(v) for v in cp), jc[0], jc[1], jc[2], jc[3]))
|
|
80
|
+
json.dump({"cena": cid, "ancoras": [tanc], "margem": MG, "quadros": linhas}, open(out, "w"), indent=0)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Câmera ajustada das cenas G, H, I, K (agente w-google, 17/10).
|
|
2
|
+
|
|
3
|
+
A câmera medida (cena-<id>.json) confundia a inclinação 3D das interfaces com giro e errava zoom/mira nos
|
|
4
|
+
chicotes. Aqui ela é refeita por homografia contra o quadro de referência (SIFT, direto ou em cadeia) e
|
|
5
|
+
decomposta em: similaridade no centro da tela (controles do CAM: zoom, mira, giro) + resto projetivo
|
|
6
|
+
(Corner Pin no comp da cena, guardado como deslocamento dos 4 cantos do quadro ampliado com margem).
|
|
7
|
+
Entradas: JSON por quadro gerados por ajustar_h.py / ecc_cadeia.py (scratchpad do agente; o essencial está aqui).
|
|
8
|
+
Saída: camera/cena-<id>-ajustada.json no formato do camera.jsx ({chaves: zoom, mx, my, giro}) + "pin" e "margem".
|
|
9
|
+
Regras: Savitzky-Golay de 5 quadros; pin zerado no quadro da troca e misturado (smoothstep) até a primeira
|
|
10
|
+
medida boa, para o encaixe encher a tela exatamente na troca.
|
|
11
|
+
uso: python camera/wg_finalizar_cam.py <id> <entrada.json> [--pin-zero t_ini t_fim_rampa] [--json-ate t] [--mistura t0 t1]
|
|
12
|
+
"""
|
|
13
|
+
import sys, json, math, numpy as np
|
|
14
|
+
from scipy.signal import savgol_filter
|
|
15
|
+
a = sys.argv; cid = a[1]; ent = json.load(open(a[2]))
|
|
16
|
+
P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
|
|
17
|
+
q = ent["quadros"]; t = np.array([x["t"] for x in q])
|
|
18
|
+
z = np.array([x["zoom"] for x in q]); mx = np.array([x["mx"] for x in q]); my = np.array([x["my"] for x in q]); g = np.array([x["giro"] for x in q])
|
|
19
|
+
cp = np.array([x.get("cp", [0.0] * 8) for x in q])
|
|
20
|
+
def ss(u): u = min(1, max(0, u)); return u * u * (3 - 2 * u)
|
|
21
|
+
if "--pin-zero" in a:
|
|
22
|
+
i = a.index("--pin-zero"); ta, tb = float(a[i + 1]), float(a[i + 2])
|
|
23
|
+
jb = int(np.argmin(abs(t - tb))); alvo = cp[jb].copy()
|
|
24
|
+
for j in range(len(t)):
|
|
25
|
+
if t[j] <= ta + 1e-6: cp[j] = 0
|
|
26
|
+
elif t[j] < tb: cp[j] = alvo * ss((t[j] - ta) / (tb - ta))
|
|
27
|
+
if "--mistura" in a: # troca suave entre a câmera json (antes de t0) e a ajustada (depois de t1)
|
|
28
|
+
i = a.index("--mistura"); t0, t1 = float(a[i + 1]), float(a[i + 2])
|
|
29
|
+
d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8"))["chaves"]
|
|
30
|
+
def jv(n, tt): return float(np.interp(tt, [x[0] for x in d[n]], [x[1] for x in d[n]]))
|
|
31
|
+
for j in range(len(t)):
|
|
32
|
+
if t[j] < t1:
|
|
33
|
+
w = ss((t[j] - t0) / (t1 - t0)) if t[j] > t0 else 0.0
|
|
34
|
+
z[j] = jv("zoom", t[j]) * (1 - w) + z[j] * w; mx[j] = jv("mx", t[j]) * (1 - w) + mx[j] * w
|
|
35
|
+
my[j] = jv("my", t[j]) * (1 - w) + my[j] * w; g[j] = jv("giro", t[j]) * (1 - w) + g[j] * w
|
|
36
|
+
cp[j] = cp[j] * w
|
|
37
|
+
z, mx, my, g = [savgol_filter(v, 5, 2) for v in (z, mx, my, g)]
|
|
38
|
+
cp = savgol_filter(cp, 5, 2, axis=0)
|
|
39
|
+
if "--pin-zero" in a:
|
|
40
|
+
for j in range(len(t)):
|
|
41
|
+
if t[j] <= ta + 1e-6: cp[j] = 0
|
|
42
|
+
src = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8"))
|
|
43
|
+
out = {"fps": 24, "ini": src["ini"], "fim": src["fim"], "ref": src["ref"], "pegada": src["pegada"], "margem": ent.get("margem", 270),
|
|
44
|
+
"origem": "wg_finalizar_cam.py (homografia + Corner Pin)",
|
|
45
|
+
"chaves": {"zoom": [[round(float(t[j]), 4), round(float(z[j]), 4)] for j in range(len(t))],
|
|
46
|
+
"mx": [[round(float(t[j]), 4), round(float(mx[j]), 3)] for j in range(len(t))],
|
|
47
|
+
"my": [[round(float(t[j]), 4), round(float(my[j]), 3)] for j in range(len(t))],
|
|
48
|
+
"giro": [[round(float(t[j]), 4), round(float(g[j]), 3)] for j in range(len(t))]},
|
|
49
|
+
"pin": [[round(float(t[j]), 4)] + [round(float(v), 2) for v in cp[j]] for j in range(len(t))]}
|
|
50
|
+
json.dump(out, open(P + "camera/cena-%s-ajustada.json" % cid, "w"))
|
|
51
|
+
print("ok", cid, len(t), "quadros; pin max %.1f" % abs(cp).max())
|
|
52
|
+
# --- cantos do Corner Pin para a margem real do comp interno (alvo em coordenadas da camada interna)
|
|
53
|
+
import cv2
|
|
54
|
+
MR = float(a[a.index("--margem-real") + 1]) if "--margem-real" in a else out["margem"]
|
|
55
|
+
MG = out["margem"]
|
|
56
|
+
CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
|
|
57
|
+
REAL = np.float32([[[-MR, -MR], [1080 + MR, -MR], [-MR, 1080 + MR], [1080 + MR, 1080 + MR]]])
|
|
58
|
+
cantos = []
|
|
59
|
+
for row in out["pin"]:
|
|
60
|
+
Pm = cv2.getPerspectiveTransform(CANT, (CANT + np.float32(row[1:]).reshape(4, 2)).astype(np.float32))
|
|
61
|
+
alvo = cv2.perspectiveTransform(REAL, Pm)[0] + MR
|
|
62
|
+
cantos.append([row[0]] + [round(float(v), 2) for v in alvo.ravel()])
|
|
63
|
+
out["margem_real"] = MR; out["cantos"] = cantos
|
|
64
|
+
json.dump(out, open(P + "camera/cena-%s-ajustada.json" % cid, "w"))
|
|
65
|
+
print("cantos para margem real", MR)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Mosaico do mundo com a câmera AJUSTADA (inclui rx, ry): python mosaico2.py <cam.json> <k> <saida.png> <x0 y0 x1 y1> [t0 t1] [--folha t,t,t saida_folha.png]
|
|
2
|
+
import sys, json, math, cv2, numpy as np
|
|
3
|
+
ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
|
|
4
|
+
a = sys.argv; cam = json.load(open(a[1])); k = float(a[2]); out = a[3]; x0, y0, x1, y1 = [float(v) for v in a[4:8]]
|
|
5
|
+
rest = [v for v in a[8:] if not v.startswith("--")]
|
|
6
|
+
t0 = float(rest[0]) if len(rest) > 0 else -1; t1 = float(rest[1]) if len(rest) > 1 else 99
|
|
7
|
+
F = 1500.0
|
|
8
|
+
def projetar(p, W):
|
|
9
|
+
z, mx, my, g, rx, ry = p; s = 2 ** z; r = math.radians(g)
|
|
10
|
+
X = W[:, 0] - mx; Y = W[:, 1] - my
|
|
11
|
+
x = s * (math.cos(r) * X - math.sin(r) * Y); y = s * (math.sin(r) * X + math.cos(r) * Y); zz = np.zeros_like(x)
|
|
12
|
+
cy, sy = math.cos(math.radians(ry)), math.sin(math.radians(ry)); x, zz = x * cy + zz * sy, -x * sy + zz * cy
|
|
13
|
+
cx, sx = math.cos(math.radians(rx)), math.sin(math.radians(rx)); y, zz = y * cx - zz * sx, y * sx + zz * cx
|
|
14
|
+
return np.column_stack([540 + F * x / (F + zz), 540 + F * y / (F + zz)])
|
|
15
|
+
MG = cam.get("margem", 270.0)
|
|
16
|
+
CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
|
|
17
|
+
def S_de(z, mx, my, g):
|
|
18
|
+
s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
|
|
19
|
+
T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
|
|
20
|
+
def Hq(q):
|
|
21
|
+
S = S_de(q["zoom"], q["mx"], q["my"], q["giro"])
|
|
22
|
+
if "cp" in q:
|
|
23
|
+
dst = (CANT + np.float32(q["cp"]).reshape(4, 2)).astype(np.float32); return cv2.getPerspectiveTransform(CANT, dst) @ S
|
|
24
|
+
p = [q[n] for n in ("zoom", "mx", "my", "giro", "rx", "ry")]
|
|
25
|
+
W = np.float32([[p[1] - 300, p[2] - 300], [p[1] + 300, p[2] - 300], [p[1] - 300, p[2] + 300], [p[1] + 300, p[2] + 300]])
|
|
26
|
+
return cv2.getPerspectiveTransform(W, projetar(p, W.astype(np.float64)).astype(np.float32))
|
|
27
|
+
Wd = int((x1 - x0) * k); Hd = int((y1 - y0) * k)
|
|
28
|
+
C = np.array([[k, 0, -x0 * k], [0, k, -y0 * k], [0, 0, 1.0]]) # mundo -> canvas
|
|
29
|
+
canvas = np.zeros((Hd, Wd, 3), np.uint8); qual = np.full((Hd, Wd), -1e9, np.float32)
|
|
30
|
+
cap = cv2.VideoCapture(ORIG); Q = {}
|
|
31
|
+
qs = cam["quadros"]
|
|
32
|
+
for i, q in enumerate(qs):
|
|
33
|
+
t = q["t"]
|
|
34
|
+
if t < t0 - 1e-6 or t > t1 + 1e-6: continue
|
|
35
|
+
f = int(round(t * 24)); cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, fr = cap.read()
|
|
36
|
+
if not ok: continue
|
|
37
|
+
H = Hq(q) # mundo -> tela
|
|
38
|
+
T = H @ np.linalg.inv(C) # canvas -> tela
|
|
39
|
+
w = cv2.warpPerspective(fr, T, (Wd, Hd), flags=cv2.INTER_LINEAR | cv2.WARP_INVERSE_MAP)
|
|
40
|
+
m = cv2.warpPerspective(np.full(fr.shape[:2], 255, np.uint8), T, (Wd, Hd), flags=cv2.INTER_NEAREST | cv2.WARP_INVERSE_MAP) > 0
|
|
41
|
+
m = cv2.erode(m.astype(np.uint8), np.ones((3, 3), np.uint8)) > 0
|
|
42
|
+
nx = qs[min(i + 1, len(qs) - 1)]
|
|
43
|
+
spd = math.hypot(nx["mx"] - q["mx"], nx["my"] - q["my"]) * 2 ** q["zoom"] + abs(nx["zoom"] - q["zoom"]) * 370 + abs(nx.get("rx", 0) - q.get("rx", 0)) * 9
|
|
44
|
+
qq = 2 ** q["zoom"] / (1 + spd / 6) if q["met"] not in ("buraco", "json") else -1e6
|
|
45
|
+
u = m & (qq > qual); canvas[u] = w[u]; qual[u] = qq
|
|
46
|
+
cv2.imwrite(out, canvas)
|
|
47
|
+
g = canvas.copy()
|
|
48
|
+
for gx in range(int(math.ceil(x0 / 100)) * 100, int(x1), 100):
|
|
49
|
+
X = int((gx - x0) * k); cv2.line(g, (X, 0), (X, Hd), (0, 0, 255) if gx % 500 == 0 else (0, 128, 255), 1); cv2.putText(g, str(gx), (X + 2, 14), 0, 0.4, (0, 0, 255), 1)
|
|
50
|
+
for gy in range(int(math.ceil(y0 / 100)) * 100, int(y1), 100):
|
|
51
|
+
Y = int((gy - y0) * k); cv2.line(g, (0, Y), (Wd, Y), (0, 0, 255) if gy % 500 == 0 else (0, 128, 255), 1); cv2.putText(g, str(gy), (2, Y - 2), 0, 0.4, (0, 0, 255), 1)
|
|
52
|
+
cv2.imwrite(out.replace(".png", "-grade.png"), g)
|
|
53
|
+
if "--folha" in a:
|
|
54
|
+
i = a.index("--folha"); tt = [float(v) for v in a[i + 1].split(",")]; fo = a[i + 2]; tiles = []
|
|
55
|
+
for t in tt:
|
|
56
|
+
q = min(qs, key=lambda x: abs(x["t"] - t)); f = int(round(q["t"] * 24)); cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, fr = cap.read()
|
|
57
|
+
pr = cv2.warpPerspective(canvas, Hq(q) @ np.linalg.inv(C), (1080, 1080))
|
|
58
|
+
row = np.hstack([cv2.resize(fr, (360, 360)), cv2.resize(pr, (360, 360)), cv2.resize(cv2.absdiff(fr, pr), (360, 360))])
|
|
59
|
+
cv2.putText(row, "%.3f %s" % (q["t"], q["met"]), (4, 16), 0, 0.5, (0, 255, 255), 1); tiles.append(row)
|
|
60
|
+
cv2.imwrite(fo, np.vstack(tiles))
|
|
61
|
+
print("ok", out, Wd, Hd)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Lyric edit em interfaces · 01 · Gramática e roteiro
|
|
2
|
+
|
|
3
|
+
> Data: 2026-10-17 · Tipo: tutorial · Status: vigente
|
|
4
|
+
|
|
5
|
+
**Resumo:** o que faz o estilo funcionar (medido nos 3 edits do @subzmotion), como escolher a música, como transformar cada frase numa interface e como montar o roteiro de cenas com os campos de letra. Volta para o [índice](../tutorial-lyric-edit-interfaces.md).
|
|
6
|
+
|
|
7
|
+
## 1. A gramática (o que não pode faltar)
|
|
8
|
+
|
|
9
|
+
1. **A palavra vira lugar.** Cada frase é digitada no campo que faz sentido para ela. Interface reconhecível em todas as cenas.
|
|
10
|
+
2. **Digitação no tempo da voz.** Letra a letra com cursor "|" (1–3 quadros por letra), começando até 0,35 s antes da palavra cantada. Em títulos e balões, a palavra também pode cair desfocada no lugar em 3–6 quadros. Interface de letra (Spotify) = karaokê, palavra cantada acesa.
|
|
11
|
+
3. **Cena dentro de cena.** A cena anterior continua tocando dentro de um elemento pequeno da seguinte (avatar, ícone de app, foto de jornal, miniatura de janela, marcador de mapa). A máscara morfa (retângulo → círculo ou quadrado arredondado) enquanto a câmera recua. Nada de corte seco.
|
|
12
|
+
4. **Câmera que nunca para**, em cadeia de nulls (parte 02). Só o último meio segundo fica parado.
|
|
13
|
+
5. **3D sem câmera:** jornal, celular, livro como camadas 3D deitadas, vistas pela câmera padrão da comp.
|
|
14
|
+
6. **Borrão de movimento** ligado, obturador ~90° (rastro ≈ ¼ do deslocamento do quadro).
|
|
15
|
+
7. **Formato:** 1080×1080 a 24 qps, música da biblioteca do Instagram (ou o mix), sem efeito sonoro além da música no original. No nosso: card do Spotify no fim e abertura do celular (parte 05).
|
|
16
|
+
|
|
17
|
+
Por que viraliza (inferência da pesquisa): música conhecida + interface que todo mundo usa = reconhecimento imediato a cada frase; take único com câmera viva; a timeline postada no dia seguinte vira prova de trabalho manual.
|
|
18
|
+
|
|
19
|
+
## 2. Música e trecho
|
|
20
|
+
|
|
21
|
+
- Trecho de 30–40 s, de preferência o refrão ou a parte mais conhecida; 5–8 versos curtos funcionam melhor que frases longas.
|
|
22
|
+
- Áudio: o arquivo da música (ou o do Reel de referência, só como referência interna) em `audio/musica.wav`.
|
|
23
|
+
- **Letra:** o autor copia a letra inteira (Google, Spotify) e cola em `ae/LETRA-COMPLETA.txt`. Pode ser a música inteira: o script acha o trecho. O Claude não escreve nem copia letra de música; explique isso em uma linha se ele pedir.
|
|
24
|
+
- **Idioma:** a interface acompanha a música (música em inglês → interface em inglês). Nada de traduzir a letra.
|
|
25
|
+
|
|
26
|
+
## 3. Frase → interface (mapa que já funcionou)
|
|
27
|
+
|
|
28
|
+
| A frase fala de… | Interface | Exemplo do On My Way |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| tempo, compromisso, "quando" | Google Agenda: título de evento, cartão de detalhes | 0–2,6 s |
|
|
31
|
+
| procurar, querer, perguntar | busca do Google, Docs, painel de conhecimento | 2,9–4,4 s; 16,7–19 s; 22,4–24,9 s |
|
|
32
|
+
| mundo, lugar grande | globo com texto curvo | 4,4–5,0 s |
|
|
33
|
+
| trocar de assunto | alt-tab do Windows / app switcher | 5,0–6,0 s |
|
|
34
|
+
| ouvir, cantar | Spotify em tela cheia de letra (karaokê) | 6,0–8,3 s |
|
|
35
|
+
| falar de alguém, status | notas do Instagram (balão em cima do avatar) | 8,3–11 s |
|
|
36
|
+
| afirmação forte, manchete | jornal 3D sobre mesa | 11–16,7 s; 24,9–28,2 s |
|
|
37
|
+
| caminho, chegar, "onde" | mapa de jogo (etiqueta-seta) ou Google Maps (pino, busca, avatar andando) | 19–22,3 s; 28,2–33,9 s |
|
|
38
|
+
| mensagem para alguém | WhatsApp, DM, iMessage | Rockabye e Love Story |
|
|
39
|
+
|
|
40
|
+
Outras do autor: Apple Maps, feed do Instagram, polaroides na mesa, livro que fecha, post-it na parede, widgets do iOS ("Hard Times").
|
|
41
|
+
|
|
42
|
+
## 4. Roteiro de cenas (como montar)
|
|
43
|
+
|
|
44
|
+
1. `python -P audio/tempos_da_voz.py audio/musica.wav`: tempo de cada palavra cantada, por verso, sem guardar a letra (`audio/musica-tempos.txt`). É a régua do roteiro. (`ae/letra_automatica.py --teste` confere o encaixe dos campos sem texto nenhum.)
|
|
45
|
+
2. Divida em cenas-mundo de 1,5–6 s. Cada cena tem: tempo de início/fim, interface, quais palavras são digitadas em quais campos, e **o buraco** (elemento onde a cena anterior entra) ou a janela (onde a próxima abre, se for avanço).
|
|
46
|
+
3. Escreva o `ROTEIRO-CENAS.md` do projeto (molde: o do projeto-modelo) e a lista de campos em `ae/letra.json`: um slot por campo, com `onde`, `palavras` (quantas palavras o campo mostra) e `canta_em` (os tempos). O `letra_automatica.py` usa janelas de tempo por campo (constante `JANELAS`) e regras de caixa alta / inicial maiúscula (`CAIXA_ALTA`, `INICIAL_MAIUSCULA`): ajuste para a música nova.
|
|
47
|
+
4. Depois que o autor colar a letra: `python -P ae/letra_automatica.py`. Ele imprime "campo · palavras (colado / original)"; diferença de ±1 em algum campo quase sempre é uma palavra que o Whisper ouviu diferente e caiu na janela vizinha: mova a divisa entre as duas janelas (foi o caso de I2/J1 no On My Way).
|
|
48
|
+
5. Pontuação e grafia vêm do texto colado (apóstrofo reto, grafia americana). Se ele quiser idêntico ao original, ele corrige no próprio json.
|
|
49
|
+
|
|
50
|
+
## 5. Personagens, imagens e fontes
|
|
51
|
+
|
|
52
|
+
- **Pessoas:** personagens nossos pelo GPT Image 2 (`python -P 08-integracoes/codex-imagem.py "prompt em inglês" saida.png`, ~50 s cada, até 3 em paralelo). Gere os que aparecem em várias cenas uma vez só em `assets/comum/` (avatar, papel de parede) e reutilize. Nada de rosto real conhecido nem marca de jogo: vira "Studio Quadro" e afins.
|
|
53
|
+
- **Fotos e texturas:** mesa de madeira vista de cima, mapa de satélite estilo jogo, céu, foto antiga P&B, renders abstratos para painéis: tudo gerado.
|
|
54
|
+
- **Ícones de apps:** oficiais do simpleicons/Wikimedia.
|
|
55
|
+
- **Fontes que usamos:** Google Sans Flex (Google), Figtree (Spotify), Roboto (Instagram/Maps), Segoe UI (Windows), Noto Serif Display ExtraCondensed (manchetes), Poppins SemiBold (texto curvo do globo). Fonte nova vai em `C:\Program Files\Common Files\Adobe\Fonts` e só aparece depois de reiniciar a cópia do AE.
|