├── .gitignore ├── LICENSE ├── README.md └── normal_map_generator.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mehdi-Antoine Mahfoudi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Normal Map Generator 2 | 3 | Normal Map Generator is a tool written in Python 4 | 5 | ## Required 6 | 7 | - Python 8 | - Scipy 9 | - Numpy 10 | 11 | ## Usage 12 | 13 | ./normal_map_generator.py input_file output_file --smooth SMOOTH_VALUE -- intensity INTENSITY_VALUE 14 | 15 | ### Required arguments: 16 | 17 | #### input_file 18 | input image path 19 | 20 | #### output_file 21 | output image path 22 | 23 | ### Optional arguments: 24 | 25 | #### -h, --help 26 | Show help message 27 | 28 | #### -s SMOOTH_VALUE, --smooth SMOOTH_VALUE 29 | Smooth gaussian blur applied on the image 30 | 31 | #### -it INTENSITY_VALUE, --intensity INTENSITY_VALUE 32 | Intensity of the normal map -------------------------------------------------------------------------------- /normal_map_generator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import math 3 | import numpy as np 4 | from scipy import ndimage 5 | from matplotlib import pyplot 6 | from PIL import Image, ImageOps 7 | import os 8 | import multiprocessing as mp 9 | 10 | def smooth_gaussian(im:np.ndarray, sigma) -> np.ndarray: 11 | 12 | if sigma == 0: 13 | return im 14 | 15 | im_smooth = im.astype(float) 16 | kernel_x = np.arange(-3*sigma,3*sigma+1).astype(float) 17 | kernel_x = np.exp((-(kernel_x**2))/(2*(sigma**2))) 18 | 19 | im_smooth = ndimage.convolve(im_smooth, kernel_x[np.newaxis]) 20 | 21 | im_smooth = ndimage.convolve(im_smooth, kernel_x[np.newaxis].T) 22 | 23 | return im_smooth 24 | 25 | 26 | def gradient(im_smooth:np.ndarray): 27 | 28 | gradient_x = im_smooth.astype(float) 29 | gradient_y = im_smooth.astype(float) 30 | 31 | kernel = np.arange(-1,2).astype(float) 32 | kernel = - kernel / 2 33 | 34 | gradient_x = ndimage.convolve(gradient_x, kernel[np.newaxis]) 35 | gradient_y = ndimage.convolve(gradient_y, kernel[np.newaxis].T) 36 | 37 | return gradient_x,gradient_y 38 | 39 | 40 | def sobel(im_smooth): 41 | gradient_x = im_smooth.astype(float) 42 | gradient_y = im_smooth.astype(float) 43 | 44 | kernel = np.array([[-1,0,1],[-2,0,2],[-1,0,1]]) 45 | 46 | gradient_x = ndimage.convolve(gradient_x, kernel) 47 | gradient_y = ndimage.convolve(gradient_y, kernel.T) 48 | 49 | return gradient_x,gradient_y 50 | 51 | 52 | def compute_normal_map(gradient_x:np.ndarray, gradient_y:np.ndarray, intensity=1): 53 | 54 | width = gradient_x.shape[1] 55 | height = gradient_x.shape[0] 56 | max_x = np.max(gradient_x) 57 | max_y = np.max(gradient_y) 58 | 59 | max_value = max_x 60 | 61 | if max_y > max_x: 62 | max_value = max_y 63 | 64 | normal_map = np.zeros((height, width, 3), dtype=np.float32) 65 | 66 | intensity = 1 / intensity 67 | 68 | strength = max_value / (max_value * intensity) 69 | 70 | normal_map[..., 0] = gradient_x / max_value 71 | normal_map[..., 1] = gradient_y / max_value 72 | normal_map[..., 2] = 1 / strength 73 | 74 | norm = np.sqrt(np.power(normal_map[..., 0], 2) + np.power(normal_map[..., 1], 2) + np.power(normal_map[..., 2], 2)) 75 | 76 | normal_map[..., 0] /= norm 77 | normal_map[..., 1] /= norm 78 | normal_map[..., 2] /= norm 79 | 80 | normal_map *= 0.5 81 | normal_map += 0.5 82 | 83 | return normal_map 84 | 85 | def normalized(a) -> float: 86 | factor = 1.0/math.sqrt(np.sum(a*a)) # normalize 87 | return a*factor 88 | 89 | def my_gauss(im:np.ndarray): 90 | return ndimage.uniform_filter(im.astype(float),size=20) 91 | 92 | def shadow(im:np.ndarray): 93 | 94 | shadowStrength = .5 95 | 96 | im1 = im.astype(float) 97 | im0 = im1.copy() 98 | im00 = im1.copy() 99 | im000 = im1.copy() 100 | 101 | for _ in range(0,2): 102 | im00 = my_gauss(im00) 103 | 104 | for _ in range(0,16): 105 | im0 = my_gauss(im0) 106 | 107 | for _ in range(0,32): 108 | im1 = my_gauss(im1) 109 | 110 | im000=normalized(im000) 111 | im00=normalized(im00) 112 | im0=normalized(im0) 113 | im1=normalized(im1) 114 | im00=normalized(im00) 115 | 116 | shadow=im00*2.0+im000-im1*2.0-im0 117 | shadow=normalized(shadow) 118 | mean = np.mean(shadow) 119 | rmse = np.sqrt(np.mean((shadow-mean)**2))*(1/shadowStrength) 120 | shadow = np.clip(shadow, mean-rmse*2.0,mean+rmse*0.5) 121 | 122 | return shadow 123 | 124 | def flipgreen(path:str): 125 | try: 126 | with Image.open(path) as img: 127 | red, green, blue, alpha= img.split() 128 | image = Image.merge("RGB",(red,ImageOps.invert(green),blue)) 129 | image.save(path) 130 | except ValueError: 131 | with Image.open(path) as img: 132 | red, green, blue = img.split() 133 | image = Image.merge("RGB",(red,ImageOps.invert(green),blue)) 134 | image.save(path) 135 | 136 | def CleanupAO(path:str): 137 | ''' 138 | Remove unnsesary channels. 139 | ''' 140 | try: 141 | with Image.open(path) as img: 142 | red, green, blue, alpha= img.split() 143 | NewG = ImageOps.colorize(green,black=(100, 100, 100),white=(255,255,255),blackpoint=0,whitepoint=180) 144 | NewG.save(path) 145 | except ValueError: 146 | with Image.open(path) as img: 147 | red, green, blue = img.split() 148 | NewG = ImageOps.colorize(green,black=(100, 100, 100),white=(255,255,255),blackpoint=0,whitepoint=180) 149 | NewG.save(path) 150 | 151 | def adjustPath(Org_Path:str,addto:str): 152 | ''' 153 | Adjust the given path to correctly save the new file. 154 | ''' 155 | 156 | path = Org_Path.split("\\") 157 | file = path[-1] 158 | filename = file.split(".")[0] 159 | fileext = file.split(".")[-1] 160 | 161 | newfilename = addto+"\\"+filename + "_" + addto + "." + fileext 162 | path.pop(-1) 163 | path.append(newfilename) 164 | 165 | newpath = '\\'.join(path) 166 | 167 | return newpath 168 | 169 | def Convert(input_file,smoothness,intensity): 170 | 171 | im = pyplot.imread(input_file) 172 | 173 | if im.ndim == 3: 174 | im_grey = np.zeros((im.shape[0],im.shape[1])).astype(float) 175 | im_grey = (im[...,0] * 0.3 + im[...,1] * 0.6 + im[...,2] * 0.1) 176 | im = im_grey 177 | 178 | im_smooth = smooth_gaussian(im, smoothness) 179 | 180 | sobel_x, sobel_y = sobel(im_smooth) 181 | 182 | normal_map = compute_normal_map(sobel_x, sobel_y, intensity) 183 | 184 | pyplot.imsave(adjustPath(input_file,"Normal"),normal_map) 185 | 186 | flipgreen(adjustPath(input_file,"Normal")) 187 | 188 | im_shadow = shadow(im) 189 | 190 | pyplot.imsave(adjustPath(input_file,"AO"),im_shadow) 191 | CleanupAO(adjustPath(input_file,"AO")) 192 | 193 | def startConvert(): 194 | 195 | parser = argparse.ArgumentParser(description='Compute normal map of an image') 196 | 197 | parser.add_argument('input_file', type=str, help='input folder path') 198 | parser.add_argument('-s', '--smooth', default=0., type=float, help='smooth gaussian blur applied on the image') 199 | parser.add_argument('-it', '--intensity', default=1., type=float, help='intensity of the normal map') 200 | 201 | args = parser.parse_args() 202 | 203 | sigma = args.smooth 204 | intensity = args.intensity 205 | input_file = args.input_file 206 | 207 | for i in ["Ao","Normal"]: 208 | final_path = os.path.join(input_file,i) 209 | if not os.path.isdir(final_path): 210 | os.makedirs (final_path) 211 | 212 | for root, _, files in os.walk(input_file, topdown=False): 213 | for name in files: 214 | input_file.append(str(os.path.join(root, name).replace("/","\\"))) 215 | 216 | if type(input_file) == str: 217 | Convert(input_file,sigma,intensity) 218 | elif type(input_file) == list: 219 | for i in input_file: 220 | ctx = mp.get_context('spawn') 221 | q = ctx.Queue() 222 | p = ctx.Process(target=Convert,args=(input_file,sigma,intensity)) 223 | p.start() 224 | p.join() 225 | --------------------------------------------------------------------------------