File size: 8,787 Bytes
3ed6f6b
7410516
59126b4
e440a1c
9ffe019
 
968be95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f6ffe0
33408a5
dce845b
59126b4
 
89e3f36
dce845b
 
 
 
d55c0bc
dce845b
968be95
 
 
 
d55c0bc
dce845b
d55c0bc
968be95
dce845b
d55c0bc
 
 
 
dce845b
d55c0bc
dce845b
 
968be95
 
 
 
 
 
 
 
 
 
 
3f6ffe0
9eabd6b
 
968be95
9eabd6b
 
 
924fa20
 
 
 
 
 
 
968be95
924fa20
968be95
924fa20
968be95
 
924fa20
9eabd6b
 
dce845b
33408a5
9ffe019
968be95
 
 
9ffe019
 
 
 
dce845b
9ffe019
dce845b
59126b4
7410516
 
8c05269
3f6ffe0
dce845b
59126b4
 
 
 
 
 
82b6ed0
59126b4
 
 
 
 
 
 
 
 
c15a98d
 
59126b4
 
 
 
 
 
 
4b3cd48
9ffe019
7410516
9ffe019
 
75233b5
9ffe019
 
dce845b
9ffe019
7410516
75233b5
02ebcc8
9ffe019
8c05269
9ffe019
 
 
 
 
 
 
dce845b
9ffe019
59126b4
dce845b
9ffe019
 
 
dce845b
01e6de9
7410516
968be95
 
dce845b
59126b4
 
c15a98d
 
 
 
dce845b
9ffe019
dce845b
9ffe019
d55c0bc
02ebcc8
75233b5
9ffe019
 
 
3f6ffe0
59126b4
 
dce845b
9ffe019
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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()