ApaCu commited on
Commit
705d763
·
verified ·
1 Parent(s): eac6c90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +312 -206
app.py CHANGED
@@ -27,18 +27,24 @@ logger = logging.getLogger(__name__)
27
  # Install required packages at runtime for Hugging Face Spaces
28
  def install_dependencies():
29
  logger.info("Checking and installing dependencies...")
30
-
31
  packages_to_install = [
32
  "opencv-python",
33
  "opencv-contrib-python", # For dnn_superres module
34
  "numpy",
35
  "pillow",
36
- "torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu",
37
- "facexlib",
38
- "basicsr",
39
  "gfpgan",
40
- "realesrgan"
 
41
  ]
 
 
 
 
 
42
 
43
  for package in packages_to_install:
44
  try:
@@ -46,17 +52,34 @@ def install_dependencies():
46
  subprocess.check_call([sys.executable, "-m", "pip", "install", package])
47
  except Exception as e:
48
  logger.warning(f"Error installing {package}: {str(e)}")
49
-
50
  logger.info("Dependencies installation complete")
51
 
52
  # Try to install dependencies on startup
53
  try:
54
  install_dependencies()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  time.sleep(2) # Give some time for packages to settle
56
  except Exception as e:
57
- logger.error(f"Failed to install dependencies: {str(e)}")
58
 
59
- # Check for GPU or CPU
60
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
61
  logger.info(f"Using device: {device}")
62
 
@@ -79,7 +102,7 @@ MODEL_OPTIONS = {
79
  "filename": "GFPGANv1.4.pth",
80
  "type": "face",
81
  "method": "gfpgan",
82
- "scale": 1
83
  },
84
  "HDR Enhancement": {
85
  "type": "hdr",
@@ -94,134 +117,156 @@ model_cache = {}
94
  # Function to load the selected model with robust fallbacks
95
  def load_model(model_name):
96
  global model_cache
97
-
98
  # Return cached model if available
99
  if model_name in model_cache:
100
  logger.info(f"Using cached model: {model_name}")
101
  return model_cache[model_name]
102
-
103
  logger.info(f"Loading model: {model_name}")
104
  config = MODEL_OPTIONS.get(model_name)
105
  if not config:
106
  return None, f"Model {model_name} not found in configuration"
107
-
108
  model_type = config["type"]
109
-
110
  try:
111
- # OpenCV based models (always available as fallback)
112
  if config["method"] == "opencv":
113
  logger.info("Loading OpenCV Super Resolution model")
114
- sr = cv2.dnn_superres.DnnSuperResImpl_create()
115
-
116
- # Use EDSR as default model
117
- model_path = hf_hub_download(
118
- repo_id="eugenesiow/edsr",
119
- filename="EDSR_x4.pb",
120
- cache_dir=CACHE_DIR
121
- )
122
-
123
- sr.readModel(model_path)
124
- sr.setModel("edsr", 4)
125
-
126
- # Set backend to cuda if available
127
- if torch.cuda.is_available():
128
- sr.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
129
- sr.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
130
-
131
- model_cache[model_name] = (sr, model_type)
132
- return sr, model_type
133
-
 
 
 
 
 
 
134
  # Real-ESRGAN models
135
  elif config["method"] == "realesrgan":
 
 
 
 
136
  try:
137
- from realesrgan import RealESRGAN
138
  logger.info("Loading Real-ESRGAN model")
139
-
140
  model_path = hf_hub_download(
141
  repo_id=config["repo_id"],
142
  filename=config["filename"],
143
  cache_dir=CACHE_DIR
144
  )
145
-
 
146
  model = RealESRGAN(device, scale=config["scale"])
147
  model.load_weights(model_path)
148
-
149
  model_cache[model_name] = (model, model_type)
150
  return model, model_type
151
- except ImportError:
152
- logger.warning("RealESRGAN not available, falling back to OpenCV")
153
- return load_model("OpenCV Super Resolution")
154
-
 
155
  # GFPGAN for face enhancement
156
  elif config["method"] == "gfpgan":
 
 
 
 
157
  try:
158
- from gfpgan import GFPGANer
159
  logger.info("Loading GFPGAN model")
160
-
161
  model_path = hf_hub_download(
162
  repo_id=config["repo_id"],
163
  filename=config["filename"],
164
  cache_dir=CACHE_DIR
165
  )
166
-
 
 
 
 
167
  face_enhancer = GFPGANer(
168
  model_path=model_path,
169
- upscale=config["scale"],
170
- arch='clean',
171
  channel_multiplier=2,
172
- bg_upsampler=None
173
  )
174
-
175
  model_cache[model_name] = (face_enhancer, model_type)
176
  return face_enhancer, model_type
177
- except ImportError:
178
- logger.warning("GFPGAN not available, falling back to OpenCV")
179
- return load_model("OpenCV Super Resolution")
180
-
 
181
  # HDR Enhancement (custom implementation)
182
  elif config["method"] == "custom":
183
  # No model to load for custom HDR
184
  model_cache[model_name] = (None, model_type)
185
  return None, model_type
186
-
187
  else:
188
- raise ValueError(f"Unknown model method: {config['method']}")
189
-
190
  except Exception as e:
191
- logger.error(f"Error loading model {model_name}: {str(e)}")
192
  import traceback
193
  traceback.print_exc()
194
-
195
- # Always provide a fallback method
196
  if model_name != "OpenCV Super Resolution":
197
- logger.info("Falling back to OpenCV Super Resolution")
198
  return load_model("OpenCV Super Resolution")
199
  else:
200
- return None, f"Failed to load model: {str(e)}"
 
 
201
 
202
  # Function to preprocess image for processing
203
  def preprocess_image(image):
204
  """Convert PIL image to numpy array for processing"""
205
  if image is None:
206
  return None
207
-
208
  if isinstance(image, Image.Image):
209
  # Convert PIL image to numpy array
210
  img = np.array(image)
211
  else:
 
212
  img = image
213
-
214
  # Handle grayscale images by converting to RGB
215
  if len(img.shape) == 2:
216
  img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
217
-
218
  # Handle RGBA images by removing alpha channel
219
  if img.shape[2] == 4:
220
  img = img[:, :, :3]
221
-
222
  # Convert RGB to BGR for OpenCV processing
223
  img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
224
-
225
  return img_bgr
226
 
227
  # Function to postprocess image for display
@@ -229,14 +274,16 @@ def postprocess_image(img_bgr):
229
  """Convert processed BGR image back to RGB PIL image"""
230
  if img_bgr is None:
231
  return None
232
-
233
  # Ensure image is uint8
234
  if img_bgr.dtype != np.uint8:
235
- img_bgr = np.clip(img_bgr, 0, 255).astype(np.uint8)
236
-
 
 
237
  # Convert BGR to RGB for PIL
238
  img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
239
-
240
  return Image.fromarray(img_rgb)
241
 
242
  # HDR enhancement function
@@ -244,152 +291,199 @@ def enhance_hdr(img_bgr, strength=1.0):
244
  """Custom HDR enhancement using OpenCV"""
245
  # Convert BGR to RGB
246
  img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
247
-
248
- # Convert to float32 for processing
249
  img_float = img_rgb.astype(np.float32) / 255.0
250
-
251
- # Convert to LAB color space for better contrast enhancement
252
- img_lab = cv2.cvtColor(img_float, cv2.COLOR_RGB2LAB)
253
- l, a, b = cv2.split(img_lab)
254
-
255
- # Apply CLAHE (Contrast Limited Adaptive Histogram Equalization)
256
- clahe = cv2.createCLAHE(clipLimit=3.0 * strength, tileGridSize=(8, 8))
257
- l_enhanced = clahe.apply(np.clip(l * 255, 0, 255).astype(np.uint8)) / 255.0
258
-
259
- # Blend original and enhanced L channel
260
- l = l * (1 - strength) + l_enhanced * strength
261
-
262
- # Merge channels
263
- img_lab_enhanced = cv2.merge([l, a, b])
264
- img_rgb_enhanced = cv2.cvtColor(img_lab_enhanced, cv2.COLOR_LAB2RGB)
265
-
266
- # Add vibrance (increase saturation of low-saturation areas)
267
- hsv = cv2.cvtColor(img_rgb_enhanced, cv2.COLOR_RGB2HSV)
268
- h, s, v = cv2.split(hsv)
269
-
270
- # Increase saturation adaptively (more for lower saturation, less for already saturated pixels)
271
- saturation_factor = 0.3 * strength
272
- s_enhanced = np.clip(s * (1 + saturation_factor * (1 - s)), 0, 1)
273
-
274
- # Increase brightness slightly
275
- v_enhanced = np.clip(v * (1 + 0.1 * strength), 0, 1)
276
-
277
- # Merge HSV channels and convert back to RGB
278
- hsv_enhanced = cv2.merge([h, s_enhanced, v_enhanced])
279
- img_enhanced = cv2.cvtColor(hsv_enhanced, cv2.COLOR_HSV2RGB)
280
-
281
- # Apply subtle detail enhancement
282
- kernel_size = 5
283
- blur = cv2.GaussianBlur(img_enhanced, (kernel_size, kernel_size), 0)
284
- detail = img_enhanced - blur
285
- img_enhanced = np.clip(img_enhanced + detail * (0.5 * strength), 0, 1)
286
-
287
- # Convert back to BGR for output
288
- img_enhanced = (img_enhanced * 255).astype(np.uint8)
289
- img_bgr_enhanced = cv2.cvtColor(img_enhanced, cv2.COLOR_RGB2BGR)
290
-
291
- return img_bgr_enhanced
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  # Main image enhancement function
294
  def enhance_image(image, model_name, strength=1.0, denoise=0.0, sharpen=0.0):
295
  """Enhance image using selected model with additional processing options"""
296
  if image is None:
297
  return "Please upload an image.", None
298
-
299
  try:
300
  # Load model
301
- model, model_type = load_model(model_name)
302
- if isinstance(model_type, str) and model_type.startswith("Failed"):
303
- return model_type, None
304
-
 
 
 
305
  # Preprocess image
306
  img_bgr = preprocess_image(image)
307
  if img_bgr is None:
308
  return "Failed to process image", None
309
-
310
  # Apply denoising if requested
311
  if denoise > 0:
312
- strength_value = int(denoise * 10)
 
 
 
313
  img_bgr = cv2.fastNlMeansDenoisingColored(
314
- img_bgr, None,
315
- h=strength_value,
316
- hColor=strength_value,
317
- templateWindowSize=7,
318
  searchWindowSize=21
319
  )
320
-
 
 
321
  # Process based on model type
322
  if model_type == "upscale":
 
 
323
  logger.info(f"Upscaling image with {model_name}")
324
 
325
  if model_name == "OpenCV Super Resolution":
326
  # OpenCV super resolution
327
  output_bgr = model.upsample(img_bgr)
328
-
329
  elif model_name == "Real-ESRGAN-x4":
330
  # Real-ESRGAN upscaling
331
- try:
332
- output_bgr = model.predict(img_bgr)
333
- except Exception as e:
334
- logger.error(f"Error with Real-ESRGAN: {str(e)}")
335
- # Fall back to OpenCV
336
- fallback_model, _ = load_model("OpenCV Super Resolution")
337
- output_bgr = fallback_model.upsample(img_bgr)
338
-
339
- else:
340
- # Default to OpenCV upscaling
341
- sr = cv2.dnn_superres.DnnSuperResImpl_create()
342
- sr.upsample(img_bgr)
343
-
344
  elif model_type == "face":
 
 
345
  logger.info(f"Enhancing face with {model_name}")
346
-
347
  if model_name == "GFPGAN (Face Enhancement)":
 
348
  try:
349
  # GFPGAN returns (cropped_faces, restored_faces, restored_img)
 
350
  _, _, output_bgr = model.enhance(
351
- img_bgr,
352
- has_aligned=False,
353
- only_center_face=False,
354
  paste_back=True
355
  )
356
  except Exception as e:
357
- logger.error(f"Error with GFPGAN: {str(e)}")
358
- # Fall back to basic upscaling
359
- fallback_model, _ = load_model("OpenCV Super Resolution")
360
- output_bgr = fallback_model.upsample(img_bgr)
361
- else:
362
- # Default upscaling for face if specific model fails
363
- sr = cv2.dnn_superres.DnnSuperResImpl_create()
364
- output_bgr = sr.upsample(img_bgr)
365
-
366
  elif model_type == "hdr":
367
- logger.info("Applying HDR enhancement")
368
- # Custom HDR enhancement
369
  output_bgr = enhance_hdr(img_bgr, strength=strength)
370
-
371
  else:
372
- return f"Unknown model type: {model_type}", None
373
-
374
- # Apply sharpening if requested
 
 
375
  if sharpen > 0:
376
- sharpen_kernel = np.array([
377
- [-1, -1, -1],
378
- [-1, 9 + sharpen * 2, -1],
379
- [-1, -1, -1]
380
- ])
381
- output_bgr = cv2.filter2D(output_bgr, -1, sharpen_kernel)
382
-
 
 
 
 
 
 
 
383
  # Post-process and return image
384
  enhanced_image = postprocess_image(output_bgr)
385
-
386
  return "Image enhanced successfully!", enhanced_image
387
-
388
  except Exception as e:
389
- logger.error(f"Error processing image: {str(e)}")
390
  import traceback
391
  traceback.print_exc()
392
- return f"Error: {str(e)}", None
 
 
 
 
 
 
 
 
 
 
393
 
394
  # Gradio interface
395
  with gr.Blocks(title="Image Upscale & Enhancement - By FebryEnsz") as demo:
@@ -397,93 +491,105 @@ with gr.Blocks(title="Image Upscale & Enhancement - By FebryEnsz") as demo:
397
  """
398
  # 🖼️ Image Upscale & Enhancement
399
  ### By FebryEnsz
400
-
401
  Upload an image and enhance it with AI-powered upscaling and enhancement.
402
-
403
  **Features:**
404
- - Super-resolution upscaling (4x)
405
- - Face enhancement for portraits
406
  - HDR enhancement for better contrast and details
 
407
  """
408
  )
409
-
410
  with gr.Row():
411
  with gr.Column(scale=1):
412
- image_input = gr.Image(label="Upload Image", type="pil")
413
 
414
- with gr.Box():
 
415
  gr.Markdown("### Enhancement Options")
416
  model_choice = gr.Dropdown(
417
  choices=list(MODEL_OPTIONS.keys()),
418
  label="Model Selection",
419
- value="OpenCV Super Resolution"
 
420
  )
421
-
422
  with gr.Accordion("Advanced Settings", open=False):
 
423
  strength_slider = gr.Slider(
424
  minimum=0.1,
425
  maximum=1.0,
426
- step=0.1,
427
- label="Enhancement Strength",
428
  value=0.8,
 
429
  )
430
-
431
  denoise_slider = gr.Slider(
432
  minimum=0.0,
433
  maximum=1.0,
434
- step=0.1,
435
- label="Noise Reduction",
436
  value=0.0,
437
  )
438
-
439
  sharpen_slider = gr.Slider(
440
  minimum=0.0,
441
  maximum=1.0,
442
- step=0.1,
443
- label="Sharpening",
444
  value=0.0,
445
  )
446
-
447
  enhance_button = gr.Button("✨ Enhance Image", variant="primary")
448
-
449
  with gr.Column(scale=1):
450
  output_text = gr.Textbox(label="Status")
451
- output_image = gr.Image(label="Enhanced Image")
452
-
453
  # Handle model change to update UI
 
454
  def on_model_change(model_name):
455
  model_config = MODEL_OPTIONS.get(model_name, {})
456
  model_type = model_config.get("type", "")
457
-
458
- # Update UI based on model type
459
  if model_type == "hdr":
460
- return gr.update(visible=True, label="HDR Intensity")
461
  elif model_type == "face":
462
- return gr.update(visible=True, label="Enhancement Strength")
 
 
463
  else:
464
- return gr.update(visible=True, label="Enhancement Strength")
465
-
466
  model_choice.change(on_model_change, inputs=[model_choice], outputs=[strength_slider])
467
-
468
  # Connect button to function
469
  enhance_button.click(
470
  fn=enhance_image,
471
  inputs=[image_input, model_choice, strength_slider, denoise_slider, sharpen_slider],
472
- outputs=[output_text, output_image]
 
473
  )
474
-
475
  # Footer information
476
  gr.Markdown(
477
  """
478
  ### Tips
479
- - For best results with face enhancement, ensure faces are clearly visible
480
- - HDR enhancement works best with images that have both bright and dark areas
481
- - For noisy images, try increasing the noise reduction slider
482
-
 
483
  ---
484
- Version 2.0 | Running on: """ + ("GPU 🚀" if torch.cuda.is_available() else "CPU ⚙️")
 
485
  )
486
 
487
  # Launch the app
488
  if __name__ == "__main__":
489
- demo.launch()
 
 
 
27
  # Install required packages at runtime for Hugging Face Spaces
28
  def install_dependencies():
29
  logger.info("Checking and installing dependencies...")
30
+
31
  packages_to_install = [
32
  "opencv-python",
33
  "opencv-contrib-python", # For dnn_superres module
34
  "numpy",
35
  "pillow",
36
+ "torch torchvision torchaudio", # Let pip handle the specific wheels
37
+ "facexlib", # Dependency for GFPGAN
38
+ "basicsr", # Dependency for RealESRGAN/GFPGAN
39
  "gfpgan",
40
+ "realesrgan",
41
+ "huggingface_hub" # Ensure hf_hub_download is available
42
  ]
43
+
44
+ # Use a standard index-url or let pip find the best one
45
+ # Forcing CPU might prevent GPU usage if available
46
+ # Let's try without forcing CPU first, Hugging Face Spaces often handles this.
47
+ # If you specifically need CPU only, you might re-add --index-url https://download.pytorch.org/whl/cpu
48
 
49
  for package in packages_to_install:
50
  try:
 
52
  subprocess.check_call([sys.executable, "-m", "pip", "install", package])
53
  except Exception as e:
54
  logger.warning(f"Error installing {package}: {str(e)}")
55
+
56
  logger.info("Dependencies installation complete")
57
 
58
  # Try to install dependencies on startup
59
  try:
60
  install_dependencies()
61
+ # Import libraries AFTER installation
62
+ import cv2
63
+ import torch
64
+ import numpy as np
65
+ from PIL import Image, ImageEnhance
66
+ from huggingface_hub import hf_hub_download
67
+ try:
68
+ from realesrgan import RealESRGAN
69
+ except ImportError:
70
+ logger.warning("RealESRGAN import failed after installation attempt.")
71
+ RealESRGAN = None # Set to None if import fails
72
+ try:
73
+ from gfpgan import GFPGANer
74
+ except ImportError:
75
+ logger.warning("GFPGANer import failed after installation attempt.")
76
+ GFPGANer = None # Set to None if import fails
77
+
78
  time.sleep(2) # Give some time for packages to settle
79
  except Exception as e:
80
+ logger.error(f"Failed to install dependencies or import libraries: {str(e)}")
81
 
82
+ # Check for GPU or CPU AFTER torch is potentially installed
83
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
84
  logger.info(f"Using device: {device}")
85
 
 
102
  "filename": "GFPGANv1.4.pth",
103
  "type": "face",
104
  "method": "gfpgan",
105
+ "scale": 1 # GFPGAN is primarily for face restoration, upscaling is secondary/handled by bg_upsampler
106
  },
107
  "HDR Enhancement": {
108
  "type": "hdr",
 
117
  # Function to load the selected model with robust fallbacks
118
  def load_model(model_name):
119
  global model_cache
120
+
121
  # Return cached model if available
122
  if model_name in model_cache:
123
  logger.info(f"Using cached model: {model_name}")
124
  return model_cache[model_name]
125
+
126
  logger.info(f"Loading model: {model_name}")
127
  config = MODEL_OPTIONS.get(model_name)
128
  if not config:
129
  return None, f"Model {model_name} not found in configuration"
130
+
131
  model_type = config["type"]
132
+
133
  try:
134
+ # OpenCV based models (always available as fallback if opencv-contrib is installed)
135
  if config["method"] == "opencv":
136
  logger.info("Loading OpenCV Super Resolution model")
137
+ try:
138
+ sr = cv2.dnn_superres.DnnSuperResImpl_create()
139
+
140
+ # Use EDSR as default model
141
+ model_path = hf_hub_download(
142
+ repo_id="eugenesiow/edsr",
143
+ filename="EDSR_x4.pb",
144
+ cache_dir=CACHE_DIR
145
+ )
146
+
147
+ sr.readModel(model_path)
148
+ sr.setModel("edsr", 4)
149
+
150
+ # Set backend to cuda if available
151
+ if torch.cuda.is_available():
152
+ sr.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
153
+ sr.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
154
+
155
+ model_cache[model_name] = (sr, model_type)
156
+ return sr, model_type
157
+ except Exception as e:
158
+ logger.error(f"Error loading OpenCV SR model: {str(e)}")
159
+ # Fallback to None if OpenCV SR fails
160
+ return None, f"Failed to load OpenCV SR model: {str(e)}"
161
+
162
+
163
  # Real-ESRGAN models
164
  elif config["method"] == "realesrgan":
165
+ if RealESRGAN is None:
166
+ logger.warning("RealESRGAN class not found, falling back to OpenCV SR.")
167
+ return load_model("OpenCV Super Resolution") # Fallback
168
+
169
  try:
 
170
  logger.info("Loading Real-ESRGAN model")
171
+
172
  model_path = hf_hub_download(
173
  repo_id=config["repo_id"],
174
  filename=config["filename"],
175
  cache_dir=CACHE_DIR
176
  )
177
+
178
+ # Initialize RealESRGAN with the correct device
179
  model = RealESRGAN(device, scale=config["scale"])
180
  model.load_weights(model_path)
181
+
182
  model_cache[model_name] = (model, model_type)
183
  return model, model_type
184
+ except Exception as e:
185
+ logger.error(f"Error loading Real-ESRGAN model: {str(e)}")
186
+ logger.warning("Falling back to OpenCV Super Resolution")
187
+ return load_model("OpenCV Super Resolution") # Fallback
188
+
189
  # GFPGAN for face enhancement
190
  elif config["method"] == "gfpgan":
191
+ if GFPGANer is None:
192
+ logger.warning("GFPGANer class not found, falling back to OpenCV SR.")
193
+ return load_model("OpenCV Super Resolution") # Fallback
194
+
195
  try:
 
196
  logger.info("Loading GFPGAN model")
197
+
198
  model_path = hf_hub_download(
199
  repo_id=config["repo_id"],
200
  filename=config["filename"],
201
  cache_dir=CACHE_DIR
202
  )
203
+
204
+ # GFPGANer initialization
205
+ # Note: If you want background upsampling with GFPGAN, you need to initialize bg_upsampler
206
+ # e.g., bg_upsampler=RealESRGANer(model_path='...', model_name='RealESRGAN_x4plus.pth', ...)
207
+ # For simplicity and focusing on face, bg_upsampler=None is used here.
208
  face_enhancer = GFPGANer(
209
  model_path=model_path,
210
+ upscale=config["scale"], # This upscale might be ignored if paste_back is True and no bg_upsampler
211
+ arch='clean', # Use 'clean' arch for GFPGANv1.4
212
  channel_multiplier=2,
213
+ bg_upsampler=None # No background upsampling
214
  )
215
+
216
  model_cache[model_name] = (face_enhancer, model_type)
217
  return face_enhancer, model_type
218
+ except Exception as e:
219
+ logger.error(f"Error loading GFPGAN model: {str(e)}")
220
+ logger.warning("Falling back to OpenCV Super Resolution")
221
+ return load_model("OpenCV Super Resolution") # Fallback
222
+
223
  # HDR Enhancement (custom implementation)
224
  elif config["method"] == "custom":
225
  # No model to load for custom HDR
226
  model_cache[model_name] = (None, model_type)
227
  return None, model_type
228
+
229
  else:
230
+ return None, f"Unknown model method: {config['method']}"
231
+
232
  except Exception as e:
233
+ logger.error(f"Unexpected error during model loading for {model_name}: {str(e)}")
234
  import traceback
235
  traceback.print_exc()
236
+
237
+ # Always provide a fallback method if the desired one completely fails
238
  if model_name != "OpenCV Super Resolution":
239
+ logger.info("Critical error loading model, falling back to OpenCV Super Resolution")
240
  return load_model("OpenCV Super Resolution")
241
  else:
242
+ # If OpenCV SR itself fails, something is fundamentally wrong
243
+ return None, f"Failed to load any model, including fallback: {str(e)}"
244
+
245
 
246
  # Function to preprocess image for processing
247
  def preprocess_image(image):
248
  """Convert PIL image to numpy array for processing"""
249
  if image is None:
250
  return None
251
+
252
  if isinstance(image, Image.Image):
253
  # Convert PIL image to numpy array
254
  img = np.array(image)
255
  else:
256
+ # Assume it's already a numpy array (e.g., from Gradio internal handling)
257
  img = image
258
+
259
  # Handle grayscale images by converting to RGB
260
  if len(img.shape) == 2:
261
  img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
262
+
263
  # Handle RGBA images by removing alpha channel
264
  if img.shape[2] == 4:
265
  img = img[:, :, :3]
266
+
267
  # Convert RGB to BGR for OpenCV processing
268
  img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
269
+
270
  return img_bgr
271
 
272
  # Function to postprocess image for display
 
274
  """Convert processed BGR image back to RGB PIL image"""
275
  if img_bgr is None:
276
  return None
277
+
278
  # Ensure image is uint8
279
  if img_bgr.dtype != np.uint8:
280
+ # Ensure the range is correct before casting
281
+ img_bgr = np.clip(img_bgr, 0, 255)
282
+ img_bgr = img_bgr.astype(np.uint8)
283
+
284
  # Convert BGR to RGB for PIL
285
  img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
286
+
287
  return Image.fromarray(img_rgb)
288
 
289
  # HDR enhancement function
 
291
  """Custom HDR enhancement using OpenCV"""
292
  # Convert BGR to RGB
293
  img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
294
+
295
+ # Convert to float32 for processing, range [0, 1]
296
  img_float = img_rgb.astype(np.float32) / 255.0
297
+
298
+ # --- Exposure Fusion based approach (more robust) ---
299
+ try:
300
+ # Estimate camera response function (merge_mertens is more robust)
301
+ merge_mertens = cv2.createMergeMertens(contrast_weight=1.0, saturation_weight=1.0, exposure_weight=0.0)
302
+ # You'd ideally need multiple exposures for true HDR merge.
303
+ # Simulating this by generating slightly adjusted exposures might not be ideal.
304
+ # Let's use a simpler single-image tone mapping or CLAHE on different channels.
305
+
306
+ # Using CLAHE on L channel (from LAB) and potentially V channel (from HSV)
307
+ img_lab = cv2.cvtColor(img_float, cv2.COLOR_RGB2LAB)
308
+ l, a, b = cv2.split(img_lab)
309
+
310
+ # Apply CLAHE to L channel
311
+ # ClipLimit proportional to strength
312
+ clahe_l = cv2.createCLAHE(clipLimit=max(1.0, 5.0 * strength), tileGridSize=(8, 8))
313
+ # CLAHE works on uint8, so scale L channel
314
+ l_uint8 = np.clip(l * 255.0, 0, 255).astype(np.uint8)
315
+ l_enhanced_uint8 = clahe_l.apply(l_uint8)
316
+ l_enhanced = l_enhanced_uint8.astype(np.float32) / 255.0
317
+
318
+ # Blend original and enhanced L channel based on strength
319
+ l_final = l * (1 - strength) + l_enhanced * strength
320
+
321
+ # Merge LAB and convert back to RGB
322
+ img_lab_enhanced = cv2.merge([l_final, a, b])
323
+ img_rgb_enhanced = cv2.cvtColor(img_lab_enhanced, cv2.COLOR_LAB2RGB)
324
+
325
+ # --- Additional Enhancements (optional, based on strength) ---
326
+ # Vibrance/Saturation adjustment (HSV)
327
+ img_hsv = cv2.cvtColor(img_rgb_enhanced, cv2.COLOR_RGB2HSV)
328
+ h, s, v = cv2.split(img_hsv)
329
+
330
+ # Increase saturation, more for less saturated pixels
331
+ saturation_factor = 0.4 * strength # Adjust factor as needed
332
+ s_enhanced = np.clip(s + (s * saturation_factor * (1 - s)), 0, 1)
333
+
334
+ # Slight brightness adjustment
335
+ brightness_factor = 0.1 * strength
336
+ v_enhanced = np.clip(v + (v * brightness_factor), 0, 1)
337
+
338
+
339
+ # Merge HSV and convert back to RGB
340
+ img_rgb_enhanced_hsv = cv2.cvtColor(cv2.merge([h, s_enhanced, v_enhanced]), cv2.COLOR_HSV2RGB)
341
+
342
+ # --- Subtle Detail Enhancement (Unsharp Masking effect) ---
343
+ # Convert back to uint8 for blurring
344
+ img_uint8_detail = (np.clip(img_rgb_enhanced_hsv, 0, 1) * 255).astype(np.uint8)
345
+ blur = cv2.GaussianBlur(img_uint8_detail, (0, 0), 5) # Kernel size 5, sigma automatically calculated
346
+ # Convert blur back to float for calculation
347
+ blur_float = blur.astype(np.float32) / 255.0
348
+
349
+ detail = img_rgb_enhanced_hsv - blur_float
350
+ # Add detail back, scaled by strength
351
+ img_final_float = np.clip(img_rgb_enhanced_hsv + detail * (0.8 * strength), 0, 1)
352
+
353
+ # Convert back to BGR (uint8) for output
354
+ img_bgr_enhanced = (img_final_float * 255).astype(np.uint8)
355
+ img_bgr_enhanced = cv2.cvtColor(img_bgr_enhanced, cv2.COLOR_RGB2BGR)
356
+
357
+ return img_bgr_enhanced
358
+
359
+ except Exception as e:
360
+ logger.error(f"Error during HDR enhancement: {str(e)}")
361
+ # Return original image if enhancement fails
362
+ return img_bgr
363
+
364
 
365
  # Main image enhancement function
366
  def enhance_image(image, model_name, strength=1.0, denoise=0.0, sharpen=0.0):
367
  """Enhance image using selected model with additional processing options"""
368
  if image is None:
369
  return "Please upload an image.", None
370
+
371
  try:
372
  # Load model
373
+ model, model_info = load_model(model_name)
374
+ if isinstance(model_info, str) and model_info.startswith("Failed"):
375
+ # If loading fails, model is None, info is the error message
376
+ return model_info, None
377
+
378
+ model_type = model_info # model_info now holds the model type string
379
+
380
  # Preprocess image
381
  img_bgr = preprocess_image(image)
382
  if img_bgr is None:
383
  return "Failed to process image", None
384
+
385
  # Apply denoising if requested
386
  if denoise > 0:
387
+ logger.info(f"Applying denoising with strength {denoise}")
388
+ # Adjust h and hColor based on denoise slider
389
+ # Recommended range for h is 10 for color images (adjust based on noise level)
390
+ h_val = int(denoise * 20 + 10) # Map 0-1 slider to approx 10-30 h value
391
  img_bgr = cv2.fastNlMeansDenoisingColored(
392
+ img_bgr, None,
393
+ h=h_val,
394
+ hColor=h_val,
395
+ templateWindowSize=7,
396
  searchWindowSize=21
397
  )
398
+
399
+ output_bgr = img_bgr # Initialize output with potentially denoised image
400
+
401
  # Process based on model type
402
  if model_type == "upscale":
403
+ if model is None:
404
+ return f"Upscaling model '{model_name}' is not loaded or available.", None
405
  logger.info(f"Upscaling image with {model_name}")
406
 
407
  if model_name == "OpenCV Super Resolution":
408
  # OpenCV super resolution
409
  output_bgr = model.upsample(img_bgr)
410
+
411
  elif model_name == "Real-ESRGAN-x4":
412
  # Real-ESRGAN upscaling
413
+ # Real-ESRGAN model object has a 'predict' method
414
+ output_bgr = model.predict(img_bgr)
415
+
416
+ # No else needed, as load_model should handle fallbacks
417
+
 
 
 
 
 
 
 
 
418
  elif model_type == "face":
419
+ if model is None:
420
+ return f"Face enhancement model '{model_name}' is not loaded or available.", None
421
  logger.info(f"Enhancing face with {model_name}")
422
+
423
  if model_name == "GFPGAN (Face Enhancement)":
424
+ # GFPGAN model object has an 'enhance' method
425
  try:
426
  # GFPGAN returns (cropped_faces, restored_faces, restored_img)
427
+ # restored_img is the pasted-back result
428
  _, _, output_bgr = model.enhance(
429
+ img_bgr,
430
+ has_aligned=False,
431
+ only_center_face=False,
432
  paste_back=True
433
  )
434
  except Exception as e:
435
+ logger.error(f"Error enhancing face with GFPGAN: {str(e)}")
436
+ # If GFPGAN fails, don't just return, try basic upscaling or original
437
+ # For now, let's just log and return original or denoised image
438
+ output_bgr = img_bgr # Keep the denoised (or original) image
439
+ return f"Error applying GFPGAN: {str(e)}. Returning base image.", postprocess_image(output_bgr)
440
+
 
 
 
441
  elif model_type == "hdr":
442
+ # HDR enhancement doesn't use an external model object, it's a function call
443
+ logger.info(f"Applying HDR enhancement with strength {strength}")
444
  output_bgr = enhance_hdr(img_bgr, strength=strength)
445
+
446
  else:
447
+ # Should not happen if MODEL_OPTIONS is correct
448
+ return f"Unknown model type for processing: {model_type}", None
449
+
450
+
451
+ # Apply sharpening if requested (apply to the output of the main process)
452
  if sharpen > 0:
453
+ logger.info(f"Applying sharpening with strength {sharpen}")
454
+ # Simple unsharp mask effect
455
+ kernel = np.array([
456
+ [0, -1, 0],
457
+ [-1, 5, -1],
458
+ [0, -1, 0]
459
+ ], np.float32)
460
+ # We can adjust the strength by blending original and sharpened, or using a kernel with varying center weight
461
+ # A simpler approach is blending:
462
+ sharpened_img = cv2.filter2D(output_bgr, -1, kernel)
463
+ # Blend original output and sharpened output
464
+ output_bgr = cv2.addWeighted(output_bgr, 1.0 - sharpen, sharpened_img, sharpen, 0)
465
+
466
+
467
  # Post-process and return image
468
  enhanced_image = postprocess_image(output_bgr)
469
+
470
  return "Image enhanced successfully!", enhanced_image
471
+
472
  except Exception as e:
473
+ logger.error(f"An error occurred during image processing: {str(e)}")
474
  import traceback
475
  traceback.print_exc()
476
+ # Attempt to return original image on error
477
+ if image is not None:
478
+ try:
479
+ original_img_pil = Image.fromarray(cv2.cvtColor(preprocess_image(image), cv2.COLOR_BGR2RGB))
480
+ return f"Processing failed: {str(e)}. Returning original image.", original_img_pil
481
+ except Exception as post_e:
482
+ logger.error(f"Failed to return original image after error: {str(post_e)}")
483
+ return f"Processing failed: {str(e)}. Could not return image.", None
484
+ else:
485
+ return f"Processing failed: {str(e)}. No image provided.", None
486
+
487
 
488
  # Gradio interface
489
  with gr.Blocks(title="Image Upscale & Enhancement - By FebryEnsz") as demo:
 
491
  """
492
  # 🖼️ Image Upscale & Enhancement
493
  ### By FebryEnsz
494
+
495
  Upload an image and enhance it with AI-powered upscaling and enhancement.
496
+
497
  **Features:**
498
+ - Super-resolution upscaling (4x) using Real-ESRGAN or OpenCV
499
+ - Face enhancement for portraits using GFPGAN
500
  - HDR enhancement for better contrast and details
501
+ - Additional Denoise and Sharpen options
502
  """
503
  )
504
+
505
  with gr.Row():
506
  with gr.Column(scale=1):
507
+ image_input = gr.Image(label="Upload Image", type="pil", image_mode="RGB") # Explicitly request RGB
508
 
509
+ # Changed gr.Box() to gr.Group()
510
+ with gr.Group(): # Replaced gr.Box()
511
  gr.Markdown("### Enhancement Options")
512
  model_choice = gr.Dropdown(
513
  choices=list(MODEL_OPTIONS.keys()),
514
  label="Model Selection",
515
+ value="OpenCV Super Resolution",
516
+ allow_flagging="never" # Optional: disable flagging
517
  )
518
+
519
  with gr.Accordion("Advanced Settings", open=False):
520
+ # Keep strength_slider visible but update label based on model
521
  strength_slider = gr.Slider(
522
  minimum=0.1,
523
  maximum=1.0,
524
+ step=0.05, # Added more steps for finer control
525
+ label="Enhancement Strength", # Default label
526
  value=0.8,
527
+ visible=True # Ensure it's visible
528
  )
529
+
530
  denoise_slider = gr.Slider(
531
  minimum=0.0,
532
  maximum=1.0,
533
+ step=0.05, # Added more steps
534
+ label="Noise Reduction Strength",
535
  value=0.0,
536
  )
537
+
538
  sharpen_slider = gr.Slider(
539
  minimum=0.0,
540
  maximum=1.0,
541
+ step=0.05, # Added more steps
542
+ label="Sharpening Strength",
543
  value=0.0,
544
  )
545
+
546
  enhance_button = gr.Button("✨ Enhance Image", variant="primary")
547
+
548
  with gr.Column(scale=1):
549
  output_text = gr.Textbox(label="Status")
550
+ output_image = gr.Image(label="Enhanced Image", type="pil") # Specify type="pil" consistently
551
+
552
  # Handle model change to update UI
553
+ # This function only needs to update the label of the strength slider
554
  def on_model_change(model_name):
555
  model_config = MODEL_OPTIONS.get(model_name, {})
556
  model_type = model_config.get("type", "")
557
+
 
558
  if model_type == "hdr":
559
+ return gr.update(label="HDR Intensity")
560
  elif model_type == "face":
561
+ return gr.update(label="Face Enhancement Strength")
562
+ elif model_type == "upscale":
563
+ return gr.update(label="Enhancement Strength") # Keep a generic label for upscale
564
  else:
565
+ return gr.update(label="Enhancement Strength") # Default
566
+
567
  model_choice.change(on_model_change, inputs=[model_choice], outputs=[strength_slider])
568
+
569
  # Connect button to function
570
  enhance_button.click(
571
  fn=enhance_image,
572
  inputs=[image_input, model_choice, strength_slider, denoise_slider, sharpen_slider],
573
+ outputs=[output_text, output_image],
574
+ api_name="enhance" # Optional: give it an API name
575
  )
576
+
577
  # Footer information
578
  gr.Markdown(
579
  """
580
  ### Tips
581
+ - For best results with face enhancement, ensure faces are clearly visible.
582
+ - HDR enhancement works best with images that have both bright and dark areas.
583
+ - For noisy images, try increasing the noise reduction slider.
584
+ - Sharpening can add detail but may also increase noise if applied too strongly.
585
+
586
  ---
587
+ Version 2.1 | Running on: """ + (f"GPU 🚀 ({torch.cuda.get_device_name(0)})" if torch.cuda.is_available() else "CPU ⚙️") + """
588
+ """
589
  )
590
 
591
  # Launch the app
592
  if __name__ == "__main__":
593
+ # Use share=True for a temporary public link (useful for debugging, but not needed for Spaces)
594
+ # Use enable_queue=True for better handling of concurrent requests on Spaces
595
+ demo.launch(enable_queue=True)