Philippe Potvin commited on
Commit
39f014d
·
1 Parent(s): f1d60fc

Fri, 03 Jul 2026 00:16 - Add Qwen face hair detailer

Browse files
Files changed (4) hide show
  1. README.md +2 -2
  2. app.py +141 -8
  3. requirements.txt +6 -9
  4. tests/test_enhance_stage.py +16 -5
README.md CHANGED
@@ -7,10 +7,10 @@ sdk_version: 5.49.1
7
  app_file: app.py
8
  pinned: false
9
  license: apache-2.0
10
- version: 1.0.1
11
  short_description: Powerful image editing - supports one or two input images.
12
  ---
13
 
14
- Pro Realism Edit Studio is a powerful image editor powered by [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) with [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer for 4-step inference. Upload one or two input images, write a prompt, get high-quality results. The enhancement stage uses Nomos-family tiled upscaling, Qwen-aware masked artifact repair, micro-detail sharpening, and final photographic grain.
15
 
16
  Video generation is disabled unless an owned Gradio video Space is configured with `VIDEO_SPACE_ID`.
 
7
  app_file: app.py
8
  pinned: false
9
  license: apache-2.0
10
+ version: 1.0.2
11
  short_description: Powerful image editing - supports one or two input images.
12
  ---
13
 
14
+ Pro Realism Edit Studio is a powerful image editor powered by [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) with [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer for 4-step inference. Upload one or two input images, write a prompt, get high-quality results. The enhancement stage uses Nomos-family tiled upscaling, Qwen-aware masked artifact repair, tiled face/hair detail regions, micro-detail sharpening, and final photographic grain.
15
 
16
  Video generation is disabled unless an owned Gradio video Space is configured with `VIDEO_SPACE_ID`.
app.py CHANGED
@@ -9,7 +9,7 @@ Advanced image editing and enhancement studio powered by:
9
  - Qwen-aware masked cleanup and final photographic detail
10
 
11
  Author: Enhanced with Hugging Face CLI and image generation expertise
12
- Version: 1.0.1
13
  """
14
 
15
  import gradio as gr
@@ -25,7 +25,7 @@ from pathlib import Path
25
  # Advanced imports
26
  from accelerate import init_empty_weights
27
  from collections import OrderedDict
28
- from PIL import Image, ImageEnhance, ImageFilter, ImageOps
29
  from diffusers.models import QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel
30
  from diffusers.models.model_loading_utils import load_model_dict_into_meta
31
  from huggingface_hub import hf_hub_download, HfApi, login, whoami
@@ -42,7 +42,7 @@ from gradio_client import Client, handle_file
42
 
43
  # Base model configuration
44
  BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511"
45
- APP_VERSION = "1.0.1"
46
  PHR00T_REPO_ID = os.environ.get("PHR00T_REPO_ID", "Phr00t/Qwen-Image-Edit-Rapid-AIO").strip()
47
  RAPID_TRANSFORMER_FILENAME = os.environ.get(
48
  "RAPID_TRANSFORMER_FILENAME",
@@ -60,6 +60,9 @@ ENHANCE_MAX_INPUT_EDGE = int(os.environ.get("ENHANCE_MAX_INPUT_EDGE", "2048"))
60
  ENHANCE_QWEN_DOWNSCALE_TRIGGER_EDGE = int(os.environ.get("ENHANCE_QWEN_DOWNSCALE_TRIGGER_EDGE", "1536"))
61
  ENHANCE_QWEN_DOWNSCALE_FACTOR = float(os.environ.get("ENHANCE_QWEN_DOWNSCALE_FACTOR", "0.75"))
62
  ENHANCE_GRAIN_STRENGTH = float(os.environ.get("ENHANCE_GRAIN_STRENGTH", "0.010"))
 
 
 
63
 
64
  # Advanced Detail Enhancement Configuration
65
  DETAIL_ENHANCEMENT_ENABLED = os.environ.get("DETAIL_ENHANCEMENT_ENABLED", "true").lower() == "true"
@@ -606,6 +609,123 @@ def qwen_defect_mask(image):
606
  mask_image = mask_image.filter(ImageFilter.MaxFilter(size=3))
607
  return mask_image.filter(ImageFilter.GaussianBlur(radius=1.1))
608
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  def masked_texture_repair(image, strength=0.55):
610
  base = image.convert("RGB")
611
  mask = qwen_defect_mask(base)
@@ -640,8 +760,8 @@ def apply_enhancement(image, enhance_mode, seed=0, progress=None):
640
  enhanced = image.convert("RGB")
641
  total_steps = {
642
  ENHANCE_MODE_UPSCALE: 2,
643
- ENHANCE_MODE_CLEAN: 3,
644
- ENHANCE_MODE_MAX_DETAIL: 5,
645
  }[mode]
646
  step = 0
647
 
@@ -657,6 +777,15 @@ def apply_enhancement(image, enhance_mode, seed=0, progress=None):
657
  enhanced = masked_texture_repair(enhanced, strength=0.55 if mode == ENHANCE_MODE_CLEAN else 0.68)
658
  enhanced = qwen_micro_detail(enhanced, amount=0.18)
659
 
 
 
 
 
 
 
 
 
 
660
  if mode == ENHANCE_MODE_MAX_DETAIL:
661
  if progress:
662
  step += 1
@@ -669,6 +798,10 @@ def apply_enhancement(image, enhance_mode, seed=0, progress=None):
669
  enhanced = tile_upscale(enhanced)
670
 
671
  if mode == ENHANCE_MODE_MAX_DETAIL:
 
 
 
 
672
  if progress:
673
  step += 1
674
  progress(0.98, desc="Adding final photographic grain...")
@@ -855,7 +988,7 @@ with gr.Blocks(css=css) as demo:
855
 
856
  Pro Tips:
857
  - Use Clean for portraits with smudged or pitted skin
858
- - Use Max Detail for texture, grain, and sharper final output
859
  """)
860
 
861
  with gr.Row():
@@ -882,8 +1015,8 @@ with gr.Blocks(css=css) as demo:
882
  Enhancement Options:
883
  - Off: No post-processing
884
  - Upscale: Qwen precondition + 4x Nomos tiled upscale
885
- - Clean: Masked Qwen artifact repair + upscaling
886
- - Max Detail: Masked repair + micro detail + upscaling + final grain
887
  """, visible=False)
888
 
889
  run_button = gr.Button("Generate!", variant="primary")
 
9
  - Qwen-aware masked cleanup and final photographic detail
10
 
11
  Author: Enhanced with Hugging Face CLI and image generation expertise
12
+ Version: 1.0.2
13
  """
14
 
15
  import gradio as gr
 
25
  # Advanced imports
26
  from accelerate import init_empty_weights
27
  from collections import OrderedDict
28
+ from PIL import Image, ImageChops, ImageEnhance, ImageFilter, ImageOps
29
  from diffusers.models import QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel
30
  from diffusers.models.model_loading_utils import load_model_dict_into_meta
31
  from huggingface_hub import hf_hub_download, HfApi, login, whoami
 
42
 
43
  # Base model configuration
44
  BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511"
45
+ APP_VERSION = "1.0.2"
46
  PHR00T_REPO_ID = os.environ.get("PHR00T_REPO_ID", "Phr00t/Qwen-Image-Edit-Rapid-AIO").strip()
47
  RAPID_TRANSFORMER_FILENAME = os.environ.get(
48
  "RAPID_TRANSFORMER_FILENAME",
 
60
  ENHANCE_QWEN_DOWNSCALE_TRIGGER_EDGE = int(os.environ.get("ENHANCE_QWEN_DOWNSCALE_TRIGGER_EDGE", "1536"))
61
  ENHANCE_QWEN_DOWNSCALE_FACTOR = float(os.environ.get("ENHANCE_QWEN_DOWNSCALE_FACTOR", "0.75"))
62
  ENHANCE_GRAIN_STRENGTH = float(os.environ.get("ENHANCE_GRAIN_STRENGTH", "0.010"))
63
+ ENHANCE_DETAILER_ENABLED = os.environ.get("ENHANCE_DETAILER_ENABLED", "true").lower() == "true"
64
+ ENHANCE_DETAILER_MAX_REGIONS = int(os.environ.get("ENHANCE_DETAILER_MAX_REGIONS", "8"))
65
+ ENHANCE_DETAILER_MIN_REGION_AREA = float(os.environ.get("ENHANCE_DETAILER_MIN_REGION_AREA", "0.003"))
66
 
67
  # Advanced Detail Enhancement Configuration
68
  DETAIL_ENHANCEMENT_ENABLED = os.environ.get("DETAIL_ENHANCEMENT_ENABLED", "true").lower() == "true"
 
609
  mask_image = mask_image.filter(ImageFilter.MaxFilter(size=3))
610
  return mask_image.filter(ImageFilter.GaussianBlur(radius=1.1))
611
 
612
+ def qwen_hair_mask(image):
613
+ base = image.convert("RGB")
614
+ rgb = np.asarray(base).astype(np.float32)
615
+ gray_image = base.convert("L")
616
+ gray = np.asarray(gray_image).astype(np.float32)
617
+ skin = np.asarray(qwen_skin_mask(base)).astype(np.float32) / 255.0
618
+ edges = np.asarray(gray_image.filter(ImageFilter.FIND_EDGES).filter(ImageFilter.GaussianBlur(radius=0.7))).astype(np.float32)
619
+ chroma = rgb.max(axis=2) - rgb.min(axis=2)
620
+
621
+ dark_strands = (gray < 122) & (edges > 8) & (skin < 0.45)
622
+ light_strands = (gray < 235) & (edges > 18) & (chroma > 8) & (skin < 0.28)
623
+ mask = (dark_strands | light_strands).astype(np.uint8) * 255
624
+
625
+ try:
626
+ import cv2
627
+ kernel = np.ones((3, 3), np.uint8)
628
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
629
+ mask = cv2.GaussianBlur(mask, (0, 0), 1.0)
630
+ except ImportError:
631
+ mask_image = Image.fromarray(mask, mode="L")
632
+ mask_image = mask_image.filter(ImageFilter.MaxFilter(size=3))
633
+ mask_image = mask_image.filter(ImageFilter.GaussianBlur(radius=1.0))
634
+ mask = np.asarray(mask_image)
635
+
636
+ return Image.fromarray(mask.astype(np.uint8), mode="L")
637
+
638
+ def qwen_face_feature_mask(image):
639
+ base = image.convert("RGB")
640
+ skin = np.asarray(qwen_skin_mask(base)).astype(np.float32) / 255.0
641
+ gray_image = base.convert("L")
642
+ edges = np.asarray(gray_image.filter(ImageFilter.FIND_EDGES)).astype(np.float32)
643
+ features = ((skin > 0.08) & (edges > 10)).astype(np.uint8) * 255
644
+ defects = np.asarray(qwen_defect_mask(base)).astype(np.uint8)
645
+ mask = np.maximum(features, defects)
646
+ mask_image = Image.fromarray(mask, mode="L")
647
+ mask_image = mask_image.filter(ImageFilter.MaxFilter(size=5))
648
+ return mask_image.filter(ImageFilter.GaussianBlur(radius=1.6))
649
+
650
+ def _mask_to_boxes(mask_image, max_regions=ENHANCE_DETAILER_MAX_REGIONS):
651
+ mask = np.asarray(mask_image.convert("L"))
652
+ binary = (mask > 24).astype(np.uint8)
653
+ min_area = max(32, int(binary.shape[0] * binary.shape[1] * ENHANCE_DETAILER_MIN_REGION_AREA))
654
+ boxes = []
655
+
656
+ try:
657
+ import cv2
658
+ count, labels, stats, _ = cv2.connectedComponentsWithStats(binary, connectivity=8)
659
+ for label in range(1, count):
660
+ x, y, width, height, area = stats[label]
661
+ if area >= min_area:
662
+ boxes.append((int(x), int(y), int(x + width), int(y + height), int(area)))
663
+ except ImportError:
664
+ bbox = mask_image.point(lambda value: 255 if value > 24 else 0).getbbox()
665
+ if bbox:
666
+ x0, y0, x1, y1 = bbox
667
+ boxes.append((x0, y0, x1, y1, (x1 - x0) * (y1 - y0)))
668
+
669
+ boxes.sort(key=lambda item: item[4], reverse=True)
670
+ return [box[:4] for box in boxes[:max_regions]]
671
+
672
+ def _expand_box(box, image_size, pad_ratio=0.18, min_size=192):
673
+ x0, y0, x1, y1 = box
674
+ width = x1 - x0
675
+ height = y1 - y0
676
+ pad = int(max(width, height) * pad_ratio)
677
+ if width < min_size:
678
+ extra = (min_size - width) // 2
679
+ x0 -= extra
680
+ x1 += extra
681
+ if height < min_size:
682
+ extra = (min_size - height) // 2
683
+ y0 -= extra
684
+ y1 += extra
685
+ return (
686
+ max(0, x0 - pad),
687
+ max(0, y0 - pad),
688
+ min(image_size[0], x1 + pad),
689
+ min(image_size[1], y1 + pad),
690
+ )
691
+
692
+ def _local_detail_crop(crop, mask_crop, strength=0.35, hair=False):
693
+ base = crop.convert("RGB")
694
+ mask = mask_crop.convert("L").filter(ImageFilter.GaussianBlur(radius=1.8))
695
+ strength = max(0.0, min(1.0, strength))
696
+
697
+ if hair:
698
+ detailed = base.filter(ImageFilter.UnsharpMask(radius=0.55, percent=int(130 + 120 * strength), threshold=2))
699
+ detailed = ImageEnhance.Contrast(detailed).enhance(1.0 + 0.08 * strength)
700
+ edge_mask = base.convert("L").filter(ImageFilter.FIND_EDGES).filter(ImageFilter.GaussianBlur(radius=0.7))
701
+ mask = ImageChops.multiply(mask, edge_mask.point(lambda value: min(255, int(value * 2.2))))
702
+ else:
703
+ repaired = masked_texture_repair(base, strength=0.35 + 0.30 * strength)
704
+ detailed = repaired.filter(ImageFilter.UnsharpMask(radius=0.75, percent=int(80 + 90 * strength), threshold=3))
705
+ detailed = ImageEnhance.Contrast(detailed).enhance(1.0 + 0.045 * strength)
706
+
707
+ mask = mask.point(lambda value: int(value * strength))
708
+ return Image.composite(detailed, base, mask)
709
+
710
+ def qwen_tiled_detailer_pass(image, strength=0.35, include_hair=True):
711
+ if not ENHANCE_DETAILER_ENABLED or strength <= 0:
712
+ return image.convert("RGB")
713
+
714
+ result = image.convert("RGB")
715
+ region_specs = [(qwen_face_feature_mask(result), strength, False, 0.22)]
716
+ if include_hair:
717
+ region_specs.append((qwen_hair_mask(result), strength * 0.85, True, 0.12))
718
+
719
+ for mask, region_strength, hair, pad_ratio in region_specs:
720
+ for box in _mask_to_boxes(mask):
721
+ expanded = _expand_box(box, result.size, pad_ratio=pad_ratio, min_size=192)
722
+ crop = result.crop(expanded)
723
+ mask_crop = mask.crop(expanded)
724
+ detailed = _local_detail_crop(crop, mask_crop, strength=region_strength, hair=hair)
725
+ result.paste(detailed, expanded, mask_crop.filter(ImageFilter.GaussianBlur(radius=2.5)))
726
+
727
+ return result
728
+
729
  def masked_texture_repair(image, strength=0.55):
730
  base = image.convert("RGB")
731
  mask = qwen_defect_mask(base)
 
760
  enhanced = image.convert("RGB")
761
  total_steps = {
762
  ENHANCE_MODE_UPSCALE: 2,
763
+ ENHANCE_MODE_CLEAN: 4,
764
+ ENHANCE_MODE_MAX_DETAIL: 7,
765
  }[mode]
766
  step = 0
767
 
 
777
  enhanced = masked_texture_repair(enhanced, strength=0.55 if mode == ENHANCE_MODE_CLEAN else 0.68)
778
  enhanced = qwen_micro_detail(enhanced, amount=0.18)
779
 
780
+ if progress:
781
+ step += 1
782
+ progress(0.85 * step / total_steps, desc="Detailing face regions...")
783
+ enhanced = qwen_tiled_detailer_pass(
784
+ enhanced,
785
+ strength=0.32 if mode == ENHANCE_MODE_CLEAN else 0.48,
786
+ include_hair=(mode == ENHANCE_MODE_MAX_DETAIL),
787
+ )
788
+
789
  if mode == ENHANCE_MODE_MAX_DETAIL:
790
  if progress:
791
  step += 1
 
798
  enhanced = tile_upscale(enhanced)
799
 
800
  if mode == ENHANCE_MODE_MAX_DETAIL:
801
+ if progress:
802
+ step += 1
803
+ progress(0.94, desc="Detailing final face and hair tiles...")
804
+ enhanced = qwen_tiled_detailer_pass(enhanced, strength=0.30, include_hair=True)
805
  if progress:
806
  step += 1
807
  progress(0.98, desc="Adding final photographic grain...")
 
988
 
989
  Pro Tips:
990
  - Use Clean for portraits with smudged or pitted skin
991
+ - Use Max Detail for hair, eyes, lashes, texture, grain, and sharper final output
992
  """)
993
 
994
  with gr.Row():
 
1015
  Enhancement Options:
1016
  - Off: No post-processing
1017
  - Upscale: Qwen precondition + 4x Nomos tiled upscale
1018
+ - Clean: Masked Qwen artifact repair + tiled face detail + upscaling
1019
+ - Max Detail: Tiled face/hair detail + micro detail + upscaling + final grain
1020
  """, visible=False)
1021
 
1022
  run_button = gr.Button("Generate!", variant="primary")
requirements.txt CHANGED
@@ -15,15 +15,12 @@ Kernels==0.11.0
15
  Peft
16
  Torchao==0.11.0
17
 
18
- # Image processing and upscaling
19
  spandrel
20
  spandrel_extra_arches
21
- Pillow
22
- Numpy
23
-
24
- # OpenCV for faster mask cleanup (optional)
25
- # Install with: pip install opencv-python
26
- # opencv-python>=4.5.0
27
 
28
  # SciPy for legacy image processing helpers (optional)
29
  # Install with: pip install scipy
@@ -41,6 +38,6 @@ Gradio_client
41
  # Performance monitoring (optional)
42
  # Install with: pip install psutil
43
 
44
- # Note: For optional CPU-side mask acceleration, run:
45
  # pip install -r requirements_enhanced.txt
46
- # pip install opencv-python scipy
 
15
  Peft
16
  Torchao==0.11.0
17
 
18
+ # Image processing and upscaling
19
  spandrel
20
  spandrel_extra_arches
21
+ opencv-python-headless
22
+ Pillow
23
+ Numpy
 
 
 
24
 
25
  # SciPy for legacy image processing helpers (optional)
26
  # Install with: pip install scipy
 
38
  # Performance monitoring (optional)
39
  # Install with: pip install psutil
40
 
41
+ # Note: For optional legacy helper coverage, run:
42
  # pip install -r requirements_enhanced.txt
43
+ # pip install scipy
tests/test_enhance_stage.py CHANGED
@@ -34,11 +34,22 @@ class EnhanceStageContractTest(unittest.TestCase):
34
  self.assertIn("spandrel", app)
35
  self.assertIn("tile_upscale", app)
36
 
37
- def test_upscaler_dependencies_are_declared(self):
38
- requirements = read_text("requirements.txt")
39
-
40
- self.assertIn("spandrel", requirements)
41
- self.assertIn("spandrel_extra_arches", requirements)
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  if __name__ == "__main__":
 
34
  self.assertIn("spandrel", app)
35
  self.assertIn("tile_upscale", app)
36
 
37
+ def test_upscaler_dependencies_are_declared(self):
38
+ requirements = read_text("requirements.txt")
39
+
40
+ self.assertIn("spandrel", requirements)
41
+ self.assertIn("spandrel_extra_arches", requirements)
42
+ self.assertIn("opencv-python-headless", requirements)
43
+
44
+ def test_qwen_detailer_is_part_of_clean_and_max_detail(self):
45
+ app = read_text("app.py")
46
+
47
+ self.assertIn("ENHANCE_DETAILER_ENABLED", app)
48
+ self.assertIn("qwen_tiled_detailer_pass", app)
49
+ self.assertIn("qwen_hair_mask", app)
50
+ self.assertIn("qwen_face_feature_mask", app)
51
+ self.assertIn("Detailing face regions", app)
52
+ self.assertIn("Detailing final face and hair tiles", app)
53
 
54
 
55
  if __name__ == "__main__":