patocolher commited on
Commit
f3da6ec
·
verified ·
1 Parent(s): 38c9347

Upload 4 files

Browse files
Files changed (1) hide show
  1. app.py +56 -31
app.py CHANGED
@@ -4,7 +4,7 @@ import os
4
  import re
5
  import json
6
  import shutil
7
- from PIL import Image, ImageDraw
8
 
9
 
10
  # ══════════════════════════════════════════════════════════════════
@@ -37,43 +37,57 @@ def contar_frames(path):
37
  # Logo — preparação, posição e preview ao vivo
38
  # ══════════════════════════════════════════════════════════════════
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def preparar_logo(logo_path, vid_w, tamanho_pct, transparencia_pct):
41
  """
42
  Abre o logo, redimensiona para tamanho_pct% da largura do vídeo,
43
- aplica transparencia_pct% de transparência (0 = opaco, 100 = invisível)
44
- e LIMPA o RGB das áreas totalmente transparentes para eliminar
45
- halos coloridos (verde / magenta) no overlay YUV do FFmpeg.
46
- Retorna (caminho, largura, altura).
47
  """
48
  img = Image.open(logo_path)
49
  if img.mode != "RGBA":
50
  img = img.convert("RGBA")
51
 
52
- # Redimensiona proporcional à largura alvo
53
  alvo_w = max(1, int(vid_w * tamanho_pct / 100))
54
  ratio = alvo_w / img.width
55
  alvo_h = max(1, int(img.height * ratio))
56
  img = img.resize((alvo_w, alvo_h), Image.LANCZOS)
57
 
58
- # Separa canais
59
  r_ch, g_ch, b_ch, a_ch = img.split()
60
 
61
- # Aplica transparência global (straight alpha)
62
  if transparencia_pct > 0:
63
  fator = 1.0 - (transparencia_pct / 100.0)
64
  a_ch = a_ch.point(lambda x: int(x * fator))
65
 
66
- # Zera RGB nas áreas TOTALMENTE transparentes (alpha == 0)
67
- # Isso elimina o tingimento verde/magenta que aparece quando o
68
- # overlay converte para yuv420p com chroma subsampling.
69
- mask_transp = a_ch.point(lambda v: 0 if v == 0 else 255)
70
  empty = Image.new("L", img.size, 0)
71
- r_ch = Image.composite(r_ch, empty, mask_transp)
72
- g_ch = Image.composite(g_ch, empty, mask_transp)
73
- b_ch = Image.composite(b_ch, empty, mask_transp)
74
 
75
  img = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
76
 
 
 
 
77
  temp_path = "/tmp/logo_overlay.png"
78
  img.save(temp_path, "PNG", optimize=False)
79
  return temp_path, alvo_w, alvo_h
@@ -128,16 +142,20 @@ def gerar_preview_logo(logo_file, logo_posicao, logo_margem,
128
  alvo_h = max(1, int(logo.height * ratio))
129
  logo = logo.resize((alvo_w, alvo_h), Image.LANCZOS)
130
 
131
- # mesma lógica de limpeza + transparência usada no render final
 
 
 
 
132
  r_ch, g_ch, b_ch, a_ch = logo.split()
133
  if logo_transparencia > 0:
134
  fator = 1.0 - (logo_transparencia / 100.0)
135
  a_ch = a_ch.point(lambda px: int(px * fator))
136
- mask_transp = a_ch.point(lambda v: 0 if v == 0 else 255)
137
  empty = Image.new("L", logo.size, 0)
138
- r_ch = Image.composite(r_ch, empty, mask_transp)
139
- g_ch = Image.composite(g_ch, empty, mask_transp)
140
- b_ch = Image.composite(b_ch, empty, mask_transp)
141
  logo = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
142
 
143
  escala = PREVIEW_W / 1920
@@ -315,22 +333,29 @@ def reencode_video(
315
  overlay_pos = calcular_posicao_logo(
316
  logo_posicao, logo_margem, logo_offset_x, logo_offset_y
317
  )
318
- # Correção do "fundo verde semi-transparente":
319
- # - logo vai para yuva420p (preserva alpha)
320
- # - overlay usa format=auto e alpha=straight (PNG é straight alpha)
321
- # - saída final força yuv420p para compatibilidade máxima
 
 
 
 
322
  if vf_parts:
323
  fc = (
324
- f"[0:v]{','.join(vf_parts)}[base];"
325
- f"[1:v]format=yuva420p[logo];"
326
- f"[base][logo]overlay={overlay_pos}:format=auto:alpha=straight,"
327
- f"format=yuv420p[vout]"
 
328
  )
329
  else:
330
  fc = (
331
- f"[1:v]format=yuva420p[logo];"
332
- f"[0:v][logo]overlay={overlay_pos}:format=auto:alpha=straight,"
333
- f"format=yuv420p[vout]"
 
 
334
  )
335
  cmd += ["-filter_complex", fc, "-map", "[vout]"]
336
  if has_audio:
 
4
  import re
5
  import json
6
  import shutil
7
+ from PIL import Image, ImageDraw, ImageChops
8
 
9
 
10
  # ══════════════════════════════════════════════════════════════════
 
37
  # Logo — preparação, posição e preview ao vivo
38
  # ══════════════════════════════════════════════════════════════════
39
 
40
+ def _premultiplicar_alpha(img_rgba):
41
+ """
42
+ Pré-multiplica os canais RGB pelo canal alpha.
43
+ Após isso, pixels semi-transparentes têm RGB já escalado por alpha,
44
+ o que ELIMINA o halo verde/magenta quando o overlay do FFmpeg
45
+ compõe em yuv420p — não existe mais cor "solta" nas bordas.
46
+ """
47
+ r_ch, g_ch, b_ch, a_ch = img_rgba.split()
48
+ r_pm = ImageChops.multiply(r_ch, a_ch) # ImageChops.multiply(A,B) = A*B/255
49
+ g_pm = ImageChops.multiply(g_ch, a_ch)
50
+ b_pm = ImageChops.multiply(b_ch, a_ch)
51
+ return Image.merge("RGBA", (r_pm, g_pm, b_pm, a_ch))
52
+
53
+
54
  def preparar_logo(logo_path, vid_w, tamanho_pct, transparencia_pct):
55
  """
56
  Abre o logo, redimensiona para tamanho_pct% da largura do vídeo,
57
+ aplica transparencia_pct% (0 = opaco, 100 = invisível),
58
+ ZERA RGB onde alpha == 0 e PRÉ-MULTIPLICA alpha em todo o resto.
59
+ O resultado é um PNG sem nenhum pixel colorido "solto" — o overlay
60
+ do FFmpeg pode usar alpha=premultiplied e não gera halo verde.
61
  """
62
  img = Image.open(logo_path)
63
  if img.mode != "RGBA":
64
  img = img.convert("RGBA")
65
 
66
+ # Redimensiona para a largura alvo no vídeo
67
  alvo_w = max(1, int(vid_w * tamanho_pct / 100))
68
  ratio = alvo_w / img.width
69
  alvo_h = max(1, int(img.height * ratio))
70
  img = img.resize((alvo_w, alvo_h), Image.LANCZOS)
71
 
 
72
  r_ch, g_ch, b_ch, a_ch = img.split()
73
 
74
+ # 1) Aplica transparência global (reduz o canal alpha proporcionalmente)
75
  if transparencia_pct > 0:
76
  fator = 1.0 - (transparencia_pct / 100.0)
77
  a_ch = a_ch.point(lambda x: int(x * fator))
78
 
79
+ # 2) Zera RGB onde alpha == 0 (mata matte verde/preto que venha no PNG)
80
+ mask_keep = a_ch.point(lambda v: 0 if v == 0 else 255)
 
 
81
  empty = Image.new("L", img.size, 0)
82
+ r_ch = Image.composite(r_ch, empty, mask_keep)
83
+ g_ch = Image.composite(g_ch, empty, mask_keep)
84
+ b_ch = Image.composite(b_ch, empty, mask_keep)
85
 
86
  img = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
87
 
88
+ # 3) Pré-multiplica alpha — fim do halo verde
89
+ img = _premultiplicar_alpha(img)
90
+
91
  temp_path = "/tmp/logo_overlay.png"
92
  img.save(temp_path, "PNG", optimize=False)
93
  return temp_path, alvo_w, alvo_h
 
142
  alvo_h = max(1, int(logo.height * ratio))
143
  logo = logo.resize((alvo_w, alvo_h), Image.LANCZOS)
144
 
145
+ # Mesma lógica de render final:
146
+ # 1) aplica transparência no alpha
147
+ # 2) zera RGB onde alpha==0 (mata matte verde)
148
+ # 3) NÃO pré-multiplica aqui, pois o alpha_composite do Pillow
149
+ # espera straight alpha e compõe em RGB — já sem halo verde.
150
  r_ch, g_ch, b_ch, a_ch = logo.split()
151
  if logo_transparencia > 0:
152
  fator = 1.0 - (logo_transparencia / 100.0)
153
  a_ch = a_ch.point(lambda px: int(px * fator))
154
+ mask_keep = a_ch.point(lambda v: 0 if v == 0 else 255)
155
  empty = Image.new("L", logo.size, 0)
156
+ r_ch = Image.composite(r_ch, empty, mask_keep)
157
+ g_ch = Image.composite(g_ch, empty, mask_keep)
158
+ b_ch = Image.composite(b_ch, empty, mask_keep)
159
  logo = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
160
 
161
  escala = PREVIEW_W / 1920
 
333
  overlay_pos = calcular_posicao_logo(
334
  logo_posicao, logo_margem, logo_offset_x, logo_offset_y
335
  )
336
+ # Correção do "fundo verde" ao redor da logo:
337
+ # 1) O PNG saiu do Pillow com alpha pré-multiplicado
338
+ # (RGB zerado onde alpha==0, RGB = cor*alpha nos demais).
339
+ # 2) Base do vídeo é convertida para RGBA antes do overlay,
340
+ # então a composição acontece em RGB puro (sem YUV 4:2:0
341
+ # introduzir halo verde/magenta nas bordas).
342
+ # 3) overlay usa alpha=premultiplied (combina com o PNG).
343
+ # 4) format=yuv420p só no fim da cadeia, para o encoder.
344
  if vf_parts:
345
  fc = (
346
+ f"[0:v]{','.join(vf_parts)},format=rgba[base];"
347
+ f"[1:v]format=rgba[logo];"
348
+ f"[base][logo]overlay={overlay_pos}:format=auto:"
349
+ f"alpha=premultiplied[composed];"
350
+ f"[composed]format=yuv420p[vout]"
351
  )
352
  else:
353
  fc = (
354
+ f"[0:v]format=rgba[base];"
355
+ f"[1:v]format=rgba[logo];"
356
+ f"[base][logo]overlay={overlay_pos}:format=auto:"
357
+ f"alpha=premultiplied[composed];"
358
+ f"[composed]format=yuv420p[vout]"
359
  )
360
  cmd += ["-filter_complex", fc, "-map", "[vout]"]
361
  if has_audio: