randomoneguy commited on
Commit
3048202
·
verified ·
1 Parent(s): 5a14c00
Files changed (4) hide show
  1. .gitignore +3 -0
  2. app.py +44 -40
  3. clip_styler.py +159 -205
  4. util.py +1 -2
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv/
2
+ .gradio/
3
+ __pycache__/
app.py CHANGED
@@ -4,39 +4,22 @@ import clip
4
  from torchvision import transforms
5
  from style_net import StyleNet
6
  from dataset import FlickrStreamer
7
- from util import DEVICE
8
- from clip_styler import ClipStyler
9
 
10
  # --- CONFIGURATION ---
11
- #DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
12
  INDEX_PATH = "flickr_embeddings.pt"
13
  SEARCH_LIMIT = 5000 # Must match the indexer limit!
14
  # ---------------------
15
 
16
- UI_EPOCHS = 100
17
- STYLE_PRESETS = {
18
- "Sketch": {
19
- "prompt": "charcoal sketch style",
20
- "source": "a Photo",
21
- },
22
- "Van Gogh": {
23
- "prompt": "Van Gogh painting style",
24
- "source": "a Photo",
25
- },
26
- "Cyberpunk": {
27
- "prompt": "neon cyberpunk style",
28
- "source": "a Photo",
29
- },
30
- }
31
-
32
  print(f"System starting on {DEVICE}...")
33
 
34
  # 1. LOAD MODELS
35
- # A. Retrieval Model (CLIP)
36
- clip_model, _ = clip.load("ViT-B/32", device=DEVICE)
37
 
38
- # B. Editing Model (CLIP-guided optimizer)
39
- clip_styler = ClipStyler(device=DEVICE, num_epochs=UI_EPOCHS)
40
 
41
  # 2. LOAD SEARCH INDEX
42
  print("Loading Search Index...")
@@ -78,25 +61,39 @@ def search_flickr(text_query):
78
 
79
  return results
80
 
81
- def edit_image(image, style_choice):
82
- if image is None or style_choice is None:
83
- return None
84
-
85
- preset = STYLE_PRESETS.get(style_choice)
86
- if preset is None:
87
- return image
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  try:
90
- return clip_styler.stylize(
91
- image=image,
92
- prompt=preset["prompt"],
93
- source=preset.get("source", "a Photo"),
94
- num_epochs=UI_EPOCHS,
95
- log_interval=0,
96
- )
97
- except Exception as exc:
98
- print(f"[edit_image] Failed to stylize image: {exc}")
99
  return image
 
 
 
 
 
 
 
 
 
100
 
101
  # --- THE UI ---
102
  with gr.Blocks() as demo:
@@ -118,11 +115,18 @@ with gr.Blocks() as demo:
118
  with gr.Row():
119
  im_in = gr.Image(type="pil", label="Input Image")
120
  style = gr.Dropdown(["Sketch", "Van Gogh", "Cyberpunk"], label="Choose Style")
 
 
 
 
 
 
 
121
 
122
  btn_edit = gr.Button("Apply")
123
  im_out = gr.Image(label="Result")
124
 
125
- btn_edit.click(edit_image, inputs=[im_in, style], outputs=im_out)
126
 
127
  if __name__ == "__main__":
128
  demo.launch(share=True)
 
4
  from torchvision import transforms
5
  from style_net import StyleNet
6
  from dataset import FlickrStreamer
7
+ from clip_styler import style_transfer
 
8
 
9
  # --- CONFIGURATION ---
10
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
11
  INDEX_PATH = "flickr_embeddings.pt"
12
  SEARCH_LIMIT = 5000 # Must match the indexer limit!
13
  # ---------------------
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  print(f"System starting on {DEVICE}...")
16
 
17
  # 1. LOAD MODELS
18
+ # A. Editing Model
19
+ #style_net = StyleNet().to(DEVICE)
20
 
21
+ # B. Retrieval Model (CLIP)
22
+ #clip_model, preprocess = clip.load("ViT-B/32", device=DEVICE)
23
 
24
  # 2. LOAD SEARCH INDEX
25
  print("Loading Search Index...")
 
61
 
62
  return results
63
 
64
+ # --- FUNCTION: EDIT ---
65
+ tf_edit = transforms.Compose([
66
+ transforms.Resize((256, 256)),
67
+ transforms.ToTensor()
68
+ ])
 
 
69
 
70
+ def edit_image(image, style_choice, num_steps):
71
+ if image is None: return None
72
+
73
+ weights_map = {
74
+ "Sketch": "model_sketch.pth",
75
+ "Van Gogh": "model_painting.pth",
76
+ "Cyberpunk": "model_cyberpunk.pth"
77
+ }
78
+ detail_prompt = {
79
+ "Sketch": "charcoal sketch",
80
+ "Van Gogh": "Van Gogh painting",
81
+ "Cyberpunk": "neon cyberpunk",
82
+ }
83
+ '''
84
  try:
85
+ style_net.load_state_dict(torch.load(weights_map.get(style_choice), map_location=DEVICE))
86
+ except Exception:
 
 
 
 
 
 
 
87
  return image
88
+
89
+ input_tensor = tf_edit(image).unsqueeze(0).to(DEVICE)
90
+ with torch.no_grad():
91
+ output_tensor = style_net(input_tensor)
92
+
93
+ output_img = transforms.ToPILImage()(output_tensor.squeeze().cpu())
94
+ return output_img
95
+ '''
96
+ return style_transfer(image, detail_prompt[style_choice], num_steps=num_steps)
97
 
98
  # --- THE UI ---
99
  with gr.Blocks() as demo:
 
115
  with gr.Row():
116
  im_in = gr.Image(type="pil", label="Input Image")
117
  style = gr.Dropdown(["Sketch", "Van Gogh", "Cyberpunk"], label="Choose Style")
118
+ steps_slider = gr.Slider(
119
+ minimum=50,
120
+ maximum=200,
121
+ value=100,
122
+ step=10,
123
+ label="Number of Steps (more = better quality but slower)"
124
+ )
125
 
126
  btn_edit = gr.Button("Apply")
127
  im_out = gr.Image(label="Result")
128
 
129
+ btn_edit.click(edit_image, inputs=[im_in, style, steps_slider], outputs=im_out)
130
 
131
  if __name__ == "__main__":
132
  demo.launch(share=True)
clip_styler.py CHANGED
@@ -1,212 +1,166 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional
4
-
5
- from PIL import Image
6
  import torch
7
- import torch.optim as optim
8
- from torchvision import transforms, models
9
- from torchvision.transforms.functional import adjust_contrast
 
10
  from transformers import CLIPModel, AutoTokenizer
 
11
 
12
- from cnn1 import UNet
13
- from util import (
14
- DEVICE,
15
- IMG_SIZE,
16
- LR,
17
- CROP_SIZE,
18
- RESIZE,
19
- NUM_EPOCHS,
20
- NUM_CROPS,
21
- PATCH_THRESHOLD,
22
- L_TV,
23
- L_PATCH,
24
- L_DIR,
25
- L_CONTENT,
26
- normalize,
27
- clip_normalize,
28
- get_features,
29
- prompt_ensemble,
30
- get_image_prior_losses,
31
- )
32
-
33
-
34
- class ClipStyler:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  """
36
- Wraps the CLIP-guided optimization loop from main.py so it can be reused
37
- by both the CLI entrypoint and the Gradio application.
 
 
 
 
 
 
 
38
  """
39
-
40
- def __init__(
41
- self,
42
- device: Optional[torch.device] = None,
43
- num_epochs: int = NUM_EPOCHS,
44
- ) -> None:
45
- self.device = device or DEVICE
46
- self.num_epochs = num_epochs
47
-
48
- # Text/image encoders
49
- self.clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(
50
- self.device
51
- )
52
- self.clip_model.eval()
53
- for param in self.clip_model.parameters():
54
- param.requires_grad_(False)
55
-
56
- self.tokenizer = AutoTokenizer.from_pretrained("openai/clip-vit-base-patch32")
57
-
58
- # Perceptual backbone
59
- self.vgg19 = models.vgg19(pretrained=True).features.to(self.device).eval()
60
- for param in self.vgg19.parameters():
61
- param.requires_grad_(False)
62
-
63
- # Image helpers
64
- self.image_transform = transforms.Compose(
65
- [transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor()]
66
- )
67
- self.to_pil = transforms.ToPILImage()
68
- self.crop_transform = transforms.RandomCrop(CROP_SIZE)
69
- self.augment_transform = transforms.Compose(
70
- [
71
- transforms.RandomPerspective(fill=0, p=1, distortion_scale=0.5),
72
- transforms.Resize(RESIZE),
73
- ]
74
- )
75
-
76
- def stylize(
77
- self,
78
- image: Image.Image,
79
- prompt: str,
80
- source: str = "a Photo",
81
- num_epochs: Optional[int] = None,
82
- log_interval: int = 20,
83
- ) -> Image.Image:
84
- """
85
- Run the CLIP-guided optimization loop and return the stylized PIL image.
86
- """
87
- if image is None:
88
- raise ValueError("Image must be provided for stylization.")
89
-
90
- epochs = num_epochs if num_epochs is not None else self.num_epochs
91
- base_img = self._pil_to_tensor(image)
92
- content_features = get_features(normalize(base_img), self.vgg19)
93
-
94
- cnn_model = UNet(text_dim=512).to(self.device)
95
- cnn_model.train()
96
-
97
- optimizer = optim.Adam(cnn_model.parameters(), lr=LR)
98
- scheduler = torch.optim.lr_scheduler.StepLR(
99
- optimizer, step_size=100, gamma=0.5
100
- )
101
-
102
- with torch.no_grad():
103
- text_features = self._encode_text(prompt)
104
- source_features = self._encode_text(source)
105
- base_img_features = self._image_features(base_img)
106
-
107
- style_direction = text_features - source_features
108
- style_direction /= style_direction.norm(dim=-1, keepdim=True)
109
-
110
- final_target = base_img
111
- for epoch in range(epochs + 1):
112
- scheduler.step()
113
- target = cnn_model(base_img, text_embedding=text_features)
114
- final_target = target
115
-
116
- target_features = get_features(normalize(target), self.vgg19)
117
- content_loss = torch.mean(
118
- (target_features["conv4_2"] - content_features["conv4_2"]) ** 2
119
- )
120
- content_loss += torch.mean(
121
- (target_features["conv5_2"] - content_features["conv5_2"]) ** 2
122
- )
123
-
124
- img_aug = self._augment_crops(target)
125
- img_aug_features = self.clip_model.get_image_features(
126
- pixel_values=clip_normalize(img_aug)
127
- )
128
- img_aug_features /= img_aug_features.norm(dim=-1, keepdim=True)
129
-
130
- img_direction = img_aug_features - base_img_features
131
- img_direction /= img_direction.norm(dim=-1, keepdim=True)
132
-
133
- patch_text_direction = style_direction.repeat(img_direction.size(0), 1)
134
- patch_text_direction /= patch_text_direction.norm(dim=-1, keepdim=True)
135
-
136
- loss_patch = 1 - torch.cosine_similarity(
137
- img_direction, patch_text_direction, dim=1
138
- )
139
- loss_patch = torch.where(
140
- loss_patch < PATCH_THRESHOLD, torch.zeros_like(loss_patch), loss_patch
141
- ).mean()
142
-
143
- glob_features = self.clip_model.get_image_features(
144
- pixel_values=clip_normalize(target)
145
- )
146
- glob_features /= glob_features.norm(dim=-1, keepdim=True)
147
-
148
- glob_direction = glob_features - base_img_features
149
- glob_direction /= glob_direction.norm(dim=-1, keepdim=True)
150
-
151
- loss_glob = (1 - torch.cosine_similarity(glob_direction, style_direction, dim=1)).mean()
152
-
153
- loss_tv = L_TV * get_image_prior_losses(target)
154
-
155
- total_loss = (
156
- (L_PATCH * loss_patch)
157
- + (L_DIR * loss_glob)
158
- + (L_CONTENT * content_loss)
159
- + loss_tv
160
- )
161
-
162
- optimizer.zero_grad()
163
- total_loss.backward()
164
- optimizer.step()
165
-
166
- '''
167
- if log_interval and epoch % log_interval == 0:
168
- print(
169
- f"[ClipStyler] Epoch {epoch}: total={total_loss.item():.4f}, "
170
- f"content={content_loss.item():.4f}, patch={loss_patch.item():.4f}, "
171
- f"dir={loss_glob.item():.4f}, tv={loss_tv.item():.4f}"
172
- )
173
- '''
174
-
175
- return self._tensor_to_pil(final_target)
176
-
177
- def _pil_to_tensor(self, image: Image.Image) -> torch.Tensor:
178
- if image.mode != "RGB":
179
- image = image.convert("RGB")
180
- tensor = self.image_transform(image).unsqueeze(0)
181
- return tensor.to(self.device)
182
-
183
- def _tensor_to_pil(self, tensor: torch.Tensor) -> Image.Image:
184
- tensor = tensor.detach().clone().cpu().clamp(0, 1).squeeze(0)
185
- tensor = adjust_contrast(tensor, 1.5)
186
- return self.to_pil(tensor)
187
-
188
- def _augment_crops(self, target: torch.Tensor) -> torch.Tensor:
189
- crops = []
190
- for _ in range(NUM_CROPS):
191
- crop = self.crop_transform(target)
192
- crop = self.augment_transform(crop)
193
- crops.append(crop)
194
- return torch.cat(crops, dim=0)
195
-
196
- @torch.no_grad()
197
- def _encode_text(self, text: str) -> torch.Tensor:
198
- prompts = prompt_ensemble(text)
199
- tokens = self.tokenizer(prompts, padding=True, return_tensors="pt").to(
200
- self.device
201
- )
202
- text_features = self.clip_model.get_text_features(**tokens)
203
- text_features = text_features.mean(dim=0, keepdim=True)
204
  text_features /= text_features.norm(dim=-1, keepdim=True)
205
- return text_features
206
-
207
- @torch.no_grad()
208
- def _image_features(self, tensor: torch.Tensor) -> torch.Tensor:
209
- feats = self.clip_model.get_image_features(pixel_values=clip_normalize(tensor))
210
- feats /= feats.norm(dim=-1, keepdim=True)
211
- return feats
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import torch
2
+ from PIL import Image
3
+ import torchvision.transforms as transforms
4
+ from util import *
5
+ from cnn1 import UNet
6
  from transformers import CLIPModel, AutoTokenizer
7
+ from torchvision.transforms.functional import adjust_contrast
8
 
9
+ clip_model = None
10
+ tokenizer = None
11
+ vgg19 = None
12
+ cnn_model = None
13
+
14
+ def load_models():
15
+ """Load models once and cache them"""
16
+ global clip_model, tokenizer, vgg19, cnn_model
17
+
18
+ if clip_model is None:
19
+ clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(DEVICE)
20
+
21
+ if tokenizer is None:
22
+ tokenizer = AutoTokenizer.from_pretrained("openai/clip-vit-base-patch32")
23
+
24
+ if vgg19 is None:
25
+ vgg19 = models.vgg19(pretrained=True).features.to(DEVICE)
26
+ for param in vgg19.parameters():
27
+ param.requires_grad_(False)
28
+
29
+ if cnn_model is None:
30
+ cnn_model = UNet(text_dim=512).to(DEVICE)
31
+
32
+ return clip_model, tokenizer, vgg19, cnn_model
33
+
34
+ def reset_cnn_model():
35
+ """Reset CNN model weights for each new image"""
36
+ global cnn_model
37
+ if cnn_model is not None:
38
+ # Reinitialize the model
39
+ for layer in cnn_model.modules():
40
+ if isinstance(layer, (torch.nn.Conv2d, torch.nn.Linear)):
41
+ torch.nn.init.xavier_uniform_(layer.weight)
42
+ if layer.bias is not None:
43
+ torch.nn.init.zeros_(layer.bias)
44
+
45
+ def style_transfer(img, prompt, num_steps=NUM_EPOCHS, source="a Photo"):
46
  """
47
+ Apply style transfer to an uploaded image
48
+
49
+ Args:
50
+ image: PIL Image uploaded by user
51
+ prompt: Text style prompt
52
+ num_steps: Number of optimization steps (fewer = faster, more = better quality)
53
+
54
+ Returns:
55
+ Stylized PIL Image
56
  """
57
+ if img is None:
58
+ return None
59
+
60
+ img = load_image(img).to(DEVICE)
61
+
62
+ # Load models
63
+ clip_model, tokenizer, vgg19, cnn_model = load_models()
64
+
65
+ # Reset CNN model for fresh start on each image
66
+ reset_cnn_model()
67
+
68
+ # image features
69
+ features = get_features(normalize(img), vgg19)
70
+
71
+ # cnn model
72
+ #cnn_model = UNet().to(DEVICE)
73
+
74
+ # load ADAM
75
+ optimizer = optim.Adam(cnn_model.parameters(), lr=LR)
76
+ scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=100, gamma=0.5)
77
+
78
+ # data augmentation
79
+ crop = transforms.Compose([transforms.RandomCrop(CROP_SIZE)])
80
+ augment = transforms.Compose([transforms.RandomPerspective(fill=0, p=1, distortion_scale=0.5), transforms.Resize(RESIZE)])
81
+
82
+ # initialize variables
83
+ content_loss_epoch = []
84
+ style_loss_epoch = []
85
+ total_loss_epoch = []
86
+ output_image = img
87
+ mean_img = torch.mean(img, dim=(2, 3), keepdim=False).squeeze(0)
88
+ mean_img = [mean_img[0].item(), mean_img[1].item(), mean_img[2].item()]
89
+ target = img.clone().requires_grad_(True).to(DEVICE)
90
+
91
+ with torch.no_grad():
92
+ edited_text = prompt_ensemble(prompt)
93
+ text_tokens = tokenizer(edited_text, padding=True, return_tensors="pt").to(DEVICE)
94
+ text_features = clip_model.get_text_features(**text_tokens)
95
+ text_features = text_features.mean(axis=0, keepdim=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  text_features /= text_features.norm(dim=-1, keepdim=True)
 
 
 
 
 
 
 
97
 
98
+ edited_source = prompt_ensemble(source)
99
+ source_tokens = tokenizer(edited_source, padding=True, return_tensors="pt").to(DEVICE)
100
+ source_features = clip_model.get_text_features(**source_tokens)
101
+ source_features = source_features.mean(axis=0, keepdim=True)
102
+ source_features /= source_features.norm(dim=-1, keepdim=True)
103
+
104
+ img_features = clip_model.get_image_features(pixel_values=clip_normalize(img))
105
+ img_features /= (img_features.clone().norm(dim=-1, keepdim=True))
106
+
107
+ for epoch in range(num_steps + 1):
108
+ scheduler.step()
109
+ #target = cnn_model(img).requires_grad_(True).to(DEVICE)
110
+ target = cnn_model(img, text_embedding=text_features).to(DEVICE)
111
+
112
+ target_features = get_features(normalize(target), vgg19)
113
+
114
+ content_loss = 0.0
115
+ content_loss += torch.mean((target_features['conv4_2'] - features['conv4_2']) ** 2)
116
+ content_loss += torch.mean((target_features['conv5_2'] - features['conv5_2']) ** 2)
117
+
118
+ loss_patch = 0
119
+ img_proc = []
120
+ for n in range(NUM_CROPS):
121
+ target_crop = crop(target)
122
+ target_crop = augment(target_crop)
123
+ img_proc.append(target_crop)
124
+
125
+ img_proc = torch.cat(img_proc, dim=0)
126
+ img_aug = img_proc
127
+
128
+ img_aug_features = clip_model.get_image_features(pixel_values=clip_normalize(img_aug))
129
+ img_aug_features /= (img_aug_features.clone().norm(dim=-1, keepdim=True))
130
+
131
+ img_direction = img_aug_features - img_features
132
+ img_direction /= img_direction.clone().norm(dim=-1, keepdim=True)
133
+
134
+ text_direction = (text_features - source_features).repeat(img_aug_features.size(0), 1)
135
+ text_direction /= text_direction.norm(dim=-1, keepdim=True)
136
+
137
+ loss_calc = (1 - torch.cosine_similarity(img_direction, text_direction, dim=1))
138
+ loss_calc[loss_calc < PATCH_THRESHOLD] = 0.0
139
+ loss_patch += loss_calc.mean()
140
+
141
+ glob_features = clip_model.get_image_features(pixel_values=clip_normalize(target))
142
+ glob_features /= (glob_features.clone().norm(dim=-1, keepdim=True))
143
+ glob_direction = glob_features - img_features
144
+ glob_direction /= glob_direction.clone().norm(dim=-1, keepdim=True)
145
+
146
+ loss_glob = (1 - torch.cosine_similarity(glob_direction, text_direction, dim=1)).mean()
147
+
148
+ loss_tv = L_TV * get_image_prior_losses(target)
149
+
150
+ total_loss = (L_PATCH * loss_patch) + (L_DIR * loss_glob) + (L_CONTENT * content_loss) + loss_tv
151
+ total_loss_epoch.append(total_loss)
152
+
153
+ optimizer.zero_grad()
154
+ total_loss.backward()
155
+ optimizer.step()
156
+
157
+ # Post-process output
158
+ output_image = target.clone().detach()
159
+ output_image = torch.clamp(output_image, 0, 1)
160
+ output_image = adjust_contrast(output_image, 1.5)
161
+
162
+ # Convert tensor back to PIL Image
163
+ output_image = output_image.squeeze(0).cpu()
164
+ output_image = transforms.ToPILImage()(output_image)
165
+
166
+ return output_image
util.py CHANGED
@@ -129,8 +129,7 @@ def clip_normalize(data):
129
  norm_data = (resized - mean) / std
130
  return norm_data
131
 
132
- def load_image(img_path):
133
- image = Image.open(img_path)
134
  image = image.resize((IMG_SIZE, IMG_SIZE))
135
  transform = transforms.Compose([transforms.ToTensor()])
136
  return transform(image)[:3, :, :].unsqueeze(0)
 
129
  norm_data = (resized - mean) / std
130
  return norm_data
131
 
132
+ def load_image(image):
 
133
  image = image.resize((IMG_SIZE, IMG_SIZE))
134
  transform = transforms.Compose([transforms.ToTensor()])
135
  return transform(image)[:3, :, :].unsqueeze(0)