supli6669 commited on
Commit
46aa171
·
1 Parent(s): 7bc1a4f

feat: add multi-category dataset crawler + Real-ESRGAN training pipeline

Browse files

- crawl_datasets.py: crawl face/landscape/anime images from HuggingFace
(FFHQ, mertcobanov/nature-dataset, amirali900/Anime-Face-Dataset-10k)
- train_realesrgan.py: setup + train Real-ESRGAN for general image enhancement
(landscape, anime, scenery) alongside CodeFormer face model
- train_sharpening_model.py: sharpening model training script
- scan_c_drive.py: disk scan utility
- CodeFormer_stage3_custom.yml: bump total_iter 350->600 to continue training

crawl_datasets.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # ─────────────────────────────────────────────
10
+ # HuggingFace dataset sources
11
+ # ─────────────────────────────────────────────
12
+ SOURCES = {
13
+ "face": {
14
+ "parquet_url": "https://huggingface.co/datasets/Ryan-sjtu/ffhq512-caption/resolve/main/data/train-00000-of-00054-9b5f7c3e6bc03b3b.parquet",
15
+ "num_images": 300,
16
+ "subdir": "faces",
17
+ "prefix": "face",
18
+ "description": "FFHQ 512x512 real face photos",
19
+ },
20
+ "landscape": {
21
+ "parquet_url": "https://huggingface.co/datasets/mertcobanov/nature-dataset/resolve/refs%2Fconvert%2Fparquet/default/train/0000.parquet",
22
+ "num_images": 300,
23
+ "subdir": "landscapes",
24
+ "prefix": "landscape",
25
+ "description": "Nature & landscape scenery (50k images)",
26
+ },
27
+ "anime": {
28
+ "parquet_url": "https://huggingface.co/datasets/amirali900/Anime-Face-Dataset-10k/resolve/refs%2Fconvert%2Fparquet/default/train/0000.parquet",
29
+ "num_images": 300,
30
+ "subdir": "anime",
31
+ "prefix": "anime",
32
+ "description": "Anime Face Dataset 10k illustrations",
33
+ },
34
+ }
35
+
36
+ # Fallback: direct image download lists for anime / landscape
37
+ ANIME_FALLBACK_URLS = [
38
+ "https://huggingface.co/datasets/huggan/anime-faces/resolve/main/data/train-00000-of-00001.parquet",
39
+ ]
40
+
41
+ LANDSCAPE_FALLBACK_URLS = [
42
+ "https://huggingface.co/datasets/jlbaker361/flickr_humans/resolve/main/data/train-00000-of-00001.parquet",
43
+ ]
44
+
45
+
46
+ def load_parquet_safe(url: str) -> pd.DataFrame | None:
47
+ """Try to load a parquet file from URL, return None on failure."""
48
+ print(f" Loading: {url}")
49
+ try:
50
+ df = pd.read_parquet(url)
51
+ print(f" [OK] Loaded {len(df)} rows.")
52
+ return df
53
+ except Exception as e:
54
+ print(f" [FAIL] Failed: {e}")
55
+ return None
56
+
57
+
58
+ def find_image_column(df: pd.DataFrame) -> str | None:
59
+ """Detect which column holds image data (bytes dict or PIL-compatible)."""
60
+ for col in df.columns:
61
+ sample = df[col].iloc[0]
62
+ if isinstance(sample, dict) and "bytes" in sample:
63
+ return col
64
+ if isinstance(sample, bytes):
65
+ return col
66
+ return None
67
+
68
+
69
+ def extract_image_bytes(cell) -> bytes | None:
70
+ """Extract raw image bytes from a dataframe cell regardless of format."""
71
+ if isinstance(cell, dict):
72
+ return cell.get("bytes")
73
+ if isinstance(cell, bytes):
74
+ return cell
75
+ return None
76
+
77
+
78
+ def crawl_source(name: str, cfg: dict, base_dataset_dir: str):
79
+ """Download images for a single source category."""
80
+ save_dir = os.path.join(base_dataset_dir, cfg["subdir"])
81
+ os.makedirs(save_dir, exist_ok=True)
82
+
83
+ print(f"\n{'='*55}")
84
+ print(f" [{name.upper()}] {cfg['description']}")
85
+ print(f" Save dir : {save_dir}")
86
+ print(f" Target : {cfg['num_images']} images")
87
+ print(f"{'='*55}")
88
+
89
+ df = load_parquet_safe(cfg["parquet_url"])
90
+
91
+ # Try fallbacks if primary failed
92
+ if df is None:
93
+ fallbacks = []
94
+ if name == "anime":
95
+ fallbacks = ANIME_FALLBACK_URLS
96
+ elif name == "landscape":
97
+ fallbacks = LANDSCAPE_FALLBACK_URLS
98
+ for fb_url in fallbacks:
99
+ print(f" Trying fallback: {fb_url}")
100
+ df = load_parquet_safe(fb_url)
101
+ if df is not None:
102
+ break
103
+
104
+ if df is None:
105
+ print(f" [FAIL] All sources failed for '{name}'. Skipping.")
106
+ return 0
107
+
108
+ img_col = find_image_column(df)
109
+ if img_col is None:
110
+ print(f" [FAIL] No image column detected in dataset. Columns: {list(df.columns)}")
111
+ return 0
112
+
113
+ print(f" Image column: '{img_col}'")
114
+
115
+ # Find existing highest index in this subdir to avoid overwrites
116
+ existing = [
117
+ f for f in os.listdir(save_dir)
118
+ if f.endswith(".png") and f.startswith(cfg["prefix"])
119
+ ]
120
+ highest_idx = -1
121
+ for f in existing:
122
+ try:
123
+ idx = int(f.replace(cfg["prefix"] + "_", "").replace(".png", ""))
124
+ highest_idx = max(highest_idx, idx)
125
+ except ValueError:
126
+ continue
127
+
128
+ start_idx = highest_idx + 1
129
+ saved = 0
130
+ target = cfg["num_images"]
131
+
132
+ for i in range(len(df)):
133
+ if saved >= target:
134
+ break
135
+ try:
136
+ cell = df[img_col].iloc[i]
137
+ img_bytes = extract_image_bytes(cell)
138
+ if not img_bytes:
139
+ continue
140
+
141
+ img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
142
+ img = img.resize((512, 512), Image.Resampling.LANCZOS)
143
+
144
+ filename = f"{cfg['prefix']}_{start_idx + saved:05d}.png"
145
+ filepath = os.path.join(save_dir, filename)
146
+ img.save(filepath, "PNG")
147
+ saved += 1
148
+
149
+ if saved % 20 == 0 or saved == target:
150
+ print(f" [{name}] {saved}/{target} images saved...")
151
+
152
+ except Exception as e:
153
+ print(f" [warn] Row {i} error: {e}")
154
+ continue
155
+
156
+ print(f"\n [OK] [{name.upper()}] Done: {saved} images saved to {save_dir}")
157
+ return saved
158
+
159
+
160
+ def main():
161
+ project_dir = os.path.dirname(os.path.abspath(__file__))
162
+ codeformer_dir = os.path.join(project_dir, "models", "CodeFormer")
163
+ base_dataset_dir = os.path.join(codeformer_dir, "datasets", "ffhq", "ffhq_512")
164
+ os.makedirs(base_dataset_dir, exist_ok=True)
165
+
166
+ print("=" * 55)
167
+ print(" MULTI-CATEGORY DATASET CRAWLER")
168
+ print(" Categories: Face | Landscape | Anime Girl")
169
+ print("=" * 55)
170
+
171
+ total_saved = 0
172
+ for name, cfg in SOURCES.items():
173
+ saved = crawl_source(name, cfg, base_dataset_dir)
174
+ total_saved += saved
175
+ time.sleep(0.5)
176
+
177
+ # Count all images recursively
178
+ all_images = []
179
+ for root, _, files in os.walk(base_dataset_dir):
180
+ for f in files:
181
+ if f.lower().endswith(".png"):
182
+ all_images.append(f)
183
+
184
+ print("\n" + "=" * 55)
185
+ print(f" CRAWL COMPLETE")
186
+ print(f" New images this run : {total_saved}")
187
+ print(f" Total dataset size : {len(all_images)} images")
188
+ print(f" Dataset location : {base_dataset_dir}")
189
+ print("=" * 55)
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
models/CodeFormer/options/CodeFormer_stage3_custom.yml CHANGED
@@ -1,10 +1,7 @@
1
- # general settings
2
  name: CodeFormer_stage3_custom
3
  model_type: CodeFormerJointModel
4
  num_gpu: 0
5
  manual_seed: 0
6
-
7
- # dataset and data loader settings
8
  datasets:
9
  train:
10
  name: CustomDataset
@@ -13,142 +10,156 @@ datasets:
13
  filename_tmpl: '{}'
14
  io_backend:
15
  type: disk
16
-
17
  in_size: 512
18
  gt_size: 512
19
- mean: [0.5, 0.5, 0.5]
20
- std: [0.5, 0.5, 0.5]
 
 
 
 
 
 
21
  use_hflip: true
22
  use_corrupt: true
23
-
24
- # Comprehensive degradation parameters (Motion blur, Gaussian blur, Downsampling, Noise, JPEG)
25
  blur_kernel_size: 41
26
  use_motion_kernel: true
27
  motion_kernel_prob: 0.05
28
- kernel_list: ['iso', 'aniso']
29
- kernel_prob: [0.5, 0.5]
30
-
31
- # Small degradation range
32
- blur_sigma: [0.1, 10.0]
33
- downsample_range: [1.0, 12.0]
34
- noise_range: [0.0, 20.0]
35
- jpeg_range: [50, 100]
36
-
37
- # Large degradation range
38
- blur_sigma_large: [1.0, 15.0]
39
- downsample_range_large: [4.0, 30.0]
40
- noise_range_large: [0.0, 30.0]
41
- jpeg_range_large: [30, 80]
42
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  latent_gt_path: null
44
-
45
- # Data loader configurations
46
  num_worker_per_gpu: 0
47
  batch_size_per_gpu: 1
48
  dataset_enlarge_ratio: 1
49
  prefetch_mode: cpu
50
-
51
- # network structures
52
  network_g:
53
  type: CodeFormer
54
  dim_embd: 512
55
  n_head: 8
56
  n_layers: 9
57
  codebook_size: 1024
58
- connect_list: ['32', '64', '128', '256']
59
- fix_modules: ['quantize', 'generator']
60
-
 
 
 
 
 
61
  network_vqgan:
62
  type: VQAutoEncoder
63
  img_size: 512
64
  nf: 64
65
- ch_mult: [1, 2, 2, 4, 4, 8]
66
- quantizer: 'nearest'
 
 
 
 
 
 
67
  codebook_size: 1024
68
-
69
  network_d:
70
  type: VQGANDiscriminator
71
  nc: 3
72
  ndf: 64
73
  n_layers: 4
74
-
75
- # path settings
76
  path:
77
- pretrain_network_g: ../../weights/CodeFormer/codeformer.pth
78
  param_key_g: params_ema
79
  strict_load_g: false
80
  pretrain_network_d: null
81
- resume_state: null
82
-
83
- # training loss and optimizer settings
84
  train:
85
  use_hq_feat_loss: true
86
  feat_loss_weight: 1.0
87
  cross_entropy_loss: true
88
  entropy_loss_weight: 0.5
89
  scale_adaptive_gan_weight: 0.1
90
-
91
  optim_g:
92
  type: Adam
93
- lr: !!float 5e-5
94
  weight_decay: 0
95
- betas: [0.9, 0.99]
 
 
96
  optim_d:
97
  type: Adam
98
- lr: !!float 5e-5
99
  weight_decay: 0
100
- betas: [0.9, 0.99]
101
-
 
102
  scheduler:
103
  type: CosineAnnealingRestartLR
104
- periods: [150000]
105
- restart_weights: [1]
106
- eta_min: !!float 2e-5
107
-
108
- total_iter: 100
 
109
  warmup_iter: -1
110
  ema_decay: 0.997
111
-
112
  pixel_opt:
113
  type: L1Loss
114
  loss_weight: 1.0
115
  reduction: mean
116
-
117
  perceptual_opt:
118
  type: LPIPSLoss
119
  loss_weight: 1.0
120
  use_input_norm: true
121
  range_norm: true
122
-
123
  gan_opt:
124
  type: GANLoss
125
  gan_type: hinge
126
- loss_weight: !!float 1.0
127
-
128
  use_adaptive_weight: true
129
  net_g_start_iter: 0
130
  net_d_iters: 1
131
  net_d_start_iter: 5001
132
  manual_seed: 0
133
-
134
- # validation settings
135
  val:
136
- val_freq: !!float 5e10
137
  save_img: true
138
  metrics:
139
  psnr:
140
  type: calculate_psnr
141
  crop_border: 4
142
  test_y_channel: false
143
-
144
- # logging settings
145
  logger:
146
  print_freq: 1
147
  save_checkpoint_freq: 5
148
  use_tb_logger: false
149
  wandb:
150
  project: null
151
-
152
  find_unused_parameters: true
153
  dist_params: null
154
  dist: false
 
 
1
  name: CodeFormer_stage3_custom
2
  model_type: CodeFormerJointModel
3
  num_gpu: 0
4
  manual_seed: 0
 
 
5
  datasets:
6
  train:
7
  name: CustomDataset
 
10
  filename_tmpl: '{}'
11
  io_backend:
12
  type: disk
 
13
  in_size: 512
14
  gt_size: 512
15
+ mean:
16
+ - 0.5
17
+ - 0.5
18
+ - 0.5
19
+ std:
20
+ - 0.5
21
+ - 0.5
22
+ - 0.5
23
  use_hflip: true
24
  use_corrupt: true
 
 
25
  blur_kernel_size: 41
26
  use_motion_kernel: true
27
  motion_kernel_prob: 0.05
28
+ kernel_list:
29
+ - iso
30
+ - aniso
31
+ kernel_prob:
32
+ - 0.5
33
+ - 0.5
34
+ blur_sigma:
35
+ - 0.1
36
+ - 10.0
37
+ downsample_range:
38
+ - 1.0
39
+ - 12.0
40
+ noise_range:
41
+ - 0.0
42
+ - 20.0
43
+ jpeg_range:
44
+ - 50
45
+ - 100
46
+ blur_sigma_large:
47
+ - 1.0
48
+ - 15.0
49
+ downsample_range_large:
50
+ - 4.0
51
+ - 30.0
52
+ noise_range_large:
53
+ - 0.0
54
+ - 30.0
55
+ jpeg_range_large:
56
+ - 30
57
+ - 80
58
  latent_gt_path: null
 
 
59
  num_worker_per_gpu: 0
60
  batch_size_per_gpu: 1
61
  dataset_enlarge_ratio: 1
62
  prefetch_mode: cpu
 
 
63
  network_g:
64
  type: CodeFormer
65
  dim_embd: 512
66
  n_head: 8
67
  n_layers: 9
68
  codebook_size: 1024
69
+ connect_list:
70
+ - '32'
71
+ - '64'
72
+ - '128'
73
+ - '256'
74
+ fix_modules:
75
+ - quantize
76
+ - generator
77
  network_vqgan:
78
  type: VQAutoEncoder
79
  img_size: 512
80
  nf: 64
81
+ ch_mult:
82
+ - 1
83
+ - 2
84
+ - 2
85
+ - 4
86
+ - 4
87
+ - 8
88
+ quantizer: nearest
89
  codebook_size: 1024
 
90
  network_d:
91
  type: VQGANDiscriminator
92
  nc: 3
93
  ndf: 64
94
  n_layers: 4
 
 
95
  path:
96
+ pretrain_network_g: null
97
  param_key_g: params_ema
98
  strict_load_g: false
99
  pretrain_network_d: null
100
+ resume_state: D:\.gemini-scratch\custom-ai-enhancer\models\CodeFormer\experiments\20260708_201102_CodeFormer_stage3_custom\training_states\350.state
 
 
101
  train:
102
  use_hq_feat_loss: true
103
  feat_loss_weight: 1.0
104
  cross_entropy_loss: true
105
  entropy_loss_weight: 0.5
106
  scale_adaptive_gan_weight: 0.1
 
107
  optim_g:
108
  type: Adam
109
+ lr: 5.0e-05
110
  weight_decay: 0
111
+ betas:
112
+ - 0.9
113
+ - 0.99
114
  optim_d:
115
  type: Adam
116
+ lr: 5.0e-05
117
  weight_decay: 0
118
+ betas:
119
+ - 0.9
120
+ - 0.99
121
  scheduler:
122
  type: CosineAnnealingRestartLR
123
+ periods:
124
+ - 150000
125
+ restart_weights:
126
+ - 1
127
+ eta_min: 2.0e-05
128
+ total_iter: 600
129
  warmup_iter: -1
130
  ema_decay: 0.997
 
131
  pixel_opt:
132
  type: L1Loss
133
  loss_weight: 1.0
134
  reduction: mean
 
135
  perceptual_opt:
136
  type: LPIPSLoss
137
  loss_weight: 1.0
138
  use_input_norm: true
139
  range_norm: true
 
140
  gan_opt:
141
  type: GANLoss
142
  gan_type: hinge
143
+ loss_weight: 1.0
 
144
  use_adaptive_weight: true
145
  net_g_start_iter: 0
146
  net_d_iters: 1
147
  net_d_start_iter: 5001
148
  manual_seed: 0
 
 
149
  val:
150
+ val_freq: 50000000000.0
151
  save_img: true
152
  metrics:
153
  psnr:
154
  type: calculate_psnr
155
  crop_border: 4
156
  test_y_channel: false
 
 
157
  logger:
158
  print_freq: 1
159
  save_checkpoint_freq: 5
160
  use_tb_logger: false
161
  wandb:
162
  project: null
 
163
  find_unused_parameters: true
164
  dist_params: null
165
  dist: false
scan_c_drive.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ def get_dir_size(path, max_depth=4, current_depth=0):
5
+ total = 0
6
+ if current_depth > max_depth:
7
+ return 0
8
+ try:
9
+ for entry in os.scandir(path):
10
+ try:
11
+ if entry.is_file(follow_symlinks=False):
12
+ total += entry.stat().st_size
13
+ elif entry.is_dir(follow_symlinks=False):
14
+ total += get_dir_size(entry.path, max_depth, current_depth + 1)
15
+ except Exception:
16
+ continue
17
+ except Exception:
18
+ pass
19
+ return total
20
+
21
+ def main():
22
+ user_home = os.path.expanduser("~")
23
+ print(f"Scanning large directories in: {user_home}...")
24
+
25
+ large_dirs = []
26
+
27
+ # List top level directories in user home
28
+ try:
29
+ entries = list(os.scandir(user_home))
30
+ except Exception as e:
31
+ print(f"Error reading home directory: {e}")
32
+ return
33
+
34
+ for entry in entries:
35
+ if entry.is_dir(follow_symlinks=False):
36
+ # Skip some standard tiny folders or system symbolic links to speed up
37
+ if entry.name.startswith('.') and entry.name not in ['.cache', '.conda', '.docker', '.npm', '.nuget', '.vscode', '.gradle']:
38
+ continue
39
+
40
+ print(f"Checking size of {entry.name}...")
41
+ size = get_dir_size(entry.path, max_depth=5)
42
+ size_gb = size / (1024 * 1024 * 1024)
43
+ if size_gb > 0.5: # larger than 500 MB
44
+ large_dirs.append((entry.path, size_gb))
45
+ print(f"-> Found large dir: {entry.name} ({size_gb:.2f} GB)")
46
+
47
+ print("\n=== Scan Results (Directories > 0.5 GB) ===")
48
+ large_dirs.sort(key=lambda x: x[1], reverse=True)
49
+ for path, size_gb in large_dirs:
50
+ print(f"- {path}: {size_gb:.2f} GB")
51
+
52
+ # If it is AppData, scan one level deeper to pinpoint the culprit
53
+ if "AppData" in path:
54
+ print(" Pinpointing AppData contents...")
55
+ for sub_name in ["Local", "Roaming", "LocalLow"]:
56
+ sub_path = os.path.join(path, sub_name)
57
+ if os.path.exists(sub_path):
58
+ sub_size = get_dir_size(sub_path, max_depth=4)
59
+ sub_size_gb = sub_size / (1024 * 1024 * 1024)
60
+ if sub_size_gb > 0.5:
61
+ print(f" - {sub_path}: {sub_size_gb:.2f} GB")
62
+ # Pinpoint Local/Roaming further
63
+ try:
64
+ for local_entry in os.scandir(sub_path):
65
+ if local_entry.is_dir(follow_symlinks=False):
66
+ l_size = get_dir_size(local_entry.path, max_depth=3)
67
+ l_size_gb = l_size / (1024 * 1024 * 1024)
68
+ if l_size_gb > 0.5:
69
+ print(f" - {local_entry.path}: {l_size_gb:.2f} GB")
70
+ except Exception:
71
+ pass
72
+
73
+ if __name__ == "__main__":
74
+ main()
train_realesrgan.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Train Real-ESRGAN for general image enhancement (landscape, anime, scenery).
3
+ This script:
4
+ 1. Clones Real-ESRGAN repo if not present
5
+ 2. Prepares a training config pointing to our crawled dataset
6
+ 3. Launches the training process
7
+ """
8
+ import os
9
+ import sys
10
+ import subprocess
11
+ import shutil
12
+ import yaml
13
+ import glob
14
+
15
+ PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
16
+ REALESRGAN_DIR = os.path.join(PROJECT_DIR, "models", "Real-ESRGAN")
17
+
18
+ # Dataset paths (populated by crawl_datasets.py)
19
+ DATASET_BASE = os.path.join(
20
+ PROJECT_DIR, "models", "CodeFormer", "datasets", "ffhq", "ffhq_512"
21
+ )
22
+ LANDSCAPE_DIR = os.path.join(DATASET_BASE, "landscapes")
23
+ ANIME_DIR = os.path.join(DATASET_BASE, "anime")
24
+ FACE_DIR = os.path.join(DATASET_BASE, "faces")
25
+
26
+ # Combined GT (high-quality) folder for Real-ESRGAN training
27
+ REALESRGAN_GT_DIR = os.path.join(PROJECT_DIR, "datasets", "realesrgan_gt")
28
+
29
+
30
+ def clone_realesrgan():
31
+ """Clone Real-ESRGAN repository if not already present."""
32
+ if os.path.exists(REALESRGAN_DIR):
33
+ print("[OK] Real-ESRGAN repo already present.")
34
+ return True
35
+ print("Cloning Real-ESRGAN repository...")
36
+ result = subprocess.run(
37
+ ["git", "clone", "https://github.com/xinntao/Real-ESRGAN.git", REALESRGAN_DIR],
38
+ capture_output=False,
39
+ )
40
+ if result.returncode != 0:
41
+ print("[FAIL] Failed to clone Real-ESRGAN.")
42
+ return False
43
+ print("[OK] Real-ESRGAN cloned successfully.")
44
+ return True
45
+
46
+
47
+ def install_realesrgan_deps():
48
+ """Install Real-ESRGAN dependencies."""
49
+ print("Installing Real-ESRGAN dependencies...")
50
+ requirements_file = os.path.join(REALESRGAN_DIR, "requirements.txt")
51
+ if os.path.exists(requirements_file):
52
+ subprocess.run(
53
+ [sys.executable, "-m", "pip", "install", "-r", requirements_file],
54
+ check=True,
55
+ )
56
+ # Also install basicsr for Real-ESRGAN
57
+ subprocess.run(
58
+ [sys.executable, "-m", "pip", "install", "basicsr", "facexlib", "gfpgan"],
59
+ check=True,
60
+ )
61
+ print("[OK] Dependencies installed.")
62
+
63
+
64
+ def prepare_gt_dataset():
65
+ """Combine landscape + anime + face images into a single GT folder."""
66
+ os.makedirs(REALESRGAN_GT_DIR, exist_ok=True)
67
+
68
+ total_copied = 0
69
+ sources = {
70
+ "landscape": LANDSCAPE_DIR,
71
+ "anime": ANIME_DIR,
72
+ "face": FACE_DIR,
73
+ }
74
+
75
+ for category, src_dir in sources.items():
76
+ if not os.path.exists(src_dir):
77
+ print(f" [skip] {category} dir not found: {src_dir}")
78
+ continue
79
+ images = glob.glob(os.path.join(src_dir, "*.png"))
80
+ print(f" Copying {len(images)} {category} images...")
81
+ for img_path in images:
82
+ dest = os.path.join(REALESRGAN_GT_DIR, os.path.basename(img_path))
83
+ if not os.path.exists(dest):
84
+ shutil.copy2(img_path, dest)
85
+ total_copied += 1
86
+
87
+ # Also copy root-level face images (original FFHQ)
88
+ root_images = glob.glob(os.path.join(DATASET_BASE, "*.png"))
89
+ print(f" Copying {len(root_images)} root FFHQ face images...")
90
+ for img_path in root_images:
91
+ dest = os.path.join(REALESRGAN_GT_DIR, os.path.basename(img_path))
92
+ if not os.path.exists(dest):
93
+ shutil.copy2(img_path, dest)
94
+ total_copied += 1
95
+
96
+ total = len([f for f in os.listdir(REALESRGAN_GT_DIR) if f.endswith(".png")])
97
+ print(f"[OK] GT dataset ready: {total} images (copied {total_copied} new)")
98
+ return total
99
+
100
+
101
+ def create_training_config(num_images: int) -> str:
102
+ """Create a Real-ESRGAN training YAML config."""
103
+ config_dir = os.path.join(REALESRGAN_DIR, "options")
104
+ os.makedirs(config_dir, exist_ok=True)
105
+ config_path = os.path.join(config_dir, "train_realesrgan_custom.yml")
106
+
107
+ # Relative path from Real-ESRGAN dir to GT dataset
108
+ gt_path_rel = os.path.relpath(REALESRGAN_GT_DIR, REALESRGAN_DIR).replace("\\", "/")
109
+
110
+ config = {
111
+ "name": "train_RealESRGAN_custom",
112
+ "model_type": "RealESRGANModel",
113
+ "scale": 4,
114
+ "num_gpu": 0,
115
+ "manual_seed": 0,
116
+ "datasets": {
117
+ "train": {
118
+ "name": "CustomMixedDataset",
119
+ "type": "RealESRGANDataset",
120
+ "dataroot_gt": gt_path_rel,
121
+ "meta_info": None,
122
+ "io_backend": {"type": "disk"},
123
+ "gt_size": 256,
124
+ "use_hflip": True,
125
+ "use_rot": False,
126
+ "blur_kernel_size": 21,
127
+ "kernel_list": ["iso", "aniso", "generalized_iso", "generalized_aniso",
128
+ "plateau_iso", "plateau_aniso"],
129
+ "kernel_prob": [0.45, 0.25, 0.12, 0.03, 0.12, 0.03],
130
+ "sinc_prob": 0.1,
131
+ "blur_sigma": [0.2, 3.0],
132
+ "betag_range": [0.5, 4.0],
133
+ "betap_range": [1, 2.0],
134
+ "blur_kernel_size2": 21,
135
+ "kernel_list2": ["iso", "aniso", "generalized_iso", "generalized_aniso",
136
+ "plateau_iso", "plateau_aniso"],
137
+ "kernel_prob2": [0.45, 0.25, 0.12, 0.03, 0.12, 0.03],
138
+ "sinc_prob2": 0.1,
139
+ "blur_sigma2": [0.2, 1.5],
140
+ "betag_range2": [0.5, 4.0],
141
+ "betap_range2": [1, 2.0],
142
+ "final_sinc_prob": 0.8,
143
+ "resize_prob": [0.2, 0.7, 0.1],
144
+ "resize_range": [0.15, 1.5],
145
+ "gaussian_noise_prob": 0.5,
146
+ "noise_range": [1, 30],
147
+ "poisson_scale_range": [0.05, 3.0],
148
+ "gray_noise_prob": 0.4,
149
+ "jpeg_range": [30, 95],
150
+ "resize_prob2": [0.3, 0.4, 0.3],
151
+ "resize_range2": [0.3, 1.2],
152
+ "gaussian_noise_prob2": 0.5,
153
+ "noise_range2": [1, 25],
154
+ "poisson_scale_range2": [0.05, 2.5],
155
+ "gray_noise_prob2": 0.4,
156
+ "jpeg_range2": [30, 95],
157
+ "gt_size_h": 256,
158
+ "gt_size_w": 256,
159
+ "batch_size_per_gpu": 4,
160
+ "num_worker_per_gpu": 0,
161
+ "dataset_enlarge_ratio": 1,
162
+ "prefetch_mode": "cpu",
163
+ "num_prefetch_queue": 1,
164
+ }
165
+ },
166
+ "network_g": {
167
+ "type": "RRDBNet",
168
+ "num_in_ch": 3,
169
+ "num_out_ch": 3,
170
+ "num_feat": 64,
171
+ "num_block": 23,
172
+ "num_grow_ch": 32,
173
+ "scale": 4,
174
+ },
175
+ "network_d": {
176
+ "type": "UNetDiscriminatorSN",
177
+ "num_in_ch": 3,
178
+ "num_feat": 64,
179
+ "skip_connection": True,
180
+ },
181
+ "path": {
182
+ "pretrain_network_g": None,
183
+ "param_key_g": "params_ema",
184
+ "strict_load_g": False,
185
+ "resume_state": None,
186
+ },
187
+ "train": {
188
+ "ema_decay": 0.999,
189
+ "optim_g": {
190
+ "type": "Adam",
191
+ "lr": 1.0e-4,
192
+ "weight_decay": 0,
193
+ "betas": [0.9, 0.99],
194
+ },
195
+ "optim_d": {
196
+ "type": "Adam",
197
+ "lr": 1.0e-4,
198
+ "weight_decay": 0,
199
+ "betas": [0.9, 0.99],
200
+ },
201
+ "scheduler": {
202
+ "type": "MultiStepLR",
203
+ "milestones": [400000],
204
+ "gamma": 0.5,
205
+ },
206
+ "total_iter": 500,
207
+ "warmup_iter": -1,
208
+ "pixel_opt": {
209
+ "type": "L1Loss",
210
+ "loss_weight": 1.0,
211
+ "reduction": "mean",
212
+ },
213
+ "perceptual_opt": {
214
+ "type": "PerceptualLoss",
215
+ "layer_weights": {"conv1_2": 0.1, "conv2_2": 0.1, "conv3_4": 1.0,
216
+ "conv4_4": 1.0, "conv5_4": 1.0},
217
+ "vgg_type": "vgg19",
218
+ "use_input_norm": True,
219
+ "range_norm": False,
220
+ "perceptual_weight": 1.0,
221
+ "style_weight": 0.0,
222
+ "criterion": "l1",
223
+ },
224
+ "gan_opt": {
225
+ "type": "GANLoss",
226
+ "gan_type": "vanilla",
227
+ "real_label_val": 1.0,
228
+ "fake_label_val": 0.0,
229
+ "loss_weight": 1.0e-1,
230
+ },
231
+ "net_d_iters": 1,
232
+ "net_d_start_iter": 0,
233
+ },
234
+ "val": {
235
+ "val_freq": 500,
236
+ "save_img": True,
237
+ },
238
+ "logger": {
239
+ "print_freq": 1,
240
+ "save_checkpoint_freq": 50,
241
+ "use_tb_logger": False,
242
+ "wandb": {"project": None},
243
+ },
244
+ "dist_params": None,
245
+ "dist": False,
246
+ "find_unused_parameters": False,
247
+ }
248
+
249
+ with open(config_path, "w", encoding="utf-8") as f:
250
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
251
+
252
+ print(f"[OK] Config written: {config_path}")
253
+ return config_path
254
+
255
+
256
+ def find_latest_state() -> str | None:
257
+ """Find the latest training state checkpoint."""
258
+ pattern = os.path.join(
259
+ REALESRGAN_DIR, "experiments", "*train_RealESRGAN_custom*",
260
+ "training_states", "*.state"
261
+ )
262
+ states = glob.glob(pattern)
263
+ if not states:
264
+ return None
265
+ latest = max(states, key=lambda p: int(os.path.basename(p).replace(".state", "")))
266
+ return latest
267
+
268
+
269
+ def main():
270
+ print("=" * 55)
271
+ print(" Real-ESRGAN Custom Training Runner")
272
+ print(" Target: Landscape + Anime + Face enhancement")
273
+ print("=" * 55)
274
+
275
+ # 1. Clone repo
276
+ if not clone_realesrgan():
277
+ sys.exit(1)
278
+
279
+ # 2. Install deps
280
+ install_realesrgan_deps()
281
+
282
+ # 3. Prepare combined GT dataset
283
+ print("\n[Step 3] Preparing GT dataset...")
284
+ num_images = prepare_gt_dataset()
285
+ if num_images == 0:
286
+ print("[FAIL] No images found in dataset directories. Run crawl_datasets.py first.")
287
+ sys.exit(1)
288
+
289
+ # 4. Create training config
290
+ print("\n[Step 4] Creating training config...")
291
+ config_path = create_training_config(num_images)
292
+
293
+ # 5. Check for resume state
294
+ latest_state = find_latest_state()
295
+ if latest_state:
296
+ iter_num = int(os.path.basename(latest_state).replace(".state", ""))
297
+ print(f"\n>>> RESUME MODE: Found checkpoint at iteration {iter_num}")
298
+ # Patch config to resume
299
+ with open(config_path, "r", encoding="utf-8") as f:
300
+ cfg = yaml.safe_load(f)
301
+ cfg["path"]["resume_state"] = latest_state
302
+ cfg["path"]["pretrain_network_g"] = None
303
+ with open(config_path, "w", encoding="utf-8") as f:
304
+ yaml.dump(cfg, f, default_flow_style=False, sort_keys=False)
305
+ else:
306
+ print("\n>>> FRESH START: No previous checkpoint. Starting from scratch.")
307
+
308
+ # 6. Run training
309
+ train_script = os.path.join("basicsr", "train.py")
310
+ cmd = [
311
+ sys.executable,
312
+ train_script,
313
+ "-opt", os.path.join("options", "train_realesrgan_custom.yml"),
314
+ "--launcher", "none",
315
+ ]
316
+
317
+ env = os.environ.copy()
318
+ env["PYTHONPATH"] = os.path.pathsep.join([REALESRGAN_DIR, env.get("PYTHONPATH", "")])
319
+
320
+ print(f"\nStarting Real-ESRGAN training...")
321
+ print(f"Working directory: {REALESRGAN_DIR}")
322
+ print(f"Command: {' '.join(cmd)}")
323
+ print("-" * 55)
324
+
325
+ try:
326
+ subprocess.run(cmd, cwd=REALESRGAN_DIR, env=env, check=True)
327
+ print("-" * 55)
328
+ print("[OK] Real-ESRGAN training completed!")
329
+ except subprocess.CalledProcessError as e:
330
+ print(f"[FAIL] Training failed with exit code: {e.returncode}")
331
+ sys.exit(e.returncode)
332
+
333
+
334
+ if __name__ == "__main__":
335
+ main()
train_sharpening_model.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ import glob
4
+ import time
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.optim as optim
9
+ from torch.utils.data import DataLoader, Dataset
10
+ from torchvision import transforms
11
+ from PIL import Image
12
+
13
+ class ImageDataset(Dataset):
14
+ def __init__(self, root_dir, transform=None):
15
+ self.paths = sorted(glob.glob(os.path.join(root_dir, "*.png")))
16
+ self.transform = transform
17
+
18
+ def __len__(self):
19
+ return len(self.paths)
20
+
21
+ def __getitem__(self, idx):
22
+ img = Image.open(self.paths[idx]).convert("RGB")
23
+ if self.transform:
24
+ img = self.transform(img)
25
+ return img
26
+
27
+ class SimpleUNet(nn.Module):
28
+ """Lightweight UNet‑style model for image sharpening (placeholder)."""
29
+ def __init__(self):
30
+ super().__init__()
31
+ self.encoder = nn.Sequential(
32
+ nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.ReLU(inplace=True),
33
+ nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True)
34
+ )
35
+ self.decoder = nn.Sequential(
36
+ nn.Conv2d(32, 16, kernel_size=3, padding=1), nn.ReLU(inplace=True),
37
+ nn.Conv2d(16, 3, kernel_size=3, padding=1)
38
+ )
39
+
40
+ def forward(self, x):
41
+ x = self.encoder(x)
42
+ x = self.decoder(x)
43
+ return x
44
+
45
+ def train(dataset_dir, checkpoint_dir, epochs, batch_size, lr, use_gpu):
46
+ device = torch.device("cuda" if torch.cuda.is_available() and use_gpu else "cpu")
47
+ transform = transforms.Compose([
48
+ transforms.Resize((512, 512)),
49
+ transforms.ToTensor()
50
+ ])
51
+ dataset = ImageDataset(dataset_dir, transform=transform)
52
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=2)
53
+
54
+ model = SimpleUNet().to(device)
55
+ criterion = nn.L1Loss()
56
+ optimizer = optim.Adam(model.parameters(), lr=lr)
57
+
58
+ print(f"Start training on {len(dataset)} images (device: {device})")
59
+ for epoch in range(1, epochs + 1):
60
+ epoch_loss = 0.0
61
+ for batch in dataloader:
62
+ batch = batch.to(device)
63
+ optimizer.zero_grad()
64
+ outputs = model(batch)
65
+ loss = criterion(outputs, batch)
66
+ loss.backward()
67
+ optimizer.step()
68
+ epoch_loss += loss.item() * batch.size(0)
69
+ epoch_loss /= len(dataset)
70
+ print(f"Epoch {epoch}/{epochs} - Loss: {epoch_loss:.4f}")
71
+ ckpt_path = os.path.join(checkpoint_dir, f"epoch_{epoch}.pth")
72
+ torch.save(model.state_dict(), ckpt_path)
73
+ final_path = os.path.join(checkpoint_dir, "sharpening_model_final.pth")
74
+ torch.save(model.state_dict(), final_path)
75
+ print("Training completed. Model saved to", final_path)
76
+
77
+ def main():
78
+ parser = argparse.ArgumentParser(description="Train sharpening model on 512x512 PNG images.")
79
+ parser.add_argument("--dataset_dir", type=str, required=True, help="Folder containing PNG images.")
80
+ parser.add_argument("--checkpoint_dir", type=str, default="checkpoints", help="Directory to store checkpoints.")
81
+ parser.add_argument("--epochs", type=int, default=10, help="Number of epochs.")
82
+ parser.add_argument("--batch_size", type=int, default=8, help="Batch size.")
83
+ parser.add_argument("--lr", type=float, default=1e-4, help="Learning rate.")
84
+ parser.add_argument("--gpu", action="store_true", help="Use GPU if available.")
85
+ args = parser.parse_args()
86
+ os.makedirs(args.checkpoint_dir, exist_ok=True)
87
+ train(args.dataset_dir, args.checkpoint_dir, args.epochs, args.batch_size, args.lr, args.gpu)
88
+
89
+ if __name__ == "__main__":
90
+ main()