VICCENTE commited on
Commit
d55c0bc
·
verified ·
1 Parent(s): 8c05269

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -187
app.py CHANGED
@@ -5,190 +5,132 @@ import numpy as np
5
  import cv2
6
  import vtracer
7
 
8
- def kmeans_segment(arr_rgba, n_colors, shadow_strength):
9
- """Segmenta imagem em regioes de cor uniforme via K-means."""
10
- r,g,b,a = arr_rgba[:,:,0],arr_rgba[:,:,1],arr_rgba[:,:,2],arr_rgba[:,:,3]
11
  opaque = a > 128
12
-
13
- # Remove sombras
14
- cmax = np.maximum(np.maximum(r,g),b).astype(np.float32)
15
- cmin = np.minimum(np.minimum(r,g),b).astype(np.float32)
16
  sat = np.where(cmax>0,(cmax-cmin)/cmax,0)
17
- lum = (0.299*r+0.587*g+0.114*b).astype(np.float32)/255
18
- shadow = (sat < shadow_strength*0.35) & opaque
19
-
20
- arr = arr_rgba.copy().astype(np.float32)
21
- for ch in range(3):
22
- arr[:,:,ch] = np.where(shadow & opaque, np.where(lum>0.5,255,0), arr[:,:,ch])
23
- arr[:,:,3] = np.where(a>128,255,0)
24
-
25
- # K-means nos pixels opacos e nao-sombra
26
- valid = (arr[:,:,3]>128) & ~shadow
27
- pixels = arr[:,:,:3][valid].astype(np.float32)
28
  if len(pixels) < n_colors:
29
- return arr.astype(np.uint8), []
30
-
31
- sample = pixels[::max(1,len(pixels)//10000)]
32
- criteria = (cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER, 30, 1.0)
33
- k = min(n_colors, len(sample))
34
- _, labels_s, centers = cv2.kmeans(sample, k, None, criteria, 5, cv2.KMEANS_PP_CENTERS)
35
-
36
- # Aplica K-means a TODOS os pixels opacos
37
- flat = arr[:,:,:3].reshape(-1,3).astype(np.float32)
38
- flat_opaque = arr[:,:,3].reshape(-1) > 128
39
-
40
- # Distancia para cada centro
41
- dists = np.sqrt(((flat[:,np.newaxis,:]-centers[np.newaxis,:,:])**2).sum(axis=2))
42
- best = np.argmin(dists,axis=1)
43
-
44
- # Substitui cada pixel pela cor do centro mais proximo
45
- result = arr.copy()
46
- h,w = arr.shape[:2]
47
- mapped = centers[best].reshape(h,w,3)
48
- result[:,:,0] = np.where(arr[:,:,3]>128, mapped[:,:,0], arr[:,:,0])
49
- result[:,:,1] = np.where(arr[:,:,3]>128, mapped[:,:,1], arr[:,:,1])
50
- result[:,:,2] = np.where(arr[:,:,3]>128, mapped[:,:,2], arr[:,:,2])
51
-
52
- # Ordena centros por frequencia
53
- counts = np.bincount(best[flat_opaque])
54
  order = np.argsort(-counts)
55
- dominant = [centers[i] for i in order if 0.299*centers[i][0]/255+0.587*centers[i][1]/255+0.114*centers[i][2]/255 > 0.05]
56
-
57
- return result.astype(np.uint8), dominant, centers[best].reshape(h,w,3), best.reshape(h,w)
58
-
59
-
60
- def clean_region(mask, close_iter=3, open_iter=1):
61
- """Fecha buracos e remove ruido numa mascara binaria."""
62
- k5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
63
- k3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
64
- m = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k5, iterations=close_iter)
65
- m = cv2.morphologyEx(m, cv2.MORPH_OPEN, k3, iterations=open_iter)
66
- return m
67
-
68
-
69
- def contours_to_svg_path(contour, epsilon_factor=0.002):
70
- """Converte contorno OpenCV em path SVG suave."""
71
- # Aproxima o contorno com menos pontos
72
- epsilon = epsilon_factor * cv2.arcLength(contour, True)
73
- approx = cv2.approxPolyDP(contour, epsilon, True)
74
- if len(approx) < 3:
75
- return None
76
- pts = approx.reshape(-1,2)
77
- d = f"M {pts[0][0]:.1f} {pts[0][1]:.1f}"
78
- for pt in pts[1:]:
79
- d += f" L {pt[0]:.1f} {pt[1]:.1f}"
80
- d += " Z"
81
- return d
82
-
83
-
84
- def rgb_to_hex(c):
85
- return f"#{int(c[0]):02x}{int(c[1]):02x}{int(c[2]):02x}"
86
-
87
-
88
- def vectorize_with_contours(arr_rgba, dominant, label_map, centers, min_area, epsilon):
89
- """Vetoriza cada regiao de cor usando contornos OpenCV."""
90
- h, w = arr_rgba.shape[:2]
91
- alpha = arr_rgba[:,:,3]
92
- svg_paths = []
93
-
94
- for ci, color in enumerate(dominant):
95
- # Mascara desta cor (onde label_map == ci)
96
- # Acha qual indice de centro corresponde a esta cor
97
- dists = np.sqrt(((centers - color)**2).sum(axis=1))
98
- center_idx = np.argmin(dists)
99
-
100
- mask = ((label_map == center_idx) & (alpha > 128)).astype(np.uint8) * 255
101
-
102
- if mask.sum() < min_area * 255:
103
- continue
104
-
105
- # Fecha buracos
106
- mask_clean = clean_region(mask)
107
-
108
- # Encontra contornos
109
- contours, _ = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)
110
-
111
- color_hex = rgb_to_hex(color)
112
- for cnt in contours:
113
- area = cv2.contourArea(cnt)
114
- if area < min_area:
115
- continue
116
- path_d = contours_to_svg_path(cnt, epsilon)
117
- if path_d:
118
- svg_paths.append(f'<path fill="{color_hex}" d="{path_d}"/>')
119
-
120
- return svg_paths
121
-
122
-
123
- def preprocess(pil_img, shadow_strength, n_colors):
124
  img = pil_img.convert('RGBA')
125
- w, h = img.size
126
- MAX, MIN = 1200, 500
127
  s = 1.0
128
- if max(w,h) > MAX: s = MAX/max(w,h)
129
- elif max(w,h) < MIN: s = MIN/max(w,h)
130
- if s != 1.0:
131
- img = img.resize((int(w*s), int(h*s)), Image.LANCZOS)
132
-
133
- arr = np.array(img)
134
- result, dominant, mapped, label_map = kmeans_segment(arr, int(n_colors), shadow_strength)
135
- return Image.fromarray(result,'RGBA'), result, dominant, mapped, label_map
 
 
 
 
 
 
 
 
 
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  def vectorize_image(
139
  input_image, shadow_strength, n_colors,
140
- use_contours, min_area, epsilon,
141
  filter_speckle, color_precision, corner_threshold,
142
  length_threshold, path_precision,
143
  ):
144
  try:
145
  if input_image is None:
146
  raise gr.Error("Nenhuma imagem enviada")
147
-
148
  pil_img = input_image.convert('RGBA')
149
- w, h = pil_img.size
150
  print(f"Vetorizando {w}x{h}...")
151
  if w>2000 or h>2000:
152
  raise gr.Error("Imagem muito grande. Maximo: 2000x2000 px.")
153
 
154
- pil_clean, arr_clean, dominant, mapped, label_map = preprocess(pil_img, shadow_strength, int(n_colors))
155
- pw, ph = pil_clean.size
156
- print(f"Pre-processamento: {pw}x{ph}, {len(dominant)} cores")
157
-
158
- centers_arr = np.array([d for d in dominant], dtype=np.float32)
159
-
160
- if use_contours and len(dominant) > 0:
161
- # Modo contornos: paths limpos ponto a ponto
162
- svg_paths = vectorize_with_contours(
163
- arr_clean, dominant, label_map, centers_arr,
164
- min_area=int(min_area), epsilon=float(epsilon)/1000
165
- )
166
-
167
- if svg_paths:
168
- svg_str = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {pw} {ph}" width="100%" height="100%">\n'
169
- svg_str += '\n'.join(svg_paths)
170
- svg_str += '\n</svg>'
171
- else:
172
- raise gr.Error("Nenhum contorno encontrado. Tente ajustar os parametros.")
173
- else:
174
- # Fallback: VTracer na imagem pre-processada
175
- buf = io.BytesIO()
176
- pil_clean.save(buf, format='PNG')
177
- svg_str = vtracer.convert_raw_image_to_svg(
178
- buf.getvalue(), img_format='png',
179
- colormode='color', hierarchical='stacked', mode='spline',
180
- filter_speckle=int(filter_speckle),
181
- color_precision=int(color_precision),
182
- layer_difference=16,
183
- corner_threshold=int(corner_threshold),
184
- length_threshold=float(length_threshold),
185
- max_iterations=10, splice_threshold=60,
186
- path_precision=int(path_precision),
187
- )
188
- if 'viewBox' not in svg_str:
189
- svg_str = svg_str.replace('<svg ', f'<svg viewBox="0 0 {pw} {ph}" ', 1)
190
- svg_str = re.sub(r'(<svg[^>]*?)width="[^"]*"', r'\1width="100%"', svg_str)
191
- svg_str = re.sub(r'(<svg[^>]*?)height="[^"]*"', r'\1height="100%"', svg_str)
192
 
193
  svg_path = f"/tmp/result_{os.getpid()}.svg"
194
  with open(svg_path,'w',encoding='utf-8') as f:
@@ -197,7 +139,7 @@ def vectorize_image(
197
  png_path = None
198
  try:
199
  import cairosvg
200
- png_bytes = cairosvg.svg2png(bytestring=svg_str.encode(), scale=1.0)
201
  png_path = f"/tmp/result_{os.getpid()}.png"
202
  with open(png_path,'wb') as f: f.write(png_bytes)
203
  except Exception as e:
@@ -211,44 +153,32 @@ def vectorize_image(
211
  except Exception as e:
212
  raise gr.Error(str(e))
213
 
214
-
215
  with gr.Blocks(title="Iluminados Vectorizer") as demo:
216
- gr.Markdown("## Iluminados Vectorizer - OpenCV + VTracer")
217
-
218
  with gr.Row():
219
  with gr.Column():
220
  inp = gr.Image(label="Imagem de entrada", type="pil")
221
-
222
  with gr.Accordion("Pre-processamento", open=True):
223
- shadow_strength = gr.Slider(0,1.0, value=0.6, step=0.05, label="Remover sombras")
224
- n_colors = gr.Slider(2,32, value=16, step=1, label="Numero de cores")
225
-
226
- with gr.Accordion("Modo de vetorizacao", open=True):
227
- use_contours = gr.Checkbox(value=True, label="Usar contornos OpenCV (linhas mais limpas)")
228
- min_area = gr.Slider(10,2000, value=100, step=10, label="Area minima de regiao")
229
- epsilon = gr.Slider(1,20, value=4, step=0.5, label="Suavidade dos contornos")
230
-
231
- with gr.Accordion("VTracer (fallback)", open=False):
232
- filter_speckle = gr.Slider(1,100,value=6, step=1, label="Filtro fragmentos")
233
  color_precision = gr.Slider(1,8, value=6, step=1, label="Precisao de cor")
234
  corner_threshold = gr.Slider(1,180,value=60, step=1, label="Limiar de canto")
235
- length_threshold = gr.Slider(0.5,10.,value=3.5,step=0.1,label="Comprimento minimo")
236
  path_precision = gr.Slider(1,10, value=5, step=1, label="Precisao do caminho")
237
-
238
  btn = gr.Button("Vetorizar", variant="primary")
239
-
240
  with gr.Column():
241
- out_svg = gr.File(label="SVG")
242
  out_png = gr.File(label="PNG vetorizado")
243
  out_code = gr.Code(label="SVG codigo", language="html", lines=10)
244
 
245
  btn.click(
246
  fn=vectorize_image,
247
- inputs=[inp, shadow_strength, n_colors,
248
- use_contours, min_area, epsilon,
249
- filter_speckle, color_precision,
250
- corner_threshold, length_threshold, path_precision],
251
- outputs=[out_svg, out_png, out_code]
252
  )
253
 
254
  demo.launch()
 
5
  import cv2
6
  import vtracer
7
 
8
+ def get_dominant_colors(arr_rgba, n_colors, shadow_strength):
9
+ r,g,b,a = arr_rgba[:,:,0].astype(np.float32),arr_rgba[:,:,1].astype(np.float32),arr_rgba[:,:,2].astype(np.float32),arr_rgba[:,:,3].astype(np.float32)
 
10
  opaque = a > 128
11
+ cmax = np.maximum(np.maximum(r,g),b)
12
+ cmin = np.minimum(np.minimum(r,g),b)
 
 
13
  sat = np.where(cmax>0,(cmax-cmin)/cmax,0)
14
+ lum = (0.299*r+0.587*g+0.114*b)/255
15
+ shadow = sat < (shadow_strength*0.35)
16
+ valid = opaque & ~shadow
17
+ pixels = arr_rgba[:,:,:3][valid].astype(np.float32)
 
 
 
 
 
 
 
18
  if len(pixels) < n_colors:
19
+ return []
20
+ criteria = (cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER,30,1.0)
21
+ k = min(n_colors, len(pixels))
22
+ sample = pixels[::max(1,len(pixels)//8000)]
23
+ _,labels,centers = cv2.kmeans(sample,k,None,criteria,5,cv2.KMEANS_PP_CENTERS)
24
+ counts = np.bincount(labels.flatten())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  order = np.argsort(-counts)
26
+ result = []
27
+ for i in order:
28
+ c = centers[i]
29
+ l = 0.299*c[0]/255+0.587*c[1]/255+0.114*c[2]/255
30
+ if l > 0.08:
31
+ result.append(c)
32
+ return result
33
+
34
+ def preprocess_clean(pil_img, shadow_strength, n_colors):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  img = pil_img.convert('RGBA')
36
+ w,h = img.size
37
+ MAX,MIN = 1200,500
38
  s = 1.0
39
+ if max(w,h)>MAX: s=MAX/max(w,h)
40
+ elif max(w,h)<MIN: s=MIN/max(w,h)
41
+ if s!=1.0:
42
+ img = img.resize((int(w*s),int(h*s)),Image.LANCZOS)
43
+
44
+ arr = np.array(img,dtype=np.float32)
45
+ r,g,b,a = arr[:,:,0],arr[:,:,1],arr[:,:,2],arr[:,:,3]
46
+ opaque = a>128
47
+
48
+ if shadow_strength>0:
49
+ cmax = np.maximum(np.maximum(r,g),b)
50
+ cmin = np.minimum(np.minimum(r,g),b)
51
+ sat = np.where(cmax>0,(cmax-cmin)/cmax,0)
52
+ lum = (0.299*r+0.587*g+0.114*b)/255
53
+ shadow = (sat<shadow_strength*0.35)&opaque
54
+ for ch in range(3):
55
+ arr[:,:,ch] = np.where(shadow,np.where(lum>0.5,255,0),arr[:,:,ch])
56
 
57
+ arr[:,:,3] = np.where(a>128,255,0)
58
+ opaque = arr[:,:,3]>128
59
+
60
+ dominant = get_dominant_colors(arr.astype(np.uint8),int(n_colors),shadow_strength)
61
+ if dominant:
62
+ dom_arr = np.array(dominant,dtype=np.float32)
63
+ h2,w2 = arr.shape[:2]
64
+ flat = arr[:,:,:3].reshape(-1,3).astype(np.float32)
65
+ dists = np.sqrt(((flat[:,np.newaxis,:]-dom_arr[np.newaxis,:,:])**2).sum(axis=2))
66
+ best_idx = np.argmin(dists,axis=1)
67
+ best_dist = dists[np.arange(len(flat)),best_idx]
68
+ best_color = dom_arr[best_idx]
69
+ snap = best_dist<25
70
+ flat_result = flat.copy()
71
+ flat_result[snap] = best_color[snap]
72
+ arr[:,:,:3] = flat_result.reshape(h2,w2,3)
73
+
74
+ alpha = arr[:,:,3].astype(np.uint8)
75
+ k3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
76
+ k5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
77
+ alpha = cv2.morphologyEx(alpha,cv2.MORPH_CLOSE,k5,iterations=3)
78
+ alpha = cv2.morphologyEx(alpha,cv2.MORPH_OPEN,k3,iterations=1)
79
+ alpha = np.where(alpha>127,255,0).astype(np.uint8)
80
+ arr[:,:,3] = alpha
81
+
82
+ if dominant:
83
+ dom_arr = np.array(dominant,dtype=np.float32)
84
+ rgb = arr[:,:,:3].astype(np.float32)
85
+ for color in dominant:
86
+ dist = np.sqrt(((rgb-color)**2).sum(axis=2))
87
+ mask = (dist<20).astype(np.uint8)*255
88
+ mask_closed = cv2.morphologyEx(mask,cv2.MORPH_CLOSE,k5,iterations=3)
89
+ new_pixels = (mask_closed>127)&(mask<127)&(alpha>127)
90
+ arr[:,:,0] = np.where(new_pixels,color[0],arr[:,:,0])
91
+ arr[:,:,1] = np.where(new_pixels,color[1],arr[:,:,1])
92
+ arr[:,:,2] = np.where(new_pixels,color[2],arr[:,:,2])
93
+
94
+ return Image.fromarray(arr.astype(np.uint8),'RGBA')
95
 
96
  def vectorize_image(
97
  input_image, shadow_strength, n_colors,
 
98
  filter_speckle, color_precision, corner_threshold,
99
  length_threshold, path_precision,
100
  ):
101
  try:
102
  if input_image is None:
103
  raise gr.Error("Nenhuma imagem enviada")
 
104
  pil_img = input_image.convert('RGBA')
105
+ w,h = pil_img.size
106
  print(f"Vetorizando {w}x{h}...")
107
  if w>2000 or h>2000:
108
  raise gr.Error("Imagem muito grande. Maximo: 2000x2000 px.")
109
 
110
+ pil_img = preprocess_clean(pil_img,shadow_strength,n_colors)
111
+ pw,ph = pil_img.size
112
+ print(f"Apos pre-processamento: {pw}x{ph}")
113
+
114
+ buf = io.BytesIO()
115
+ pil_img.save(buf,format='PNG')
116
+ img_bytes = buf.getvalue()
117
+
118
+ svg_str = vtracer.convert_raw_image_to_svg(
119
+ img_bytes, img_format='png',
120
+ colormode='color', hierarchical='stacked', mode='spline',
121
+ filter_speckle=int(filter_speckle),
122
+ color_precision=int(color_precision),
123
+ layer_difference=16,
124
+ corner_threshold=int(corner_threshold),
125
+ length_threshold=float(length_threshold),
126
+ max_iterations=10, splice_threshold=60,
127
+ path_precision=int(path_precision),
128
+ )
129
+
130
+ if 'viewBox' not in svg_str:
131
+ svg_str = svg_str.replace('<svg ',f'<svg viewBox="0 0 {pw} {ph}" ',1)
132
+ svg_str = re.sub(r'(<svg[^>]*?)width="[^"]*"',r'\1width="100%"',svg_str)
133
+ svg_str = re.sub(r'(<svg[^>]*?)height="[^"]*"',r'\1height="100%"',svg_str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  svg_path = f"/tmp/result_{os.getpid()}.svg"
136
  with open(svg_path,'w',encoding='utf-8') as f:
 
139
  png_path = None
140
  try:
141
  import cairosvg
142
+ png_bytes = cairosvg.svg2png(bytestring=svg_str.encode(),scale=1.0)
143
  png_path = f"/tmp/result_{os.getpid()}.png"
144
  with open(png_path,'wb') as f: f.write(png_bytes)
145
  except Exception as e:
 
153
  except Exception as e:
154
  raise gr.Error(str(e))
155
 
 
156
  with gr.Blocks(title="Iluminados Vectorizer") as demo:
157
+ gr.Markdown("## Iluminados Vectorizer - VTracer API")
 
158
  with gr.Row():
159
  with gr.Column():
160
  inp = gr.Image(label="Imagem de entrada", type="pil")
 
161
  with gr.Accordion("Pre-processamento", open=True):
162
+ shadow_strength = gr.Slider(0,1.0,value=0.65,step=0.05,label="Remover sombras e gradientes")
163
+ n_colors = gr.Slider(4,32, value=16, step=1, label="Numero de cores")
164
+ with gr.Accordion("Vetorizacao", open=False):
165
+ filter_speckle = gr.Slider(1,100,value=6, step=1, label="Filtro de fragmentos")
 
 
 
 
 
 
166
  color_precision = gr.Slider(1,8, value=6, step=1, label="Precisao de cor")
167
  corner_threshold = gr.Slider(1,180,value=60, step=1, label="Limiar de canto")
168
+ length_threshold = gr.Slider(0.5,10.,value=3.5,step=0.1,label="Limiar de comprimento")
169
  path_precision = gr.Slider(1,10, value=5, step=1, label="Precisao do caminho")
 
170
  btn = gr.Button("Vetorizar", variant="primary")
 
171
  with gr.Column():
172
+ out_svg = gr.File(label="SVG para download")
173
  out_png = gr.File(label="PNG vetorizado")
174
  out_code = gr.Code(label="SVG codigo", language="html", lines=10)
175
 
176
  btn.click(
177
  fn=vectorize_image,
178
+ inputs=[inp,shadow_strength,n_colors,
179
+ filter_speckle,color_precision,
180
+ corner_threshold,length_threshold,path_precision],
181
+ outputs=[out_svg,out_png,out_code]
 
182
  )
183
 
184
  demo.launch()