DageBjorne commited on
Commit
022f82c
·
1 Parent(s): 07104cf

Add ONNX neural style transfer with embedding-matched keywords

Browse files

Five CPU-friendly fast neural style models (cartoon, mosaic, impressionist, abstract, pointillism) selected via the existing MiniLM embedding planner.

README.md CHANGED
@@ -23,6 +23,13 @@ Upload an image and type an instruction. Transforms are selected by **semantic s
23
 
24
  ## Supported instructions
25
 
 
 
 
 
 
 
 
26
  **Color / classical** (examples — paraphrases work too)
27
  - `rotate`, `perspective`, `make it brighter`, `more contrast`, `vintage warm look`, `flip`, `blur softly`
28
 
@@ -67,5 +74,6 @@ First run downloads the embedding model, RapidOCR, and rembg u2netp weights.
67
  | `sentence-transformers/all-MiniLM-L6-v2` | Instruction → keyword matching | Apache 2.0 |
68
  | RapidOCR (ONNX) | Text region detection | Apache 2.0 |
69
  | rembg u2netp | Foreground segmentation | MIT |
 
70
 
71
  See [backgrounds/README.md](backgrounds/README.md) for background prompt examples.
 
23
 
24
  ## Supported instructions
25
 
26
+ **Artistic style transfer** (ONNX fast neural style, CPU-friendly — one style per request)
27
+ - `cartoon` / `cartoon style` — candy/comic look
28
+ - `mosaic` / `mosaic painting` — mosaic art
29
+ - `impressionist` / `painterly` — soft painted look
30
+ - `abstract art` / `cubist style` — bold abstract look
31
+ - `pointillism` — dotted painting style
32
+
33
  **Color / classical** (examples — paraphrases work too)
34
  - `rotate`, `perspective`, `make it brighter`, `more contrast`, `vintage warm look`, `flip`, `blur softly`
35
 
 
74
  | `sentence-transformers/all-MiniLM-L6-v2` | Instruction → keyword matching | Apache 2.0 |
75
  | RapidOCR (ONNX) | Text region detection | Apache 2.0 |
76
  | rembg u2netp | Foreground segmentation | MIT |
77
+ | ONNX fast neural style (onnxmodelzoo) | Artistic style transfer | See ONNX Model Zoo |
78
 
79
  See [backgrounds/README.md](backgrounds/README.md) for background prompt examples.
app.py CHANGED
@@ -48,6 +48,9 @@ EXAMPLE_PROMPTS = [
48
  ["add cutout"],
49
  ["transparent cutout"],
50
  ["perspective transform"],
 
 
 
51
  ["flip horizontally"],
52
  ["blur everything softly"],
53
  ]
 
48
  ["add cutout"],
49
  ["transparent cutout"],
50
  ["perspective transform"],
51
+ ["cartoon style"],
52
+ ["mosaic painting"],
53
+ ["impressionist look"],
54
  ["flip horizontally"],
55
  ["blur everything softly"],
56
  ]
pipeline/augment.py CHANGED
@@ -5,6 +5,7 @@ import numpy as np
5
  from PIL import Image, ImageEnhance, ImageFilter, ImageOps
6
 
7
  from pipeline.spatial import apply_spatial_ops
 
8
 
9
  ALLOWED_TAGS = {
10
  "brighten",
@@ -27,7 +28,7 @@ ALLOWED_TAGS = {
27
  "tint_blue",
28
  "invert",
29
  "perspective",
30
- }
31
 
32
 
33
  def _clamp_strength(strength: float) -> float:
@@ -129,6 +130,9 @@ def _apply_tag(image: Image.Image, tag: str, strength: float) -> Image.Image:
129
  return cropped.resize((w, h), Image.Resampling.LANCZOS)
130
  if tag == "perspective":
131
  return _apply_perspective(image, s)
 
 
 
132
 
133
  return image
134
 
@@ -185,7 +189,18 @@ def apply_augmentations(
185
  )
186
 
187
  for tag in tags:
188
- if tag not in ALLOWED_TAGS:
 
 
 
 
 
 
 
 
 
 
 
189
  continue
190
  out = _apply_tag(result, tag, strength)
191
  if isinstance(out, tuple):
 
5
  from PIL import Image, ImageEnhance, ImageFilter, ImageOps
6
 
7
  from pipeline.spatial import apply_spatial_ops
8
+ from pipeline.style_transfer import STYLE_MODELS, STYLE_TAGS, apply_style
9
 
10
  ALLOWED_TAGS = {
11
  "brighten",
 
28
  "tint_blue",
29
  "invert",
30
  "perspective",
31
+ } | set(STYLE_TAGS)
32
 
33
 
34
  def _clamp_strength(strength: float) -> float:
 
130
  return cropped.resize((w, h), Image.Resampling.LANCZOS)
131
  if tag == "perspective":
132
  return _apply_perspective(image, s)
133
+ if tag in STYLE_TAGS:
134
+ styled = apply_style(image, tag, strength)
135
+ return styled, f"style({STYLE_MODELS[tag]['label']})"
136
 
137
  return image
138
 
 
189
  )
190
 
191
  for tag in tags:
192
+ if tag not in ALLOWED_TAGS or tag in STYLE_TAGS:
193
+ continue
194
+ out = _apply_tag(result, tag, strength)
195
+ if isinstance(out, tuple):
196
+ result, label = out
197
+ applied.append(label)
198
+ else:
199
+ result = out
200
+ applied.append(tag)
201
+
202
+ for tag in tags:
203
+ if tag not in STYLE_TAGS:
204
  continue
205
  out = _apply_tag(result, tag, strength)
206
  if isinstance(out, tuple):
pipeline/embedding_planner.py CHANGED
@@ -16,6 +16,13 @@ MUTUAL_EXCLUSION_GROUPS: tuple[tuple[str, ...], ...] = (
16
  ("tint_red", "tint_green", "tint_blue"),
17
  ("cover_random", "cover_avoid_text", "cover_and_cutout", "cover_and_cutout_avoid_text"),
18
  ("add_cutout_transparent", "add_cutout_solid", "add_cutout", "add_cutout_avoid_text"),
 
 
 
 
 
 
 
19
  )
20
 
21
  _model: SentenceTransformer | None = None
 
16
  ("tint_red", "tint_green", "tint_blue"),
17
  ("cover_random", "cover_avoid_text", "cover_and_cutout", "cover_and_cutout_avoid_text"),
18
  ("add_cutout_transparent", "add_cutout_solid", "add_cutout", "add_cutout_avoid_text"),
19
+ (
20
+ "style_candy",
21
+ "style_mosaic",
22
+ "style_rain_princess",
23
+ "style_udnie",
24
+ "style_pointilism",
25
+ ),
26
  )
27
 
28
  _model: SentenceTransformer | None = None
pipeline/keyword_catalog.py CHANGED
@@ -250,6 +250,72 @@ AUGMENT_KEYWORDS: tuple[AugmentKeyword, ...] = (
250
  kind="tag",
251
  phrases=("blur", "soften", "make blurry", "gaussian blur", "soft focus"),
252
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  )
254
 
255
  KEYWORD_BY_ID = {kw.id: kw for kw in AUGMENT_KEYWORDS}
 
250
  kind="tag",
251
  phrases=("blur", "soften", "make blurry", "gaussian blur", "soft focus"),
252
  ),
253
+ # Neural style transfer (ONNX fast neural style)
254
+ AugmentKeyword(
255
+ id="style_candy",
256
+ kind="tag",
257
+ phrases=(
258
+ "cartoon",
259
+ "cartoon style",
260
+ "comic style",
261
+ "comic book look",
262
+ "illustrated look",
263
+ "candy style",
264
+ "make it cartoon",
265
+ "like a cartoon",
266
+ ),
267
+ ),
268
+ AugmentKeyword(
269
+ id="style_mosaic",
270
+ kind="tag",
271
+ phrases=(
272
+ "mosaic",
273
+ "mosaic style",
274
+ "mosaic painting",
275
+ "stained glass look",
276
+ "tiled art style",
277
+ "mosaic art",
278
+ ),
279
+ ),
280
+ AugmentKeyword(
281
+ id="style_rain_princess",
282
+ kind="tag",
283
+ phrases=(
284
+ "impressionist",
285
+ "impressionist style",
286
+ "impressionist painting",
287
+ "painterly",
288
+ "soft painting",
289
+ "rain princess style",
290
+ "artistic painting",
291
+ "painted look",
292
+ ),
293
+ ),
294
+ AugmentKeyword(
295
+ id="style_udnie",
296
+ kind="tag",
297
+ phrases=(
298
+ "abstract art",
299
+ "abstract style",
300
+ "abstract painting",
301
+ "cubist style",
302
+ "expressive brushstrokes",
303
+ "udnie style",
304
+ "bold artistic style",
305
+ ),
306
+ ),
307
+ AugmentKeyword(
308
+ id="style_pointilism",
309
+ kind="tag",
310
+ phrases=(
311
+ "pointillism",
312
+ "pointillist",
313
+ "pointillist style",
314
+ "dotted painting",
315
+ "dot art",
316
+ "stippled look",
317
+ ),
318
+ ),
319
  )
320
 
321
  KEYWORD_BY_ID = {kw.id: kw for kw in AUGMENT_KEYWORDS}
pipeline/style_transfer.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fast neural style transfer via ONNX Model Zoo models (CPU-friendly)."""
2
+
3
+ import numpy as np
4
+ import onnxruntime as ort
5
+ from huggingface_hub import hf_hub_download
6
+ from PIL import Image
7
+
8
+ STYLE_MODELS: dict[str, dict[str, str]] = {
9
+ "style_candy": {"repo": "onnxmodelzoo/candy-9", "file": "candy-9.onnx", "label": "candy"},
10
+ "style_mosaic": {"repo": "onnxmodelzoo/mosaic-9", "file": "mosaic-9.onnx", "label": "mosaic"},
11
+ "style_rain_princess": {
12
+ "repo": "onnxmodelzoo/rain-princess-9",
13
+ "file": "rain-princess-9.onnx",
14
+ "label": "rain-princess",
15
+ },
16
+ "style_udnie": {"repo": "onnxmodelzoo/udnie-9", "file": "udnie-9.onnx", "label": "udnie"},
17
+ "style_pointilism": {
18
+ "repo": "onnxmodelzoo/pointilism-9",
19
+ "file": "pointilism-9.onnx",
20
+ "label": "pointilism",
21
+ },
22
+ }
23
+
24
+ STYLE_TAGS = frozenset(STYLE_MODELS)
25
+
26
+ MAX_EDGE = 512
27
+ MODEL_SIZE = 224
28
+
29
+ _sessions: dict[str, ort.InferenceSession] = {}
30
+
31
+
32
+ def _get_session(tag: str) -> ort.InferenceSession:
33
+ if tag not in _sessions:
34
+ meta = STYLE_MODELS[tag]
35
+ path = hf_hub_download(repo_id=meta["repo"], filename=meta["file"])
36
+ _sessions[tag] = ort.InferenceSession(path, providers=["CPUExecutionProvider"])
37
+ return _sessions[tag]
38
+
39
+
40
+ def warmup(tag: str = "style_candy") -> None:
41
+ """Pre-download and cache one style model (optional; others load on demand)."""
42
+ if tag in STYLE_MODELS:
43
+ _get_session(tag)
44
+
45
+
46
+ def apply_style(image: Image.Image, tag: str, strength: float = 1.0) -> Image.Image:
47
+ if tag not in STYLE_MODELS:
48
+ return image
49
+
50
+ session = _get_session(tag)
51
+ input_name = session.get_inputs()[0].name
52
+
53
+ original = image.convert("RGB")
54
+ width, height = original.size
55
+ scale = min(1.0, MAX_EDGE / max(width, height))
56
+ if scale < 1.0:
57
+ proc_w = max(1, int(width * scale))
58
+ proc_h = max(1, int(height * scale))
59
+ working = original.resize((proc_w, proc_h), Image.Resampling.LANCZOS)
60
+ else:
61
+ working = original
62
+ proc_w, proc_h = width, height
63
+
64
+ model_input = working.resize((MODEL_SIZE, MODEL_SIZE), Image.Resampling.LANCZOS)
65
+
66
+ tensor = np.array(model_input).astype(np.float32)
67
+ tensor = np.transpose(tensor, (2, 0, 1))
68
+ tensor = np.expand_dims(tensor, axis=0)
69
+
70
+ output = session.run(None, {input_name: tensor})[0]
71
+ styled_arr = np.clip(output[0], 0, 255).transpose(1, 2, 0).astype(np.uint8)
72
+ styled = Image.fromarray(styled_arr).resize((proc_w, proc_h), Image.Resampling.LANCZOS)
73
+
74
+ if (proc_w, proc_h) != (width, height):
75
+ styled = styled.resize((width, height), Image.Resampling.LANCZOS)
76
+
77
+ alpha = min(1.0, max(0.5, strength))
78
+ if alpha < 1.0:
79
+ styled = Image.blend(original, styled, alpha)
80
+
81
+ return styled
requirements.txt CHANGED
@@ -8,4 +8,5 @@ numpy>=1.24.0
8
  rapidocr-onnxruntime>=1.3.0
9
  opencv-python-headless>=4.8.0
10
  rembg[cpu]>=2.0.50
 
11
  requests>=2.28.0
 
8
  rapidocr-onnxruntime>=1.3.0
9
  opencv-python-headless>=4.8.0
10
  rembg[cpu]>=2.0.50
11
+ onnxruntime>=1.16.0
12
  requests>=2.28.0
scripts/validate_planner.py CHANGED
@@ -18,6 +18,9 @@ planning_cases = [
18
  ("replace background with beach sunset", ["replace_background"]),
19
  ("put a table behind it", None),
20
  ("blur everything softly", ["blur"]),
 
 
 
21
  ("asdf qwerty nonsense", None),
22
  ("more contrast", ["contrast_up"]),
23
  ("transparent cutout", ["add_cutout_transparent"]),
 
18
  ("replace background with beach sunset", ["replace_background"]),
19
  ("put a table behind it", None),
20
  ("blur everything softly", ["blur"]),
21
+ ("cartoon style", ["style_candy"]),
22
+ ("mosaic painting", ["style_mosaic"]),
23
+ ("impressionist look", ["style_rain_princess"]),
24
  ("asdf qwerty nonsense", None),
25
  ("more contrast", ["contrast_up"]),
26
  ("transparent cutout", ["add_cutout_transparent"]),