alexscottcodes commited on
Commit
7f57474
·
1 Parent(s): fd95c6d

Add app files.

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +315 -0
  3. requirements.txt +5 -0
README.md CHANGED
@@ -11,4 +11,4 @@ license: apache-2.0
11
  short_description: Lightweight style transfer, that can work on CPU.
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
11
  short_description: Lightweight style transfer, that can work on CPU.
12
  ---
13
 
14
+ The local script is optimized for CPU but is still pretty slow. If you're low on time, it works in my cloud, which I provide for free (up to a point. Eventually I run out of my limited cloud credits. Hopefully I can get a grant from the folks at HuggingFace.).
app.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import torch.nn as nn
4
+ import torchvision.transforms as transforms
5
+ import torchvision.models as models
6
+ from PIL import Image
7
+ import numpy as np
8
+ import os
9
+ import requests
10
+ import base64
11
+ import io
12
+
13
+ # CPU optimization: Disable CUDA and use optimized CPU threads
14
+ torch.set_num_threads(4) # Adjust based on your CPU
15
+ device = torch.device("cpu")
16
+
17
+ # Get QuickCloud API URL from environment variable
18
+ QUICKCLOUD_API_URL = os.environ.get("QUICKCLOUD_API_URL", "")
19
+
20
+ class LightingStyleTransfer:
21
+ def __init__(self):
22
+ # Use VGG16 for feature extraction (lighter than VGG19)
23
+ vgg = models.vgg16(pretrained=True).features.to(device).eval()
24
+
25
+ # Freeze parameters for CPU efficiency
26
+ for param in vgg.parameters():
27
+ param.requires_grad = False
28
+
29
+ self.model = vgg
30
+
31
+ # Layer indices for content and style
32
+ self.style_layers = [0, 5, 10, 17] # Reduced layers for CPU
33
+ self.content_layers = [17]
34
+
35
+ def preprocess(self, img, max_size=512):
36
+ """Resize and normalize image - smaller size for CPU"""
37
+ # CPU optimization: Use smaller image size
38
+ w, h = img.size
39
+ scale = max_size / max(w, h)
40
+ new_size = (int(w * scale), int(h * scale))
41
+
42
+ img = img.resize(new_size, Image.LANCZOS)
43
+
44
+ transform = transforms.Compose([
45
+ transforms.ToTensor(),
46
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
47
+ std=[0.229, 0.224, 0.225])
48
+ ])
49
+
50
+ return transform(img).unsqueeze(0).to(device)
51
+
52
+ def deprocess(self, tensor):
53
+ """Convert tensor back to image"""
54
+ img = tensor.cpu().clone().squeeze(0)
55
+ img = img.clamp(0, 1)
56
+ img = transforms.ToPILImage()(img)
57
+ return img
58
+
59
+ def gram_matrix(self, tensor):
60
+ """Compute Gram matrix for style representation"""
61
+ b, c, h, w = tensor.size()
62
+ features = tensor.view(b * c, h * w)
63
+ G = torch.mm(features, features.t())
64
+ return G.div(b * c * h * w)
65
+
66
+ def get_features(self, image):
67
+ """Extract features from specified layers"""
68
+ features = {}
69
+ x = image
70
+
71
+ for idx, layer in enumerate(self.model):
72
+ x = layer(x)
73
+ if idx in self.style_layers:
74
+ features[f'style_{idx}'] = x
75
+ if idx in self.content_layers:
76
+ features[f'content_{idx}'] = x
77
+
78
+ return features
79
+
80
+ def transfer(self, content_img, style_img, steps=150, style_weight=1e6,
81
+ content_weight=1):
82
+ """Perform lighting style transfer"""
83
+ # Preprocess images
84
+ content = self.preprocess(content_img)
85
+ style = self.preprocess(style_img)
86
+
87
+ # Initialize target as content image
88
+ target = content.clone().requires_grad_(True)
89
+
90
+ # Get features
91
+ content_features = self.get_features(content)
92
+ style_features = self.get_features(style)
93
+
94
+ # Compute style gram matrices
95
+ style_grams = {k: self.gram_matrix(v) for k, v in style_features.items()
96
+ if 'style' in k}
97
+
98
+ # CPU optimization: Use LBFGS optimizer (faster convergence)
99
+ optimizer = torch.optim.LBFGS([target], max_iter=20)
100
+
101
+ step = [0]
102
+
103
+ def closure():
104
+ target.data.clamp_(0, 1)
105
+ optimizer.zero_grad()
106
+
107
+ target_features = self.get_features(target)
108
+
109
+ # Content loss
110
+ content_loss = 0
111
+ for k in content_features:
112
+ if 'content' in k:
113
+ content_loss += torch.mean((target_features[k] -
114
+ content_features[k]) ** 2)
115
+
116
+ # Style loss
117
+ style_loss = 0
118
+ for k in style_grams:
119
+ target_gram = self.gram_matrix(target_features[k])
120
+ style_loss += torch.mean((target_gram - style_grams[k]) ** 2)
121
+
122
+ # Total loss
123
+ total_loss = content_weight * content_loss + style_weight * style_loss
124
+ total_loss.backward()
125
+
126
+ step[0] += 1
127
+ if step[0] % 30 == 0:
128
+ print(f"Step {step[0]}, Loss: {total_loss.item():.2f}")
129
+
130
+ return total_loss
131
+
132
+ # Optimization loop
133
+ epochs = steps // 20 # LBFGS takes ~20 iterations per step
134
+ for i in range(epochs):
135
+ optimizer.step(closure)
136
+
137
+ if step[0] >= steps:
138
+ break
139
+
140
+ # Final clamp and return
141
+ target.data.clamp_(0, 1)
142
+ return self.deprocess(target)
143
+
144
+ def process_with_quickcloud(content_img, style_img, steps, style_strength):
145
+ """Process using QuickCloud API (powered by Modal.com)"""
146
+ if not QUICKCLOUD_API_URL:
147
+ return None, "❌ QuickCloud API URL not configured. Please set QUICKCLOUD_API_URL environment variable."
148
+
149
+ try:
150
+ # Convert PIL images to bytes
151
+ content_bytes = io.BytesIO()
152
+ style_bytes = io.BytesIO()
153
+
154
+ content_img.save(content_bytes, format='PNG')
155
+ style_img.save(style_bytes, format='PNG')
156
+
157
+ # Encode to base64
158
+ content_b64 = base64.b64encode(content_bytes.getvalue()).decode()
159
+ style_b64 = base64.b64encode(style_bytes.getvalue()).decode()
160
+
161
+ # Prepare request
162
+ payload = {
163
+ "content_image": content_b64,
164
+ "style_image": style_b64,
165
+ "steps": steps,
166
+ "style_weight": style_strength * 1e6,
167
+ "content_weight": 1.0,
168
+ "learning_rate": 0.03
169
+ }
170
+
171
+ print("Sending request to NamelessAI QuickCloud (H100 GPU)...")
172
+
173
+ # Make API request
174
+ response = requests.post(QUICKCLOUD_API_URL, json=payload, timeout=300)
175
+ response.raise_for_status()
176
+
177
+ # Decode result
178
+ result_data = response.json()
179
+ result_bytes = base64.b64decode(result_data["result_image"])
180
+ result_img = Image.open(io.BytesIO(result_bytes))
181
+
182
+ return result_img, "✅ Processing complete via QuickCloud (H100 GPU)!"
183
+
184
+ except requests.exceptions.Timeout:
185
+ return None, "❌ Request timed out. Please try again."
186
+ except requests.exceptions.RequestException as e:
187
+ return None, f"❌ API Error: {str(e)}"
188
+ except Exception as e:
189
+ return None, f"❌ Error: {str(e)}"
190
+
191
+ def process_locally(content_img, style_img, steps, style_strength):
192
+ """Process using local CPU"""
193
+ try:
194
+ # Adjust style weight
195
+ style_weight = style_strength * 1e6
196
+
197
+ # Perform transfer
198
+ result = style_transfer.transfer(
199
+ content_img,
200
+ style_img,
201
+ steps=steps,
202
+ style_weight=style_weight,
203
+ content_weight=1
204
+ )
205
+
206
+ return result, "✅ Processing complete via Local CPU!"
207
+
208
+ except Exception as e:
209
+ return None, f"❌ Error: {str(e)}"
210
+
211
+ def process_images(content_img, style_img, steps, style_strength, use_quickcloud):
212
+ """Process the style transfer based on selected mode"""
213
+ if content_img is None or style_img is None:
214
+ return None, "⚠️ Please upload both content and style images."
215
+
216
+ # Convert to PIL if needed
217
+ if isinstance(content_img, np.ndarray):
218
+ content_img = Image.fromarray(content_img)
219
+ if isinstance(style_img, np.ndarray):
220
+ style_img = Image.fromarray(style_img)
221
+
222
+ if use_quickcloud:
223
+ return process_with_quickcloud(content_img, style_img, steps, style_strength)
224
+ else:
225
+ return process_locally(content_img, style_img, steps, style_strength)
226
+
227
+ # Initialize local model (done once at startup)
228
+ print("Loading local model... This may take a moment.")
229
+ style_transfer = LightingStyleTransfer()
230
+ print("Local model loaded successfully!")
231
+
232
+ # Check if QuickCloud is available
233
+ quickcloud_available = bool(QUICKCLOUD_API_URL)
234
+ if quickcloud_available:
235
+ print(f"✓ QuickCloud API configured and available")
236
+ else:
237
+ print("✗ QuickCloud API not configured (set QUICKCLOUD_API_URL environment variable)")
238
+
239
+ # Create Gradio interface
240
+ with gr.Blocks(title="AI Lighting Style Transfer") as demo:
241
+ gr.Markdown("""
242
+ # 🎨 AI-Powered Lighting Style Transfer
243
+
244
+ Transfer the lighting and color style from one image to another using neural style transfer.
245
+
246
+ ## Processing Options:
247
+ - **Local (CPU)**: Runs on your machine. Takes 1-3 minutes. Free.
248
+ - **NamelessAI QuickCloud**: Runs on H100 GPU cloud. Takes 5-10 seconds. Requires API key.
249
+ - *Powered by Modal.com*
250
+
251
+ ## How to use:
252
+ 1. Upload your **content image** (the image you want to transform)
253
+ 2. Upload your **style image** (the image whose lighting you want to copy)
254
+ 3. Choose processing mode (Local or QuickCloud)
255
+ 4. Adjust settings if desired
256
+ 5. Click "Transfer Style" and wait for processing
257
+ """)
258
+
259
+ with gr.Row():
260
+ with gr.Column():
261
+ content_input = gr.Image(label="Content Image", type="pil")
262
+ style_input = gr.Image(label="Style Image", type="pil")
263
+
264
+ with gr.Column():
265
+ output = gr.Image(label="Result")
266
+ status_text = gr.Textbox(label="Status", interactive=False)
267
+
268
+ with gr.Row():
269
+ use_quickcloud = gr.Checkbox(
270
+ label="Use NamelessAI QuickCloud (H100 GPU - Powered by Modal.com)",
271
+ value=False,
272
+ interactive=quickcloud_available,
273
+ info="5-10 seconds vs 1-3 minutes locally" if quickcloud_available else "API URL not configured"
274
+ )
275
+
276
+ with gr.Row():
277
+ steps_slider = gr.Slider(
278
+ minimum=50,
279
+ maximum=300,
280
+ value=150,
281
+ step=10,
282
+ label="Optimization Steps (more = better quality, slower)"
283
+ )
284
+
285
+ style_strength = gr.Slider(
286
+ minimum=0.5,
287
+ maximum=3.0,
288
+ value=1.0,
289
+ step=0.1,
290
+ label="Style Strength"
291
+ )
292
+
293
+ transfer_btn = gr.Button("Transfer Style", variant="primary", size="lg")
294
+
295
+ gr.Markdown("""
296
+ ### Tips:
297
+ - **Local Mode**: Images resized to 512px, use 100-150 steps for balance
298
+ - **QuickCloud Mode**: Handles 1024px images, 300 steps recommended for best quality
299
+ - Increase style strength for more dramatic lighting effects
300
+ - Works best with images that have distinct lighting patterns
301
+
302
+ ### QuickCloud Setup:
303
+ To use QuickCloud, set the `QUICKCLOUD_API_URL` environment variable to your Modal API endpoint.
304
+ """)
305
+
306
+ # Set up the button click
307
+ transfer_btn.click(
308
+ fn=process_images,
309
+ inputs=[content_input, style_input, steps_slider, style_strength, use_quickcloud],
310
+ outputs=[output, status_text]
311
+ )
312
+
313
+ # Launch the app
314
+ if __name__ == "__main__":
315
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio
2
+ torch
3
+ torchvision
4
+ pillow
5
+ numpy