supli6669 commited on
Commit ·
f9f499d
1
Parent(s): fd6554d
feat: complete custom training pipeline bugfixes, dataset crawling, and validation
Browse files- app.py +106 -84
- datasets/crop_and_align_facexlib.py +28 -7
- datasets/resize_and_clean.py +1 -1
- download_starrail.py +16 -1
- pipeline.py +3 -7
- requirements.txt +1 -0
- tools/download_ffhq_extended.py +123 -0
- tools/download_highres_datasets.py +167 -0
- tools/download_weights.py +5 -5
- tools/export_onnx.py +13 -3
- tools/test_pipeline.py +1 -1
- train_custom.py +21 -8
- train_realesrgan.py +28 -4
app.py
CHANGED
|
@@ -440,6 +440,16 @@ with st.sidebar:
|
|
| 440 |
except Exception as e:
|
| 441 |
st.error(f"Failed to import settings: {e}")
|
| 442 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
# ── Training Dashboard ──────────────────────────────────────────────────────
|
| 444 |
st.markdown("<div class='sidebar-section'>📊 Training Dashboard</div>", unsafe_allow_html=True)
|
| 445 |
status = get_training_status()
|
|
@@ -605,35 +615,37 @@ with tab_single:
|
|
| 605 |
|
| 606 |
st.markdown("<hr>", unsafe_allow_html=True)
|
| 607 |
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
|
|
|
| 635 |
|
| 636 |
-
|
| 637 |
try:
|
| 638 |
result = pipeline.process_image(
|
| 639 |
img,
|
|
@@ -649,68 +661,78 @@ with tab_single:
|
|
| 649 |
batch_size=4,
|
| 650 |
face_restore=enable_face_restoration
|
| 651 |
)
|
| 652 |
-
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
| 654 |
except Exception as e:
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
'initialization': '🔧 Initializing',
|
| 671 |
-
'detection': '👁️ Detecting Faces',
|
| 672 |
-
'background': '🖼️ Upscaling Background',
|
| 673 |
-
'restoration': '✨ Restoring Faces',
|
| 674 |
-
'blending': '🎨 Blending Faces',
|
| 675 |
-
'complete': '✅ Complete'
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
while st.session_state.processing:
|
| 679 |
-
time.sleep(0.1)
|
| 680 |
-
progress_data = st.session_state.progress_state
|
| 681 |
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
|
| 691 |
-
|
|
|
|
| 692 |
stage_name = stage_names.get(progress_data['stage'], progress_data['stage'])
|
| 693 |
stage_text.text(f"**{stage_name}**")
|
| 694 |
status_text.text(progress_data['message'])
|
| 695 |
progress_bar.progress(progress_data['progress'])
|
| 696 |
else:
|
| 697 |
status_text.text('⏳ Starting...')
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
| 711 |
st.stop()
|
| 712 |
-
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
history_item = {
|
| 715 |
'name': img_name,
|
| 716 |
'timestamp': time.time(),
|
|
@@ -725,12 +747,9 @@ with tab_single:
|
|
| 725 |
'enhanced_shape': enhanced_img.shape[:2]
|
| 726 |
}
|
| 727 |
st.session_state.history.insert(0, history_item)
|
| 728 |
-
# Keep only last 10 items
|
| 729 |
if len(st.session_state.history) > 10:
|
| 730 |
st.session_state.history = st.session_state.history[:10]
|
| 731 |
-
|
| 732 |
-
st.warning('Processing is already running. Please wait.')
|
| 733 |
-
st.stop()
|
| 734 |
|
| 735 |
h_orig, w_orig = img.shape[:2]
|
| 736 |
h_enh, w_enh = enhanced_img.shape[:2]
|
|
@@ -853,6 +872,9 @@ with tab_batch:
|
|
| 853 |
)
|
| 854 |
if uploaded_files:
|
| 855 |
if st.button("🚀 Process Batch", key="batch_button"):
|
|
|
|
|
|
|
|
|
|
| 856 |
import zipfile
|
| 857 |
from io import BytesIO
|
| 858 |
|
|
|
|
| 440 |
except Exception as e:
|
| 441 |
st.error(f"Failed to import settings: {e}")
|
| 442 |
|
| 443 |
+
if st.button("🔄 Reset UI State"):
|
| 444 |
+
st.session_state.processing = False
|
| 445 |
+
st.session_state.enhanced_img = None
|
| 446 |
+
st.session_state.processing_error = None
|
| 447 |
+
st.session_state.last_run_params = None
|
| 448 |
+
if pipeline:
|
| 449 |
+
pipeline.cancel_flag = False
|
| 450 |
+
st.success("UI State reset successfully!")
|
| 451 |
+
st.rerun()
|
| 452 |
+
|
| 453 |
# ── Training Dashboard ──────────────────────────────────────────────────────
|
| 454 |
st.markdown("<div class='sidebar-section'>📊 Training Dashboard</div>", unsafe_allow_html=True)
|
| 455 |
status = get_training_status()
|
|
|
|
| 615 |
|
| 616 |
st.markdown("<hr>", unsafe_allow_html=True)
|
| 617 |
|
| 618 |
+
# Track parameters to auto-rerun if they change
|
| 619 |
+
current_params = {
|
| 620 |
+
'img_name': img_name,
|
| 621 |
+
'w': fidelity_weight,
|
| 622 |
+
'detection_model': face_detector,
|
| 623 |
+
'upscale': upscale_factor,
|
| 624 |
+
'blend_softness': blend_softness,
|
| 625 |
+
'bg_upsampler': 'realesrgan' if bg_upscale_toggle else None,
|
| 626 |
+
'det_threshold': det_threshold,
|
| 627 |
+
'sharpen_amount': sharpen_amount,
|
| 628 |
+
'face_upsample': face_upscale_toggle,
|
| 629 |
+
'face_restore': enable_face_restoration
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
if st.session_state.get('last_run_params') != current_params:
|
| 633 |
+
st.session_state.enhanced_img = None
|
| 634 |
+
st.session_state.processing_error = None
|
| 635 |
+
st.session_state.processing = False
|
| 636 |
+
st.session_state.process_duration = None
|
| 637 |
+
if pipeline:
|
| 638 |
+
pipeline.cancel_flag = False
|
| 639 |
+
|
| 640 |
+
if st.session_state.enhanced_img is None and st.session_state.get('processing_error') is None:
|
| 641 |
+
if not st.session_state.get('processing'):
|
| 642 |
+
st.session_state.processing = True
|
| 643 |
+
st.session_state.progress_state = {'stage': 'initialization', 'progress': 0.0, 'message': 'Starting...', 'active': True, 'cancelled': False}
|
| 644 |
+
st.session_state.start_time = time.time()
|
| 645 |
+
if pipeline:
|
| 646 |
+
pipeline.cancel_flag = False
|
| 647 |
|
| 648 |
+
def _run():
|
| 649 |
try:
|
| 650 |
result = pipeline.process_image(
|
| 651 |
img,
|
|
|
|
| 661 |
batch_size=4,
|
| 662 |
face_restore=enable_face_restoration
|
| 663 |
)
|
| 664 |
+
if pipeline.cancel_flag:
|
| 665 |
+
return
|
| 666 |
+
st.session_state.enhanced_img = result
|
| 667 |
+
st.session_state.process_duration = time.time() - st.session_state.start_time
|
| 668 |
+
st.session_state.last_run_params = current_params
|
| 669 |
except Exception as e:
|
| 670 |
+
import traceback
|
| 671 |
+
traceback.print_exc()
|
| 672 |
+
st.session_state.processing_error = str(e)
|
| 673 |
+
finally:
|
| 674 |
+
st.session_state.processing = False
|
| 675 |
+
st.session_state.progress_state['active'] = False
|
| 676 |
+
|
| 677 |
+
threading.Thread(target=_run, daemon=True).start()
|
| 678 |
+
st.rerun()
|
| 679 |
+
|
| 680 |
+
else:
|
| 681 |
+
# We are actively processing
|
| 682 |
+
progress_bar = st.progress(0)
|
| 683 |
+
status_text = st.empty()
|
| 684 |
+
stage_text = st.empty()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
+
stage_names = {
|
| 687 |
+
'initialization': '🔧 Initializing',
|
| 688 |
+
'detection': '👁️ Detecting Faces',
|
| 689 |
+
'background': '🖼️ Upscaling Background',
|
| 690 |
+
'restoration': '✨ Restoring Faces',
|
| 691 |
+
'blending': '🎨 Blending Faces',
|
| 692 |
+
'complete': '✅ Complete'
|
| 693 |
+
}
|
| 694 |
|
| 695 |
+
progress_data = st.session_state.progress_state
|
| 696 |
+
if progress_data.get('active'):
|
| 697 |
stage_name = stage_names.get(progress_data['stage'], progress_data['stage'])
|
| 698 |
stage_text.text(f"**{stage_name}**")
|
| 699 |
status_text.text(progress_data['message'])
|
| 700 |
progress_bar.progress(progress_data['progress'])
|
| 701 |
else:
|
| 702 |
status_text.text('⏳ Starting...')
|
| 703 |
+
|
| 704 |
+
# Render Cancel button
|
| 705 |
+
cancel_col, _ = st.columns([1, 3])
|
| 706 |
+
with cancel_col:
|
| 707 |
+
cancel_button = st.button("❌ Cancel", type="secondary")
|
| 708 |
+
|
| 709 |
+
if cancel_button:
|
| 710 |
+
if pipeline:
|
| 711 |
+
pipeline.cancel_flag = True
|
| 712 |
+
st.session_state.processing = False
|
| 713 |
+
st.session_state.progress_state['cancelled'] = True
|
| 714 |
+
st.warning("⚠️ Processing cancelled by user")
|
| 715 |
+
st.rerun()
|
| 716 |
+
|
| 717 |
+
time.sleep(0.1)
|
| 718 |
+
st.rerun()
|
| 719 |
st.stop()
|
| 720 |
+
|
| 721 |
+
if st.session_state.get('processing_error') is not None:
|
| 722 |
+
st.error(f"❌ Processing failed: {st.session_state.processing_error}")
|
| 723 |
+
st.info("💡 Try reducing the upscale factor or disabling some features.")
|
| 724 |
+
if st.button("🔄 Try Again"):
|
| 725 |
+
st.session_state.processing_error = None
|
| 726 |
+
st.session_state.processing = False
|
| 727 |
+
st.rerun()
|
| 728 |
+
st.stop()
|
| 729 |
+
|
| 730 |
+
# If we reach here, we have the enhanced image
|
| 731 |
+
enhanced_img = st.session_state.enhanced_img
|
| 732 |
+
process_duration = st.session_state.process_duration
|
| 733 |
+
|
| 734 |
+
# Save to history
|
| 735 |
+
if st.session_state.get('history_added_for') != current_params:
|
| 736 |
history_item = {
|
| 737 |
'name': img_name,
|
| 738 |
'timestamp': time.time(),
|
|
|
|
| 747 |
'enhanced_shape': enhanced_img.shape[:2]
|
| 748 |
}
|
| 749 |
st.session_state.history.insert(0, history_item)
|
|
|
|
| 750 |
if len(st.session_state.history) > 10:
|
| 751 |
st.session_state.history = st.session_state.history[:10]
|
| 752 |
+
st.session_state.history_added_for = current_params
|
|
|
|
|
|
|
| 753 |
|
| 754 |
h_orig, w_orig = img.shape[:2]
|
| 755 |
h_enh, w_enh = enhanced_img.shape[:2]
|
|
|
|
| 872 |
)
|
| 873 |
if uploaded_files:
|
| 874 |
if st.button("🚀 Process Batch", key="batch_button"):
|
| 875 |
+
if pipeline is None:
|
| 876 |
+
st.error("Cannot process batch: The AI Enhancer pipeline is offline or failed to initialize. Please check the logs.")
|
| 877 |
+
st.stop()
|
| 878 |
import zipfile
|
| 879 |
from io import BytesIO
|
| 880 |
|
datasets/crop_and_align_facexlib.py
CHANGED
|
@@ -16,9 +16,11 @@ from facelib.utils.face_restoration_helper import FaceRestoreHelper
|
|
| 16 |
# Configuration
|
| 17 |
# ----------------------------------------------------------------------
|
| 18 |
SRC_ROOT = os.path.join(project_dir, "datasets", "game_characters")
|
| 19 |
-
DST_ROOT = os.path.join(project_dir, "datasets", "ffhq", "ffhq_512")
|
| 20 |
DETECTOR = "retinaface_mobile0.25" # Fast CPU detector
|
| 21 |
BLUR_THRESHOLD = 80.0 # Discard crops with Laplacian variance below this
|
|
|
|
|
|
|
| 22 |
|
| 23 |
def is_blurry(img, threshold=BLUR_THRESHOLD):
|
| 24 |
"""Check if an image is blurry using the variance of Laplacian method."""
|
|
@@ -33,9 +35,7 @@ def main():
|
|
| 33 |
# Configure face detector path
|
| 34 |
os.environ['FACE_DETECTOR_PATH'] = os.path.join(project_dir, "weights", "facelib")
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
if os.path.exists(DST_ROOT):
|
| 38 |
-
shutil.rmtree(DST_ROOT)
|
| 39 |
os.makedirs(DST_ROOT, exist_ok=True)
|
| 40 |
|
| 41 |
# Initialize face helper
|
|
@@ -61,6 +61,8 @@ def main():
|
|
| 61 |
count = 0
|
| 62 |
failures = 0
|
| 63 |
skipped_blur = 0
|
|
|
|
|
|
|
| 64 |
|
| 65 |
for i, img_path in enumerate(raw_images):
|
| 66 |
try:
|
|
@@ -69,6 +71,12 @@ def main():
|
|
| 69 |
failures += 1
|
| 70 |
continue
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
face_helper.clean_all()
|
| 73 |
face_helper.read_image(img)
|
| 74 |
|
|
@@ -87,18 +95,29 @@ def main():
|
|
| 87 |
face_helper.align_warp_face()
|
| 88 |
|
| 89 |
# Save cropped faces
|
| 90 |
-
for cropped_face in face_helper.cropped_faces:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
# Blur filter check
|
| 92 |
if is_blurry(cropped_face):
|
| 93 |
skipped_blur += 1
|
| 94 |
continue
|
| 95 |
|
| 96 |
count += 1
|
| 97 |
-
dest_path = os.path.join(DST_ROOT, f"
|
| 98 |
cv2.imwrite(dest_path, cropped_face)
|
| 99 |
|
| 100 |
if (i + 1) % 100 == 0:
|
| 101 |
-
print(f" Processed {i + 1}/{len(raw_images)} images. Extracted {count} faces (skipped {skipped_blur} blurry)...")
|
| 102 |
|
| 103 |
except Exception as e:
|
| 104 |
# print(f"Error processing {img_path}: {e}")
|
|
@@ -108,6 +127,8 @@ def main():
|
|
| 108 |
print(f" Total raw images: {len(raw_images)}")
|
| 109 |
print(f" Extracted face crops: {count}")
|
| 110 |
print(f" Skipped blurry crops: {skipped_blur}")
|
|
|
|
|
|
|
| 111 |
print(f" Failed loads: {failures}")
|
| 112 |
print(f" Destination: {DST_ROOT}\n")
|
| 113 |
|
|
|
|
| 16 |
# Configuration
|
| 17 |
# ----------------------------------------------------------------------
|
| 18 |
SRC_ROOT = os.path.join(project_dir, "datasets", "game_characters")
|
| 19 |
+
DST_ROOT = os.path.join(project_dir, "models", "CodeFormer", "datasets", "ffhq", "ffhq_512")
|
| 20 |
DETECTOR = "retinaface_mobile0.25" # Fast CPU detector
|
| 21 |
BLUR_THRESHOLD = 80.0 # Discard crops with Laplacian variance below this
|
| 22 |
+
MIN_RESOLUTION = 128 # Discard raw images smaller than this
|
| 23 |
+
MIN_FACE_COVERAGE = 0.15 # Discard face crops that cover less than 15% of the original image area
|
| 24 |
|
| 25 |
def is_blurry(img, threshold=BLUR_THRESHOLD):
|
| 26 |
"""Check if an image is blurry using the variance of Laplacian method."""
|
|
|
|
| 35 |
# Configure face detector path
|
| 36 |
os.environ['FACE_DETECTOR_PATH'] = os.path.join(project_dir, "weights", "facelib")
|
| 37 |
|
| 38 |
+
# Ensure destination folder exists
|
|
|
|
|
|
|
| 39 |
os.makedirs(DST_ROOT, exist_ok=True)
|
| 40 |
|
| 41 |
# Initialize face helper
|
|
|
|
| 61 |
count = 0
|
| 62 |
failures = 0
|
| 63 |
skipped_blur = 0
|
| 64 |
+
skipped_res = 0
|
| 65 |
+
skipped_coverage = 0
|
| 66 |
|
| 67 |
for i, img_path in enumerate(raw_images):
|
| 68 |
try:
|
|
|
|
| 71 |
failures += 1
|
| 72 |
continue
|
| 73 |
|
| 74 |
+
# Resolution check
|
| 75 |
+
h_img, w_img, _ = img.shape
|
| 76 |
+
if h_img < MIN_RESOLUTION or w_img < MIN_RESOLUTION:
|
| 77 |
+
skipped_res += 1
|
| 78 |
+
continue
|
| 79 |
+
|
| 80 |
face_helper.clean_all()
|
| 81 |
face_helper.read_image(img)
|
| 82 |
|
|
|
|
| 95 |
face_helper.align_warp_face()
|
| 96 |
|
| 97 |
# Save cropped faces
|
| 98 |
+
for idx, cropped_face in enumerate(face_helper.cropped_faces):
|
| 99 |
+
# Face coverage check
|
| 100 |
+
if hasattr(face_helper, 'face_det_list') and idx < len(face_helper.face_det_list):
|
| 101 |
+
det_box = face_helper.face_det_list[idx]
|
| 102 |
+
x1, y1, x2, y2 = det_box[:4]
|
| 103 |
+
face_area = (x2 - x1) * (y2 - y1)
|
| 104 |
+
img_area = w_img * h_img
|
| 105 |
+
ratio = face_area / img_area
|
| 106 |
+
if ratio < MIN_FACE_COVERAGE:
|
| 107 |
+
skipped_coverage += 1
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
# Blur filter check
|
| 111 |
if is_blurry(cropped_face):
|
| 112 |
skipped_blur += 1
|
| 113 |
continue
|
| 114 |
|
| 115 |
count += 1
|
| 116 |
+
dest_path = os.path.join(DST_ROOT, f"face_align_{count:06d}.png")
|
| 117 |
cv2.imwrite(dest_path, cropped_face)
|
| 118 |
|
| 119 |
if (i + 1) % 100 == 0:
|
| 120 |
+
print(f" Processed {i + 1}/{len(raw_images)} images. Extracted {count} faces (skipped {skipped_blur} blurry, {skipped_coverage} small, {skipped_res} low-res)...")
|
| 121 |
|
| 122 |
except Exception as e:
|
| 123 |
# print(f"Error processing {img_path}: {e}")
|
|
|
|
| 127 |
print(f" Total raw images: {len(raw_images)}")
|
| 128 |
print(f" Extracted face crops: {count}")
|
| 129 |
print(f" Skipped blurry crops: {skipped_blur}")
|
| 130 |
+
print(f" Skipped low-res images: {skipped_res}")
|
| 131 |
+
print(f" Skipped small face crops: {skipped_coverage}")
|
| 132 |
print(f" Failed loads: {failures}")
|
| 133 |
print(f" Destination: {DST_ROOT}\n")
|
| 134 |
|
datasets/resize_and_clean.py
CHANGED
|
@@ -10,7 +10,7 @@ import os, cv2, glob
|
|
| 10 |
# Source folder containing the raw character folders
|
| 11 |
SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "game_characters"))
|
| 12 |
# Destination folder expected by CodeFormer
|
| 13 |
-
DST_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "ffhq", "ffhq_512"))
|
| 14 |
|
| 15 |
os.makedirs(DST_ROOT, exist_ok=True)
|
| 16 |
|
|
|
|
| 10 |
# Source folder containing the raw character folders
|
| 11 |
SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "game_characters"))
|
| 12 |
# Destination folder expected by CodeFormer
|
| 13 |
+
DST_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "models", "CodeFormer", "datasets", "ffhq", "ffhq_512"))
|
| 14 |
|
| 15 |
os.makedirs(DST_ROOT, exist_ok=True)
|
| 16 |
|
download_starrail.py
CHANGED
|
@@ -355,7 +355,22 @@ def main():
|
|
| 355 |
print("#" * 60)
|
| 356 |
# A. Official Skins (113 characters)
|
| 357 |
download_official_skins("bluearchive", bluearchive_dir)
|
| 358 |
-
# B. Safebooru
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
download_from_safebooru("blue_archive", bluearchive_dir, "ba_general", 300)
|
| 360 |
|
| 361 |
# ─────────────────────────────────────────────────────────────
|
|
|
|
| 355 |
print("#" * 60)
|
| 356 |
# A. Official Skins (113 characters)
|
| 357 |
download_official_skins("bluearchive", bluearchive_dir)
|
| 358 |
+
# B. Safebooru Characters
|
| 359 |
+
ba_chars = {
|
| 360 |
+
"shiroko": "shiroko_(blue_archive)",
|
| 361 |
+
"aru": "aru_(blue_archive)",
|
| 362 |
+
"hoshino": "hoshino_(blue_archive)",
|
| 363 |
+
"nonomi": "nonomi_(blue_archive)",
|
| 364 |
+
"serika": "serika_(blue_archive)",
|
| 365 |
+
"momoi": "momoi_(blue_archive)",
|
| 366 |
+
"midori": "midori_(blue_archive)",
|
| 367 |
+
"yuzu": "yuzu_(blue_archive)",
|
| 368 |
+
"mutsuki": "mutsuki_(blue_archive)",
|
| 369 |
+
"iori": "iori_(blue_archive)"
|
| 370 |
+
}
|
| 371 |
+
for key, tag in ba_chars.items():
|
| 372 |
+
download_from_safebooru(tag, bluearchive_dir, f"ba_{key}", 30)
|
| 373 |
+
# C. Safebooru Mix
|
| 374 |
download_from_safebooru("blue_archive", bluearchive_dir, "ba_general", 300)
|
| 375 |
|
| 376 |
# ─────────────────────────────────────────────────────────────
|
pipeline.py
CHANGED
|
@@ -38,6 +38,7 @@ class LocalAIEnhancerPipeline:
|
|
| 38 |
self.device = torch.device(device)
|
| 39 |
|
| 40 |
self.progress_callback = progress_callback
|
|
|
|
| 41 |
|
| 42 |
print(f"[Pipeline] Initializing pipeline on device: {self.device}")
|
| 43 |
|
|
@@ -104,12 +105,7 @@ class LocalAIEnhancerPipeline:
|
|
| 104 |
|
| 105 |
def _check_cancelled(self):
|
| 106 |
"""Check if processing was cancelled by user."""
|
| 107 |
-
|
| 108 |
-
# Check session state through callback
|
| 109 |
-
import streamlit as st
|
| 110 |
-
if hasattr(st, 'session_state') and 'progress_state' in st.session_state:
|
| 111 |
-
return st.session_state.progress_state.get('cancelled', False)
|
| 112 |
-
return False
|
| 113 |
|
| 114 |
def _get_onnx_session(self, path, providers=None):
|
| 115 |
"""Get or create cached ONNX session."""
|
|
@@ -154,7 +150,7 @@ class LocalAIEnhancerPipeline:
|
|
| 154 |
|
| 155 |
def run_onnx_batch(self, faces_np, w_val):
|
| 156 |
"""Helper to run ONNX batch inference."""
|
| 157 |
-
w_np = np.full((faces_np.shape[0],
|
| 158 |
ort_inputs = {
|
| 159 |
self.ort_session_cf.get_inputs()[0].name: faces_np,
|
| 160 |
self.ort_session_cf.get_inputs()[1].name: w_np
|
|
|
|
| 38 |
self.device = torch.device(device)
|
| 39 |
|
| 40 |
self.progress_callback = progress_callback
|
| 41 |
+
self.cancel_flag = False
|
| 42 |
|
| 43 |
print(f"[Pipeline] Initializing pipeline on device: {self.device}")
|
| 44 |
|
|
|
|
| 105 |
|
| 106 |
def _check_cancelled(self):
|
| 107 |
"""Check if processing was cancelled by user."""
|
| 108 |
+
return self.cancel_flag
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
def _get_onnx_session(self, path, providers=None):
|
| 111 |
"""Get or create cached ONNX session."""
|
|
|
|
| 150 |
|
| 151 |
def run_onnx_batch(self, faces_np, w_val):
|
| 152 |
"""Helper to run ONNX batch inference."""
|
| 153 |
+
w_np = np.full((faces_np.shape[0],), w_val, dtype=np.float32)
|
| 154 |
ort_inputs = {
|
| 155 |
self.ort_session_cf.get_inputs()[0].name: faces_np,
|
| 156 |
self.ort_session_cf.get_inputs()[1].name: w_np
|
requirements.txt
CHANGED
|
@@ -116,3 +116,4 @@ websockets==16.0
|
|
| 116 |
Werkzeug==3.1.8
|
| 117 |
wheel==0.47.0
|
| 118 |
yapf==0.43.0
|
|
|
|
|
|
| 116 |
Werkzeug==3.1.8
|
| 117 |
wheel==0.47.0
|
| 118 |
yapf==0.43.0
|
| 119 |
+
streamlit-image-comparison
|
tools/download_ffhq_extended.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import io
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import requests
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
SHARDS = [
|
| 10 |
+
"train-00001-of-00054-c76023902046cea3.parquet",
|
| 11 |
+
"train-00002-of-00054-b43f6d454561b047.parquet",
|
| 12 |
+
"train-00003-of-00054-0637d6a1c5f28946.parquet",
|
| 13 |
+
"train-00004-of-00054-17a50ec36be02fa9.parquet",
|
| 14 |
+
"train-00005-of-00054-b370d8ca7905b127.parquet",
|
| 15 |
+
"train-00006-of-00054-e7f70526908ad428.parquet",
|
| 16 |
+
"train-00007-of-00054-36469d3079484e03.parquet",
|
| 17 |
+
"train-00008-of-00054-564af51540d658a8.parquet",
|
| 18 |
+
"train-00009-of-00054-57224ef56d82da23.parquet"
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
def load_parquet_safe(url):
|
| 22 |
+
print(f" Loading: {url}")
|
| 23 |
+
try:
|
| 24 |
+
df = pd.read_parquet(url)
|
| 25 |
+
print(f" [OK] Loaded {len(df)} rows.")
|
| 26 |
+
return df
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f" [FAIL] Failed: {e}")
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
def find_image_column(df):
|
| 32 |
+
for col in df.columns:
|
| 33 |
+
sample = df[col].iloc[0]
|
| 34 |
+
if isinstance(sample, dict) and "bytes" in sample:
|
| 35 |
+
return col
|
| 36 |
+
if isinstance(sample, bytes):
|
| 37 |
+
return col
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
def extract_image_bytes(cell):
|
| 41 |
+
if isinstance(cell, dict):
|
| 42 |
+
return cell.get("bytes")
|
| 43 |
+
if isinstance(cell, bytes):
|
| 44 |
+
return cell
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
def main():
|
| 48 |
+
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 49 |
+
codeformer_dir = os.path.join(project_dir, "models", "CodeFormer")
|
| 50 |
+
save_dir = os.path.join(codeformer_dir, "datasets", "ffhq", "ffhq_512", "faces")
|
| 51 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
print("=" * 55)
|
| 54 |
+
print(" EXTENDED FFHQ REAL FACE DOWNLOADER")
|
| 55 |
+
print(f" Target directory: {save_dir}")
|
| 56 |
+
print("=" * 55)
|
| 57 |
+
|
| 58 |
+
# Check current count in faces subdirectory to determine start_idx
|
| 59 |
+
existing = [f for f in os.listdir(save_dir) if f.endswith('.png') and f.startswith('face')]
|
| 60 |
+
highest_idx = -1
|
| 61 |
+
for f in existing:
|
| 62 |
+
try:
|
| 63 |
+
idx = int(f.replace('face_', '').replace('.png', ''))
|
| 64 |
+
highest_idx = max(highest_idx, idx)
|
| 65 |
+
except ValueError:
|
| 66 |
+
continue
|
| 67 |
+
start_idx = highest_idx + 1
|
| 68 |
+
print(f"Starting index for new files: face_{start_idx:05d}.png")
|
| 69 |
+
|
| 70 |
+
images_per_shard = 300
|
| 71 |
+
total_saved = 0
|
| 72 |
+
|
| 73 |
+
for shard_idx, shard_name in enumerate(SHARDS):
|
| 74 |
+
print(f"\n[{shard_idx + 1}/{len(SHARDS)}] Processing {shard_name}...")
|
| 75 |
+
url = f"https://huggingface.co/datasets/Ryan-sjtu/ffhq512-caption/resolve/main/data/{shard_name}"
|
| 76 |
+
|
| 77 |
+
df = load_parquet_safe(url)
|
| 78 |
+
if df is None:
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
img_col = find_image_column(df)
|
| 82 |
+
if img_col is None:
|
| 83 |
+
print(" No image column found in dataframe.")
|
| 84 |
+
continue
|
| 85 |
+
|
| 86 |
+
saved_in_shard = 0
|
| 87 |
+
for i in range(len(df)):
|
| 88 |
+
if saved_in_shard >= images_per_shard:
|
| 89 |
+
break
|
| 90 |
+
try:
|
| 91 |
+
cell = df[img_col].iloc[i]
|
| 92 |
+
img_bytes = extract_image_bytes(cell)
|
| 93 |
+
if not img_bytes:
|
| 94 |
+
continue
|
| 95 |
+
|
| 96 |
+
img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
|
| 97 |
+
img = img.resize((512, 512), Image.Resampling.LANCZOS)
|
| 98 |
+
|
| 99 |
+
filename = f"face_{start_idx + total_saved:05d}.png"
|
| 100 |
+
filepath = os.path.join(save_dir, filename)
|
| 101 |
+
img.save(filepath, "PNG")
|
| 102 |
+
|
| 103 |
+
saved_in_shard += 1
|
| 104 |
+
total_saved += 1
|
| 105 |
+
|
| 106 |
+
if saved_in_shard % 50 == 0 or saved_in_shard == images_per_shard:
|
| 107 |
+
print(f" Saved {saved_in_shard}/{images_per_shard} from this shard...")
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
print(f" [warn] Row {i} error: {e}")
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
print(f" Shard completed. Saved {saved_in_shard} images.")
|
| 114 |
+
time.sleep(0.5)
|
| 115 |
+
|
| 116 |
+
print("\n" + "=" * 55)
|
| 117 |
+
print(" DOWNLOAD COMPLETED")
|
| 118 |
+
print(f" Total downloaded this run: {total_saved}")
|
| 119 |
+
print(f" Total images in directory: {len(os.listdir(save_dir))}")
|
| 120 |
+
print("=" * 55)
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
main()
|
tools/download_highres_datasets.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import argparse
|
| 4 |
+
import urllib.request
|
| 5 |
+
import zipfile
|
| 6 |
+
import shutil
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
# Setup sys.path to bypass local "datasets" folder conflict
|
| 10 |
+
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
+
DATASET_BASE = os.path.join(PROJECT_DIR, "models", "CodeFormer", "datasets", "ffhq", "ffhq_512")
|
| 12 |
+
|
| 13 |
+
# Setup clean path for HuggingFace datasets
|
| 14 |
+
sys.path = [p for p in sys.path if p not in ('', '.')]
|
| 15 |
+
from datasets import load_dataset
|
| 16 |
+
|
| 17 |
+
def download_div2k(test_run=False):
|
| 18 |
+
print("\n" + "="*50)
|
| 19 |
+
print(" DOWNLOADING DIV2K DATASET (2K RESOLUTION)")
|
| 20 |
+
print("="*50)
|
| 21 |
+
|
| 22 |
+
target_dir = os.path.join(DATASET_BASE, "div2k")
|
| 23 |
+
os.makedirs(target_dir, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# Check if we already have images
|
| 26 |
+
existing = len([f for f in os.listdir(target_dir) if f.endswith(".png")])
|
| 27 |
+
if existing >= (5 if test_run else 800):
|
| 28 |
+
print(f"[OK] DIV2K already contains {existing} images. Skipping download.")
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
zip_url = "http://data.vision.ee.ethz.ch/cvl/DIV2K/DIV2K_train_HR.zip"
|
| 32 |
+
zip_path = os.path.join(PROJECT_DIR, "DIV2K_train_HR.zip")
|
| 33 |
+
extract_dir = os.path.join(PROJECT_DIR, "DIV2K_temp")
|
| 34 |
+
|
| 35 |
+
# In test mode we don't download 3.5GB zip, we just download 5 images directly if possible
|
| 36 |
+
if test_run:
|
| 37 |
+
print("[Test Mode] Downloading 5 sample images directly from DIV2K repo...")
|
| 38 |
+
for i in range(1, 6):
|
| 39 |
+
img_url = f"https://raw.githubusercontent.com/eugenesiow/super-image-data/master/div2k/DIV2K_train_HR/{i:04d}.png"
|
| 40 |
+
dest = os.path.join(target_dir, f"div2k_{i:04d}.png")
|
| 41 |
+
try:
|
| 42 |
+
urllib.request.urlretrieve(img_url, dest)
|
| 43 |
+
print(f" Saved sample {i}/5 -> {dest}")
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f" Failed sample {i}: {e}")
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
print(f"Downloading {zip_url} (approx. 3.5 GB)...")
|
| 49 |
+
def report_hook(block_num, block_size, total_size):
|
| 50 |
+
read_so_far = block_num * block_size
|
| 51 |
+
if total_size > 0:
|
| 52 |
+
percent = min(100, read_so_far * 100 / total_size)
|
| 53 |
+
sys.stdout.write(f"\rProgress: {percent:.2f}% ({read_so_far / 1024 / 1024:.2f} MB of {total_size / 1024 / 1024:.2f} MB)")
|
| 54 |
+
else:
|
| 55 |
+
sys.stdout.write(f"\rProgress: {read_so_far / 1024 / 1024:.2f} MB")
|
| 56 |
+
sys.stdout.flush()
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
urllib.request.urlretrieve(zip_url, zip_path, reporthook=report_hook)
|
| 60 |
+
print("\n[OK] DIV2K Zip downloaded. Extracting...")
|
| 61 |
+
|
| 62 |
+
os.makedirs(extract_dir, exist_ok=True)
|
| 63 |
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
| 64 |
+
zip_ref.extractall(extract_dir)
|
| 65 |
+
|
| 66 |
+
print("[*] Copying images to target directory...")
|
| 67 |
+
copied = 0
|
| 68 |
+
for root, _, files in os.walk(extract_dir):
|
| 69 |
+
for file in files:
|
| 70 |
+
if file.lower().endswith(".png"):
|
| 71 |
+
src = os.path.join(root, file)
|
| 72 |
+
dest = os.path.join(target_dir, f"div2k_{file}")
|
| 73 |
+
shutil.copy2(src, dest)
|
| 74 |
+
copied += 1
|
| 75 |
+
|
| 76 |
+
print(f"[OK] Successfully extracted and copied {copied} DIV2K images.")
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"\n[FAIL] Error occurred during DIV2K setup: {e}")
|
| 80 |
+
finally:
|
| 81 |
+
# Cleanup
|
| 82 |
+
try:
|
| 83 |
+
if os.path.exists(zip_path):
|
| 84 |
+
os.remove(zip_path)
|
| 85 |
+
if os.path.exists(extract_dir):
|
| 86 |
+
shutil.rmtree(extract_dir)
|
| 87 |
+
except Exception:
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
def download_huggingface_dataset(repo_name, target_subdir, max_images, image_key='image', prefix='img', test_run=False):
|
| 91 |
+
print("\n" + "="*50)
|
| 92 |
+
print(f" STREAMING DATASET FROM HF: {repo_name}")
|
| 93 |
+
print("="*50)
|
| 94 |
+
|
| 95 |
+
target_dir = os.path.join(DATASET_BASE, target_subdir)
|
| 96 |
+
os.makedirs(target_dir, exist_ok=True)
|
| 97 |
+
|
| 98 |
+
limit = 5 if test_run else max_images
|
| 99 |
+
|
| 100 |
+
existing = len([f for f in os.listdir(target_dir) if f.endswith(".png")])
|
| 101 |
+
if existing >= limit:
|
| 102 |
+
print(f"[OK] {target_subdir} already contains {existing} images. Skipping.")
|
| 103 |
+
return
|
| 104 |
+
|
| 105 |
+
print(f"Streaming from HF hub. Target count: {limit} images...")
|
| 106 |
+
try:
|
| 107 |
+
ds = load_dataset(repo_name, split="train", streaming=True)
|
| 108 |
+
saved = 0
|
| 109 |
+
for i, item in enumerate(ds):
|
| 110 |
+
if saved >= limit:
|
| 111 |
+
break
|
| 112 |
+
|
| 113 |
+
dest = os.path.join(target_dir, f"{prefix}_{saved:06d}.png")
|
| 114 |
+
if os.path.exists(dest):
|
| 115 |
+
saved += 1
|
| 116 |
+
continue
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
# Retrieve the PIL image
|
| 120 |
+
pil_img = item[image_key]
|
| 121 |
+
if not isinstance(pil_img, Image.Image):
|
| 122 |
+
# In case it's a dict with bytes
|
| 123 |
+
import io
|
| 124 |
+
pil_img = Image.open(io.BytesIO(pil_img['bytes']))
|
| 125 |
+
|
| 126 |
+
# Keep high-res, save as PNG
|
| 127 |
+
pil_img.convert("RGB").save(dest, "PNG")
|
| 128 |
+
saved += 1
|
| 129 |
+
if saved % 100 == 0 or test_run:
|
| 130 |
+
print(f" -> Saved {saved}/{limit} images")
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f" [Error] Failed to save image index {i}: {e}")
|
| 133 |
+
|
| 134 |
+
print(f"[OK] Successfully streamed and saved {saved} images to {target_dir}")
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f"[FAIL] Error streaming dataset {repo_name}: {e}")
|
| 137 |
+
|
| 138 |
+
def main():
|
| 139 |
+
parser = argparse.ArgumentParser(description="Download and integrate high-resolution datasets for super-resolution training.")
|
| 140 |
+
parser.add_argument("--test-run", action="store_true", help="Only download 5 images from each dataset for verification.")
|
| 141 |
+
args = parser.parse_args()
|
| 142 |
+
|
| 143 |
+
# 1. Download DIV2K (2K resolution)
|
| 144 |
+
download_div2k(test_run=args.test_run)
|
| 145 |
+
|
| 146 |
+
# 2. Stream Flickr2K Subset (2K resolution, yangtao9009/Flickr2K)
|
| 147 |
+
download_huggingface_dataset(
|
| 148 |
+
repo_name="yangtao9009/Flickr2K",
|
| 149 |
+
target_subdir="flickr2k_subset",
|
| 150 |
+
max_images=1000,
|
| 151 |
+
image_key="image",
|
| 152 |
+
prefix="flickr2k",
|
| 153 |
+
test_run=args.test_run
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# 3. Stream FFHQ-1024x1024 Subset (1024px resolution, Iceclear/FFHQ-HQ1024)
|
| 157 |
+
download_huggingface_dataset(
|
| 158 |
+
repo_name="Iceclear/FFHQ-HQ1024",
|
| 159 |
+
target_subdir="ffhq1024_subset",
|
| 160 |
+
max_images=10000,
|
| 161 |
+
image_key="image",
|
| 162 |
+
prefix="ffhq1024",
|
| 163 |
+
test_run=args.test_run
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
main()
|
tools/download_weights.py
CHANGED
|
@@ -31,11 +31,11 @@ def download_file(url, save_path):
|
|
| 31 |
|
| 32 |
def main():
|
| 33 |
weights_to_download = {
|
| 34 |
-
"weights/CodeFormer/codeformer.pth": "https://
|
| 35 |
-
"weights/facelib/detection_Resnet50_Final.pth": "https://
|
| 36 |
-
"weights/facelib/parsing_parsenet.pth": "https://
|
| 37 |
-
"weights/facelib/yolov5l-face.pth": "https://
|
| 38 |
-
"weights/realesrgan/RealESRGAN_x2plus.pth": "https://
|
| 39 |
}
|
| 40 |
|
| 41 |
for save_path, url in weights_to_download.items():
|
|
|
|
| 31 |
|
| 32 |
def main():
|
| 33 |
weights_to_download = {
|
| 34 |
+
"weights/CodeFormer/codeformer.pth": "https://huggingface.co/spaces/sczhou/CodeFormer/resolve/main/weights/CodeFormer/codeformer.pth",
|
| 35 |
+
"weights/facelib/detection_Resnet50_Final.pth": "https://huggingface.co/spaces/sczhou/CodeFormer/resolve/main/weights/facelib/detection_Resnet50_Final.pth",
|
| 36 |
+
"weights/facelib/parsing_parsenet.pth": "https://huggingface.co/spaces/sczhou/CodeFormer/resolve/main/weights/facelib/parsing_parsenet.pth",
|
| 37 |
+
"weights/facelib/yolov5l-face.pth": "https://huggingface.co/spaces/sczhou/CodeFormer/resolve/main/weights/facelib/yolov5l-face.pth",
|
| 38 |
+
"weights/realesrgan/RealESRGAN_x2plus.pth": "https://huggingface.co/spaces/sczhou/CodeFormer/resolve/main/weights/realesrgan/RealESRGAN_x2plus.pth"
|
| 39 |
}
|
| 40 |
|
| 41 |
for save_path, url in weights_to_download.items():
|
tools/export_onnx.py
CHANGED
|
@@ -104,7 +104,7 @@ def export_codeformer():
|
|
| 104 |
print(f"[WARNING] ONNX validation failed: {e}")
|
| 105 |
return True
|
| 106 |
|
| 107 |
-
def export_realesrgan():
|
| 108 |
print("\n--- Exporting Real-ESRGAN to ONNX ---")
|
| 109 |
device = torch.device("cpu")
|
| 110 |
|
|
@@ -115,21 +115,26 @@ def export_realesrgan():
|
|
| 115 |
if checkpoint_path:
|
| 116 |
print(f"Found custom Real-ESRGAN checkpoint: {checkpoint_path}")
|
| 117 |
scale = 4
|
|
|
|
| 118 |
else:
|
| 119 |
checkpoint_path = os.path.join(project_dir, "weights", "realesrgan", "RealESRGAN_x2plus.pth")
|
| 120 |
print(f"No custom checkpoint found. Using pretrained weights: {checkpoint_path}")
|
| 121 |
scale = 2
|
|
|
|
| 122 |
|
| 123 |
if not os.path.exists(checkpoint_path):
|
| 124 |
print(f"[ERROR] Real-ESRGAN weights not found at: {checkpoint_path}")
|
| 125 |
return False
|
| 126 |
|
|
|
|
|
|
|
|
|
|
| 127 |
# 2. Instantiate RRDBNet architecture with correct scale
|
| 128 |
net = RRDBNet(
|
| 129 |
num_in_ch=3,
|
| 130 |
num_out_ch=3,
|
| 131 |
num_feat=64,
|
| 132 |
-
num_block=
|
| 133 |
num_grow_ch=32,
|
| 134 |
scale=scale
|
| 135 |
)
|
|
@@ -175,8 +180,13 @@ def export_realesrgan():
|
|
| 175 |
return True
|
| 176 |
|
| 177 |
def main():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
success_cf = export_codeformer()
|
| 179 |
-
success_re = export_realesrgan()
|
| 180 |
if success_cf and success_re:
|
| 181 |
print("\n=== ALL MODELS EXPORTED SUCCESSFULLY ===")
|
| 182 |
else:
|
|
|
|
| 104 |
print(f"[WARNING] ONNX validation failed: {e}")
|
| 105 |
return True
|
| 106 |
|
| 107 |
+
def export_realesrgan(num_block=None):
|
| 108 |
print("\n--- Exporting Real-ESRGAN to ONNX ---")
|
| 109 |
device = torch.device("cpu")
|
| 110 |
|
|
|
|
| 115 |
if checkpoint_path:
|
| 116 |
print(f"Found custom Real-ESRGAN checkpoint: {checkpoint_path}")
|
| 117 |
scale = 4
|
| 118 |
+
default_blocks = 6
|
| 119 |
else:
|
| 120 |
checkpoint_path = os.path.join(project_dir, "weights", "realesrgan", "RealESRGAN_x2plus.pth")
|
| 121 |
print(f"No custom checkpoint found. Using pretrained weights: {checkpoint_path}")
|
| 122 |
scale = 2
|
| 123 |
+
default_blocks = 23
|
| 124 |
|
| 125 |
if not os.path.exists(checkpoint_path):
|
| 126 |
print(f"[ERROR] Real-ESRGAN weights not found at: {checkpoint_path}")
|
| 127 |
return False
|
| 128 |
|
| 129 |
+
blocks = num_block if num_block is not None else default_blocks
|
| 130 |
+
print(f"Instantiating RRDBNet with scale={scale}, num_block={blocks}")
|
| 131 |
+
|
| 132 |
# 2. Instantiate RRDBNet architecture with correct scale
|
| 133 |
net = RRDBNet(
|
| 134 |
num_in_ch=3,
|
| 135 |
num_out_ch=3,
|
| 136 |
num_feat=64,
|
| 137 |
+
num_block=blocks,
|
| 138 |
num_grow_ch=32,
|
| 139 |
scale=scale
|
| 140 |
)
|
|
|
|
| 180 |
return True
|
| 181 |
|
| 182 |
def main():
|
| 183 |
+
import argparse
|
| 184 |
+
parser = argparse.ArgumentParser(description="Export CodeFormer and Real-ESRGAN models to ONNX")
|
| 185 |
+
parser.add_argument('--num-block', type=int, default=None, help="Number of RRDB blocks for Real-ESRGAN (defaults to 6 for custom checkpoint, 23 for pretrained)")
|
| 186 |
+
args = parser.parse_args()
|
| 187 |
+
|
| 188 |
success_cf = export_codeformer()
|
| 189 |
+
success_re = export_realesrgan(num_block=args.num_block)
|
| 190 |
if success_cf and success_re:
|
| 191 |
print("\n=== ALL MODELS EXPORTED SUCCESSFULLY ===")
|
| 192 |
else:
|
tools/test_pipeline.py
CHANGED
|
@@ -36,7 +36,7 @@ def main():
|
|
| 36 |
enhanced_img = pipeline.process_image(
|
| 37 |
img,
|
| 38 |
w=0.5,
|
| 39 |
-
detection_model='
|
| 40 |
upscale=upscale,
|
| 41 |
blend_softness=0.5
|
| 42 |
)
|
|
|
|
| 36 |
enhanced_img = pipeline.process_image(
|
| 37 |
img,
|
| 38 |
w=0.5,
|
| 39 |
+
detection_model='retinaface_mobile0.25',
|
| 40 |
upscale=upscale,
|
| 41 |
blend_softness=0.5
|
| 42 |
)
|
train_custom.py
CHANGED
|
@@ -7,7 +7,13 @@ import yaml
|
|
| 7 |
import subprocess
|
| 8 |
import glob
|
| 9 |
|
|
|
|
|
|
|
| 10 |
def main():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
project_dir = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
codeformer_dir = os.path.join(project_dir, "models", "CodeFormer")
|
| 13 |
|
|
@@ -84,24 +90,31 @@ def main():
|
|
| 84 |
except ValueError:
|
| 85 |
continue
|
| 86 |
|
| 87 |
-
if latest_state
|
| 88 |
print(f"\n>>> RESUME MODE: Found checkpoint at iteration {latest_state_iter}")
|
| 89 |
print(f" State file: {latest_state}")
|
| 90 |
config["path"]["resume_state"] = latest_state
|
| 91 |
-
# When resuming, we don't need pretrain_network_g as it will be loaded from state
|
| 92 |
config["path"]["pretrain_network_g"] = None
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
| 98 |
else:
|
| 99 |
print("\n>>> FRESH START: No previous checkpoint found. Starting from pretrained weights.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
# Write back the updated configuration
|
| 102 |
with open(config_path, "w", encoding="utf-8") as f:
|
| 103 |
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
| 104 |
-
print(f"Updated configuration file
|
| 105 |
|
| 106 |
# 5. Run the training process
|
| 107 |
train_script = os.path.join("basicsr", "train.py")
|
|
|
|
| 7 |
import subprocess
|
| 8 |
import glob
|
| 9 |
|
| 10 |
+
import argparse
|
| 11 |
+
|
| 12 |
def main():
|
| 13 |
+
parser = argparse.ArgumentParser(description="Train CodeFormer with custom parameters.")
|
| 14 |
+
parser.add_argument("--verify", action="store_true", help="Run 2 iterations for verification purposes.")
|
| 15 |
+
args = parser.parse_args()
|
| 16 |
+
|
| 17 |
project_dir = os.path.dirname(os.path.abspath(__file__))
|
| 18 |
codeformer_dir = os.path.join(project_dir, "models", "CodeFormer")
|
| 19 |
|
|
|
|
| 90 |
except ValueError:
|
| 91 |
continue
|
| 92 |
|
| 93 |
+
if latest_state:
|
| 94 |
print(f"\n>>> RESUME MODE: Found checkpoint at iteration {latest_state_iter}")
|
| 95 |
print(f" State file: {latest_state}")
|
| 96 |
config["path"]["resume_state"] = latest_state
|
|
|
|
| 97 |
config["path"]["pretrain_network_g"] = None
|
| 98 |
+
if args.verify:
|
| 99 |
+
config["train"]["total_iter"] = latest_state_iter + 2
|
| 100 |
+
print(f" Resuming training from iter {latest_state_iter} to {config['train']['total_iter']} (verification mode)...")
|
| 101 |
+
else:
|
| 102 |
+
if latest_state_iter >= config.get("train", {}).get("total_iter", 20000):
|
| 103 |
+
print(f"\n>>> Training already completed ({latest_state_iter} >= {config.get('train', {}).get('total_iter', 20000)} total_iter)")
|
| 104 |
+
sys.exit(0)
|
| 105 |
+
print(f" Resuming training from iter {latest_state_iter} to {config['train']['total_iter']}...")
|
| 106 |
else:
|
| 107 |
print("\n>>> FRESH START: No previous checkpoint found. Starting from pretrained weights.")
|
| 108 |
+
if args.verify:
|
| 109 |
+
config["train"]["total_iter"] = 2
|
| 110 |
+
print(f" Training from scratch to {config['train']['total_iter']} (verification mode)...")
|
| 111 |
+
else:
|
| 112 |
+
print(f" Training from scratch to {config.get('train', {}).get('total_iter', 20000)}...")
|
| 113 |
|
| 114 |
# Write back the updated configuration
|
| 115 |
with open(config_path, "w", encoding="utf-8") as f:
|
| 116 |
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
| 117 |
+
print(f"Updated configuration file: device={device.upper()}, num_gpu={num_gpus}, total_iter={config['train']['total_iter']}")
|
| 118 |
|
| 119 |
# 5. Run the training process
|
| 120 |
train_script = os.path.join("basicsr", "train.py")
|
train_realesrgan.py
CHANGED
|
@@ -24,6 +24,9 @@ ANIME_DIR = os.path.join(DATASET_BASE, "anime")
|
|
| 24 |
FACE_DIR = os.path.join(DATASET_BASE, "faces")
|
| 25 |
STARRAIL_DIR = os.path.join(DATASET_BASE, "starrail")
|
| 26 |
BLUEARCHIVE_DIR = os.path.join(DATASET_BASE, "bluearchive")
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Combined GT (high-quality) folder for Real-ESRGAN training
|
| 29 |
REALESRGAN_GT_DIR = os.path.join(PROJECT_DIR, "datasets", "realesrgan_gt")
|
|
@@ -76,6 +79,9 @@ def prepare_gt_dataset():
|
|
| 76 |
"face": FACE_DIR,
|
| 77 |
"starrail": STARRAIL_DIR,
|
| 78 |
"bluearchive": BLUEARCHIVE_DIR,
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
for category, src_dir in sources.items():
|
|
@@ -300,6 +306,11 @@ def find_latest_state() -> str | None:
|
|
| 300 |
|
| 301 |
|
| 302 |
def main():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
print("=" * 55)
|
| 304 |
print(" Real-ESRGAN Custom Training Runner")
|
| 305 |
print(" Target: Landscape + Anime + Face enhancement")
|
|
@@ -347,10 +358,27 @@ def main():
|
|
| 347 |
cfg = yaml.safe_load(f)
|
| 348 |
cfg["path"]["resume_state"] = latest_state
|
| 349 |
cfg["path"]["pretrain_network_g"] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
with open(config_path, "w", encoding="utf-8") as f:
|
| 351 |
yaml.dump(cfg, f, default_flow_style=False, sort_keys=False)
|
| 352 |
else:
|
| 353 |
print("\n>>> FRESH START: No previous checkpoint. Starting from scratch.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
# 6. Run training
|
| 356 |
train_script = os.path.join("realesrgan", "train.py")
|
|
@@ -384,12 +412,8 @@ def main():
|
|
| 384 |
# Real-ESRGAN's train.py imports from basicsr.
|
| 385 |
# CodeFormer's basicsr lacks degradations module, so we use the full
|
| 386 |
# BasicSR source cloned at D:\Temp\BasicSR_src (no install needed).
|
| 387 |
-
basicsr_src = r"D:\Temp\BasicSR_src"
|
| 388 |
-
codeformer_dir = os.path.join(PROJECT_DIR, "models", "CodeFormer")
|
| 389 |
env["PYTHONPATH"] = os.path.pathsep.join([
|
| 390 |
REALESRGAN_DIR,
|
| 391 |
-
basicsr_src, # full BasicSR with degradations
|
| 392 |
-
codeformer_dir, # fallback
|
| 393 |
env.get("PYTHONPATH", ""),
|
| 394 |
])
|
| 395 |
|
|
|
|
| 24 |
FACE_DIR = os.path.join(DATASET_BASE, "faces")
|
| 25 |
STARRAIL_DIR = os.path.join(DATASET_BASE, "starrail")
|
| 26 |
BLUEARCHIVE_DIR = os.path.join(DATASET_BASE, "bluearchive")
|
| 27 |
+
DIV2K_DIR = os.path.join(DATASET_BASE, "div2k")
|
| 28 |
+
FLICKR2K_DIR = os.path.join(DATASET_BASE, "flickr2k_subset")
|
| 29 |
+
FFHQ1024_DIR = os.path.join(DATASET_BASE, "ffhq1024_subset")
|
| 30 |
|
| 31 |
# Combined GT (high-quality) folder for Real-ESRGAN training
|
| 32 |
REALESRGAN_GT_DIR = os.path.join(PROJECT_DIR, "datasets", "realesrgan_gt")
|
|
|
|
| 79 |
"face": FACE_DIR,
|
| 80 |
"starrail": STARRAIL_DIR,
|
| 81 |
"bluearchive": BLUEARCHIVE_DIR,
|
| 82 |
+
"div2k": DIV2K_DIR,
|
| 83 |
+
"flickr2k": FLICKR2K_DIR,
|
| 84 |
+
"ffhq1024": FFHQ1024_DIR,
|
| 85 |
}
|
| 86 |
|
| 87 |
for category, src_dir in sources.items():
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
def main():
|
| 309 |
+
import argparse
|
| 310 |
+
parser = argparse.ArgumentParser(description="Train Real-ESRGAN with custom parameters.")
|
| 311 |
+
parser.add_argument("--verify", action="store_true", help="Run 2 iterations for verification purposes.")
|
| 312 |
+
args = parser.parse_args()
|
| 313 |
+
|
| 314 |
print("=" * 55)
|
| 315 |
print(" Real-ESRGAN Custom Training Runner")
|
| 316 |
print(" Target: Landscape + Anime + Face enhancement")
|
|
|
|
| 358 |
cfg = yaml.safe_load(f)
|
| 359 |
cfg["path"]["resume_state"] = latest_state
|
| 360 |
cfg["path"]["pretrain_network_g"] = None
|
| 361 |
+
if args.verify:
|
| 362 |
+
cfg["train"]["total_iter"] = iter_num + 2
|
| 363 |
+
print(f" Resuming training from iter {iter_num} to {cfg['train']['total_iter']} (verification mode)...")
|
| 364 |
+
else:
|
| 365 |
+
if iter_num >= cfg.get("train", {}).get("total_iter", 15000):
|
| 366 |
+
print(f"\n>>> Training already completed ({iter_num} >= {cfg.get('train', {}).get('total_iter', 15000)} total_iter)")
|
| 367 |
+
sys.exit(0)
|
| 368 |
+
print(f" Resuming training from iter {iter_num} to {cfg['train']['total_iter']}...")
|
| 369 |
with open(config_path, "w", encoding="utf-8") as f:
|
| 370 |
yaml.dump(cfg, f, default_flow_style=False, sort_keys=False)
|
| 371 |
else:
|
| 372 |
print("\n>>> FRESH START: No previous checkpoint. Starting from scratch.")
|
| 373 |
+
with open(config_path, "r", encoding="utf-8") as f:
|
| 374 |
+
cfg = yaml.safe_load(f)
|
| 375 |
+
if args.verify:
|
| 376 |
+
cfg["train"]["total_iter"] = 2
|
| 377 |
+
print(f" Training from scratch to {cfg['train']['total_iter']} (verification mode)...")
|
| 378 |
+
else:
|
| 379 |
+
print(f" Training from scratch to {cfg.get('train', {}).get('total_iter', 15000)}...")
|
| 380 |
+
with open(config_path, "w", encoding="utf-8") as f:
|
| 381 |
+
yaml.dump(cfg, f, default_flow_style=False, sort_keys=False)
|
| 382 |
|
| 383 |
# 6. Run training
|
| 384 |
train_script = os.path.join("realesrgan", "train.py")
|
|
|
|
| 412 |
# Real-ESRGAN's train.py imports from basicsr.
|
| 413 |
# CodeFormer's basicsr lacks degradations module, so we use the full
|
| 414 |
# BasicSR source cloned at D:\Temp\BasicSR_src (no install needed).
|
|
|
|
|
|
|
| 415 |
env["PYTHONPATH"] = os.path.pathsep.join([
|
| 416 |
REALESRGAN_DIR,
|
|
|
|
|
|
|
| 417 |
env.get("PYTHONPATH", ""),
|
| 418 |
])
|
| 419 |
|