primocode 10.13.0 → 10.14.1

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.
Files changed (196) hide show
  1. package/README.md +66 -12
  2. package/bin/primocode.js +148 -8
  3. package/lib/act.js +56 -3
  4. package/lib/catalogo.js +12 -1
  5. package/lib/config.js +1 -0
  6. package/lib/estudio/cota.js +157 -0
  7. package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
  8. package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
  9. package/lib/estudio/dados/projetos/clube-agent.json +120 -0
  10. package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
  11. package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
  12. package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
  13. package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
  14. package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
  15. package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
  16. package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
  17. package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
  18. package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
  19. package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
  20. package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
  21. package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
  22. package/lib/estudio/dados/projetos/clube-clube.json +108 -0
  23. package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
  24. package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
  25. package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
  26. package/lib/estudio/dados/projetos/clube-dub.json +119 -0
  27. package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
  28. package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
  29. package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
  30. package/lib/estudio/dados/projetos/clube-frame.json +108 -0
  31. package/lib/estudio/dados/projetos/clube-glass.json +118 -0
  32. package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
  33. package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
  34. package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
  35. package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
  36. package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
  37. package/lib/estudio/dados/projetos/clube-morph.json +119 -0
  38. package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
  39. package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
  40. package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
  41. package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
  42. package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
  43. package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
  44. package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
  45. package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
  46. package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
  47. package/lib/estudio/dados/projetos/clube-promo.json +114 -0
  48. package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
  49. package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
  50. package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
  51. package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
  52. package/lib/estudio/dados/projetos/clube-robo.json +125 -0
  53. package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
  54. package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
  55. package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
  56. package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
  57. package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
  58. package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
  59. package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
  60. package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
  61. package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
  62. package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
  63. package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
  64. package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
  65. package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
  66. package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
  67. package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
  68. package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
  69. package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
  70. package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
  71. package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
  72. package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
  73. package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
  74. package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
  75. package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
  76. package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
  77. package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
  78. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
  79. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
  80. package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
  81. package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
  82. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
  83. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
  84. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
  85. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
  86. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
  87. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
  88. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
  89. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
  90. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
  91. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
  92. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
  93. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
  94. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
  95. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
  96. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
  97. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
  98. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
  99. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
  100. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
  101. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
  102. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
  103. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
  104. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
  105. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
  106. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
  107. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
  108. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
  109. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
  110. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
  111. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
  112. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
  113. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
  114. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
  115. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
  116. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
  117. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
  118. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
  119. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
  120. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
  121. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
  122. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
  123. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
  124. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
  125. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
  126. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
  127. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
  128. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
  129. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
  130. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
  131. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
  132. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
  133. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
  134. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
  135. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
  136. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
  137. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
  138. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
  139. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
  140. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
  141. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
  142. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
  143. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
  144. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
  145. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
  146. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
  147. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
  148. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
  149. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
  150. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
  151. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
  152. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
  153. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
  154. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
  155. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
  156. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
  157. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
  158. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
  159. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
  160. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
  161. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
  162. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
  163. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
  164. package/lib/estudio/editor.js +290 -0
  165. package/lib/estudio/effectcraft.js +152 -0
  166. package/lib/estudio/exportar.js +181 -0
  167. package/lib/estudio/index.js +97 -2
  168. package/lib/estudio/kit-ae/base.jsx +1146 -0
  169. package/lib/estudio/kit-ae/cenas.jsx +395 -0
  170. package/lib/estudio/kit-ae/primitivas.jsx +911 -0
  171. package/lib/estudio/maquina-diretor.js +393 -0
  172. package/lib/estudio/maquina-elementos.js +134 -0
  173. package/lib/estudio/maquina-layout.js +198 -0
  174. package/lib/estudio/maquina-som.js +478 -0
  175. package/lib/estudio/maquina-trilha.js +160 -0
  176. package/lib/estudio/maquina-voz.js +429 -0
  177. package/lib/estudio/maquina.js +516 -0
  178. package/lib/estudio/projetos.js +72 -0
  179. package/lib/headless.js +71 -0
  180. package/lib/hooks.js +72 -0
  181. package/lib/limites.js +100 -0
  182. package/lib/local/cerebro.json +398 -12
  183. package/lib/local/entrada.js +46 -2
  184. package/lib/marca-kit.js +312 -0
  185. package/lib/mcp.js +286 -6
  186. package/lib/perigo.js +31 -3
  187. package/lib/planos.js +49 -19
  188. package/lib/preparar.js +111 -0
  189. package/lib/retomar.js +55 -0
  190. package/lib/sandbox.js +197 -0
  191. package/lib/skills.js +3 -1
  192. package/lib/tools.js +13 -4
  193. package/lib/ui.js +1 -1
  194. package/package.json +4 -2
  195. package/skills/after-effects/SKILL.md +52 -0
  196. 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)