Nekochu commited on
Commit
d0dc88e
·
verified ·
1 Parent(s): ffbec28

Initial commit

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. README.md +3 -3
  3. app.py +321 -0
  4. example.png +3 -0
  5. requirements.txt +4 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ example.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: Material Map Generator
3
- emoji: 🦀
4
- colorFrom: indigo
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.1.0
8
  app_file: app.py
 
1
  ---
2
  title: Material Map Generator
3
+ emoji: 🌍
4
+ colorFrom: purple
5
+ colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.1.0
8
  app_file: app.py
app.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Material Map Generator - Gradio Space
3
+ Generate Normal, Roughness, and Displacement maps from diffuse textures using AI.
4
+
5
+ Original project by Joey Ballentine:
6
+ https://github.com/JoeyBallentine/Material-Map-Generator
7
+
8
+ Models: ESRGAN-based "CX-Lite" trained for material map generation
9
+ Architecture: RRDB-Net from ESRGAN by Xinntao
10
+ """
11
+
12
+ import numpy as np
13
+ import math
14
+ import torch
15
+ import torch.nn as nn
16
+ from collections import OrderedDict
17
+
18
+ # ==================== Architecture (from block.py) ====================
19
+
20
+ def act(act_type, inplace=True, neg_slope=0.2, n_prelu=1):
21
+ act_type = act_type.lower()
22
+ if act_type == 'relu':
23
+ layer = nn.ReLU(inplace)
24
+ elif act_type == 'leakyrelu':
25
+ layer = nn.LeakyReLU(neg_slope, inplace)
26
+ elif act_type == 'prelu':
27
+ layer = nn.PReLU(num_parameters=n_prelu, init=neg_slope)
28
+ else:
29
+ raise NotImplementedError('activation layer [{:s}] is not found'.format(act_type))
30
+ return layer
31
+
32
+ def norm(norm_type, nc):
33
+ norm_type = norm_type.lower()
34
+ if norm_type == 'batch':
35
+ layer = nn.BatchNorm2d(nc, affine=True)
36
+ elif norm_type == 'instance':
37
+ layer = nn.InstanceNorm2d(nc, affine=False)
38
+ else:
39
+ raise NotImplementedError('normalization layer [{:s}] is not found'.format(norm_type))
40
+ return layer
41
+
42
+ def pad(pad_type, padding):
43
+ pad_type = pad_type.lower()
44
+ if padding == 0:
45
+ return None
46
+ if pad_type == 'reflect':
47
+ layer = nn.ReflectionPad2d(padding)
48
+ elif pad_type == 'replicate':
49
+ layer = nn.ReplicationPad2d(padding)
50
+ else:
51
+ raise NotImplementedError('padding layer [{:s}] is not implemented'.format(pad_type))
52
+ return layer
53
+
54
+ def get_valid_padding(kernel_size, dilation):
55
+ kernel_size = kernel_size + (kernel_size - 1) * (dilation - 1)
56
+ padding = (kernel_size - 1) // 2
57
+ return padding
58
+
59
+ class ShortcutBlock(nn.Module):
60
+ def __init__(self, submodule):
61
+ super(ShortcutBlock, self).__init__()
62
+ self.sub = submodule
63
+
64
+ def forward(self, x):
65
+ output = x + self.sub(x)
66
+ return output
67
+
68
+ def sequential(*args):
69
+ if len(args) == 1:
70
+ if isinstance(args[0], OrderedDict):
71
+ raise NotImplementedError('sequential does not support OrderedDict input.')
72
+ return args[0]
73
+ modules = []
74
+ for module in args:
75
+ if isinstance(module, nn.Sequential):
76
+ for submodule in module.children():
77
+ modules.append(submodule)
78
+ elif isinstance(module, nn.Module):
79
+ modules.append(module)
80
+ return nn.Sequential(*modules)
81
+
82
+ def conv_block(in_nc, out_nc, kernel_size, stride=1, dilation=1, groups=1, bias=True,
83
+ pad_type='zero', norm_type=None, act_type='relu', mode='CNA'):
84
+ assert mode in ['CNA', 'NAC', 'CNAC'], 'Wrong conv mode [{:s}]'.format(mode)
85
+ padding = get_valid_padding(kernel_size, dilation)
86
+ p = pad(pad_type, padding) if pad_type and pad_type != 'zero' else None
87
+ padding = padding if pad_type == 'zero' else 0
88
+
89
+ c = nn.Conv2d(in_nc, out_nc, kernel_size=kernel_size, stride=stride, padding=padding,
90
+ dilation=dilation, bias=bias, groups=groups)
91
+ a = act(act_type) if act_type else None
92
+ if 'CNA' in mode:
93
+ n = norm(norm_type, out_nc) if norm_type else None
94
+ return sequential(p, c, n, a)
95
+ elif mode == 'NAC':
96
+ if norm_type is None and act_type is not None:
97
+ a = act(act_type, inplace=False)
98
+ n = norm(norm_type, in_nc) if norm_type else None
99
+ return sequential(n, a, p, c)
100
+
101
+ class ResidualDenseBlock_5C(nn.Module):
102
+ def __init__(self, nc, kernel_size=3, gc=32, stride=1, bias=True, pad_type='zero',
103
+ norm_type=None, act_type='leakyrelu', mode='CNA'):
104
+ super(ResidualDenseBlock_5C, self).__init__()
105
+ self.conv1 = conv_block(nc, gc, kernel_size, stride, bias=bias, pad_type=pad_type,
106
+ norm_type=norm_type, act_type=act_type, mode=mode)
107
+ self.conv2 = conv_block(nc+gc, gc, kernel_size, stride, bias=bias, pad_type=pad_type,
108
+ norm_type=norm_type, act_type=act_type, mode=mode)
109
+ self.conv3 = conv_block(nc+2*gc, gc, kernel_size, stride, bias=bias, pad_type=pad_type,
110
+ norm_type=norm_type, act_type=act_type, mode=mode)
111
+ self.conv4 = conv_block(nc+3*gc, gc, kernel_size, stride, bias=bias, pad_type=pad_type,
112
+ norm_type=norm_type, act_type=act_type, mode=mode)
113
+ if mode == 'CNA':
114
+ last_act = None
115
+ else:
116
+ last_act = act_type
117
+ self.conv5 = conv_block(nc+4*gc, nc, 3, stride, bias=bias, pad_type=pad_type,
118
+ norm_type=norm_type, act_type=last_act, mode=mode)
119
+
120
+ def forward(self, x):
121
+ x1 = self.conv1(x)
122
+ x2 = self.conv2(torch.cat((x, x1), 1))
123
+ x3 = self.conv3(torch.cat((x, x1, x2), 1))
124
+ x4 = self.conv4(torch.cat((x, x1, x2, x3), 1))
125
+ x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1))
126
+ return x5.mul(0.2) + x
127
+
128
+ class RRDB(nn.Module):
129
+ def __init__(self, nc, kernel_size=3, gc=32, stride=1, bias=True, pad_type='zero',
130
+ norm_type=None, act_type='leakyrelu', mode='CNA'):
131
+ super(RRDB, self).__init__()
132
+ self.RDB1 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type,
133
+ norm_type, act_type, mode)
134
+ self.RDB2 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type,
135
+ norm_type, act_type, mode)
136
+ self.RDB3 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type,
137
+ norm_type, act_type, mode)
138
+
139
+ def forward(self, x):
140
+ out = self.RDB1(x)
141
+ out = self.RDB2(out)
142
+ out = self.RDB3(out)
143
+ return out.mul(0.2) + x
144
+
145
+ def upconv_blcok(in_nc, out_nc, upscale_factor=2, kernel_size=3, stride=1, bias=True,
146
+ pad_type='zero', norm_type=None, act_type='relu', mode='nearest'):
147
+ upsample = nn.Upsample(scale_factor=upscale_factor, mode=mode)
148
+ conv = conv_block(in_nc, out_nc, kernel_size, stride, bias=bias,
149
+ pad_type=pad_type, norm_type=norm_type, act_type=act_type)
150
+ return sequential(upsample, conv)
151
+
152
+ def pixelshuffle_block(in_nc, out_nc, upscale_factor=2, kernel_size=3, stride=1, bias=True,
153
+ pad_type='zero', norm_type=None, act_type='relu'):
154
+ conv = conv_block(in_nc, out_nc * (upscale_factor ** 2), kernel_size, stride, bias=bias,
155
+ pad_type=pad_type, norm_type=None, act_type=None)
156
+ pixel_shuffle = nn.PixelShuffle(upscale_factor)
157
+ n = norm(norm_type, out_nc) if norm_type else None
158
+ a = act(act_type) if act_type else None
159
+ return sequential(conv, pixel_shuffle, n, a)
160
+
161
+ # ==================== RRDB_Net (from architecture.py) ====================
162
+
163
+ class RRDB_Net(nn.Module):
164
+ def __init__(self, in_nc, out_nc, nf, nb, gc=32, upscale=4, norm_type=None,
165
+ act_type='leakyrelu', mode='CNA', res_scale=1, upsample_mode='upconv'):
166
+ super(RRDB_Net, self).__init__()
167
+ n_upscale = int(math.log(upscale, 2))
168
+ if upscale == 3:
169
+ n_upscale = 1
170
+
171
+ fea_conv = conv_block(in_nc, nf, kernel_size=3, norm_type=None, act_type=None)
172
+ rb_blocks = [RRDB(nf, kernel_size=3, gc=32, stride=1, bias=True, pad_type='zero',
173
+ norm_type=norm_type, act_type=act_type, mode='CNA') for _ in range(nb)]
174
+ LR_conv = conv_block(nf, nf, kernel_size=3, norm_type=norm_type, act_type=None, mode=mode)
175
+
176
+ if upsample_mode == 'upconv':
177
+ upsample_block = upconv_blcok
178
+ elif upsample_mode == 'pixelshuffle':
179
+ upsample_block = pixelshuffle_block
180
+ else:
181
+ raise NotImplementedError('upsample mode [%s] is not found' % upsample_mode)
182
+ if upscale == 3:
183
+ upsampler = upsample_block(nf, nf, 3, act_type=act_type)
184
+ else:
185
+ upsampler = [upsample_block(nf, nf, act_type=act_type) for _ in range(n_upscale)]
186
+ HR_conv0 = conv_block(nf, nf, kernel_size=3, norm_type=None, act_type=act_type)
187
+ HR_conv1 = conv_block(nf, out_nc, kernel_size=3, norm_type=None, act_type=None)
188
+
189
+ self.model = sequential(fea_conv, ShortcutBlock(sequential(*rb_blocks, LR_conv)),
190
+ *upsampler, HR_conv0, HR_conv1)
191
+
192
+ def forward(self, x):
193
+ x = self.model(x)
194
+ return x
195
+
196
+ # ==================== Tile Processing (from imgops.py) ====================
197
+
198
+ def esrgan_launcher_split_merge(input_image, upscale_function, models, scale_factor=4, tile_size=512, tile_padding=0.125):
199
+ width, height, depth = input_image.shape
200
+ output_width = width * scale_factor
201
+ output_height = height * scale_factor
202
+ output_shape = (output_width, output_height, depth)
203
+
204
+ output_images = [np.zeros(output_shape, np.uint8) for i in range(len(models))]
205
+ tile_padding = math.ceil(tile_size * tile_padding)
206
+ tile_size = math.ceil(tile_size / scale_factor)
207
+
208
+ tiles_x = math.ceil(width / tile_size)
209
+ tiles_y = math.ceil(height / tile_size)
210
+
211
+ for y in range(tiles_y):
212
+ for x in range(tiles_x):
213
+ ofs_x = x * tile_size
214
+ ofs_y = y * tile_size
215
+
216
+ input_start_x = ofs_x
217
+ input_end_x = min(ofs_x + tile_size, width)
218
+ input_start_y = ofs_y
219
+ input_end_y = min(ofs_y + tile_size, height)
220
+
221
+ input_start_x_pad = max(input_start_x - tile_padding, 0)
222
+ input_end_x_pad = min(input_end_x + tile_padding, width)
223
+ input_start_y_pad = max(input_start_y - tile_padding, 0)
224
+ input_end_y_pad = min(input_end_y + tile_padding, height)
225
+
226
+ input_tile_width = input_end_x - input_start_x
227
+ input_tile_height = input_end_y - input_start_y
228
+ input_tile = input_image[input_start_x_pad:input_end_x_pad, input_start_y_pad:input_end_y_pad]
229
+
230
+ for idx, model in enumerate(models):
231
+ output_tile = upscale_function(input_tile, model)
232
+
233
+ output_start_x = input_start_x * scale_factor
234
+ output_end_x = input_end_x * scale_factor
235
+ output_start_y = input_start_y * scale_factor
236
+ output_end_y = input_end_y * scale_factor
237
+
238
+ output_start_x_tile = (input_start_x - input_start_x_pad) * scale_factor
239
+ output_end_x_tile = output_start_x_tile + input_tile_width * scale_factor
240
+ output_start_y_tile = (input_start_y - input_start_y_pad) * scale_factor
241
+ output_end_y_tile = output_start_y_tile + input_tile_height * scale_factor
242
+
243
+ output_images[idx][output_start_x:output_end_x, output_start_y:output_end_y] = \
244
+ output_tile[output_start_x_tile:output_end_x_tile, output_start_y_tile:output_end_y_tile]
245
+
246
+ return output_images
247
+
248
+ # ==================== Model Loading ====================
249
+
250
+ # CPU Optimizations
251
+ torch.set_num_threads(4) # Use available CPU cores
252
+ torch.set_grad_enabled(False) # Disable gradient computation globally
253
+
254
+ device = torch.device('cpu')
255
+ normal_model = None
256
+ other_model = None
257
+
258
+ def load_models():
259
+ global normal_model, other_model
260
+ if normal_model is None:
261
+ print("Loading Normal Map model...")
262
+ normal_model = RRDB_Net(3, 3, 32, 12, gc=32, upscale=1, norm_type=None,
263
+ act_type='leakyrelu', mode='CNA', upsample_mode='upconv')
264
+ normal_model.load_state_dict(torch.load('models/1x_NormalMapGenerator-CX-Lite_200000_G.pth',
265
+ map_location=device, weights_only=True))
266
+ normal_model.eval()
267
+ if other_model is None:
268
+ print("Loading Roughness/Displacement model...")
269
+ other_model = RRDB_Net(3, 3, 32, 12, gc=32, upscale=1, norm_type=None,
270
+ act_type='leakyrelu', mode='CNA', upsample_mode='upconv')
271
+ other_model.load_state_dict(torch.load('models/1x_FrankenMapGenerator-CX-Lite_215000_G.pth',
272
+ map_location=device, weights_only=True))
273
+ other_model.eval()
274
+
275
+ # ==================== Image Processing ====================
276
+
277
+ def process_image(img, model):
278
+ """Process a single image through the model."""
279
+ img = np.array(img).astype(np.float32) / 255.0
280
+ if len(img.shape) == 2: # Grayscale
281
+ img = np.stack([img, img, img], axis=-1)
282
+ if img.shape[2] == 4: # RGBA
283
+ img = img[:, :, :3]
284
+ img = torch.from_numpy(np.transpose(img.copy(), (2, 0, 1))).float().unsqueeze(0)
285
+ output = model(img).squeeze(0).clamp_(0, 1).detach().numpy()
286
+ output = np.transpose(output, (1, 2, 0)) # CHW to HWC
287
+ return (output * 255).astype(np.uint8)
288
+
289
+ def generate_maps(input_image):
290
+ """Generate Normal, Roughness, and Displacement maps from input diffuse texture."""
291
+ if input_image is None:
292
+ return None, None, None
293
+ load_models()
294
+ normal_map = process_image(input_image, normal_model)
295
+ other_output = process_image(input_image, other_model)
296
+ roughness = other_output[:, :, 1]
297
+ displacement = other_output[:, :, 2]
298
+ import gc; gc.collect() # Free memory
299
+ return normal_map, roughness, displacement
300
+
301
+ # ==================== Gradio Interface / CLI ====================
302
+
303
+ if __name__ == "__main__":
304
+ import sys; from PIL import Image
305
+ if len(sys.argv) > 1: [Image.fromarray(m).save(f"{sys.argv[1].rsplit('.',1)[0]}_{n}.png") for n,m in zip(["Normal","Roughness","Displacement"], generate_maps(Image.open(sys.argv[1])))]
306
+ else:
307
+ import gradio as gr
308
+ with gr.Blocks(title="Material Map Generator") as demo:
309
+ gr.Markdown("# Material Map Generator\nGenerate Normal, Roughness & Displacement maps from diffuse textures. [Credits: Joey Ballentine](https://github.com/JoeyBallentine/Material-Map-Generator)")
310
+ with gr.Row():
311
+ with gr.Column(scale=1):
312
+ input_img = gr.Image(type="pil", label="Diffuse Texture", height=200)
313
+ btn = gr.Button("Generate Maps", variant="primary")
314
+ gr.Examples(examples=["example.png"], inputs=input_img)
315
+ with gr.Column(scale=3):
316
+ with gr.Row():
317
+ normal_out = gr.Image(label="Normal", height=180)
318
+ rough_out = gr.Image(label="Roughness", height=180)
319
+ disp_out = gr.Image(label="Displacement", height=180)
320
+ btn.click(fn=generate_maps, inputs=input_img, outputs=[normal_out, rough_out, disp_out])
321
+ demo.launch(theme=gr.themes.Soft())
example.png ADDED

Git LFS Details

  • SHA256: d547578a02cd4413dec64fe8bdb14c50fda718d7c695415419d94c9be0328e26
  • Pointer size: 131 Bytes
  • Size of remote file: 571 kB
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch
2
+ numpy
3
+ Pillow
4
+ gradio