├── LICENSE ├── README.md ├── Texture Synthesis.ipynb ├── TextureSynthesisNonParametricSampling.hipnc ├── TextureSynthesis_Example.gif ├── imgs ├── 1.jpg └── 2.jpg ├── makeGif.py └── textureSynthesis.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 anopara 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 | # Texture Synthesis 2 | Based on "Texture Synthesis with Non-Parametric Sampling" paper by Alexei A. Efros and Thomas K. Leung 3 | 4 | ![](TextureSynthesis_Example.gif) 5 | 6 | There are 2 versions: Python and Houdini 7 | 8 | ### Python 9 | 10 | For Python version you would need: 11 | * Python 3.7 12 | * Jupyter Notebook (my version is 5.6.0) 13 | * Numpy (my version is 1.15.1) 14 | * Matplotlib (2.2.3) 15 | * Scipy (1.1.0) 16 | * Skimage (0.14.0) 17 | * imageio (2.4.1) (if you want to make a GIF :)) 18 | * PIL (5.2.0) 19 | 20 | To start, open the Jupyter Notebook file "Texture Synthesis", and follow the instructions :) 21 | 22 | ### Houdini 23 | 24 | For Houdini: 25 | * Houdini 17 (might work with earlier versions, haven't tried) 26 | 27 | To start, open TextureSynthesis<...>.hipnc file and follow the instructions inside (they are scarce, but present) 28 | Use this file at your own risk :D WARNING: it's very very slow! (you might wanna use it on smaller images...) and the results are less nice that the Python version 29 | -------------------------------------------------------------------------------- /Texture Synthesis.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "The code is by Anastasia Opara (www.anastasiaopara.com)\n", 8 | "\n", 9 | "Provided for use under the MIT license\n", 10 | "\n", 11 | "Based on \"Texture Synthesis with Non-parametric Sampling\" paper by Alexei A. Efros and Thomas K. Leung" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "#DONT FORGET TO RUN THIS :)\n", 21 | "from textureSynthesis import *\n", 22 | "from makeGif import *" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "### Here is the part that requires your input! :)\n", 30 | "##### So, what are all those parameters anyway? Glad you asked!\n", 31 | "* **exampleMapPath** - a string with a path to the example image that you want to generate more of!\n", 32 | "\n", 33 | "* **outputPath** - a path where you want your output image(s) to be saved to! (the algorithm will also create a txt file with your parameters, so you don't forget what your setting for each generation were ^^)\n", 34 | "\n", 35 | "* **outputSize** - the size of the generated image\n", 36 | "\n", 37 | "* **searchKernelSize** - is how 'far' each pixel is aware of neighbouring pixels. With bigger value you will capture more 'global' structures (but it will be slower)\n", 38 | "\n", 39 | "* **truncation** - once we have an X number of candidate colors sampled from the example map for a given pixel, we need to choose which one we go with. Truncation makes sure you don't pick too unlikely samples. Make sure to keep the value in [0,1) range, where 0 is no truncation at all, and 0.9 means you will keep only 10% best samples and choose from them\n", 40 | "\n", 41 | "* **attenuation** - it goes together with truncation! attenuation is a 2nd step and it makes sure you will prioritize higher probability samples (if you want to of course! you can turn it off by setting value to 1). Make sure to keep it in [1, inf). If you feel very experimental, you can set it <1 which, on the contrary, will prioritize lower likelihood samples! (haven't tried myself)\n", 42 | "\n", 43 | "* **snapshots** - will save an image per iteration (if False, only save the final image) - needed if you want to make a gif :)\n", 44 | "\n", 45 | "And...that's all! Have fun :)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 3, 51 | "metadata": {}, 52 | "outputs": [ 53 | { 54 | "data": { 55 | "image/png": "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\n", 56 | "text/plain": [ 57 | "
" 58 | ] 59 | }, 60 | "metadata": { 61 | "needs_background": "light" 62 | }, 63 | "output_type": "display_data" 64 | }, 65 | { 66 | "data": { 67 | "text/plain": [ 68 | "None" 69 | ] 70 | }, 71 | "metadata": {}, 72 | "output_type": "display_data" 73 | } 74 | ], 75 | "source": [ 76 | "#PUT YOUR PARAMETERS HERE\n", 77 | "exampleMapPath = \"imgs/2.jpg\"\n", 78 | "outputSize = [75,75]\n", 79 | "outputPath = \"out/1/\"\n", 80 | "searchKernelSize = 15\n", 81 | "\n", 82 | "textureSynthesis(exampleMapPath, outputSize, searchKernelSize, outputPath, attenuation = 80, truncation = 0.8, snapshots = True)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "## *Make GIF!*\n", 90 | "If you chose 'snapshots = True' option, then you can convert the sequence of images into an animated GIF! \n", 91 | "* **frame_every_X_steps** - sometimes you want your GIF to not include *every* frame, this one allows you to skip X number of frames! (don't worry, it will always end up on the last frame to show the fully resolved image)\n", 92 | "* **repeat_ending** - specify how many frames the GIF will loop over the final resolved image" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 4, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "gifOutputPath = \"out/outGif.gif\"\n", 102 | "\n", 103 | "makeGif(outputPath, gifOutputPath, frame_every_X_steps = 15, repeat_ending = 15)" 104 | ] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "Python 3", 110 | "language": "python", 111 | "name": "python3" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": { 115 | "name": "ipython", 116 | "version": 3 117 | }, 118 | "file_extension": ".py", 119 | "mimetype": "text/x-python", 120 | "name": "python", 121 | "nbconvert_exporter": "python", 122 | "pygments_lexer": "ipython3", 123 | "version": "3.7.0" 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 2 128 | } 129 | -------------------------------------------------------------------------------- /TextureSynthesisNonParametricSampling.hipnc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anopara/texture-synthesis-nonparametric-sampling/59e815de1fc9eeb6c9279ac47575fe24f5f9e867/TextureSynthesisNonParametricSampling.hipnc -------------------------------------------------------------------------------- /TextureSynthesis_Example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anopara/texture-synthesis-nonparametric-sampling/59e815de1fc9eeb6c9279ac47575fe24f5f9e867/TextureSynthesis_Example.gif -------------------------------------------------------------------------------- /imgs/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anopara/texture-synthesis-nonparametric-sampling/59e815de1fc9eeb6c9279ac47575fe24f5f9e867/imgs/1.jpg -------------------------------------------------------------------------------- /imgs/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anopara/texture-synthesis-nonparametric-sampling/59e815de1fc9eeb6c9279ac47575fe24f5f9e867/imgs/2.jpg -------------------------------------------------------------------------------- /makeGif.py: -------------------------------------------------------------------------------- 1 | #for gif making 2 | import imageio 3 | import numpy as np 4 | import os 5 | from PIL import Image 6 | from math import floor 7 | 8 | def makeGif(savePath, outputPath, frame_every_X_steps = 15, repeat_ending = 15): 9 | number_files = len(os.listdir(savePath))-1 10 | frame_every_X_steps = frame_every_X_steps 11 | repeat_ending = repeat_ending 12 | steps = np.arange(floor(number_files/frame_every_X_steps)) * frame_every_X_steps 13 | steps = steps + (number_files - np.max(steps)) 14 | 15 | images = [] 16 | for f in steps: 17 | filename = savePath + 'out' + str(f) + '.jpg' 18 | images.append(imageio.imread(filename)) 19 | 20 | #repeat ending 21 | for _ in range(repeat_ending): 22 | filename = savePath + 'out' + str(number_files) + '.jpg' 23 | images.append(imageio.imread(filename)) 24 | 25 | imageio.mimsave(outputPath, images) -------------------------------------------------------------------------------- /textureSynthesis.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib as plt 3 | import scipy.stats as st #for gaussian kernel 4 | import scipy.misc 5 | import os 6 | 7 | from random import randint, gauss 8 | from math import floor 9 | from skimage import io, feature, transform 10 | from IPython.display import clear_output 11 | 12 | #for gif making 13 | import imageio 14 | from PIL import Image 15 | 16 | def textureSynthesis(exampleMapPath, outputSize, searchKernelSize, savePath, attenuation = 80, truncation = 0.8, snapshots = True): 17 | 18 | #PARAMETERS 19 | PARM_attenuation = attenuation 20 | PARM_truncation = truncation 21 | #write 22 | text_file = open(savePath + 'params.txt', "w") 23 | text_file.write("Attenuation: %d \n Truncation: %f \n KernelSize: %d" % (PARM_attenuation, PARM_truncation, searchKernelSize)) 24 | text_file.close() 25 | 26 | #check whether searchKernelSize is odd: 27 | if searchKernelSize % 2 == 0: 28 | searchKernelSize = searchKernelSize + 1 29 | 30 | #load example map image 31 | exampleMap = loadExampleMap(exampleMapPath) 32 | imgRows, imgCols, imgChs = np.shape(exampleMap) 33 | 34 | #initialize the image to be generated = canvas; + take a random 3x3 patch and put it in the center of the canvas 35 | canvas, filledMap = initCanvas(exampleMap, outputSize) 36 | 37 | #precalculate the array of examples patches from the example map 38 | examplePatches = prepareExamplePatches(exampleMap, searchKernelSize) 39 | 40 | #find pixels that need resolution (weighted by the amount of resolved neighbours they have) 41 | resolved_pixels = 3 * 3 42 | pixels_to_resolve = outputSize[0]*outputSize[1] 43 | 44 | #MAIN LOOP------------------------------------------------------- 45 | 46 | #init a map of best candidates to be resolved (we want to reuse the information) 47 | bestCandidateMap = np.zeros(np.shape(filledMap)) 48 | 49 | while resolved_pixels < pixels_to_resolve: 50 | 51 | #update Candidate Map 52 | updateCandidateMap(bestCandidateMap, filledMap, 5) 53 | 54 | #get best candidate coordinates 55 | candidate_row, candidate_col = getBestCandidateCoord(bestCandidateMap, outputSize) 56 | 57 | #get a candidatePatch to compare to 58 | candidatePatch = getNeighbourhood(canvas, searchKernelSize, candidate_row, candidate_col) 59 | 60 | #get a maskMap 61 | candidatePatchMask = getNeighbourhood(filledMap, searchKernelSize, candidate_row, candidate_col) 62 | #weight it by gaussian 63 | candidatePatchMask *= gkern(np.shape(candidatePatchMask)[0], np.shape(candidatePatchMask)[1]) 64 | #cast to 3d array 65 | candidatePatchMask = np.repeat(candidatePatchMask[:, :, np.newaxis], 3, axis=2) 66 | 67 | #now we need to compare it with every examplePatch and construct the distance metric 68 | #copy everything to match the dimensions of the examplesPatches 69 | examplePatches_num = np.shape(examplePatches)[0] 70 | candidatePatchMask = np.repeat(candidatePatchMask[np.newaxis, :, :, :, ], examplePatches_num, axis=0) 71 | candidatePatch = np.repeat(candidatePatch[np.newaxis, :, :, :, ], examplePatches_num, axis=0) 72 | 73 | distances = candidatePatchMask * pow(examplePatches - candidatePatch, 2) 74 | distances = np.sum(np.sum(np.sum(distances, axis=3), axis=2), axis=1) #sum all pixels of a patch into single number 75 | 76 | #convert distances into probabilities 77 | probabilities = distances2probability(distances, PARM_truncation, PARM_attenuation) 78 | 79 | #sample the constructed PMF and fetch the appropriate pixel value 80 | sample = np.random.choice(np.arange(examplePatches_num), 1, p=probabilities) 81 | chosenPatch = examplePatches[sample] 82 | halfKernel = floor(searchKernelSize / 2) 83 | chosenPixel = np.copy(chosenPatch[0, halfKernel, halfKernel]) 84 | 85 | #resolvePixel 86 | canvas[candidate_row, candidate_col, :] = chosenPixel 87 | filledMap[candidate_row, candidate_col] = 1 88 | 89 | #show live update 90 | plt.pyplot.imshow(canvas) 91 | clear_output(wait=True) 92 | display(plt.pyplot.show()) 93 | 94 | resolved_pixels = resolved_pixels+1 95 | 96 | #save image 97 | if snapshots: 98 | img = Image.fromarray(np.uint8(canvas*255)) 99 | img = img.resize((300, 300), resample=0, box=None) 100 | img.save(savePath + 'out' + str(resolved_pixels-9) + '.jpg') 101 | 102 | #save image 103 | if snapshots==False: 104 | img = Image.fromarray(np.uint8(canvas*255)) 105 | img = img.resize((300, 300), resample=0, box=None) 106 | img.save(savePath + 'out.jpg') 107 | 108 | def distances2probability(distances, PARM_truncation, PARM_attenuation): 109 | 110 | probabilities = 1 - distances / np.max(distances) 111 | probabilities *= (probabilities > PARM_truncation) 112 | probabilities = pow(probabilities, PARM_attenuation) #attenuate the values 113 | #check if we didn't truncate everything! 114 | if np.sum(probabilities) == 0: 115 | #then just revert it 116 | probabilities = 1 - distances / np.max(distances) 117 | probabilities *= (probabilities > PARM_truncation*np.max(probabilities)) # truncate the values (we want top truncate%) 118 | probabilities = pow(probabilities, PARM_attenuation) 119 | probabilities /= np.sum(probabilities) #normalize so they add up to one 120 | 121 | return probabilities 122 | 123 | def getBestCandidateCoord(bestCandidateMap, outputSize): 124 | 125 | candidate_row = floor(np.argmax(bestCandidateMap) / outputSize[0]) 126 | candidate_col = np.argmax(bestCandidateMap) - candidate_row * outputSize[1] 127 | 128 | return candidate_row, candidate_col 129 | 130 | def loadExampleMap(exampleMapPath): 131 | exampleMap = io.imread(exampleMapPath) #returns an MxNx3 array 132 | exampleMap = exampleMap / 255.0 #normalize 133 | #make sure it is 3channel RGB 134 | if (np.shape(exampleMap)[-1] > 3): 135 | exampleMap = exampleMap[:,:,:3] #remove Alpha Channel 136 | elif (len(np.shape(exampleMap)) == 2): 137 | exampleMap = np.repeat(exampleMap[np.newaxis, :, :], 3, axis=0) #convert from Grayscale to RGB 138 | return exampleMap 139 | 140 | def getNeighbourhood(mapToGetNeighbourhoodFrom, kernelSize, row, col): 141 | 142 | halfKernel = floor(kernelSize / 2) 143 | 144 | if mapToGetNeighbourhoodFrom.ndim == 3: 145 | npad = ((halfKernel, halfKernel), (halfKernel, halfKernel), (0, 0)) 146 | elif mapToGetNeighbourhoodFrom.ndim == 2: 147 | npad = ((halfKernel, halfKernel), (halfKernel, halfKernel)) 148 | else: 149 | print('ERROR: getNeighbourhood function received a map of invalid dimension!') 150 | 151 | paddedMap = np.lib.pad(mapToGetNeighbourhoodFrom, npad, 'constant', constant_values=0) 152 | 153 | shifted_row = row + halfKernel 154 | shifted_col = col + halfKernel 155 | 156 | row_start = shifted_row - halfKernel 157 | row_end = shifted_row + halfKernel + 1 158 | col_start = shifted_col - halfKernel 159 | col_end = shifted_col + halfKernel + 1 160 | 161 | return paddedMap[row_start:row_end, col_start:col_end] 162 | 163 | def updateCandidateMap(bestCandidateMap, filledMap, kernelSize): 164 | bestCandidateMap *= 1 - filledMap #remove all resolved from the map 165 | #check if bestCandidateMap is empty 166 | if np.argmax(bestCandidateMap) == 0: 167 | #populate from sratch 168 | for r in range(np.shape(bestCandidateMap)[0]): 169 | for c in range(np.shape(bestCandidateMap)[1]): 170 | bestCandidateMap[r, c] = np.sum(getNeighbourhood(filledMap, kernelSize, r, c)) 171 | 172 | def initCanvas(exampleMap, size): 173 | 174 | #get exampleMap dimensions 175 | imgRows, imgCols, imgChs = np.shape(exampleMap) 176 | 177 | #create empty canvas 178 | canvas = np.zeros((size[0], size[1], imgChs)) #inherit number of channels from example map 179 | filledMap = np.zeros((size[0], size[1])) #map showing which pixels have been resolved 180 | 181 | #init a random 3x3 block 182 | margin = 1 183 | rand_row = randint(margin, imgRows - margin - 1) 184 | rand_col = randint(margin, imgCols - margin - 1) 185 | exampleMap_patch = exampleMap[rand_row-margin:rand_row+margin+1, rand_col-margin:rand_col+margin+1] #need +1 because last element not included 186 | #plt.pyplot.imshow(exampleMap_patch) 187 | #print(np.shape(exampleMap_patch)) 188 | 189 | #put it in the center of our canvas 190 | center_row = floor(size[0] / 2) 191 | center_col = floor(size[1] / 2) 192 | canvas[center_row-margin:center_row+margin+1, center_col-margin:center_col+margin+1] = exampleMap_patch 193 | filledMap[center_row-margin:center_row+margin+1, center_col-margin:center_col+margin+1] = 1 #mark those pixels as resolved 194 | 195 | return canvas, filledMap 196 | 197 | def prepareExamplePatches(exampleMap, searchKernelSize): 198 | 199 | #get exampleMap dimensions 200 | imgRows, imgCols, imgChs = np.shape(exampleMap) 201 | 202 | #find out possible steps for a search window to slide along the image 203 | num_horiz_patches = imgRows - (searchKernelSize-1); 204 | num_vert_patches = imgCols - (searchKernelSize-1); 205 | 206 | #init candidates array 207 | examplePatches = np.zeros((num_horiz_patches*num_vert_patches, searchKernelSize, searchKernelSize, imgChs)) 208 | 209 | #populate the array 210 | for r in range(num_horiz_patches): 211 | for c in range(num_vert_patches): 212 | examplePatches[r*num_vert_patches + c] = exampleMap[r:r+searchKernelSize, c:c+searchKernelSize] 213 | 214 | return examplePatches 215 | 216 | def gkern(kern_x, kern_y, nsig=3): 217 | """Returns a 2D Gaussian kernel array.""" 218 | """altered copy from https://stackoverflow.com/questions/29731726/how-to-calculate-a-gaussian-kernel-matrix-efficiently-in-numpy""" 219 | 220 | # X 221 | interval = (2*nsig+1.)/(kern_x) 222 | x = np.linspace(-nsig-interval/2., nsig+interval/2., kern_x+1) 223 | kern1d_x = np.diff(st.norm.cdf(x)) 224 | # Y 225 | interval = (2*nsig+1.)/(kern_y) 226 | x = np.linspace(-nsig-interval/2., nsig+interval/2., kern_y+1) 227 | kern1d_y = np.diff(st.norm.cdf(x)) 228 | 229 | kernel_raw = np.sqrt(np.outer(kern1d_x, kern1d_y)) 230 | kernel = kernel_raw/kernel_raw.sum() 231 | 232 | return kernel 233 | --------------------------------------------------------------------------------