Delete app.py
Browse files
app.py
DELETED
|
@@ -1,379 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Vektor-Studio — PNG/JPG zu SVG
|
| 3 |
-
Saubere Linienführung für generierte Bilder.
|
| 4 |
-
|
| 5 |
-
Hugging Face Space (Gradio SDK). Start lokal: python app.py
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import os
|
| 9 |
-
import re
|
| 10 |
-
import base64
|
| 11 |
-
import tempfile
|
| 12 |
-
import uuid
|
| 13 |
-
|
| 14 |
-
import numpy as np
|
| 15 |
-
from PIL import Image, ImageOps, ImageFilter
|
| 16 |
-
import gradio as gr
|
| 17 |
-
import vtracer
|
| 18 |
-
|
| 19 |
-
MAX_SIDE = 2600 # Obergrenze nach dem Hochskalieren (Speed/Timeout-Schutz)
|
| 20 |
-
OUT_DIR = os.path.join(tempfile.gettempdir(), "svg_out")
|
| 21 |
-
os.makedirs(OUT_DIR, exist_ok=True)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# --------------------------------------------------------------------------
|
| 25 |
-
# Bildvorbereitung
|
| 26 |
-
# --------------------------------------------------------------------------
|
| 27 |
-
def flatten(img: Image.Image) -> Image.Image:
|
| 28 |
-
"""Alpha auf Weiß legen, damit Transparenz nicht als Schwarz getract wird."""
|
| 29 |
-
img = ImageOps.exif_transpose(img)
|
| 30 |
-
if img.mode in ("RGBA", "LA", "P"):
|
| 31 |
-
img = img.convert("RGBA")
|
| 32 |
-
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
| 33 |
-
img = Image.alpha_composite(bg, img)
|
| 34 |
-
return img.convert("RGB")
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def otsu_threshold(arr: np.ndarray) -> int:
|
| 38 |
-
hist = np.bincount(arr.ravel(), minlength=256).astype(np.float64)
|
| 39 |
-
total = hist.sum()
|
| 40 |
-
omega = np.cumsum(hist) / total
|
| 41 |
-
mu = np.cumsum(hist * np.arange(256)) / total
|
| 42 |
-
mu_t = mu[-1]
|
| 43 |
-
denom = omega * (1.0 - omega)
|
| 44 |
-
denom[denom == 0] = 1e-12
|
| 45 |
-
sigma_b = (mu_t * omega - mu) ** 2 / denom
|
| 46 |
-
return int(np.argmax(sigma_b))
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def binarize(gray: Image.Image, method: str, level: int, block: int, offset: int) -> np.ndarray:
|
| 50 |
-
"""Gibt eine Bool-Maske zurück: True = Tinte (Linie)."""
|
| 51 |
-
arr = np.asarray(gray, dtype=np.uint8)
|
| 52 |
-
|
| 53 |
-
if method == "Adaptiv (ungleiche Beleuchtung)":
|
| 54 |
-
radius = max(1, int(block) // 2)
|
| 55 |
-
local = np.asarray(gray.filter(ImageFilter.BoxBlur(radius)), dtype=np.int16)
|
| 56 |
-
return arr.astype(np.int16) < (local - int(offset))
|
| 57 |
-
|
| 58 |
-
if method == "Otsu (automatisch)":
|
| 59 |
-
level = otsu_threshold(arr)
|
| 60 |
-
|
| 61 |
-
return arr < int(level)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def zhang_suen(mask: np.ndarray, max_iter: int = 60) -> np.ndarray:
|
| 65 |
-
"""Skelettierung: reduziert jede Linie auf 1 px Mittellinie."""
|
| 66 |
-
img = mask.copy()
|
| 67 |
-
for _ in range(max_iter):
|
| 68 |
-
changed = False
|
| 69 |
-
for step in (0, 1):
|
| 70 |
-
P = np.pad(img, 1, constant_values=False)
|
| 71 |
-
P2, P3, P4 = P[:-2, 1:-1], P[:-2, 2:], P[1:-1, 2:]
|
| 72 |
-
P5, P6, P7 = P[2:, 2:], P[2:, 1:-1], P[2:, :-2]
|
| 73 |
-
P8, P9 = P[1:-1, :-2], P[:-2, :-2]
|
| 74 |
-
|
| 75 |
-
ring = [P2, P3, P4, P5, P6, P7, P8, P9, P2]
|
| 76 |
-
B = sum(x.astype(np.uint8) for x in ring[:8])
|
| 77 |
-
A = np.zeros(img.shape, np.uint8)
|
| 78 |
-
for i in range(8):
|
| 79 |
-
A += (~ring[i] & ring[i + 1]).astype(np.uint8)
|
| 80 |
-
|
| 81 |
-
base = img & (B >= 2) & (B <= 6) & (A == 1)
|
| 82 |
-
if step == 0:
|
| 83 |
-
cond = base & ~(P2 & P4 & P6) & ~(P4 & P6 & P8)
|
| 84 |
-
else:
|
| 85 |
-
cond = base & ~(P2 & P4 & P8) & ~(P2 & P6 & P8)
|
| 86 |
-
|
| 87 |
-
if cond.any():
|
| 88 |
-
img &= ~cond
|
| 89 |
-
changed = True
|
| 90 |
-
if not changed:
|
| 91 |
-
break
|
| 92 |
-
return img
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def set_stroke_width(mask: np.ndarray, width: int) -> np.ndarray:
|
| 96 |
-
"""Skelett wieder auf eine gleichmäßige Strichstärke aufdicken."""
|
| 97 |
-
if width <= 1:
|
| 98 |
-
return mask
|
| 99 |
-
ink = Image.fromarray(np.where(mask, 255, 0).astype(np.uint8))
|
| 100 |
-
size = width if width % 2 == 1 else width + 1
|
| 101 |
-
ink = ink.filter(ImageFilter.MaxFilter(size))
|
| 102 |
-
return np.asarray(ink) > 127
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def cap_size(img: Image.Image) -> Image.Image:
|
| 106 |
-
w, h = img.size
|
| 107 |
-
if max(w, h) <= MAX_SIDE:
|
| 108 |
-
return img
|
| 109 |
-
s = MAX_SIDE / max(w, h)
|
| 110 |
-
return img.resize((max(1, int(w * s)), max(1, int(h * s))), Image.LANCZOS)
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
# --------------------------------------------------------------------------
|
| 114 |
-
# SVG-Nachbearbeitung
|
| 115 |
-
# --------------------------------------------------------------------------
|
| 116 |
-
def strip_background(svg: str) -> str:
|
| 117 |
-
"""Entfernt fast-weiße Füllflächen (typisch: der Hintergrund im Farbmodus)."""
|
| 118 |
-
def is_light(hexcol: str) -> bool:
|
| 119 |
-
r, g, b = (int(hexcol[i:i + 2], 16) for i in (1, 3, 5))
|
| 120 |
-
return r > 243 and g > 243 and b > 243
|
| 121 |
-
|
| 122 |
-
return re.sub(
|
| 123 |
-
r'<path[^>]*fill="(#[0-9a-fA-F]{6})"[^>]*/>\s*',
|
| 124 |
-
lambda m: "" if is_light(m.group(1)) else m.group(0),
|
| 125 |
-
svg,
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def recolor(svg: str, color: str) -> str:
|
| 130 |
-
return re.sub(r'fill="#000000"', f'fill="{color}"', svg)
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def rescale_root(svg: str, out_w: int, out_h: int) -> str:
|
| 134 |
-
"""viewBox ergänzen und die Anzeigegröße auf die Originalmaße setzen."""
|
| 135 |
-
m = re.search(r'<svg([^>]*)width="(\d+)"\s+height="(\d+)"', svg)
|
| 136 |
-
if not m:
|
| 137 |
-
return svg
|
| 138 |
-
vw, vh = m.group(2), m.group(3)
|
| 139 |
-
new_tag = (
|
| 140 |
-
f'<svg{m.group(1)}width="{out_w}" height="{out_h}" '
|
| 141 |
-
f'viewBox="0 0 {vw} {vh}"'
|
| 142 |
-
)
|
| 143 |
-
return svg[: m.start()] + new_tag + svg[m.end():]
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
def preview_html(svg: str, label: str) -> str:
|
| 147 |
-
if len(svg) > 4_000_000:
|
| 148 |
-
return (
|
| 149 |
-
f"<div class='vs-note'>{label}: Die Datei ist sehr groß "
|
| 150 |
-
f"({len(svg) // 1024} KB) — bitte herunterladen statt hier ansehen. "
|
| 151 |
-
"Tipp: „Kleine Flecken filtern“ erhöhen.</div>"
|
| 152 |
-
)
|
| 153 |
-
b64 = base64.b64encode(svg.encode("utf-8")).decode("ascii")
|
| 154 |
-
return (
|
| 155 |
-
"<div class='vs-stage'>"
|
| 156 |
-
f"<img src='data:image/svg+xml;base64,{b64}' alt='{label}'/>"
|
| 157 |
-
"</div>"
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
# --------------------------------------------------------------------------
|
| 162 |
-
# Hauptfunktion
|
| 163 |
-
# --------------------------------------------------------------------------
|
| 164 |
-
def vectorize(
|
| 165 |
-
image,
|
| 166 |
-
mode,
|
| 167 |
-
invert,
|
| 168 |
-
thr_method,
|
| 169 |
-
thr_level,
|
| 170 |
-
block,
|
| 171 |
-
offset,
|
| 172 |
-
denoise,
|
| 173 |
-
upscale,
|
| 174 |
-
uniform,
|
| 175 |
-
stroke_w,
|
| 176 |
-
curve_mode,
|
| 177 |
-
speckle,
|
| 178 |
-
corner,
|
| 179 |
-
length_thr,
|
| 180 |
-
splice,
|
| 181 |
-
precision,
|
| 182 |
-
colors,
|
| 183 |
-
layer_diff,
|
| 184 |
-
hierarchical,
|
| 185 |
-
drop_bg,
|
| 186 |
-
line_color,
|
| 187 |
-
progress=gr.Progress(),
|
| 188 |
-
):
|
| 189 |
-
if image is None:
|
| 190 |
-
raise gr.Error("Lade zuerst ein PNG oder JPG hoch.")
|
| 191 |
-
|
| 192 |
-
progress(0.1, desc="Bild vorbereiten")
|
| 193 |
-
src = flatten(image)
|
| 194 |
-
orig_w, orig_h = src.size
|
| 195 |
-
|
| 196 |
-
work = src
|
| 197 |
-
if upscale > 1:
|
| 198 |
-
work = work.resize(
|
| 199 |
-
(int(orig_w * upscale), int(orig_h * upscale)), Image.LANCZOS
|
| 200 |
-
)
|
| 201 |
-
work = cap_size(work)
|
| 202 |
-
|
| 203 |
-
line_mode = mode.startswith("Strich")
|
| 204 |
-
|
| 205 |
-
if line_mode:
|
| 206 |
-
progress(0.3, desc="Linien freistellen")
|
| 207 |
-
gray = ImageOps.autocontrast(work.convert("L"), cutoff=1)
|
| 208 |
-
if invert:
|
| 209 |
-
gray = ImageOps.invert(gray)
|
| 210 |
-
if denoise > 0:
|
| 211 |
-
gray = gray.filter(ImageFilter.MedianFilter(int(denoise) * 2 + 1))
|
| 212 |
-
|
| 213 |
-
mask = binarize(gray, thr_method, thr_level, block, offset)
|
| 214 |
-
|
| 215 |
-
if uniform:
|
| 216 |
-
progress(0.45, desc="Mittellinien berechnen")
|
| 217 |
-
mask = set_stroke_width(zhang_suen(mask), int(stroke_w))
|
| 218 |
-
|
| 219 |
-
prepped = Image.fromarray(np.where(mask, 0, 255).astype(np.uint8)).convert("RGB")
|
| 220 |
-
else:
|
| 221 |
-
progress(0.35, desc="Farbflächen reduzieren")
|
| 222 |
-
prepped = work
|
| 223 |
-
if denoise > 0:
|
| 224 |
-
prepped = prepped.filter(ImageFilter.MedianFilter(int(denoise) * 2 + 1))
|
| 225 |
-
|
| 226 |
-
stem = uuid.uuid4().hex[:10]
|
| 227 |
-
tmp_png = os.path.join(OUT_DIR, f"{stem}.png")
|
| 228 |
-
out_svg = os.path.join(OUT_DIR, "vektor.svg" if line_mode else "vektor-farbe.svg")
|
| 229 |
-
prepped.save(tmp_png)
|
| 230 |
-
|
| 231 |
-
progress(0.6, desc="Pfade tracen")
|
| 232 |
-
vtracer.convert_image_to_svg_py(
|
| 233 |
-
tmp_png,
|
| 234 |
-
out_svg,
|
| 235 |
-
colormode="binary" if line_mode else "color",
|
| 236 |
-
hierarchical="cutout" if hierarchical == "Ausgeschnitten" else "stacked",
|
| 237 |
-
mode="polygon" if curve_mode.startswith("Polygon") else "spline",
|
| 238 |
-
filter_speckle=int(speckle),
|
| 239 |
-
color_precision=int(colors),
|
| 240 |
-
layer_difference=int(layer_diff),
|
| 241 |
-
corner_threshold=int(corner),
|
| 242 |
-
length_threshold=float(length_thr),
|
| 243 |
-
max_iterations=10,
|
| 244 |
-
splice_threshold=int(splice),
|
| 245 |
-
path_precision=int(precision),
|
| 246 |
-
)
|
| 247 |
-
|
| 248 |
-
progress(0.85, desc="SVG aufräumen")
|
| 249 |
-
svg = open(out_svg, "r", encoding="utf-8").read()
|
| 250 |
-
if drop_bg:
|
| 251 |
-
svg = strip_background(svg)
|
| 252 |
-
if line_mode:
|
| 253 |
-
svg = recolor(svg, line_color)
|
| 254 |
-
svg = rescale_root(svg, orig_w, orig_h)
|
| 255 |
-
|
| 256 |
-
with open(out_svg, "w", encoding="utf-8") as f:
|
| 257 |
-
f.write(svg)
|
| 258 |
-
try:
|
| 259 |
-
os.remove(tmp_png)
|
| 260 |
-
except OSError:
|
| 261 |
-
pass
|
| 262 |
-
|
| 263 |
-
n_paths = svg.count("<path")
|
| 264 |
-
stats = (
|
| 265 |
-
f"**{n_paths} Pfade** · {len(svg) / 1024:.1f} KB · "
|
| 266 |
-
f"Zeichenfläche {orig_w}×{orig_h} px · getract bei {prepped.size[0]}×{prepped.size[1]} px"
|
| 267 |
-
)
|
| 268 |
-
return preview_html(svg, "Vektorergebnis"), out_svg, stats
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
# --------------------------------------------------------------------------
|
| 272 |
-
# Oberfläche
|
| 273 |
-
# --------------------------------------------------------------------------
|
| 274 |
-
CSS = """
|
| 275 |
-
.vs-stage {
|
| 276 |
-
background-color: #f7f5f2;
|
| 277 |
-
background-image:
|
| 278 |
-
linear-gradient(45deg, #e6e2dc 25%, transparent 25%, transparent 75%, #e6e2dc 75%),
|
| 279 |
-
linear-gradient(45deg, #e6e2dc 25%, transparent 25%, transparent 75%, #e6e2dc 75%);
|
| 280 |
-
background-size: 18px 18px;
|
| 281 |
-
background-position: 0 0, 9px 9px;
|
| 282 |
-
border-radius: 10px; padding: 14px; min-height: 240px;
|
| 283 |
-
display: flex; align-items: center; justify-content: center;
|
| 284 |
-
}
|
| 285 |
-
.vs-stage img { max-width: 100%; max-height: 62vh; }
|
| 286 |
-
.vs-note { padding: 18px; font-size: 0.9rem; line-height: 1.5; }
|
| 287 |
-
footer { display: none !important; }
|
| 288 |
-
"""
|
| 289 |
-
|
| 290 |
-
# Gradio 6 erwartet theme/css in launch(), Gradio 5 im Blocks-Konstruktor.
|
| 291 |
-
_MAJOR = int(gr.__version__.split(".")[0])
|
| 292 |
-
_STYLE = {"theme": gr.themes.Soft(), "css": CSS}
|
| 293 |
-
_BLOCKS_KW = {} if _MAJOR >= 6 else _STYLE
|
| 294 |
-
_LAUNCH_KW = _STYLE if _MAJOR >= 6 else {}
|
| 295 |
-
|
| 296 |
-
with gr.Blocks(title="PNG/JPG → SVG", **_BLOCKS_KW) as demo:
|
| 297 |
-
gr.Markdown(
|
| 298 |
-
"## PNG / JPG → SVG\n"
|
| 299 |
-
"Vektorisiert Bitmaps zu sauberen, skalierbaren Pfaden. "
|
| 300 |
-
"Für KI-generierte Zeichnungen, Logos und Skizzen."
|
| 301 |
-
)
|
| 302 |
-
|
| 303 |
-
with gr.Row():
|
| 304 |
-
# ---------------- linke Spalte: Eingabe + Regler ----------------
|
| 305 |
-
with gr.Column(scale=4):
|
| 306 |
-
image = gr.Image(label="Bild", type="pil", sources=["upload", "clipboard"],
|
| 307 |
-
image_mode="RGBA", height=300)
|
| 308 |
-
mode = gr.Radio(
|
| 309 |
-
["Strichzeichnung (Schwarzweiß)", "Farbig (Flächen)"],
|
| 310 |
-
value="Strichzeichnung (Schwarzweiß)",
|
| 311 |
-
label="Modus",
|
| 312 |
-
)
|
| 313 |
-
run = gr.Button("Vektorisieren", variant="primary")
|
| 314 |
-
|
| 315 |
-
with gr.Accordion("Linien freistellen", open=True) as line_box:
|
| 316 |
-
thr_method = gr.Radio(
|
| 317 |
-
["Otsu (automatisch)", "Global", "Adaptiv (ungleiche Beleuchtung)"],
|
| 318 |
-
value="Otsu (automatisch)",
|
| 319 |
-
label="Schwellenwert",
|
| 320 |
-
)
|
| 321 |
-
thr_level = gr.Slider(0, 255, 128, step=1, label="Grenzwert (nur Global)")
|
| 322 |
-
block = gr.Slider(5, 151, 41, step=2, label="Fensterbreite (nur Adaptiv)")
|
| 323 |
-
offset = gr.Slider(0, 40, 8, step=1, label="Empfindlichkeit (nur Adaptiv)")
|
| 324 |
-
invert = gr.Checkbox(False, label="Invertieren (helle Linien auf dunklem Grund)")
|
| 325 |
-
denoise = gr.Slider(0, 3, 1, step=1, label="Rauschen glätten")
|
| 326 |
-
upscale = gr.Slider(1, 4, 2, step=1,
|
| 327 |
-
label="Vorher hochskalieren (glattere Kurven)")
|
| 328 |
-
uniform = gr.Checkbox(
|
| 329 |
-
False, label="Gleichmäßige Strichstärke (Mittellinie berechnen)"
|
| 330 |
-
)
|
| 331 |
-
stroke_w = gr.Slider(1, 9, 3, step=2, label="Strichstärke in px")
|
| 332 |
-
|
| 333 |
-
with gr.Accordion("Pfadqualität", open=False):
|
| 334 |
-
curve_mode = gr.Radio(
|
| 335 |
-
["Spline (weiche Kurven)", "Polygon (harte Kanten)"],
|
| 336 |
-
value="Spline (weiche Kurven)", label="Kurventyp",
|
| 337 |
-
)
|
| 338 |
-
speckle = gr.Slider(0, 40, 6, step=1, label="Kleine Flecken filtern")
|
| 339 |
-
corner = gr.Slider(0, 180, 60, step=1, label="Eckenschwelle")
|
| 340 |
-
length_thr = gr.Slider(0.5, 10, 4, step=0.5, label="Kürzeste Segmentlänge")
|
| 341 |
-
splice = gr.Slider(0, 180, 45, step=1, label="Kurven verbinden")
|
| 342 |
-
precision = gr.Slider(1, 8, 4, step=1, label="Koordinatengenauigkeit")
|
| 343 |
-
|
| 344 |
-
with gr.Accordion("Farbe & Ausgabe", open=False):
|
| 345 |
-
colors = gr.Slider(1, 8, 6, step=1, label="Farbtiefe (nur Farbmodus)")
|
| 346 |
-
layer_diff = gr.Slider(0, 64, 16, step=1, label="Ebenenabstand (nur Farbmodus)")
|
| 347 |
-
hierarchical = gr.Radio(
|
| 348 |
-
["Gestapelt", "Ausgeschnitten"], value="Gestapelt", label="Ebenenaufbau"
|
| 349 |
-
)
|
| 350 |
-
drop_bg = gr.Checkbox(True, label="Weißen Hintergrund entfernen")
|
| 351 |
-
line_color = gr.ColorPicker("#111111", label="Linienfarbe")
|
| 352 |
-
|
| 353 |
-
# ---------------- rechte Spalte: Ergebnis ----------------
|
| 354 |
-
with gr.Column(scale=6):
|
| 355 |
-
preview = gr.HTML("<div class='vs-stage'><span class='vs-note'>"
|
| 356 |
-
"Das Ergebnis erscheint hier.</span></div>",
|
| 357 |
-
label="Vorschau")
|
| 358 |
-
stats = gr.Markdown("")
|
| 359 |
-
download = gr.File(label="SVG herunterladen", height=90)
|
| 360 |
-
|
| 361 |
-
gr.Markdown(
|
| 362 |
-
"**Für saubere Linien:** hochskalieren auf 2–3×, Spline-Kurven, Flecken-Filter "
|
| 363 |
-
"auf 6–12. Wirkt eine Zeichnung ausgefranst, hilft „Gleichmäßige Strichstärke“ — "
|
| 364 |
-
"das zieht jede Linie auf eine Mittelachse und zeichnet sie mit konstanter Breite neu."
|
| 365 |
-
)
|
| 366 |
-
|
| 367 |
-
def toggle(m):
|
| 368 |
-
return gr.update(open=m.startswith("Strich"))
|
| 369 |
-
|
| 370 |
-
mode.change(toggle, mode, line_box)
|
| 371 |
-
|
| 372 |
-
inputs = [image, mode, invert, thr_method, thr_level, block, offset, denoise,
|
| 373 |
-
upscale, uniform, stroke_w, curve_mode, speckle, corner, length_thr,
|
| 374 |
-
splice, precision, colors, layer_diff, hierarchical, drop_bg, line_color]
|
| 375 |
-
|
| 376 |
-
run.click(vectorize, inputs, [preview, download, stats])
|
| 377 |
-
|
| 378 |
-
if __name__ == "__main__":
|
| 379 |
-
demo.queue(max_size=20).launch(**_LAUNCH_KW)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|