VICCENTE's picture
Update app.py
968be95 verified
Raw
History Blame Contribute Delete
8.79 kB
import gradio as gr
from PIL import Image, ImageFilter
import io, os, re
import numpy as np
import vtracer
def remove_background(arr):
"""Remove fundo detectando cor dos cantos."""
h, w = arr.shape[:2]
corners = []
for cy, cx in [(0,0),(0,w-1),(h-1,0),(h-1,w-1),(h//2,0),(h//2,w-1),(0,w//2),(h-1,w//2)]:
corners.append(arr[cy,cx,:3])
bg_color = np.median(corners, axis=0)
bg_dist = np.sqrt(((arr[:,:,:3].astype(np.float32)-bg_color)**2).sum(axis=2))
arr[:,:,3] = np.where(bg_dist < 30, 0, arr[:,:,3])
return arr, bg_color
def get_dominant_colors(arr, n_colors):
"""Extrai cores dominantes via quantizacao."""
opaque = arr[:,:,3] > 128
pixels = arr[:,:,:3][opaque].astype(np.uint8)
if len(pixels) < n_colors:
return []
sample = Image.fromarray(pixels.reshape(-1,1,3), 'RGB')
q = sample.quantize(colors=int(n_colors), method=Image.Quantize.MEDIANCUT)
pal = q.getpalette()
colors = []
for i in range(int(n_colors)):
c = np.array([pal[i*3],pal[i*3+1],pal[i*3+2]], dtype=np.float32)
lum = 0.299*c[0]/255 + 0.587*c[1]/255 + 0.114*c[2]/255
if lum > 0.05: # ignora preto puro
colors.append(c)
return colors
def snap_to_dominant(arr, dominant, threshold=35):
"""Forca cada pixel para a cor dominante mais proxima."""
if not dominant:
return arr
h, w = arr.shape[:2]
dom_arr = np.array(dominant, dtype=np.float32)
flat = arr[:,:,:3].reshape(-1,3).astype(np.float32)
opaque_flat = arr[:,:,3].reshape(-1) > 128
dists = np.sqrt(((flat[:,np.newaxis,:]-dom_arr[np.newaxis,:,:])**2).sum(axis=2))
best_idx = np.argmin(dists, axis=1)
best_dist = dists[np.arange(len(flat)), best_idx]
best_color = dom_arr[best_idx]
# Snap agressivo: todos os pixels opacos vao para a cor dominante
snap = opaque_flat # snap em TODOS os pixels opacos
flat[snap] = best_color[snap]
arr[:,:,:3] = flat.reshape(h,w,3)
return arr
def clean_borders(arr):
"""Remove pixels de borda com cor misturada usando erosao + expansao."""
try:
import cv2
alpha = arr[:,:,3].astype(np.uint8)
k3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
k5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
# Erode para remover pixels de borda misturados
alpha_clean = cv2.erode(alpha, k3, iterations=2)
# Fecha para recuperar forma interna
alpha_clean = cv2.morphologyEx(alpha_clean, cv2.MORPH_CLOSE, k5, iterations=2)
arr[:,:,3] = np.where(alpha_clean > 127, 255, 0)
except ImportError:
arr[:,:,3] = np.where(arr[:,:,3] > 128, 255, 0)
return arr
def preprocess(pil_img, shadow_strength, n_colors, upscale):
img = pil_img.convert('RGBA')
w, h = img.size
MAX, MIN = 1000, 400
s = 1.0
if max(w,h) > MAX: s = MAX/max(w,h)
elif max(w,h) < MIN: s = MIN/max(w,h)
if s != 1.0:
img = img.resize((int(w*s), int(h*s)), Image.LANCZOS)
arr = np.array(img, dtype=np.float32)
# 1. Remove fundo automaticamente
arr, bg_color = remove_background(arr)
r,g,b,a = arr[:,:,0],arr[:,:,1],arr[:,:,2],arr[:,:,3]
opaque = a > 128
# 2. Remove sombras por saturacao
if shadow_strength > 0:
cmax = np.maximum(np.maximum(r,g),b)
cmin = np.minimum(np.minimum(r,g),b)
sat = np.where(cmax>0,(cmax-cmin)/cmax,0)
lum = (0.299*r+0.587*g+0.114*b)/255
shadow = (sat < shadow_strength*0.35) & opaque
for ch in range(3):
arr[:,:,ch] = np.where(shadow, np.where(lum>0.5,255,0), arr[:,:,ch])
# 3. Alpha threshold
arr[:,:,3] = np.where(arr[:,:,3]>128, 255, 0)
# 4. Detecta cores dominantes
dominant = get_dominant_colors(arr.astype(np.uint8), int(n_colors))
# 5. Snap AGRESSIVO para cor dominante — elimina todas as variações
arr = snap_to_dominant(arr, dominant, threshold=40)
# 6. Limpa bordas com erosao
arr = clean_borders(arr)
img = Image.fromarray(arr.astype(np.uint8), 'RGBA')
# 7. Upscale
if upscale > 1:
nw, nh = int(img.size[0]*upscale), int(img.size[1]*upscale)
img = img.resize((nw, nh), Image.LANCZOS)
arr2 = np.array(img, dtype=np.float32)
try:
import cv2
alpha2 = arr2[:,:,3].astype(np.uint8)
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
alpha2 = cv2.erode(alpha2, k, iterations=1)
alpha2 = cv2.dilate(alpha2, k, iterations=1)
arr2[:,:,3] = np.where(alpha2>128, 255, 0)
except ImportError:
arr2[:,:,3] = np.where(arr2[:,:,3]>128, 255, 0)
# Snap novamente após upscale para eliminar pixels intermediários
arr2 = snap_to_dominant(arr2, dominant, threshold=40)
img = Image.fromarray(arr2.astype(np.uint8), 'RGBA')
return img
def vectorize_image(
input_image, shadow_strength, n_colors, upscale,
filter_speckle, color_precision, corner_threshold,
length_threshold, path_precision,
):
try:
if input_image is None:
raise gr.Error("Nenhuma imagem enviada")
pil_img = input_image.convert('RGBA')
w, h = pil_img.size
print(f"Entrada: {w}x{h}")
if w>2000 or h>2000:
raise gr.Error("Imagem muito grande. Maximo: 2000x2000 px.")
pil_img = preprocess(pil_img, shadow_strength, n_colors, float(upscale))
pw, ph = pil_img.size
print(f"Processada: {pw}x{ph}")
buf = io.BytesIO()
pil_img.save(buf, format='PNG')
svg_str = vtracer.convert_raw_image_to_svg(
buf.getvalue(),
img_format='png',
colormode='color',
hierarchical='stacked',
mode='spline',
filter_speckle=int(filter_speckle),
color_precision=int(color_precision),
layer_difference=16,
corner_threshold=int(corner_threshold),
length_threshold=float(length_threshold),
max_iterations=10,
splice_threshold=45,
path_precision=int(path_precision),
)
if 'viewBox' not in svg_str:
svg_str = svg_str.replace('<svg ', f'<svg viewBox="0 0 {pw} {ph}" ', 1)
svg_str = re.sub(r'(<svg[^>]*?)width="[^"]*"', r'\1width="100%"', svg_str)
svg_str = re.sub(r'(<svg[^>]*?)height="[^"]*"', r'\1height="100%"', svg_str)
svg_path = f"/tmp/result_{os.getpid()}.svg"
with open(svg_path,'w',encoding='utf-8') as f:
f.write(svg_str)
png_path = None
try:
import cairosvg
png_bytes = cairosvg.svg2png(bytestring=svg_str.encode(), scale=1.0)
png_path = f"/tmp/result_{os.getpid()}.png"
with open(png_path,'wb') as f: f.write(png_bytes)
except Exception as e:
print(f"cairosvg: {e}")
print(f"SVG: {len(svg_str)} chars")
return svg_path, png_path, svg_str
except gr.Error:
raise
except Exception as e:
raise gr.Error(str(e))
with gr.Blocks(title="Iluminados Vectorizer") as demo:
gr.Markdown("## Iluminados Vectorizer - VTracer API")
with gr.Row():
with gr.Column():
inp = gr.Image(label="Imagem de entrada", type="pil")
with gr.Accordion("Pre-processamento", open=True):
shadow_strength = gr.Slider(0,1.0,value=0.6, step=0.05,label="Remover sombras")
n_colors = gr.Slider(2,32, value=16, step=1, label="Numero de cores dominantes")
upscale = gr.Slider(1,4, value=2, step=0.5, label="Upscale")
with gr.Accordion("Vetorizacao", open=False):
filter_speckle = gr.Slider(1,100,value=4, step=1, label="Filtro de fragmentos")
color_precision = gr.Slider(1,8, value=6, step=1, label="Precisao de cor")
corner_threshold = gr.Slider(1,180,value=60, step=1, label="Limiar de canto")
length_threshold = gr.Slider(0.5,10,value=4.0,step=0.5,label="Comprimento minimo")
path_precision = gr.Slider(1,10, value=8, step=1, label="Precisao do caminho")
btn = gr.Button("Vetorizar", variant="primary")
with gr.Column():
out_svg = gr.File(label="SVG para download")
out_png = gr.File(label="PNG vetorizado")
out_code = gr.Code(label="SVG codigo", language="html", lines=10)
btn.click(
fn=vectorize_image,
inputs=[inp, shadow_strength, n_colors, upscale,
filter_speckle, color_precision,
corner_threshold, length_threshold, path_precision],
outputs=[out_svg, out_png, out_code]
)
demo.launch()