Spaces:
Runtime error
Runtime error
Download preprocess/ui.py from vickwayne3/lora-preprocess-ui: direct link, hf CLI and curl.
- Browser
- Download file 9.64 kB
-
https://huggingface.co/spaces/vickwayne3/lora-preprocess-ui/resolve/main/preprocess/ui.py
- Command line
-
hf download hf://spaces/vickwayne3/lora-preprocess-ui/preprocess/ui.py
-
curl -L -o ui.py https://huggingface.co/spaces/vickwayne3/lora-preprocess-ui/resolve/main/preprocess/ui.py
9.64 kB
| import gradio as gr | |
| import cv2 | |
| import numpy as np | |
| import os | |
| import uuid | |
| import json | |
| from PIL import Image | |
| from pathlib import Path | |
| import imagehash | |
| import requests | |
| from io import BytesIO | |
| from rembg import remove, new_session | |
| # Lazy global for background-removal session (small model). | |
| _bg_session = None | |
| import mediapipe as mp # still used for future extensions; not required for detection now | |
| # Paths | |
| ROOT = Path(__file__).resolve().parent | |
| OUTPUT_ROOT = ROOT.parent / "outputs" | |
| OUTPUT_ROOT.mkdir(exist_ok=True) | |
| # Quality thresholds | |
| VAR_LAP_MIN = 60 # focus | |
| BRIGHT_MIN, BRIGHT_MAX = 30, 230 | |
| FACE_CONF_MIN = 0.85 | |
| CLEAN_BG_COLOR = (128, 128, 128, 255) | |
| # OpenCV DNN face detector | |
| MODEL_DIR = ROOT / "models" | |
| PROTOTXT = MODEL_DIR / "deploy.prototxt" | |
| CAFFEMODEL = MODEL_DIR / "res10_300x300_ssd_iter_140000.caffemodel" | |
| if not PROTOTXT.exists() or not CAFFEMODEL.exists(): | |
| raise FileNotFoundError("Face detector weights not found. Expected deploy.prototxt and res10_300x300_ssd_iter_140000.caffemodel in preprocess/models/") | |
| FACE_NET = cv2.dnn.readNetFromCaffe(str(PROTOTXT), str(CAFFEMODEL)) | |
| def pil_to_cv(img: Image.Image): | |
| return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) | |
| def cv_to_pil(img: np.ndarray): | |
| return Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) | |
| def variance_of_laplacian(image): | |
| return cv2.Laplacian(image, cv2.CV_64F).var() | |
| def basic_qc(pil_img): | |
| cv_img = pil_to_cv(pil_img) | |
| gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) | |
| focus = variance_of_laplacian(gray) | |
| mean = gray.mean() | |
| ok_focus = focus >= VAR_LAP_MIN | |
| ok_bright = BRIGHT_MIN <= mean <= BRIGHT_MAX | |
| return { | |
| "focus": round(float(focus), 2), | |
| "brightness": round(float(mean), 2), | |
| "ok_focus": ok_focus, | |
| "ok_brightness": ok_bright, | |
| } | |
| def face_detect(pil_img): | |
| cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) | |
| h, w, _ = cv_img.shape | |
| blob = cv2.dnn.blobFromImage(cv_img, 1.0, (300, 300), (104.0, 177.0, 123.0)) | |
| FACE_NET.setInput(blob) | |
| detections = FACE_NET.forward() | |
| boxes = [] | |
| for i in range(detections.shape[2]): | |
| score = detections[0, 0, i, 2] | |
| if score < FACE_CONF_MIN: | |
| continue | |
| box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) | |
| x1, y1, x2, y2 = box.astype(int) | |
| boxes.append({ | |
| "x1": int(max(0, x1)), | |
| "y1": int(max(0, y1)), | |
| "x2": int(min(w, x2)), | |
| "y2": int(min(h, y2)), | |
| "score": float(score), | |
| }) | |
| return boxes | |
| def crop_with_margin(pil_img, box, margin=0.15, square=True, target=1024): | |
| w, h = pil_img.size | |
| x1, y1, x2, y2 = box | |
| bw, bh = x2 - x1, y2 - y1 | |
| mx, my = int(bw * margin), int(bh * margin) | |
| x1 = max(0, x1 - mx) | |
| y1 = max(0, y1 - my) | |
| x2 = min(w, x2 + mx) | |
| y2 = min(h, y2 + my) | |
| if square: | |
| # make square by padding | |
| bw, bh = x2 - x1, y2 - y1 | |
| if bw > bh: | |
| diff = bw - bh | |
| y1 = max(0, y1 - diff // 2) | |
| y2 = min(h, y2 + diff - diff // 2) | |
| else: | |
| diff = bh - bw | |
| x1 = max(0, x1 - diff // 2) | |
| x2 = min(w, x2 + diff - diff // 2) | |
| cropped = pil_img.crop((x1, y1, x2, y2)).resize((target, target), Image.Resampling.LANCZOS) | |
| return cropped | |
| def get_bg_session(): | |
| global _bg_session | |
| if _bg_session is None: | |
| # u2netp is ~4 MB; default u2net is ~176 MB. | |
| _bg_session = new_session("u2netp") | |
| return _bg_session | |
| def segment_person(pil_img): | |
| # Use lightweight model; pass session explicitly to avoid arg conflicts. | |
| rgba = remove(pil_img.convert("RGB"), session=get_bg_session()) | |
| return rgba.convert("RGBA") | |
| def face_identity_crop(img: Image.Image, face_box): | |
| """ | |
| Mask everything outside the face box to neutral gray to reduce LoRA memorizing clothes/jewelry. | |
| """ | |
| x1, y1, x2, y2 = face_box | |
| w, h = img.size | |
| pad_x = int((x2 - x1) * 0.15) | |
| pad_y = int((y2 - y1) * 0.20) | |
| fx1 = max(0, x1 - pad_x) | |
| fy1 = max(0, y1 - pad_y) | |
| fx2 = min(w, x2 + pad_x) | |
| fy2 = min(h, y2 + pad_y) | |
| mask_np = np.zeros((h, w), dtype=np.uint8) | |
| mask_np[fy1:fy2, fx1:fx2] = 255 | |
| mask = Image.fromarray(mask_np) | |
| base = Image.new("RGBA", img.size, CLEAN_BG_COLOR) | |
| identity = Image.composite(img.convert("RGBA"), base, mask) | |
| return identity.convert("RGB") | |
| def florence_caption(pil_img: Image.Image): | |
| """ | |
| Optional Florence-2-Large captioning. Set FLORENCE_URL and FLORENCE_KEY env vars. | |
| """ | |
| url = os.environ.get("FLORENCE_URL") | |
| key = os.environ.get("FLORENCE_KEY") | |
| if not url or not key: | |
| return None | |
| try: | |
| buf = BytesIO() | |
| pil_img.save(buf, format="PNG") | |
| resp = requests.post( | |
| url, | |
| headers={"Authorization": f"Bearer {key}"}, | |
| files={"file": ("image.png", buf.getvalue(), "image/png")}, | |
| timeout=15, | |
| ) | |
| if resp.ok: | |
| data = resp.json() | |
| return data.get("caption") or data.get("text") | |
| except Exception: | |
| return None | |
| return None | |
| def dedupe_hashes(images): | |
| hashes = [] | |
| unique = [] | |
| for img in images: | |
| h = imagehash.phash(img) | |
| dup = False | |
| for seen in hashes: | |
| if h - seen <= 4: # Hamming distance threshold | |
| dup = True | |
| break | |
| if not dup: | |
| hashes.append(h) | |
| unique.append(img) | |
| return unique | |
| def process_images(files): | |
| if not files: | |
| return "No images uploaded", None, None | |
| imgs = [Image.open(f.name if hasattr(f, 'name') else f).convert("RGB") for f in files] | |
| # Deduplicate | |
| imgs = dedupe_hashes(imgs) | |
| processed = [] | |
| manifest = [] | |
| for idx, img in enumerate(imgs): | |
| qc = basic_qc(img) | |
| faces = face_detect(img) | |
| # prefer largest face if multiple | |
| if faces: | |
| face = sorted(faces, key=lambda b: b["score"] * ((b["x2"]-b["x1"])*(b["y2"]-b["y1"])), reverse=True)[0] | |
| crop = crop_with_margin(img, (face["x1"], face["y1"], face["x2"], face["y2"]), margin=0.15, square=True, target=1024) | |
| caption = florence_caption(crop) or "photo of sks_person, close-up, neutral lighting, straight face" | |
| else: | |
| # fallback: center crop | |
| w, h = img.size | |
| side = min(w, h) | |
| x1 = (w - side)//2 | |
| y1 = (h - side)//2 | |
| crop = img.crop((x1, y1, x1+side, y1+side)).resize((1024, 1024), Image.Resampling.LANCZOS) | |
| caption = florence_caption(crop) or "photo of sks_person" | |
| # segmentation variant | |
| cutout = segment_person(crop) | |
| # flat background | |
| bg = Image.new("RGBA", cutout.size, CLEAN_BG_COLOR) | |
| clean = Image.alpha_composite(bg, cutout).convert("RGB") | |
| # face-neutralized variant to suppress clothes/jewelry | |
| if faces: | |
| face_box = (face["x1"], face["y1"], face["x2"], face["y2"]) | |
| identity_only = face_identity_crop(clean, face_box) | |
| else: | |
| identity_only = clean | |
| uid = uuid.uuid4().hex[:8] | |
| base_dir = OUTPUT_ROOT / "preprocessed" | |
| base_dir.mkdir(parents=True, exist_ok=True) | |
| out_clean = base_dir / f"img_{idx}_{uid}_clean.png" | |
| out_orig = base_dir / f"img_{idx}_{uid}_orig.png" | |
| out_face = base_dir / f"img_{idx}_{uid}_face.png" | |
| clean.save(out_clean) | |
| crop.save(out_orig) | |
| identity_only.save(out_face) | |
| manifest.append({ | |
| "image": str(out_clean), | |
| "identity_masked": str(out_face), | |
| "caption": caption, | |
| "focus": qc["focus"], | |
| "brightness": qc["brightness"], | |
| "has_face": bool(faces), | |
| }) | |
| processed.append((crop, clean, identity_only, caption, qc)) | |
| manifest_path = OUTPUT_ROOT / "preprocessed" / "captions.jsonl" | |
| with open(manifest_path, "w") as f: | |
| for row in manifest: | |
| f.write(json.dumps(row) + "\n") | |
| # Build gallery display | |
| gallery = [] | |
| for crop, clean, identity_only, cap, qc in processed: | |
| gallery.append((crop, f"Aligned | {cap}")) | |
| gallery.append((clean, f"Clean bg | {cap} | focus {qc['focus']} | bright {qc['brightness']}")) | |
| gallery.append((identity_only, f"Face-only | {cap} | focus {qc['focus']}")) | |
| summary = f"Saved {len(processed)} images to {manifest_path.parent}. captions.jsonl created." | |
| return summary, gallery, str(manifest_path) | |
| def build_ui(): | |
| with gr.Blocks(title="LoRA Prep") as demo: | |
| gr.Markdown("# Image Preprocess for LoRA\nUpload ~8 photos (selfies + body).") | |
| with gr.Row(): | |
| inp = gr.File(file_count="multiple", file_types=["image"], label="Upload images") | |
| run_btn = gr.Button("Run Preprocess") | |
| status = gr.Markdown() | |
| gallery = gr.Gallery(label="Crops & Cleaned", columns=2, height=600) | |
| manifest_path = gr.Textbox(label="captions.jsonl path", interactive=False) | |
| run_btn.click(fn=process_images, inputs=inp, outputs=[status, gallery, manifest_path]) | |
| return demo | |
| if __name__ == "__main__": | |
| app = build_ui() | |
| # Expose locally and offer a Gradio share link for easy access if localhost is blocked. | |
| launch_info = app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True, | |
| show_error=True, | |
| inbrowser=False, | |
| ) | |
| try: | |
| share_url = getattr(launch_info, "share_url", None) | |
| if share_url: | |
| print(f"GRADIO_SHARE_URL: {share_url}", flush=True) | |
| except Exception: | |
| pass | |