Hadimeeee commited on
Commit
d8a6de2
·
verified ·
1 Parent(s): 900ae4b

Upload pipeline.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. pipeline.py +150 -0
pipeline.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional, Tuple
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import torch
7
+ from diffusers import ControlNetModel, StableDiffusionXLControlNetImg2ImgPipeline
8
+ from PIL import Image
9
+ from rembg import remove
10
+
11
+
12
+ DEFAULT_PROMPT = (
13
+ "16x16 pixel art sprite, NES style, cute stuffed animal character, "
14
+ "strictly pixelated, chunky visible pixels, limited flat color palette, "
15
+ "sharp pixel boundaries, no anti-aliasing, no gradients, no shading, "
16
+ "indie RPG game sprite style, warm saturated color palette, "
17
+ "chibi proportions, thick dark outlines, flat 2-tone coloring, "
18
+ "white background, full body, front-facing, "
19
+ "bold black outlines, clean pixel edges"
20
+ )
21
+
22
+ DEFAULT_NEGATIVE_PROMPT = (
23
+ "realistic, 3d render, blurry, smooth, photograph, gradient, shadow, "
24
+ "anti-aliasing, soft edges, painterly, watercolor, sketch, detailed texture"
25
+ )
26
+
27
+
28
+ class PixelArtLoRAPipeline:
29
+ """End-to-end inference pipeline for the uploaded SDXL LoRA."""
30
+
31
+ def __init__(
32
+ self,
33
+ lora_path: str = ".",
34
+ base_model: str = "stabilityai/stable-diffusion-xl-base-1.0",
35
+ controlnet_model: str = "diffusers/controlnet-canny-sdxl-1.0",
36
+ device: Optional[str] = None,
37
+ dtype: Optional[torch.dtype] = None,
38
+ ):
39
+ self.lora_path = lora_path
40
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
41
+ self.dtype = dtype or (torch.float16 if self.device == "cuda" else torch.float32)
42
+
43
+ controlnet = ControlNetModel.from_pretrained(
44
+ controlnet_model,
45
+ torch_dtype=self.dtype,
46
+ use_safetensors=True,
47
+ )
48
+
49
+ self.pipe = StableDiffusionXLControlNetImg2ImgPipeline.from_pretrained(
50
+ base_model,
51
+ controlnet=controlnet,
52
+ torch_dtype=self.dtype,
53
+ use_safetensors=True,
54
+ )
55
+ self.pipe.load_lora_weights(lora_path)
56
+ self.pipe.to(self.device)
57
+ self.pipe.enable_attention_slicing()
58
+
59
+ @staticmethod
60
+ def remove_background(image: Image.Image) -> Image.Image:
61
+ removed = remove(image.convert("RGBA"))
62
+ white_bg = Image.new("RGBA", removed.size, (255, 255, 255, 255))
63
+ white_bg.paste(removed, mask=removed.split()[3])
64
+ return white_bg.convert("RGB")
65
+
66
+ @staticmethod
67
+ def rembg_succeeded(image: Image.Image, threshold: float = 0.40) -> bool:
68
+ arr = np.array(image.convert("RGB"))
69
+ bg_mask = (arr[:, :, 0] >= 245) & (arr[:, :, 1] >= 245) & (arr[:, :, 2] >= 245)
70
+ return float(bg_mask.sum()) / bg_mask.size >= threshold
71
+
72
+ @staticmethod
73
+ def extract_canny(image: Image.Image, low: int = 80, high: int = 180) -> Image.Image:
74
+ gray = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2GRAY)
75
+ edges = cv2.Canny(gray, low, high)
76
+ return Image.fromarray(np.stack([edges] * 3, axis=-1))
77
+
78
+ @staticmethod
79
+ def quantize_colors(image: Image.Image, colors: int = 32) -> Image.Image:
80
+ quantized = image.convert("RGB").quantize(
81
+ colors=colors,
82
+ method=Image.Quantize.MEDIANCUT,
83
+ dither=Image.Dither.NONE,
84
+ )
85
+ return quantized.convert("RGB")
86
+
87
+ def prepare_image(
88
+ self,
89
+ image: Image.Image,
90
+ size: Tuple[int, int] = (512, 512),
91
+ bg_min_ratio: float = 0.40,
92
+ canny_low: int = 80,
93
+ canny_high: int = 180,
94
+ ) -> Tuple[Image.Image, Image.Image, bool]:
95
+ original = image.convert("RGB").resize(size)
96
+ bg_removed = self.remove_background(original)
97
+
98
+ if self.rembg_succeeded(bg_removed, bg_min_ratio):
99
+ source = bg_removed
100
+ rembg_ok = True
101
+ else:
102
+ source = original
103
+ rembg_ok = False
104
+
105
+ canny = self.extract_canny(source, canny_low, canny_high)
106
+ return source, canny, rembg_ok
107
+
108
+ def __call__(
109
+ self,
110
+ image: Image.Image,
111
+ prompt: str = DEFAULT_PROMPT,
112
+ negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
113
+ num_inference_steps: int = 50,
114
+ guidance_scale: float = 7.5,
115
+ controlnet_conditioning_scale: float = 0.8,
116
+ strength: float = 0.75,
117
+ quantize: bool = True,
118
+ n_colors: int = 32,
119
+ seed: Optional[int] = None,
120
+ ) -> dict:
121
+ source, canny, rembg_ok = self.prepare_image(image)
122
+
123
+ generator = None
124
+ if seed is not None:
125
+ generator = torch.Generator(device=self.device).manual_seed(seed)
126
+
127
+ result = self.pipe(
128
+ prompt=prompt,
129
+ negative_prompt=negative_prompt,
130
+ image=source,
131
+ control_image=canny,
132
+ num_inference_steps=num_inference_steps,
133
+ guidance_scale=guidance_scale,
134
+ controlnet_conditioning_scale=controlnet_conditioning_scale,
135
+ strength=strength,
136
+ generator=generator,
137
+ ).images[0]
138
+
139
+ final = self.quantize_colors(result, n_colors) if quantize else result
140
+ return {
141
+ "image": final,
142
+ "raw_image": result,
143
+ "source_image": source,
144
+ "canny_image": canny,
145
+ "rembg_ok": rembg_ok,
146
+ }
147
+
148
+
149
+ def load_pipeline(lora_path: Optional[str] = None) -> PixelArtLoRAPipeline:
150
+ return PixelArtLoRAPipeline(lora_path=lora_path or os.getenv("LORA_PATH", "."))