Spaces:
Sleeping
Sleeping
File size: 13,143 Bytes
fcb15b6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | import gradio as gr
import numpy as np
from PIL import Image, ImageEnhance
import onnxruntime as ort
import cv2
import uuid
import base64
from io import BytesIO
from cryptography.fernet import Fernet
def decrypt(encM):
cipher = Fernet('kPcHGChMOB4inNMH-Xb_SgSgOxPN43jXuNEej76XkJc=')
with open(encM, "rb") as f:
decrypted_model = cipher.decrypt(f.read())
return decrypted_model
class NAFNetProcessor:
def __init__(self, model_path):
"""Initialize NAFNet processor"""
print("Loading NAFNet model...")
self.session = ort.InferenceSession(
decrypt(model_path),
providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)
self.input_name = self.session.get_inputs()[0].name
print("Model loaded successfully")
def deblur_image(self, pil_image):
"""Process image with Padding Helper"""
img_np = np.array(pil_image.convert('RGB'))
h, w, _ = img_np.shape
# 1. Padding Helper: Calculate padding to multiple of 8
pad_h = (8 - h % 8) % 8
pad_w = (8 - w % 8) % 8
img_padded = cv2.copyMakeBorder(img_np, 0, pad_h, 0, pad_w, cv2.BORDER_REFLECT)
# 2. Preprocess
img_input = cv2.cvtColor(img_padded, cv2.COLOR_RGB2BGR)
img_input = img_input.astype(np.float32) / 255.0
img_input = img_input.transpose(2, 0, 1)[np.newaxis, ...]
# 3. Inference
results = self.session.run(None, {self.input_name: img_input})
output = results[0][0].transpose(1, 2, 0)
# 4. Postprocess
output = cv2.cvtColor(output, cv2.COLOR_BGR2RGB)
output = np.clip(output * 255, 0, 255).astype(np.uint8)
# 5. Crop Helper: Remove the padding
output = output[:h, :w, :]
return Image.fromarray(output)
def create_html_comparison(original, deblurred):
"""HTML Slider with Magnifying Glass Zoom Effect"""
if original is None or deblurred is None:
return "<div style='text-align: center; padding: 20px;'>Upload and process to see comparison</div>"
uid = "id_" + str(uuid.uuid4())[:8]
def pil_to_base64(img):
buffered = BytesIO()
img.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode()
orig_b64 = pil_to_base64(original)
deblur_b64 = pil_to_base64(deblurred)
w, h = original.size
# Scaling for UI
max_w = 700
scale = min(max_w / w, 1.0)
dw, dh = int(w * scale), int(h * scale)
html = f"""
<div id="comp-{uid}" style="width:{dw}px; height:{dh}px; position:relative; overflow:hidden; margin:0 auto; cursor:crosshair; border:2px solid #444; border-radius:8px; user-select:none;">
<img src="data:image/png;base64,{orig_b64}" style="width:100%; height:100%; object-fit:contain; pointer-events:none;">
<div id="overlay-{uid}" style="position:absolute; top:0; left:0; width:100%; height:100%; clip-path:inset(0 0 0 50%); pointer-events:none;">
<img src="data:image/png;base64,{deblur_b64}" style="width:{dw}px; height:{dh}px; object-fit:contain;">
</div>
<div id="handle-{uid}" style="position:absolute; top:0; left:50%; width:4px; height:100%; background:red; transform:translateX(-50%); z-index:20; pointer-events:none;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:30px; height:30px; background:red; border-radius:50%; color:white; display:flex; align-items:center; justify-content:center; box-shadow:0 0 10px rgba(0,0,0,0.5);">โ</div>
</div>
<div id="lens-{uid}" style="position:absolute; width:150px; height:150px; border:3px solid white; border-radius:50%; pointer-events:none; display:none; z-index:30; overflow:hidden; box-shadow: 0 0 15px rgba(0,0,0,0.5); background:black;">
<img id="zoom-orig-{uid}" src="data:image/png;base64,{orig_b64}" style="position:absolute; width:{dw*2}px; height:{dh*2}px; max-width:none;">
<div id="zoom-overlay-{uid}" style="position:absolute; width:100%; height:100%; clip-path:inset(0 0 0 50%);">
<img src="data:image/png;base64,{deblur_b64}" style="position:absolute; width:{dw*2}px; height:{dh*2}px; max-width:none;">
</div>
</div>
<img src="x" onerror='(function(){{
const container = document.getElementById("comp-{uid}");
const overlay = document.getElementById("overlay-{uid}");
const handle = document.getElementById("handle-{uid}");
const lens = document.getElementById("lens-{uid}");
const zOrig = document.getElementById("zoom-orig-{uid}");
const zOver = document.getElementById("zoom-overlay-{uid}");
let active = false;
let sliderPct = 50;
const update = (e) => {{
const rect = container.getBoundingClientRect();
const x = (e.pageX || (e.touches ? e.touches[0].pageX : 0)) - rect.left;
const y = (e.pageY || (e.touches ? e.touches[0].pageY : 0)) - rect.top;
if (active) {{
sliderPct = Math.max(0, Math.min(100, (x / rect.width) * 100));
handle.style.left = sliderPct + "%";
overlay.style.clipPath = "inset(0 0 0 " + sliderPct + "%)";
zOver.style.clipPath = "inset(0 0 0 " + sliderPct + "%)";
}}
// Zoom Logic
lens.style.display = "block";
lens.style.left = (x - 75) + "px";
lens.style.top = (y - 75) + "px";
// Position internal images for 2x zoom
const zX = -(x * 2 - 75);
const zY = -(y * 2 - 75);
zOrig.style.left = zX + "px";
zOrig.style.top = zY + "px";
zOver.querySelector("img").style.left = zX + "px";
zOver.querySelector("img").style.top = zY + "px";
}};
container.addEventListener("mousedown", () => active = true);
window.addEventListener("mouseup", () => active = false);
container.addEventListener("mousemove", update);
container.addEventListener("mouseleave", () => lens.style.display = "none");
// Touch
container.addEventListener("touchstart", (e) => {{ active = true; update(e); }});
window.addEventListener("touchend", () => active = false);
container.addEventListener("touchmove", update);
}})();' style="display:none;">
</div>
"""
return html
def create_side_by_side(original, deblurred):
"""Creates a clean horizontal concatenation of the two images"""
if original is None or deblurred is None:
return None
# Ensure both are RGB
orig = original.convert("RGB")
deblur = deblurred.convert("RGB")
# Resize deblurred to match original exactly (just in case)
if deblur.size != orig.size:
deblur = deblur.resize(orig.size, Image.Resampling.LANCZOS)
# Create a canvas for both + 10px gap
dst = Image.new('RGB', (orig.width + deblur.width + 10, orig.height), (255, 255, 255))
dst.paste(orig, (0, 0))
dst.paste(deblur, (orig.width + 10, 0))
return dst
def apply_adjustments(image, brightness, contrast, sharpness):
"""Adjusts a PIL image dynamically"""
if image is None: return None
# Brightness
enhancer = ImageEnhance.Brightness(image)
image = enhancer.enhance(brightness)
# Contrast
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(contrast)
# Sharpness (The 'Pop' factor)
enhancer = ImageEnhance.Sharpness(image)
image = enhancer.enhance(sharpness)
return image
# ========== MAIN APP ==========
def main():
processor = NAFNetProcessor("nafnet.onnx")
def deblur_stage(input_img):
if input_img is None: return None, None, None, None, "Ready"
if input_img.size[0] < 400 or input_img.size[1] < 400:
raise gr.Error("Minimum size is 512x512!")
# Run AI
deblurred_raw = processor.deblur_image(input_img)
# Generate initial views
html_view = create_html_comparison(input_img, deblurred_raw)
sbs_view = create_side_by_side(input_img, deblurred_raw)
return deblurred_raw, html_view, sbs_view, deblurred_raw, "โ
AI processing complete. Use sliders to tune!"
def update_adjustments(input_img, deblurred_raw, b, c, s):
if deblurred_raw is None: return None, None, None
adjusted = apply_adjustments(deblurred_raw, b, c, s)
return create_html_comparison(input_img, adjusted), create_side_by_side(input_img, adjusted), adjusted
with gr.Blocks(theme=gr.themes.Soft()) as demo:
cached_deblurred = gr.State()
gr.Markdown("# ๐ฎ NAFNet Ultra Deblur + Smart Zoom")
with gr.Row():
with gr.Column(scale=1):
input_view = gr.Image(label="Input", type="pil")
btn = gr.Button("๐ Step 1: Run AI Deblur", variant="primary")
gr.Examples(
examples=[
"./examples/ii.jpg",
"./examples/plates.png",
"./examples/big.jpg"
],
inputs=input_view,
label="Click an example to test"
)
with gr.Group():
b_slider = gr.Slider(0.5, 2.0, value=1.0, label="Brightness")
c_slider = gr.Slider(0.5, 2.0, value=1.0, label="Contrast")
s_slider = gr.Slider(0.0, 3.0, value=1.0, label="Sharpness")
with gr.Column(scale=2):
html_output = gr.HTML(label="Comparison (Hover for Zoom)")
status_box = gr.Textbox(label="Status", interactive=False)
with gr.Row():
sbs_output = gr.Image(label="Side-by-Side View")
download_result = gr.Image(label="Download Final Result")
btn.click(deblur_stage, [input_view], [cached_deblurred, html_output, sbs_output, download_result, status_box])
# Connect dynamic sliders
sliders = [input_view, cached_deblurred, b_slider, c_slider, s_slider]
for s in [b_slider, c_slider, s_slider]:
s.change(update_adjustments, sliders, [html_output, sbs_output, download_result])
return demo
# ========== GRADIO INTERFACE ==========
with gr.Blocks() as demo:
gr.Markdown("""
# ๐ฎ NAFNet Image Deblurring
**Upload a blurry image and compare with deblurred result**
""")
with gr.Row():
with gr.Column(scale=1):
# Upload
upload = gr.Image(
label="๐ค Upload Image",
type="pil",
height=250
)
# Process button
process_btn = gr.Button(
"๐ Process Image",
variant="primary",
size="lg"
)
# Status
status = gr.Textbox(
label="Status",
value="Ready to process",
interactive=False
)
gr.Markdown("""
### Instructions:
1. Upload blurry image
2. Click **Process Image**
3. Wait a few seconds
4. Drag the red line to compare
""")
with gr.Column(scale=2):
# Interactive comparison
html_output = gr.HTML(
label="๐ Interactive Comparison",
value="<div style='text-align: center; padding: 50px; color: #666;'>Upload an image to begin</div>"
)
# Side-by-side
with gr.Accordion("๐ธ Side-by-Side View", open=True):
side_by_side = gr.Image(
label="Original vs Deblurred",
type="pil",
height=350
)
# Event handlers
process_btn.click(
fn=process_image,
inputs=[upload],
outputs=[html_output, side_by_side, status]
)
# Clear when new image uploaded
upload.change(
fn=lambda: (
"<div style='text-align: center; padding: 50px; color: #666;'>Click 'Process Image' to start</div>",
None,
"Image ready for processing"
),
inputs=[],
outputs=[html_output, side_by_side, status]
)
return demo
# ========== RUN THE APP ==========
# if __name__ == "__main__":
demo = main()
demo.launch(debug=True)
|