├── docs ├── default_panel ├── example1_1.png ├── synthmap.png ├── comparison.jpeg ├── panel_opened.png ├── default_panel.jpg ├── scale_textures.png └── Suzanne_synthesis.png ├── licenses.md ├── README.md ├── uv_synthesize.py ├── uv_prepare.py ├── __init__.py ├── LICENSE └── tex_synthesize.py /docs/default_panel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/default_panel -------------------------------------------------------------------------------- /docs/example1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/example1_1.png -------------------------------------------------------------------------------- /docs/synthmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/synthmap.png -------------------------------------------------------------------------------- /docs/comparison.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/comparison.jpeg -------------------------------------------------------------------------------- /docs/panel_opened.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/panel_opened.png -------------------------------------------------------------------------------- /docs/default_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/default_panel.jpg -------------------------------------------------------------------------------- /docs/scale_textures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/scale_textures.png -------------------------------------------------------------------------------- /docs/Suzanne_synthesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeus/syntexmex/HEAD/docs/Suzanne_synthesis.png -------------------------------------------------------------------------------- /licenses.md: -------------------------------------------------------------------------------- 1 | software used: 2 | 3 | # commercial license 4 | 5 | syntexmex, this version: GPL 6 | 7 | # other licenses: 8 | 9 | python: PSF license 10 | scikit-learn: BSD-new License 11 | skimage: BSD License 12 | shapely: BSD License 13 | scipy: BSD-new License 14 | pynndescent: BSD2-License 15 | Pillow: Python Imaging Library license (MIT-like) 16 | numba: BSD 2-Clause "Simplified" License 17 | networkx: BSD License 18 | decorator: BSD 2-Clause "Simplified" License 19 | llvmlite: BSD 2-Clause "Simplified" License 20 | numpy: BSD 21 | 22 | blender: GPL 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Syntexmex V0.2 3 | subtitle: UV Texture Synthesis based on syntexmex for blender 4 | date: 2020-03-20 5 | tags: ["plugins", "blender", "texture", "PBR", "synthesis"] 6 | bigimg: [{src: "/doc/unwrappedhead.jpg", desc: "unwrapped head"}] 7 | --- 8 | 9 | Syntexmex is an addon for blender which helps generating 10 | UV-textures (PBR textures work as well) based on examples. 11 | 12 | taking an image such as this one: 13 | 14 | 15 | 16 | we can synthesize new texture from the example image: 17 | 18 | 19 | 20 | or make a texture seamless, also using an example image (example 21 | image can be the texture itself): 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 37 | 38 | ## Installation 39 | 40 | ### Option 1: 41 | 42 | Download the zip file from this location: 43 | 44 | -> [prebuilt-packages (Linux/Windows)](https://github.com/yeus/syntexmex/releases) 45 | 46 | After downloading, open blender and go to: 47 | 48 | Edit -> Preferences -> Add-ons -> Install... 49 | 50 | Choose the downloaded zip file and press 51 | 52 | "Install Add-on from File..." 53 | 54 | Afterwards in Preferences, 55 | search for the **Syntexmex** plugin and 56 | put a checkmark in the box to enable the addon.nn 57 | 58 | 65 | ### Option 2 (not recommended): 66 | 67 | building the addon form source is quiet complicated as it involves 68 | a lot of 3rd party libraries. 69 | 70 | These are: 71 | 72 | skimage, shapely, pynndescent, Pillow, decorator, sklearn, scipy, numba, 73 | networkx, PIL, llvmlite 74 | 75 | after the add-on directory has been copied into 76 | the blender addons directory (TODO; where?), 77 | these libraries have to be copied into a "lib/" directory 78 | inside of the blender plugin. 79 | 80 | ## How to Use the Add-on 81 | 82 | ### Where to find it 83 | 84 | The Addon can be accessed through the properties sidebar in 85 | 3D View (Access with shortcut key: 'N') in a the *syntexmex* tab 86 | 87 | 88 | 89 | ### Quick introduction 90 | 91 | 102 | 103 | quick explanation: 104 | 105 | - the plugin takes a copy of an existing material. 106 | - it generates a new image texture on UV islands 107 | based on the selected images in the material. 108 | - it makes that image texture seamless. 109 | - it creates a *synthmap* which gets used to synthesize all other 110 | images in the textures the exact same way. 111 | - it replaces the other images afterwards 112 | 113 | Example of a generated synthmap for Suzanne. You can see the generated patches 114 | at the borders of the UV islands. The synthmap also helps for debugging and 115 | finding the right parameters for the texture: 116 | 117 | synthmap 118 | 119 | Example of Suzanne *before* making edges seamless and *after*: 120 | 121 | withseams 122 | seamless 123 | 124 | 125 | ### Buttons 126 | 127 | 128 | 129 | **Synthesize UV example based texture** 130 | : Synthesize textures for UV islands and make edges between them seamless. 131 | 132 | **Synthesize textures to UV islands** 133 | : Only synthesize texture for each UV island without making edges seamless. 134 | 135 | **Make UV seams seamless** 136 | : Make edges between UV islands seamless. 137 | 138 | ### Parameters 139 | 140 | **source material** 141 | : Choose the source material which should get used for synthesizing 142 | a new material without seems on a given UV map 143 | 144 | **source texture** 145 | : choose one of the textures within the material as the basis for 146 | the synthesis 147 | 148 | **ex. scaling** 149 | : the example texture can be scaled up /down by a factor. Use this 150 | if you want to achieve a different scaling of the example texture 151 | in the final result. 152 | 153 | **library size** 154 | : This controls the quality of the algorithm. It defines the size of 155 | the search-index that the algorithm builds. Higher values will 156 | need more calculation time and memory. The quality is limited 157 | by the amount of available memory on your system. Normally values 158 | up to 100000 should not be a problem for most systems (~1-2GB). 159 | 160 | **patch size ratio** 161 | : The algorithm stitches together patches from the original example 162 | image. The size of the patches in relation to the example image 163 | can be controlled with this parameter. Larger sizes tend to 164 | improve the quality (although not always) and the coherence 165 | of the final result. 166 | 167 | **synth-res** 168 | : The resolution of the target texture. The higher this value 169 | the "smaller" synthesized features will become and vice versa. 170 | 171 | **seed value** 172 | : The algorithm makes use of some random internal parameters 173 | this value enables an option to get different versions of a 174 | synthesized texture 175 | 176 | ### Advanced Settings 177 | 178 | **debugging options** 179 | : Turn detailed debugging in console on/off. 180 | 181 | ## Some hints 182 | 183 | - usually its a good idea to first make a test run with a low value 184 | for library size to get a quick preview of what it might look like 185 | then once you are satisfied with the patch size, example scaling etc... 186 | turn library size up to the maximum value possible with the 187 | available memory. 188 | 189 | - make Sure to unwrap the UVs of your model. You will want 190 | to make sure that there are no overlapping UVs. The same rules apply here 191 | as for every other UV-unwrapping process. Good results can be achieved 192 | when UVs aren't too warped, and areas of UV faces correspond to the actual 193 | face areas. Also there should be a little gap of at least a couple pixels 194 | between each UV island to prevent textures from bleeding into 195 | other uv faces. 196 | 197 | - make sure when using "subdivision modifier" to put Options on "Sharp", 198 | because otherwise seams are going to be visible again due to the smoothing 199 | of UV coordinates 200 | 201 | - increase patch size for better adherence to the patterns found 202 | in the example image 203 | 204 | 205 | - alorithm automatically iterates over the longer of two edges and 206 | writes pixels from there into the face of the shorter edge 207 | -------------------------------------------------------------------------------- /uv_synthesize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | ################################################################################ 5 | Copyright (C) 2020 Thomas Meschede a.k.a. yeus 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ################################################################################ 20 | 21 | 22 | @author: Thomas Meschede a.k.a. yeus (yeusblender@gmail.com) 23 | 24 | This texture synthesis algorithm takes inspiration from three papers and 25 | combines their papers into a new algorithm: 26 | 27 | - Image Quilting for Texture Synthesis and Transfer [Efros, Freeman] 28 | - taking the optimal-patch seam strategy and 29 | - Fast Texture Synthesis using Tree-structured Vector Quantization [Wei, Levoy] 30 | - iterations, non-causal buildup local neighbourhood search 31 | - Real-Time Texture Synthesis by Patch-Based Sampling [Liang et al.] 32 | - building a gaussian image pyramid combined with KD-Trees for 33 | fast searches 34 | 35 | and some more inspiration from: 36 | - wfc synthesis algorithm 37 | 38 | TODO: optionally in the future: 39 | - Graphcut Textures: Image and Video Synthesis Using Graph Cuts [Kwatra, Schödl] 40 | """ 41 | 42 | __author__ = "Thomas Meschede / Thomas Meschede Rose" 43 | __copyright__ = "Copyright (C) 2020 Thomas Rose Meschede" 44 | __license__ = "Proprietary" 45 | __version__ = "1.0" 46 | 47 | import random 48 | import numpy as np 49 | import skimage 50 | import skimage.io 51 | import skimage.transform 52 | import threading 53 | #import gc 54 | import math 55 | import functools 56 | sign = functools.partial(math.copysign, 1) # either of these 57 | import logging 58 | logger = logging.getLogger(__name__) 59 | try: 60 | from . import tex_synthesize as ts 61 | except ImportError: 62 | import tex_synthesize as ts 63 | #import tex_synthesize as ts 64 | import pickle 65 | 66 | 67 | #def norm(x): return np.sqrt(x.dot(x)) 68 | def norm(x): return np.sqrt((x*x).sum(-1)) 69 | #need to be transposed for correct ultiplcation along axis 1 70 | def normalized(x): return (x.T /norm(x)).T 71 | 72 | def calc_angle_vec(u, v): 73 | """ 74 | >>> u = vec((1.0,1.0,0.0)) 75 | >>> v = vec((1.0,0.0,0.0)) 76 | >>> calc_angle_vec(u,v)*rad 77 | 45.00000000000001 78 | >>> u = vec((1.0,0.0,0.0)) 79 | >>> v = vec((-1.0,0.0,0.0)) 80 | >>> calc_angle_vec(u,v)*rad 81 | 180.0 82 | >>> u = vec([-9.38963669e-01, 3.44016319e-01, 1.38777878e-17]) 83 | >>> v = vec([-0.93896367, 0.34401632, 0.]) 84 | >>> u @ v / (norm(v)*norm(u)) 85 | 1.0000000000000002 86 | >>> calc_angle_vec(u,v)*rad 87 | 0.0 88 | """ 89 | #angle = np.arctan2(norm(np.cross(u,v)), np.dot(u,v)) 90 | res = np.sum(u*v) / (norm(u) * norm(v)) 91 | t = np.clip(res,-1.0,1.0) 92 | angle = np.arccos(t) 93 | return angle 94 | 95 | def tqdm(iterator, *args, **kwargs): 96 | return iterator 97 | 98 | GB = 1.0/1024**3 #GB factor 99 | 100 | def normalized_synthmap(synthmap, example): 101 | return synthmap/(*example.shape[:2][::-1],1) 102 | 103 | @ts.timing 104 | def synthesize_textures_on_uvs(synth_tex=False, 105 | seamless_UVs=False, 106 | msg_queue=None, 107 | stop_event=None, 108 | edge_iterations=0, 109 | *argv, **kwargs): 110 | """ 111 | msg_queue lets the algorithm share intermediate steps 112 | when using threaded calculations (queue.Queue) 113 | """ 114 | target = kwargs['target'] 115 | example = kwargs['example'] 116 | patch_ratio = kwargs['patch_ratio'] 117 | libsize = kwargs.get('libsize',128*128) 118 | face_uvs = kwargs['face_uvs'] 119 | islands = kwargs['islands'] 120 | edge_infos = kwargs['edge_infos'] 121 | seed = kwargs.get('seed_value', 0) 122 | logger.info(f"seed: {seed}") 123 | if msg_queue is None: msg_queue=False 124 | if stop_event is None: stop_event=threading.Event() 125 | 126 | #set random seed for algorithm 127 | np.random.seed(seed) 128 | random.seed(seed) 129 | 130 | ta_map = None 131 | 132 | #TODO: check whether we have "left or right" sided coordinate system 133 | 134 | if synth_tex: #generate initial textures 135 | #TODO: make sure all islands are taken into account 136 | logger.info("synthesize uv islands") 137 | res = target.shape[:2] 138 | for island in islands: 139 | island_uvs = [face_uvs[i] for i in island] 140 | island_uvs_px = np.array([uv[...,::-1] * res[:2] for uv in island_uvs]) 141 | #get a boundingbox for the entire island 142 | isl_mins = np.array([isl_px.min(axis=0) for isl_px in island_uvs_px]) 143 | ymin,xmin = isl_mins.min(axis=0).astype(int)#-(1,1) 144 | ymin,xmin = max(ymin,0),max(xmin,0) 145 | isl_mins = np.array([isl_px.max(axis=0) for isl_px in island_uvs_px]) 146 | ymax,xmax = isl_mins.max(axis=0).astype(int)#+(1,1) 147 | ymax,xmax = min(ymax,res[0]),min(xmax,res[1]) 148 | 149 | #add .5 so that uv coordinates refer to the middle of a pixel 150 | # this has to be done after the "mins" where found 151 | island_uvs_px = [isl + (-0.5,-0.5) for isl in island_uvs_px] 152 | 153 | island_mask = np.zeros(target.shape[:2]) 154 | for uvs in island_uvs_px: 155 | island_mask[skimage.draw.polygon(*uvs.T)]=1.0 156 | island_mask = island_mask[ymin:ymax,xmin:xmax]>0 157 | 158 | target, ta_map = ts.fill_area_with_texture(target, example, ta_map, 159 | patch_ratio=patch_ratio, libsize = libsize, 160 | bounding_box=(ymin,xmin,ymax,xmax), 161 | mask = island_mask) 162 | if msg_queue: msg_queue.put((target,normalized_synthmap(ta_map,example))) 163 | 164 | if stop_event.is_set(): 165 | logger.info("stopping_thread") 166 | return 167 | 168 | if seamless_UVs: 169 | tree_info = None 170 | if ta_map is None: 171 | ta_map = np.ones([*target.shape[:2],3])*-1 172 | for i,(e1,e2) in enumerate(edge_infos): 173 | logger.info(f"making edge seamless: #{i}") 174 | #TODO: add pre-calculated island mask to better find "valid" uv pixels 175 | edge1 = e1[0],face_uvs[e1[1]][:,::-1]*target.shape[:2] 176 | edge2 = e2[0],face_uvs[e2[1]][:,::-1]*target.shape[:2] 177 | target, ta_map, tree_info = ts.make_seamless_edge(edge1, edge2, 178 | target, example, ta_map, 179 | patch_ratio, libsize, 180 | tree_info=tree_info, 181 | debug_level=0) 182 | if msg_queue: msg_queue.put((target,normalized_synthmap(ta_map,example))) 183 | if (edge_iterations != 0) and (i >= edge_iterations): break 184 | if stop_event.is_set(): 185 | logger.info("stopping_thread") 186 | return 187 | #debug_image(target2) 188 | #import ipdb; ipdb.set_trace() # BREAKPOINT 189 | 190 | return (target, 191 | normalized_synthmap(ta_map,example), 192 | ta_map) 193 | 194 | def check_face_orientation(face): 195 | edge_vecs = np.roll(face,1,0)-face 196 | return np.cross(np.roll(edge_vecs,1,0),edge_vecs) 197 | 198 | def paint_uv_dots(faces, target): 199 | for f in faces.values(): 200 | for v in f[:,::-1]*target.shape[:2]: 201 | #v=v[::-1] 202 | target[skimage.draw.circle(v[0],v[1],2)]=(1,0,0,1) 203 | 204 | def reconstruct_synthmap(synthmap,example,mode="coordinates"): 205 | """ 206 | TODO: reconstruct from multiple examples as well (third channel in ta_map) 207 | 208 | modes: "coordinates", "normalized" 209 | """ 210 | if mode=="coordinates": 211 | return example[synthmap[:,:,1],synthmap[:,:,0]].copy() 212 | elif mode=='normalized': 213 | synthmap = synthmap * (*example.shape[:2][::-1],1) 214 | synthmap = synthmap.astype(int) 215 | return example[synthmap[:,:,1],synthmap[:,:,0]].copy() 216 | 217 | if __name__=="__main__": 218 | logging.basicConfig(level=logging.INFO) 219 | logger.setLevel(logging.INFO) 220 | logging.getLogger('tex_synthesize').setLevel(logging.INFO) 221 | 222 | #logging.get 223 | 224 | with open('uv_test_island.pickle', 'rb') as handle: 225 | uv_info = pickle.load(handle) 226 | 227 | #skimage.io.imshow_collection([uv_info["target"],uv_info["example"]]) 228 | 229 | target=uv_info['target'] 230 | example=uv_info['example'] 231 | #skimage.io.imshow_collection([uv_info['target']]) 232 | #skimage.io.imshow_collection([example]) 233 | #paint_uv_dots(uv_info['face_uvs'],target) 234 | 235 | 236 | ta, ta_map1, ta_map2 = synthesize_textures_on_uvs(synth_tex=False, 237 | seamless_UVs=True, 238 | edge_iterations=0, 239 | **uv_info) 240 | logger.info("finished test!") 241 | skimage.io.imshow_collection([target, ta_map1, ta_map2]) 242 | 243 | skimage.io.imsave("test.png",ta_map1) 244 | 245 | ta_map_recon = ta_map1*(*example.shape[:2],1) 246 | ((ta_map_recon-ta_map2)**2).sum() #test the rounding-error (its pretty good) 247 | 248 | ta_map = ta_map2.astype(int) 249 | 250 | conv = reconstruct_synthmap(ta_map1,example, mode="normalized") 251 | skimage.io.imshow_collection([ta,conv, ((ta-conv)**2)[...,:3]]) 252 | mask = np.all(ta_map1[:,:,:2]>0,axis=2) 253 | edge_seams = ts.copy_img(ta_map1.copy(),conv[:,:,:3],pos=(0,0),mask=mask) 254 | skimage.io.imshow_collection([target,ta, edge_seams]) 255 | 256 | #uv_info['edge_infos'][0] 257 | 258 | #faces = uv_info['island_uvs'] 259 | 260 | #[check_face_orientation(f) for f in faces] 261 | 262 | -------------------------------------------------------------------------------- /uv_prepare.py: -------------------------------------------------------------------------------- 1 | """ 2 | ################################################################################ 3 | Copyright (C) 2020 Thomas Meschede a.k.a. yeus 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ################################################################################ 18 | """ 19 | 20 | import bpy 21 | import bmesh 22 | import numpy as np 23 | import itertools 24 | import skimage 25 | import skimage.io 26 | import skimage.transform 27 | import networkx as nx 28 | import logging 29 | import pickle 30 | logger = logging.getLogger(__name__) 31 | 32 | #def norm(x): return np.sqrt(x.dot(x)) 33 | def norm(x): return np.sqrt((x*x).sum(-1)) 34 | #need to be transposed for correct ultiplcation along axis 1 35 | def normalized(x): return (x.T /norm(x)).T 36 | 37 | def debug_image(img, name = None): 38 | if img.shape[2]<4:#add alpha channel 39 | img = np.pad(img,((0,0),(0,0),(0,1)),constant_values=1) 40 | 41 | new_tex = bpy.data.images.new("debug", width=img.shape[1], 42 | height=img.shape[0]) 43 | new_tex.pixels[:] = img.flatten() 44 | 45 | def create_bmesh_from_active_object(): 46 | if bpy.context.active_object.mode == 'OBJECT': 47 | ob = bpy.context.selected_objects[0] 48 | me = ob.data 49 | bm = bmesh.new() # create an empty BMesh 50 | bm.from_mesh(me) # fill it in from a Mesh 51 | else: 52 | ob = bpy.context.edit_object 53 | me = ob.data 54 | bm = bmesh.from_edit_mesh(me) 55 | 56 | # the next step makes operations possible in non-edit mode 57 | bm.faces.ensure_lookup_table() 58 | return ob,bm 59 | 60 | #TODO: find_uv_origin(uv): #so that we know where and how 61 | # to start the texture synthesis 62 | def find_uv_origin(uv): 63 | """ find out orientation of the uv face by 64 | calculating the normal in the direction of the loops 65 | we want the "left" of the two uv vertices 66 | to be the starting point. and that depends on the 67 | orientation of the uv face""" 68 | uv_n = (uv[1] - uv[0]).cross(uv[2] - uv[1]) 69 | if uv_n < 0: #means loop direction is clockwise 70 | origin = uv[1] 71 | else: #means loop direction is counter clockwise 72 | origin = uv[0] 73 | 74 | def find_minmax(uv): 75 | y_max,idx = max((v[1],i) for i,v in enumerate(uv)) 76 | x_max,idx = max((v[0],i) for i,v in enumerate(uv)) 77 | 78 | y_min,idx = min((v[1],i) for i,v in enumerate(uv)) 79 | x_min,idx = min((v[0],i) for i,v in enumerate(uv)) 80 | return x_min,x_max,y_min,y_max 81 | 82 | def create_initial_image(res): 83 | import skimage as skim 84 | #TODO: add option to remove "random shapes" 85 | tmp_img,_ = random_shapes((res[1],res[0]), 86 | max_shapes=20, 87 | intensity_range=((100, 255),)) 88 | 89 | img = skim.util.img_as_float(tmp_img) 90 | #add padding 91 | img = np.pad(img,((res[1],res[1]),(res[0],res[0]),(0,0)),constant_values=0) 92 | return img 93 | 94 | def add_uvs(img, xy): 95 | # draw uvs in the image for debugging purposes: 96 | for x,y in xy: 97 | rr,cc = skim.draw.circle(y, x, 5) 98 | img[rr,cc,:]=(0,0,1) 99 | rr,cc = skim.draw.circle(y, x, 0.5) 100 | img[rr,cc,:]=(1,0,0) 101 | 102 | def add_face_shadow(img, xy): 103 | x,y = zip(*xy) 104 | rr,cc = skim.draw.polygon(y, x) 105 | img[rr,cc,2] = 0 106 | img = np.clip(img,0.0,1.0) 107 | 108 | def copy_img(target, src, pos, mask=None): 109 | """ 110 | copy image src to target at pos 111 | """ 112 | #TODO: handle border clipping problems 113 | # - when copying images that exten over "left" and "top" edges 114 | sh,sw,sch = src.shape 115 | th,tw,tch = target.shape 116 | 117 | i0x = pos[0] 118 | i0y = pos[1] 119 | i1x = i0x+sw 120 | i1y = i0y+sh 121 | t_ix0 = max(i0x, 0) 122 | t_iy0 = max(i0y, 0) 123 | t_ix1 = min(i1x, tw) 124 | t_iy1 = min(i1y, th) 125 | 126 | #cut patch to right size 127 | pw, ph = t_ix1 - t_ix0, t_iy1 - t_iy0 128 | 129 | if mask is None: 130 | tch = sch 131 | #print(pos) 132 | #import ipdb; ipdb.set_trace() # BREAKPOINT 133 | target[t_iy0:t_iy1, t_ix0:t_ix1, 0:tch] = src[0:ph, 0:pw] 134 | else: 135 | m = mask 136 | target[t_iy0:t_iy1, t_ix0:t_ix1, 0:tch][m] = src[m] 137 | 138 | 139 | def create_mask(vecs): 140 | #TODO: create a "soft" mask 141 | #rr,cc = skim.draw.polygon(y, x) 142 | #mask = np.zeros_like(img) 143 | #mask[rr,cc,0] = 0.5 144 | mask = skim.draw.polygon2mask(img.shape[:2], vecs) 145 | return mask 146 | 147 | def draw_face_outline(img, uv_p, uv_vp): 148 | #color only the corners 149 | abs = ((0,0),(0,1),(1,0),(1,1)) 150 | for a,b in abs: 151 | #corners 152 | cbase = uv_p[0] + a * uv_vp[0] + b * uv_vp[3] 153 | rr,cc = skim.draw.circle(cbase[1], cbase[0], 5) 154 | target[rr,cc,:3]=(0,0,1) 155 | 156 | #edge1 157 | a_length = mh.norm(uv_vp[0]) 158 | for i in np.arange(0,a_length,0.3): 159 | a = i/a_length 160 | b = 0 161 | base = uv_p[0] + a * uv_vp[0] + b * uv_vp[3] 162 | idx = base.astype(int) 163 | img[idx[1],idx[0],:3]=(0,1,0) 164 | #import ipdb; ipdb.set_trace() # BREAKPOINT 165 | 166 | b_length = mh.norm(uv_vp[3]) 167 | for i in np.arange(0,a_length,0.3): 168 | a = 0 169 | b = i/a_length 170 | base = uv_p[0] + a * uv_vp[0] + b * uv_vp[3] 171 | idx = base.astype(int) 172 | img[idx[1],idx[0],:3]=(0,1,0) 173 | #import ipdb; ipdb.set_trace() # BREAKPOINT 174 | 175 | target[cbase[1].astype(int), cbase[0].astype(int) 176 | ,:3]=(1,0,0) 177 | 178 | def draw_triangle_outline(img, uv_p, uv_vp): 179 | a_length = mh.norm(uv_vp[0]) 180 | b_length = mh.norm(uv_vp[1]) 181 | for b in np.arange(0,b_length,0.5): 182 | #for a in np.arange() 183 | a = 1-b/b_length 184 | b = b/b_length 185 | x = uv_p[0] + a * uv_vp[0] + b * uv_vp[3] 186 | img[x[1].astype(int),x[0].astype(int),:3]=(1,1,0) 187 | 188 | 189 | def sign(p1, p2, p3): 190 | return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); 191 | 192 | def PointInTriangle(pt, v1, v2, v3): 193 | d1 = sign(pt, v1, v2) 194 | d2 = sign(pt, v2, v3) 195 | d3 = sign(pt, v3, v1) 196 | 197 | has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0); 198 | has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0); 199 | 200 | return not (has_neg and has_pos); 201 | 202 | def idx_sanitize(co, img): 203 | x_max = img.shape[:2][::-1] 204 | x_min = (0,0) 205 | z = np.min([x_max,co], axis=0) 206 | return np.max([z,x_min], axis=0) 207 | 208 | def idx_check(co, img): 209 | x_max = img.shape[:2][::-1] 210 | x_min = np.array((0,0)) 211 | return np.all((co=x_min)) 212 | 213 | def init_face_map(img, bm): 214 | #TODO: get rid of empty pixels 215 | #TODO: make algorithm more efficient 216 | for face in bm.faces:#[:1]: 217 | uv = np.array([l[uv_layer].uv for l in face.loops]) 218 | uv_p = uv * res[:2] #transform to pixel space 219 | 220 | #get four uv edge vectors: 221 | uv_v = np.array([uv[1]-uv[0], 222 | uv[2]-uv[1], 223 | uv[3]-uv[2], 224 | uv[0]-uv[3]]) 225 | 226 | 227 | #TODO: do the followgin for both face triangles 228 | tris = [(0,1,2),(2,3,0)] 229 | img = face_map 230 | for tr in tris: 231 | base = uv_p[tr[1]] 232 | uv_v = np.array([uv[tr[0]]-uv[tr[1]], 233 | uv[tr[2]]-uv[tr[1]]]) 234 | 235 | #transform to pixel space 236 | uv_vp = uv_v * res[:2] 237 | 238 | #draw patches 239 | a_length = mh.norm(uv_vp[0]) 240 | b_length = mh.norm(uv_vp[1]) 241 | st_len = 0.8 242 | for b in np.arange(0,b_length,st_len): 243 | #for a in np.arange() 244 | b_ = b/b_length 245 | c_ = a_length * (1-b_) 246 | #import ipdb; ipdb.set_trace() # BREAKPOINT 247 | for a_ in np.arange(0,c_,st_len): 248 | a_ = a_/a_length 249 | x = base + a_ * uv_vp[0] + b_ * uv_vp[1] 250 | x = x.astype(int) 251 | #if x[1]==300: 252 | # import ipdb; ipdb.set_trace() # BREAKPOINT 253 | if idx_check(x, img): 254 | img[x[1],x[0]]=face.index 255 | 256 | #draw_face_outline(target, uv_p, uv_vp) 257 | #draw_triangle_outline(target, uv_p, uv_vp) 258 | 259 | #import ipdb; ipdb.set_trace() # BREAKPOINT 260 | 261 | def empty_copy(img, dtype): 262 | #return np.zeros(img.shape[:2], dtype = dtype) 263 | return np.full(img.shape[:2], -1, dtype = dtype) 264 | 265 | 266 | def blimage2array(blender_source): 267 | res = np.array((blender_source.size[1],blender_source.size[0])) 268 | return np.array(list(blender_source.pixels)).reshape(*res,4) 269 | 270 | def init_texture_buffers(example_image, target_texture, example_scaling): 271 | # numpy handles shapes in a different way then images in blender, 272 | # because of this, the convention, when indexing looks like this: 273 | # shape = (y from bottom, x from left, alpha) 274 | res_ex = np.array((example_image.size[1],example_image.size[0])) 275 | nt = target_texture 276 | res = np.array((nt.size[1], nt.size[0])) 277 | # define numpy buffers for image data access 278 | target = np.array(list(nt.pixels)).reshape(*res,4) # create an editable copy 279 | example = np.array(example_image.pixels).reshape(*res_ex,4) 280 | example = skimage.transform.rescale(example, example_scaling, 281 | multichannel=True) 282 | 283 | return example, target 284 | 285 | #import ipdb; ipdb.set_trace() # BREAKPOINT 286 | 287 | # TODO: calculate face area and scale example 288 | # accordingly 289 | 290 | #TODO: 291 | #find center, so that we can either start the 292 | #algorithm in the center or extend a little bit beyond the 293 | # borders of the face: 294 | #uv_center = np.sum(uv, axis=0)/4 295 | 296 | """ 297 | face_map = empty_copy(target, int) 298 | init_face_map(face_map, bm) 299 | 300 | #convert facemap into image 301 | num_faces = len(bm.faces) 302 | #create random colors for faces (+1 because index starts with 0) 303 | rand_colors = np.random.rand(num_faces+1,3) 304 | map2 = np.array([rand_colors[px] if px != -1 else (0,0,0) for px in face_map.flatten()]) 305 | shape = (*target.shape[0:2],3) 306 | map2 = map2.reshape(shape) 307 | #if display_face_map: 308 | target[:,:,0:3] = map2 309 | #import ipdb; ipdb.set_trace() # BREAKPOINT 310 | """ 311 | 312 | #debug_img(tmp0) 313 | #import ipdb; ipdb.set_trace() # BREAKPOINT 314 | 315 | #randomly copy patch to target image 316 | #coords = np.random.rand(2)*res[:2]/2 + res[:2]/4 317 | #copy_img(target, patch, coords.astype(int)) 318 | 319 | #TODO: get rid of empty pixels 320 | #TODO: make algorithm more efficient 321 | 322 | #TODO: make a map of "remaining" pixels which did not get painted 323 | # to get rid of holes 324 | 325 | """ 326 | img = target 327 | for face in bm.faces[:]: 328 | uv = np.array([l[uv_layer].uv for l in face.loops]) 329 | uv_p = uv * res[:2] #transform to pixel space 330 | 331 | #get four uv edge vectors: 332 | uv_v = np.array([uv[1]-uv[0], 333 | uv[2]-uv[1], 334 | uv[3]-uv[2], 335 | uv[0]-uv[3]]) 336 | 337 | 338 | #TODO: do the followgin for both face triangles 339 | tris = [(0,1,2),(2,3,0)] 340 | for tr in tris: 341 | base = uv_p[tr[1]] 342 | uv_v = np.array([uv[tr[0]]-uv[tr[1]], 343 | uv[tr[2]]-uv[tr[1]]]) 344 | 345 | #transform to pixel space 346 | uv_vp = uv_v * res[:2] 347 | 348 | #draw patches 349 | a_length = mh.norm(uv_vp[0]) 350 | b_length = mh.norm(uv_vp[1]) 351 | st_len = 20.0 352 | for b in np.arange(0,b_length,st_len): 353 | #for a in np.arange() 354 | b_ = b/b_length 355 | c_ = a_length * (1-b_) 356 | #import ipdb; ipdb.set_trace() # BREAKPOINT 357 | for a_ in np.arange(0,c_,st_len): 358 | a_ = a_/a_length 359 | x = base + a_ * uv_vp[0] + b_ * uv_vp[1] 360 | patch = random.choice(patches) 361 | coords = mh.vec((x[0]-res_patch[0]*0.5, 362 | x[1]-res_patch[1]*0.5)).astype(int) 363 | #if all(coords == ( -8, 1013)): 364 | #import ipdb; ipdb.set_trace() # BREAKPOINT 365 | copy_img(target, patch, coords) 366 | #target[coords[1],coords[0],:3]=(0.5,0.5,1.0) 367 | #img[x[1].astype(int),x[0].astype(int)]=patch 368 | """ 369 | 370 | def get_uv_levelset(uvs): 371 | from shapely.geometry import Polygon, Point 372 | 373 | uv_p = uvs * res[:2] #transform to pixel space 374 | face = Polygon(uv_p) 375 | boundary = face.boundary 376 | face_container = face.buffer(+10.0) #add two pixels on the container 377 | bbox = face_container.bounds 378 | minx, miny, maxx, maxy = bbox_px = np.round(np.array(bbox)).astype(int) 379 | w,h = maxx - minx, maxy-miny 380 | 381 | def distance(x,y): 382 | d = boundary.distance(Point(x,y)) 383 | if face.contains(Point(x,y)): return d 384 | else: return -d 385 | 386 | bbcoords = itertools.product(range(miny,maxy), range(minx, maxx)) 387 | levelset = np.array([distance(x,y) for y,x in bbcoords]).reshape(h,w) 388 | #normalize levelset: 389 | #levelset = np.maximum(levelset/levelset.max(),0.0) 390 | return levelset, bbox_px 391 | 392 | #get all UVs per face: 393 | 394 | def get_uvs(bm): 395 | uvs = {}#defaultdict(list) 396 | for face in bm.faces:#[:1]: 397 | uvs[face.index] = get_face_uvs(face,bm) 398 | return uvs 399 | 400 | def get_face_uvs(face, bm): 401 | uv_layer = bm.loops.layers.uv['UVMap'] 402 | uv = np.array([l[uv_layer].uv for l in face.loops]) 403 | return uv 404 | 405 | def get_edge_uvs(edge, bm): 406 | uv_layer = bm.loops.layers.uv['UVMap'] 407 | #if len(edge.link_loops) == 2: 408 | l = edge.link_loops #the two "opposite loops" 409 | ln = [l.link_loop_next for l in edge.link_loops] #for the next uv 410 | uv_edges = np.array([[l[uv_layer].uv for l in lln] for lln in zip(l,ln)]) 411 | #uv_edge2 = np.array([l[uv_layer].uv for l in [l2,l2n]]) 412 | return uv_edges 413 | 414 | 415 | def loops_connected(edge, bm): 416 | """check if the uv coordinates of an edge are the 417 | same for both connected faces""" 418 | uv_edges = get_edge_uvs(edge, bm) 419 | if len(uv_edges)<2: 420 | return False 421 | else: 422 | if np.all(uv_edges[0]==uv_edges[1][::-1]): return True 423 | else: return False 424 | 425 | def is_border_edge(edge, bm): 426 | if len(edge.link_loops)<2: 427 | return True 428 | else: return False 429 | 430 | def generate_edge_loop_uvs(bm_edges, res, bm): 431 | """generates an edge_info structure that can be used 432 | as input to make seamless edges""" 433 | edge_uvs = [get_edge_uvs(e, bm) for e in bm_edges] #get uvs 434 | #[len(e) for e in edge_uvs] 435 | #import ipdb; ipdb.set_trace() # BREAKPOINT 436 | #switching x and y coordinates to adapt to numpy column-row convention 437 | edge_uvs = np.array(edge_uvs)[...,::-1]*res + (-0.5,-0.5) #switch xy to numpy yx convention and transform into pixel space 438 | #import ipdb; ipdb.set_trace() # BREAKPOINT 439 | face_uvs = np.array([(e.link_faces[0].index, 440 | e.link_faces[1].index) for e in bm_edges]) 441 | #import ipdb; ipdb.set_trace() 442 | #for face its a little more complicated switching the coordinates 443 | #as there can have different number of vertices and thus 444 | # can not be put into a homogenous numpy array 445 | #face_uvs = [[f[...,::-1]*res + (-0.5,-0.5) for f in fs] for fs in face_uvs] 446 | #face_uvs = [np.array(face_uvs)[...,::-1]*res + (-0.5,-0.5)] 447 | #import ipdb; ipdb.set_trace() # BREAKPOINT 448 | edge_infos1 = tuple(zip(edge_uvs[:,0,:,:],face_uvs[:,0])) 449 | edge_infos2 = tuple(zip(edge_uvs[:,1,:,:],face_uvs[:,1])) 450 | return edge_infos1,edge_infos2 451 | 452 | def prepare_uv_synth_info(example, 453 | target, bm, 454 | patch_ratio, 455 | libsize): 456 | logger.info("generate uv synth info") 457 | #import ipdb; ipdb.set_trace() # BREAKPOINT 458 | #generate a list of connected faces 459 | bm.edges.index_update() 460 | connected_edges = [e for e in bm.edges if loops_connected(e, bm)] 461 | #build a graph of connected faces 462 | connected_faces = [(e.link_faces[0].index,e.link_faces[1].index, e.index) 463 | for e in connected_edges] 464 | G = nx.Graph() 465 | G.add_weighted_edges_from(connected_faces, weight='index') 466 | islands = list(nx.connected_components(G)) 467 | 468 | res = np.array(target.shape[:2]) 469 | #res_ex 470 | 471 | #import ipdb; ipdb.set_trace() # BREAKPOINT 472 | 473 | #build a list of edge pairs 474 | #iterate through edges 475 | #https://b3d.interplanety.org/en/learning-loops/ 476 | bm.edges.index_update() 477 | continuous_edges = [edge for edge in bm.edges if not is_border_edge(edge,bm)] 478 | unconnected_edges = [edge for edge in continuous_edges if not 479 | loops_connected(edge, bm)] 480 | 481 | #import ipdb; ipdb.set_trace() # BREAKPOINT 482 | edge_infos1, edge_infos2 = generate_edge_loop_uvs(unconnected_edges, 483 | res, bm) 484 | uv_info = { 485 | "target":target, 486 | "example":example, 487 | "patch_ratio":patch_ratio, 488 | "libsize":libsize, 489 | "face_uvs":get_uvs(bm), 490 | "islands":islands, 491 | "edge_infos":list(zip(edge_infos1, edge_infos2)) 492 | } 493 | 494 | #uncomment, if picklig is needed for debugging the senthesis algorithm 495 | #needs to be debugged outside of blender 496 | if False: 497 | logger.info("saving pickle") 498 | with open('uv_test_island.pickle', 'wb') as handle: 499 | pickle.dump(uv_info, handle, protocol=pickle.HIGHEST_PROTOCOL) 500 | 501 | return uv_info 502 | 503 | 504 | """for face in bm.faces[:1]: 505 | uv = np.array([l[uv_layer].uv for l in face.loops]) 506 | 507 | import ipdb; ipdb.set_trace() # BREAKPOINT 508 | """ 509 | #for uvs in list(get_uvs(bm).values())[:]: 510 | # #iterate through each edge 511 | # import ipdb; ipdb.set_trace() # BREAKPOINT 512 | 513 | 514 | #TODO: find out which edges are already "connected", because 515 | 516 | #make UVs seamless 517 | 518 | #for these we don't need to copy the pixels 519 | 520 | #TODO: for directly connected faces we can omit the whole 521 | #seamless procedure and render the entire "metaface" as a whole 522 | #and thn concentrate on indiviual edges oly afterwards 523 | 524 | #debug_image(cospxs) 525 | #debug_image(f2) 526 | #debug_image(f1) 527 | #import ipdb; ipdb.set_trace() # BREAKPOINT 528 | 529 | 530 | """ 531 | #get four uv edge vectors: 532 | uv_v = np.array([uv[1]-uv[0], 533 | uv[2]-uv[1], 534 | uv[3]-uv[2], 535 | uv[0]-uv[3]]) 536 | """ 537 | 538 | #find points on the "outside" of uv_edges: which correspond to certain 539 | #points in other faces according to some rule (probably try to resemble a 540 | # straight line as much as possible) 541 | 542 | 543 | #debug_image(face_map) 544 | 545 | #bmesh.update_edit_mesh(me, True) 546 | 547 | 548 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Syntexmex 3 | 4 | - a plugin for texture synthesis on 3d meshes and removing edge seams 5 | 6 | Copyright (C) 2020 Thomas Meschede a.k.a. yeus 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | import importlib 23 | import bpy 24 | import os 25 | import sys 26 | import logging 27 | import numpy as np 28 | import threading, queue, time 29 | import functools 30 | logger = logging.getLogger(__name__) 31 | logger.setLevel(logging.INFO) 32 | 33 | """ 34 | To set up the logging module to your needs, create a file $HOME/.config/blender/{version}/scripts/startup/setup_logging.py (this is on Linux, but you’re likely a developer, so you’ll know where to find this directory). If the folder doesn’t exist yet, create it. I like to put something like this in there: 35 | 36 | import logging 37 | 38 | logging.basicConfig(level=logging.INFO, 39 | format='%(asctime)-15s %(levelname)8s %(name)s %(message)s') 40 | 41 | for name in ('blender_id', 'blender_cloud'): 42 | logging.getLogger(name).setLevel(logging.DEBUG) 43 | 44 | def register(): 45 | pass 46 | """ 47 | 48 | 49 | # 50 | #blend_dir = os.path.dirname(bpy.data.filepath) 51 | # if blend_dir not in sys.path: 52 | # sys.path.append(blend_dir) 53 | # temporarily appends the folder containing this file into sys.path 54 | #main_dir = os.path.dirname(bpy.data.filepath) #blender directory 55 | #sys.path.append(main_dir) 56 | main_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib') #addon dir "__init__.py" + lib 57 | sys.path.append(main_dir) 58 | logger.info(f"add library directory to syntexmex: {main_dir}") 59 | 60 | from . import uv_prepare as up 61 | importlib.reload(up) 62 | from . import uv_synthesize as us 63 | importlib.reload(us) 64 | importlib.reload(us.ts) 65 | 66 | __author__ = "yeus " 67 | __status__ = "alpha" 68 | __version__ = "0.9.2" 69 | __date__ = "2020 Feb 29th" 70 | 71 | #TODO: fill this out 72 | bl_info = { 73 | "name": "Syntexmex Blender", 74 | "author": "yeus Properties > syntexmex", 78 | "description": "Generate example-based seamless textures", 79 | "warning": "", 80 | "wiki_url": "http://yeus.gitlab.io/syntexmex TODO", 81 | "category": "View3D", 82 | "support": "COMMUNITY", 83 | } 84 | 85 | #from operator import * 86 | 87 | # TODO: remove tabs 88 | # https://blender.stackexchange.com/questions/97502/removing-tabs-from-tool-shelf-t-key/97503#97503 89 | 90 | synth_progress = 0.0 91 | 92 | def multiline_label(layout,text): 93 | for t in text.split("\n"): 94 | layout.label(text=t) 95 | 96 | class syntexmex(bpy.types.Operator): 97 | """This operator synthesizes texture in various ways on UV textures""" 98 | bl_idname = "texture.syntexmex" 99 | bl_label = "Synthesize Operations on UV Textures" 100 | bl_category = 'syntexmex' 101 | bl_options = {'REGISTER', 'UNDO'} 102 | 103 | patch_size: bpy.props.FloatProperty( 104 | name="Patch Size Ratio", 105 | description="Set width of patches as a ratio of shortest edge of an image", 106 | min=0.0, 107 | max=0.5, 108 | default=0.1, 109 | precision=3, 110 | step=0.1 111 | ) 112 | example_image: bpy.props.StringProperty() 113 | example_scaling: bpy.props.FloatProperty( 114 | name="Example Scaling", 115 | description="""Scale Example to a certain size which will be used 116 | to generate the texture""", 117 | min=0.0, 118 | max=1.0, 119 | default=1.0, 120 | precision=3, 121 | step=1 122 | ) 123 | target_image: bpy.props.StringProperty() 124 | target_resolution: bpy.props.IntVectorProperty(name="synth-res.", 125 | size=2, 126 | min=0, 127 | max=10000, 128 | default=(1024,1024)) 129 | synth_tex: bpy.props.BoolProperty(name="synthesize textures") 130 | seamless_UVs: bpy.props.BoolProperty(name="seamless UV islands") 131 | libsize: bpy.props.IntProperty(name="patch library size") 132 | seed_value: bpy.props.IntProperty( 133 | name="Seed Value", 134 | description="Seed value for predictable texture generation", 135 | default = 0 136 | ) 137 | replace_material: bpy.props.BoolProperty(name="replace material", 138 | default=False) 139 | 140 | _timer = None 141 | 142 | @classmethod 143 | def poll(cls, context): 144 | #TODO: this operator should also be able to execute in other 145 | # windows as well 146 | return context.space_data.type in {'VIEW_3D'} 147 | 148 | def run_algorithm(self, context): 149 | logger.info("start synthesizing algorithm") 150 | #scene = context.scene 151 | 152 | #TODO: select specific object in menu 153 | obj, bm = up.create_bmesh_from_active_object() 154 | uv_layer = bm.loops.layers.uv['UVMap'] 155 | 156 | logging.info(self.example_image) 157 | logging.info(self.target_image) 158 | 159 | example_image = bpy.data.images[self.example_image] 160 | if self.target_image is "": 161 | self.target_image = 'synthtarget' 162 | 163 | bpy.data.images.new(self.target_image, 164 | self.target_resolution[0], 165 | self.target_resolution[1], 166 | alpha=False,float_buffer=True) 167 | self.target = bpy.data.images[self.target_image] 168 | 169 | ta_width,ta_height=self.target.size 170 | self.ta_map = bpy.data.images.new("ta_map",ta_width,ta_height, 171 | alpha=False,float_buffer=True, 172 | is_data=True) 173 | #BlendDataImages.new(name, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False, tiled=False) 174 | 175 | examplebuf, targetbuf = up.init_texture_buffers(example_image, 176 | self.target, self.example_scaling) 177 | 178 | self.msg_queue = queue.Queue() 179 | args = (examplebuf, 180 | targetbuf, 181 | bm, 182 | self.patch_size, 183 | self.libsize) 184 | kwargs = dict() 185 | 186 | uv_info = up.prepare_uv_synth_info(*args,**kwargs) 187 | uv_info['seed_value']=self.seed_value 188 | self.algorithm_steps=self.synth_tex+len(uv_info['edge_infos']*self.seamless_UVs) 189 | global synth_progress 190 | synth_progress=0.01 191 | context.scene.syntexmexsettings.synth_progress=0.01 192 | self._killthread = threading.Event() 193 | synthtex = functools.partial(us.synthesize_textures_on_uvs, 194 | synth_tex=self.synth_tex, 195 | seamless_UVs=self.seamless_UVs, 196 | msg_queue=self.msg_queue, 197 | stop_event=self._killthread, 198 | **uv_info) 199 | self._thread = threading.Thread(target = synthtex, 200 | daemon=True, 201 | args = [], 202 | kwargs = {}) 203 | self._thread.start() 204 | 205 | def write_images(self,target,ta_map=None): 206 | # Write back to blender image. 207 | self.target.pixels[:] = target.flatten() 208 | self.target.update() 209 | 210 | #import ipdb; ipdb.set_trace() # BREAKPOINT 211 | 212 | if ta_map is not None: 213 | ta_map = np.dstack((ta_map,np.ones(ta_map.shape[:2]))) 214 | self.ta_map.pixels[:] = ta_map.flatten() 215 | self.ta_map.update() 216 | 217 | logger.info("synced images!") 218 | 219 | def execute(self, context): 220 | def testworker(conn): 221 | for i in range(10): 222 | time.sleep(1.0) 223 | logger.info(f"working thread at {i}") 224 | conn.put(i) 225 | logger.info("working thread finished!") 226 | 227 | self.run_algorithm(context) 228 | 229 | # start timer to check thread status 230 | wm = context.window_manager 231 | self._timer = wm.event_timer_add(0.5, window=context.window) 232 | wm.modal_handler_add(self) 233 | self._region = context.region 234 | self._area = context.area 235 | return {'RUNNING_MODAL'} 236 | 237 | def modal(self, context, event): 238 | #TODO: https://stackoverflow.com/questions/21409683/passing-numpy-arrays-through-multiprocessing-queue for passing images from the other (sub) process 239 | if event.type in {'ESC'}:#{'RIGHTMOUSE', 'ESC'}: 240 | self.cancel(context) 241 | logger.info("thread was cancelled!") 242 | return {'CANCELLED'} 243 | 244 | if event.type == 'TIMER': 245 | # change theme color, silly! 246 | if self._thread.is_alive(): 247 | self.receive(context) 248 | #print(".", end = '') 249 | else: 250 | logger.info("thread seems to have finished!") 251 | self.receive(context) 252 | self.ta_map.pack() 253 | self.target.pack() 254 | if self.replace_material: 255 | bpy.ops.texture.syntexmex_pbr_texture( 256 | synth_map=self.ta_map.name, 257 | source_material=context.scene.syntexmexsettings.source_material.name 258 | ) 259 | 260 | self.cancel(context) 261 | return {'FINISHED'} 262 | 263 | return {'PASS_THROUGH'} 264 | 265 | def receive(self, context): 266 | try: 267 | msg = self.msg_queue.get_nowait() 268 | except queue.Empty: 269 | msg = None 270 | else: 271 | self.msg_queue.task_done() 272 | logger.info(f"received a new msg!!") 273 | if msg is not None: 274 | target,ta_map = msg 275 | self.write_images(target,ta_map) 276 | global synth_progress 277 | synth_progress += 1.0/self.algorithm_steps 278 | context.scene.syntexmexsettings.synth_progress += 1.0/self.algorithm_steps 279 | self._region.tag_redraw() 280 | self._area.tag_redraw() 281 | return msg 282 | 283 | def cancel(self, context): 284 | logger.info("cleaning up timer!") 285 | self._killthread.set() 286 | global synth_progress 287 | synth_progress = 0.0 288 | context.scene.syntexmexsettings.synth_progress=0.0 289 | self._region.tag_redraw() 290 | self._area.tag_redraw() 291 | wm = context.window_manager 292 | wm.event_timer_remove(self._timer) 293 | 294 | class clear_target_texture(bpy.types.Operator): 295 | """Clear target texture to make it black""" 296 | bl_idname = "texture.clear_target_texture" 297 | bl_label = "Clear target texture (black)" 298 | bl_options = {'REGISTER', 'UNDO'} 299 | 300 | def execute(self, context): 301 | logger.info("clear texture") 302 | ta = context.scene.syntexmexsettings.target_image 303 | 304 | target = np.zeros((ta.size[1], ta.size[0],4)) 305 | target[...,3]=1.0 306 | 307 | ta.pixels[:] = target.flatten() 308 | ta.update() 309 | ta.pack() 310 | 311 | return {'FINISHED'} 312 | 313 | def calc_syntexmex_info(ex_img, props): 314 | res_ex = np.array((ex_img.size[1],ex_img.size[0])) * props.example_scaling 315 | if props.target_image: 316 | ta_img = props.target_image 317 | res_ta = np.array((ta_img.size[1],ta_img.size[0])) 318 | else: 319 | res_ta = np.array((props.target_resolution[1], 320 | props.target_resolution[0])) 321 | scaling = us.ts.calc_lib_scaling(res_ex, props.libsize) 322 | (res_patch, res_patch0, 323 | overlap, overlap0) = us.ts.create_patch_params2(res_ex, 324 | scaling, 325 | 1/6.0, props.patch_size) 326 | #mem_reqs = tu.ts.check_memory_requirements2(res_ex, 327 | # res_patch, ch_num, ) 328 | mem_reqs = 2*us.ts.calculate_memory_consumption( 329 | res_ex*scaling, 330 | res_patch, 331 | ch_num = ex_img.channels, 332 | itemsize = 8) #itemsize= 8 comes from the floatingpoint size of 8 bytes in numpy arrays 333 | 334 | maxmem=us.ts.get_mem_limit() 335 | 336 | return (res_ex,res_ta,scaling,res_patch, res_patch0, 337 | overlap, overlap0, mem_reqs, maxmem) 338 | 339 | #TODO: ame everything according to here: 340 | #https://wiki.blender.org/wiki/Reference/Release_Na.otes/2.80/Python_API/Addons 341 | class syntexmex_panel(bpy.types.Panel): 342 | bl_label = "Syntexmex Configuration" 343 | bl_idname = "SYNTEXMEX_PT_syntexmex_panel" 344 | bl_space_type = 'VIEW_3D' 345 | bl_region_type = 'UI' 346 | bl_category = 'syntexmex' 347 | #bl_context = "tool" 348 | 349 | def copy_to_operator(self, op, settings): 350 | op.example_image = settings.example_image.name 351 | if settings.target_image: 352 | op.target_image = settings.target_image.name 353 | op.example_scaling = settings.example_scaling 354 | op.libsize = settings.libsize 355 | op.patch_size = settings.patch_size 356 | op.synth_tex=True 357 | op.seamless_UVs=True 358 | op.seed_value=settings.seed_value 359 | op.replace_material=settings.replace_material 360 | 361 | def draw(self, context): 362 | global synth_progress 363 | scene = context.scene 364 | props = scene.syntexmexsettings 365 | 366 | layout = self.layout 367 | layout.use_property_split = True 368 | 369 | 370 | ex_img = props.example_image 371 | 372 | #calculate algorithm properties and display some help how to set 373 | #it up correctly 374 | col2 = layout.column() 375 | if ex_img: 376 | (res_ex,res_ta,scaling, 377 | res_patch, res_patch0, 378 | overlap, overlap0, 379 | mem_reqs, maxmem) = calc_syntexmex_info(ex_img, props) 380 | if synth_progress > 0.0001: 381 | showtext=f"synthesize texture...\npress 'ESC' to cancel." 382 | elif mem_reqs > maxmem: 383 | showtext=f"algorithm uses too much memory: \n~{mem_reqs:.2f} GB\nmore than the available {maxmem:.2f} GB.\nMaybe close down other \nprograms to free up memory?" 384 | canrun=False 385 | col2.alert=True 386 | elif np.any(np.array(res_patch)<2): 387 | showtext=f"algorithm needs patch\nsizes >2 to run!" 388 | col2.alert=True 389 | canrun=False 390 | elif np.any(np.array(overlap)<1): 391 | showtext=f"algorithm needs overlap\nsizes > 1" 392 | col2.alert=True 393 | canrun=False 394 | else: 395 | showtext="algorithm is ready..." 396 | canrun=True 397 | elif not ex_img: 398 | showtext="choose an example img.!" 399 | col2.alert=True 400 | elif synth_progress > 0.0001: 401 | showtext="press 'ESC' to stop texture synthesis" 402 | 403 | #display helptext 404 | col2.scale_y = 0.7 405 | multiline_label(col2,"> "+showtext) 406 | 407 | layout.separator(factor=2.0) 408 | if synth_progress < 0.0001: #if algorithm isnt running 409 | ###### algorithm start buttons for different run modes 410 | col3 = layout.column() 411 | if ex_img and canrun: col3.enabled=True 412 | else: col3.enabled=False 413 | col2 = col3.column() 414 | col2.scale_y = 2.0 415 | op1 = col2.operator("texture.syntexmex", 416 | text = "Synthesize UV example based texture") 417 | 418 | #col.scale_y = 1.0 419 | col3.separator(factor=2.0) 420 | op2 = col3.operator("texture.syntexmex", 421 | text = "Synthesize textures to UV islands") 422 | 423 | op3 = col3.operator("texture.syntexmex", 424 | text = "Make UV seams seamless") 425 | 426 | if ex_img and canrun: 427 | self.copy_to_operator(op1,props) 428 | self.copy_to_operator(op2,props) 429 | op2.synth_tex=True 430 | op2.seamless_UVs=False 431 | self.copy_to_operator(op3,props) 432 | op3.synth_tex=False 433 | op3.seamless_UVs=True 434 | 435 | 436 | #####algorithm properties 437 | layout.separator(factor=2.0) 438 | layout.prop(props, "replace_material") 439 | #for prop in scene.syntexmexsettings.__annotations__.keys(): 440 | #layout.label(text="Example Material") 441 | layout.prop_search(props,"source_material", bpy.data, "materials", 442 | text="Source Mat.") 443 | if props.source_material: 444 | layout.prop(props, "material_textures",text="Source Tex.") 445 | else: 446 | layout.prop_search(props, "example_image", bpy.data, "images", 447 | text="Source Tex.") 448 | #layout.prop_search(props,"example_image", props, "material_textures", 449 | # text="Source Tex.") 450 | layout.prop(props, "example_scaling", text='Ex. Scaling') 451 | layout.prop(props, "libsize") 452 | #TODO: make patch_size dependend on target_texture 453 | layout.prop(props, "patch_size") 454 | layout.prop(props, "target_resolution") 455 | layout.prop(props, "seed_value") 456 | 457 | 458 | class syntexmex_info_panel(bpy.types.Panel): 459 | bl_label = "Info Panel" 460 | bl_idname = "SYNTEXMEX_PT_syntexmex_info_panel" 461 | bl_space_type = 'VIEW_3D' 462 | bl_region_type = 'UI' 463 | bl_category = 'syntexmex' 464 | #bl_context = "tool" 465 | #bl_options = {'DEFAULT_CLOSED'} 466 | 467 | def draw(self, context): 468 | layout = self.layout 469 | scene = context.scene 470 | props = scene.syntexmexsettings 471 | 472 | ex_img = props.example_image 473 | 474 | if ex_img: 475 | (res_ex,res_ta,scaling, 476 | res_patch, res_patch0, 477 | overlap, overlap0, 478 | mem_reqs, maxmem) = calc_syntexmex_info(ex_img, props) 479 | 480 | #layout.box() 481 | layout.prop(props, "synth_progress") 482 | layout.enabled=False 483 | 484 | layout.label(text="Algorithm Data:") 485 | b = layout.box() 486 | b.scale_y = 0.3 487 | 488 | multiline_label(b,f"""memory requirements: {mem_reqs:.2f}/{maxmem:.2f} GB 489 | source scaling: {props.example_scaling*100:.0f}% 490 | source resolution: [{res_ex[1]:.0f} {res_ex[0]:.0f}] px 491 | target resolution: {res_ta[::-1]} px 492 | synth resolution scaling: {scaling:.2f} 493 | patches (highres): {res_patch0[::-1]} px 494 | patches (lowres): {res_patch[::-1]} px 495 | overlap (highres): {overlap0[::-1]} 496 | overlap (lowres): {overlap[::-1]}""") 497 | 498 | class syntexmex_advanced_panel(bpy.types.Panel): 499 | bl_label = "Advanced Settings" 500 | bl_idname = "SYNTEXMEX_PT_syntexmex_advanced_panel" 501 | bl_space_type = 'VIEW_3D' 502 | bl_region_type = 'UI' 503 | bl_category = 'syntexmex' 504 | #bl_context = "tool" 505 | bl_options = {'DEFAULT_CLOSED'} 506 | 507 | def draw(self, context): 508 | layout = self.layout 509 | col = layout.column() 510 | 511 | scene = context.scene 512 | props = scene.syntexmexsettings 513 | 514 | col.label(text="debugging options:") 515 | col.prop(props,"advanced_debugging", 516 | text="enable advanced console logs") 517 | 518 | 519 | def set_debugging(on=True): 520 | logging.basicConfig(level=logging.INFO) 521 | if on: 522 | logging.disable(logging.NOTSET) 523 | else: 524 | logging.disable(logging.INFO) 525 | 526 | 527 | def update_debugging(self, context): 528 | scene = context.scene 529 | props = scene.syntexmexsettings 530 | logger.info(f"Set advanced infos to: {props.advanced_debugging}") 531 | set_debugging(props.advanced_debugging) 532 | return None 533 | 534 | 535 | class synth_PBR_texture(bpy.types.Operator): 536 | """This operator synthesizes PBR textures from a synthmap 537 | (an image in which the pixel values are coordinates to an example image)""" 538 | bl_idname = "texture.syntexmex_pbr_texture" 539 | bl_label = "Synthesize PBR textures using a synth-map" 540 | bl_category = 'syntexmex' 541 | bl_options = {'REGISTER', 'UNDO'} 542 | 543 | synth_map: bpy.props.StringProperty() 544 | source_material : bpy.props.StringProperty() 545 | source_image : bpy.props.StringProperty() 546 | 547 | def execute(self, context): 548 | logger.info("start PBR synthesis") 549 | 550 | logger.info(f"synthesize textures: {self.synth_map},"\ 551 | f"{self.source_material},{self.source_image}") 552 | 553 | synthmap = up.blimage2array(bpy.data.images[self.synth_map])[...,:3] 554 | mask = np.all(synthmap[:,:,:2]>=0,axis=2) 555 | 556 | #scn = context.scene 557 | obj = context.active_object 558 | 559 | if self.source_material is not None: 560 | logger.info("create new seamless material") 561 | obj.active_material = mat = bpy.data.materials[self.source_material].copy() 562 | 563 | #duplicate texture and duplicate images inside 564 | #TODO: make "original" texture permanently saved 565 | imgnodes = [n for n in mat.node_tree.nodes if n.type=='TEX_IMAGE'] 566 | images = [up.blimage2array(n.image)[...,:3] for n in imgnodes] 567 | synth_images = [us.reconstruct_synthmap(synthmap, 568 | img, 569 | mode='normalized') 570 | for img in images] 571 | 572 | #import ipdb; ipdb.set_trace() # BREAKPOINT 573 | if not np.all(mask): #only do this if we need a masked copy 574 | logger.info("copying seams to original textures") 575 | scaled_images = [us.skimage.transform.resize( 576 | img, synthmap.shape[:2]) 577 | for img in images] 578 | synth_images = [us.ts.copy_img(img,simg,mask=mask) 579 | for img, simg in zip(scaled_images,synth_images)] 580 | 581 | 582 | 583 | #functionality not implemented right now 584 | #elif self.source_image is not None: 585 | # images = [up.blimage2array(self.source_image)[...,:3]] 586 | 587 | for simg,node in zip(synth_images,imgnodes): 588 | logger.info(f"processing image: {node.image.name}") 589 | if node.image.colorspace_settings.name=='Non-Color': is_data=True 590 | else: is_data=False 591 | new_img = bpy.data.images.new(node.image.name+"_synth", 592 | synthmap.shape[1],synthmap.shape[0], 593 | alpha=False,float_buffer=node.image.is_float, 594 | is_data=is_data) 595 | 596 | #add alpha channel and upload into blender 597 | new_img.pixels[:] = np.dstack((simg,np.ones(simg.shape[:2]))).flatten() 598 | 599 | #new_img.colorspace_settings.name = node.image.colorspace_settings.name 600 | node.image = new_img 601 | new_img.update() 602 | new_img.pack() 603 | 604 | 605 | #us.reconstruct_synthmap(self.) 606 | return {'FINISHED'} 607 | 608 | 609 | 610 | #TODO: if we want a list o something: 611 | #https://sinestesia.co/blog/tutorials/using-uilists-in-blender/ 612 | class syntexmex_texture_operations_panel(bpy.types.Panel): 613 | bl_label = "texture operations" 614 | bl_idname = "SYNTEXMEX_PT_syntexmex_texture_operations_panel" 615 | bl_space_type = 'VIEW_3D' 616 | bl_region_type = 'UI' 617 | bl_category = 'syntexmex' 618 | #bl_context = "tool" 619 | bl_options = {'DEFAULT_CLOSED'} 620 | 621 | def draw(self, context): 622 | layout = self.layout 623 | layout.use_property_decorate = False 624 | ob = context.active_object 625 | 626 | scene = context.scene 627 | props = scene.syntexmexsettings 628 | op = layout.operator("texture.syntexmex_pbr_texture", 629 | text = "Synthesize UV example based texture") 630 | 631 | ex_img = props.example_image 632 | ta_img = props.target_image 633 | img_init=(ex_img is not None) and (ta_img is not None) 634 | 635 | if props.active_synthmap: 636 | op.synth_map = props.active_synthmap.name 637 | if props.source_material: 638 | op.source_material = props.source_material.name 639 | if props.source_image: 640 | op.source_image = props.source_image.name 641 | 642 | layout.use_property_split = True 643 | layout.label(text="active synthmap:") 644 | layout.template_ID(props,"active_synthmap", open="image.open") 645 | layout.prop_search(props,"source_material", bpy.data, "materials") 646 | layout.label(text="source image:") 647 | layout.template_ID(props,"source_image", open="image.open") 648 | 649 | 650 | # if prop in images: 651 | layout.label(text="Example Image:") 652 | layout.template_ID(props, "example_image", open="image.open") 653 | layout.prop(props, "example_scaling") 654 | layout.prop(props, "libsize") 655 | #TODO: make patch_size dependend on target_texture 656 | layout.prop(props, "patch_size") 657 | 658 | 659 | layout.separator(factor=2.0) 660 | layout.label(text="Target Image:") 661 | layout.template_ID(props, "target_image", 662 | new="image.new",open="image.open") 663 | if (ta_img is not None): 664 | layout.operator("texture.clear_target_texture") 665 | 666 | def get_textures_from_material(self, context): 667 | mat = self.source_material 668 | texlist = [] 669 | if mat is not None: 670 | for i,n in enumerate(mat.node_tree.nodes): 671 | if n.type=="TEX_IMAGE": 672 | if n.image: 673 | texlist.append((n.image.name,n.image.name,"",i)) 674 | #print(f"updating images!{n.image.name},{i}") 675 | return texlist 676 | #images = [n.image for n in mat.node_tree.nodes if n.type=='TEX_IMAGE'] 677 | #return None#images 678 | 679 | def update_source_image_from_enum(self, context): 680 | props = context.scene.syntexmexsettings 681 | sel_tex = props.material_textures 682 | props.example_image = bpy.data.images[sel_tex] 683 | 684 | class SyntexmexSettings(bpy.types.PropertyGroup): 685 | #https://docs.blender.org/api/current/bpy.props.html 686 | synth_progress: bpy.props.FloatProperty( 687 | name="synthetization progress", 688 | description="synthetization progress", 689 | min=0.0,max=1.0, 690 | default=0.0, 691 | subtype = "PERCENTAGE", 692 | options = {'SKIP_SAVE'} 693 | ) 694 | patch_size: bpy.props.FloatProperty( 695 | name="Patch Size Ratio", 696 | description="Set width of patches as a ratio of shortest edge of an image", 697 | min=0.0001, 698 | max=0.5, 699 | default=0.1, 700 | precision=3, 701 | step=0.1, 702 | ) 703 | example_image: bpy.props.PointerProperty(name="Ex.Tex.", type=bpy.types.Image) 704 | example_scaling: bpy.props.FloatProperty( 705 | name="Example Scaling", 706 | description="""Scale Example to a certain size which will be used 707 | to generate the texture""", 708 | min=0.0001, 709 | max=1.0, 710 | default=1.0, 711 | precision=3, 712 | step=1 713 | ) 714 | target_image: bpy.props.PointerProperty(name="target tex", type=bpy.types.Image) 715 | target_resolution: bpy.props.IntVectorProperty(name="synth-res.", 716 | size=2, 717 | min=0, 718 | max=10000, 719 | default=(1024,1024)) 720 | libsize: bpy.props.IntProperty(name="Library Size", 721 | description="defines the quality of the texture (higher=better, but needs more memory)", 722 | min=10*10, default = 128*128, 723 | step=10#TODO: currently not implemented in blender 724 | ) 725 | seed_value: bpy.props.IntProperty( 726 | name="Seed Value", 727 | description="Seed value for predictable texture generation", 728 | default = 0, 729 | ) 730 | source_material : bpy.props.PointerProperty(name="source material", 731 | type=bpy.types.Material) 732 | #material_textures : bpy.props.CollectionProperty(type=TextureList) 733 | #material_textures : bpy.props.CollectionProperty(type=bpy.types.Image) 734 | material_textures : bpy.props.EnumProperty(items = get_textures_from_material, 735 | update = update_source_image_from_enum) 736 | source_image : bpy.props.PointerProperty(name="source image", 737 | type=bpy.types.Image) 738 | active_synthmap : bpy.props.PointerProperty(name="active synthmap", 739 | type=bpy.types.Image) 740 | advanced_debugging: bpy.props.BoolProperty( 741 | name="Advanced Debugging", 742 | description="Enable Advanced Debugging (in console)", 743 | default = True, 744 | update = update_debugging 745 | ) 746 | replace_material : bpy.props.BoolProperty( 747 | name="replace material", 748 | description=("replace all textures in material with" 749 | "a seamless version"), 750 | default=True 751 | ) 752 | 753 | 754 | classes = ( 755 | syntexmex_info_panel, 756 | syntexmex_panel, 757 | #TODO: enable this again: syntexmex_texture_operations_panel, 758 | syntexmex_advanced_panel, 759 | syntexmex, 760 | synth_PBR_texture, 761 | clear_target_texture 762 | ) 763 | register_panel, unregister_panel = bpy.utils.register_classes_factory(classes) 764 | 765 | @bpy.app.handlers.persistent 766 | def on_load(dummy): 767 | print("initializing syntexmex!") 768 | if bpy.data.scenes[0].syntexmexsettings.advanced_debugging: 769 | print("turn on advanced debugging!") 770 | set_debugging(True) 771 | else: 772 | print("turn off advanced debugging!") 773 | set_debugging(False) 774 | 775 | def register(): 776 | #syntexmex_panel.register() 777 | bpy.utils.register_class(SyntexmexSettings) 778 | bpy.types.Scene.syntexmexsettings = bpy.props.PointerProperty(type=SyntexmexSettings) 779 | register_panel() 780 | global synth_progress 781 | synth_progress = 0.0 782 | #bpy.app.timers.register(in_5_seconds, first_interval=5) 783 | bpy.app.handlers.load_post.append(on_load) 784 | #TODO: initialize/deiniailize debuggin according to how the variable was set 785 | #init propertygroup 786 | 787 | def unregister(): 788 | #syntexmex_panel.unregister() 789 | unregister_panel() 790 | bpy.utils.unregister_class(SyntexmexSettings) 791 | del(bpy.types.Scene.syntexmexsettings) 792 | #del(bpy.context.scene['syntexmexsettings']) 793 | 794 | if __name__ == "__main__": 795 | logger.info("register syntexmex") 796 | # pipe_operator.register() 797 | # bpy.ops.mesh.add_pipes() 798 | register() 799 | # debugging 800 | #bpy.ops.mesh.add_pipes(number=5, mode='skin', seed = 11) 801 | 802 | #obj = bpy.context.selected_objects[0] 803 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | piperator 635 | Copyright (C) 2019 Thomas Meschede 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | piperator Copyright (C) 2019 Thomas Meschede 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /tex_synthesize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sun Jan 5 13:39:03 2020 5 | 6 | ################################################################################ 7 | Copyright (C) 2020 Thomas Meschede a.k.a. yeus 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program. If not, see . 21 | ################################################################################ 22 | 23 | @author: Thomas Meschede a.k.a. yeus (yeusblender@gmail.com) 24 | 25 | This texture synthesis algorithm takes inspiration from three papers and 26 | combines their papers into a new algorithm: 27 | 28 | - Image Quilting for Texture Synthesis and Transfer [Efros, Freeman] 29 | - taking the optimal-patch seam strategy and 30 | - Fast Texture Synthesis using Tree-structured Vector Quantization [Wei, Levoy] 31 | - iterations, non-causal buildup local neighbourhood search 32 | - Real-Time Texture Synthesis by Patch-Based Sampling [Liang et al.] 33 | - building a gaussian image pyramid combined with KD-Trees for 34 | fast searches 35 | """ 36 | 37 | import random 38 | import numpy as np 39 | import skimage 40 | import skimage.io 41 | import skimage.transform 42 | import itertools 43 | from functools import wraps 44 | import time 45 | #from pynndescent import NNDescent 46 | #import gc 47 | import math 48 | import os, sys 49 | import functools 50 | sign = functools.partial(math.copysign, 1) # either of these 51 | #import scipy 52 | import shapely 53 | import shapely.geometry 54 | import logging 55 | #import psutil #TODO: not easy in blender, because of a lacking Python.h 56 | logger = logging.getLogger(__name__) 57 | 58 | #ann_library = "pynndescent" 59 | ann_library = "sklearn" 60 | use_pynnd, use_sklearn=False,False 61 | if ann_library=="pynndescent": 62 | import pynndescent as pynnd 63 | use_pynnd=True 64 | elif ann_library=='sklearn': 65 | import sklearn 66 | import sklearn.neighbors 67 | use_sklearn=True 68 | 69 | #def norm(x): return np.sqrt(x.dot(x)) 70 | def norm(x): return np.sqrt((x*x).sum(-1)) 71 | #need to be transposed for correct ultiplcation along axis 1 72 | def normalized(x): return (x.T /norm(x)).T 73 | 74 | def calc_angle_vec(u, v): 75 | """ 76 | >>> u = vec((1.0,1.0,0.0)) 77 | >>> v = vec((1.0,0.0,0.0)) 78 | >>> calc_angle_vec(u,v)*rad 79 | 45.00000000000001 80 | >>> u = vec((1.0,0.0,0.0)) 81 | >>> v = vec((-1.0,0.0,0.0)) 82 | >>> calc_angle_vec(u,v)*rad 83 | 180.0 84 | >>> u = vec([-9.38963669e-01, 3.44016319e-01, 1.38777878e-17]) 85 | >>> v = vec([-0.93896367, 0.34401632, 0.]) 86 | >>> u @ v / (norm(v)*norm(u)) 87 | 1.0000000000000002 88 | >>> calc_angle_vec(u,v)*rad 89 | 0.0 90 | """ 91 | #angle = np.arctan2(norm(np.cross(u,v)), np.dot(u,v)) 92 | res = np.sum(u*v) / (norm(u) * norm(v)) 93 | t = np.clip(res,-1.0,1.0) 94 | angle = np.arccos(t) 95 | return angle 96 | 97 | #consider replacing sklearn KDTree with scipy KDTree 98 | #https://jakevdp.github.io/blog/2013/04/29/benchmarking-nearest-neighbor-searches-in-python/ 99 | #from tqdm import tqdm 100 | def tqdm(iterator, *args, **kwargs): 101 | return iterator 102 | 103 | GB = 1.0/1024**3 #GB factor 104 | 105 | def timing(f): 106 | @wraps(f) 107 | def wrap(*args, **kw): 108 | ts = time.time() 109 | result = f(*args, **kw) 110 | te = time.time() 111 | logger.info(f'func:{f.__name__} took: {te-ts:2.4f} sec') 112 | return result 113 | return wrap 114 | 115 | @timing 116 | def init_ann_index(data): 117 | """ 118 | TODO: parameterize quality of ANN search 119 | """ 120 | #metric='euclidean' 121 | metric='manhattan' 122 | if use_pynnd: 123 | index = pynnd.NNDescent(data,metric, 124 | n_neighbors=3,#steers the quality of the algorithm 125 | n_jobs=-1 #-1: use all processors 126 | ) 127 | elif use_sklearn: 128 | index = sklearn.neighbors.KDTree(data, metric=metric)#'l2' 129 | return index 130 | 131 | def query_index(index, data, k): 132 | if use_pynnd: 133 | idx, e = index.query([data], k=k) 134 | return e,idx[0] 135 | elif sklearn: 136 | e, idx = index.query([data], k=k) 137 | e, idx = e.flatten(), idx.flatten() 138 | return e,idx 139 | 140 | def get_mem_limit(factor=0.5): 141 | #stats = psutil.virtual_memory() # returns a named tuple 142 | #avilable_memory = getattr(stats, 'available')/1024**3 # available memory in GB 143 | #mem_limit = avilable_memory*factor 144 | #tot_m, used_m, free_m = map(int, os.popen('free -t -m').readlines()[-1].split()[1:]) 145 | if sys.platform=='linux': 146 | free_m = int(os.popen('free -t -m').readlines()[1].split()[-1]) 147 | else: #TODO: include windows/mac 148 | free_m = 2000 #MB 149 | return free_m * factor / 1024 #in GB 150 | 151 | def copy_img(target, src, pos=(0,0), mask=None): 152 | """ 153 | copy image src to target at pos 154 | 155 | careful! x & y are switched around here (normal order) in contrast to other 156 | functions of this library. order: pos=(x,y) 157 | """ 158 | #TODO: handle border clipping problems 159 | # - when copying images that exten over "left" and "top" edges 160 | sh,sw,sch = src.shape 161 | th,tw,tch = target.shape 162 | 163 | i0x = np.clip(pos[0],0,tw) 164 | i0y = np.clip(pos[1],0,th) 165 | i1x = np.clip(pos[0]+sw,0,tw) 166 | i1y = np.clip(pos[1]+sh,0,th) 167 | 168 | #cut source patch to right size 169 | pw, ph = max(i1x - i0x,0), max(i1y - i0y,0) 170 | 171 | if mask is None: 172 | tch = sch 173 | #print(pos) 174 | #import ipdb; ipdb.set_trace() # BREAKPOINT 175 | target[i0y:i1y, i0x:i1x, 0:tch] = src[0:ph, 0:pw] 176 | else: 177 | m = mask 178 | target[i0y:i1y, i0x:i1x, 0:tch][m] = src[m] 179 | 180 | return target 181 | 182 | def mask_blend(m, img1, img2): 183 | """ 184 | this blends two images by blending them using a mask 185 | """ 186 | mT = np.expand_dims(m,axis=2) 187 | b1 = img1 * (1 - mT) 188 | b2 = img2 * mT 189 | new_img = b1+b2 190 | return new_img 191 | 192 | @timing 193 | def transform_patch_grid_to_tex(res, res_patch, pg, example, 194 | overlap, 195 | use_quilting=True): 196 | """ 197 | synthesize texture from pre-calculated grid 198 | 199 | overlap = (horizontal_ovelap, vertical_overlap) 200 | """ 201 | #TODO: create a generate- "info" function so that information doesn't have 202 | # to be exchanged so often 203 | #TODO: adaptive overlap (wih larger patch sizes it might not 204 | #make enough sense anymore, because textures become too repetetive ...) 205 | #overlap = np.ceil((res_patch/6)).astype(int) 206 | #TODO: the overlap can actually be different from the overlap when searching 207 | # for the images. This might make some things more efficient 208 | # or we can just generate a standad overlap in all functions 209 | 210 | ch_num = example.shape[-1] 211 | 212 | #def draw_patch_grid(): 213 | rpg = np.array(res_patch) - overlap 214 | if res is None: res = rpg * pg.shape[:2] + overlap 215 | target = np.zeros((res[0],res[1],ch_num), dtype = example.dtype) 216 | ta_map = np.zeros((res[0],res[1],3)) 217 | 218 | target_map_patch_base = gen_coordinate_map(res_patch) 219 | onemask = np.ones(res_patch) 220 | for iy,ix in np.ndindex(pg.shape[:2]): 221 | #if (ix, iy) == (3,2): break 222 | x = ix * rpg[1] 223 | y = iy * rpg[0] 224 | y0,x0 = pa_coords = pg[iy,ix] #get coords of patch in example texture 225 | 226 | #TODO: searching new patches based on the already existing image also 227 | # helps when having all sorts of constraints 228 | #TODO: maybe pass a "constraint-error-function" for the search function? 229 | #TODO: get rid of resolution parameters to make dynamic sized patches 230 | # possible --> this doesn#t work with a "grid" function 231 | if all(pa_coords == (-1,-1)): # --> a part of the grid that is not assigned 232 | pa = np.zeros((res_patch[0],res_patch[1],4)) 233 | pa[:,:,:]=(0, 0.31, 0.22, 1) #TODO: make different fill colors possible 234 | 235 | #get corresponding overlaps: 236 | if iy==0: ovs=(overlap[1],0,0,0) #first row 237 | elif ix==0: ovs=(0,overlap[0],0,0) #first column 238 | else: ovs=(overlap[1],overlap[0],0,0) #rest of the grid 239 | 240 | if (iy==0 and ix==0) or (not use_quilting): 241 | pa = example[y0:y0+res_patch[0],x0:x0+res_patch[1]].copy() 242 | mask = onemask 243 | else: 244 | ta_patch = target[y:y+res_patch[0],x:x+res_patch[1]] 245 | pa, _, _, mask = optimal_patch(ta_patch, example, 246 | res_patch, ovs, (y0,x0), (y,x)) 247 | 248 | #skimage.io.imshow_collection([pa, ov_h[0], b_h, ov_v[0], b_v]) 249 | copy_img(target, pa, (x,y)) 250 | #print((ix,iy),pg[iy,ix]) 251 | 252 | ta_map_patch = target_map_patch_base + (x0,y0,0) 253 | #TODO: find a better method how to use "partial" coordinate transfer 254 | #or in other words: "mixes" which appear for example at smoothed 255 | #out and blended optimal borders 256 | copy_img(ta_map, ta_map_patch, (x,y), mask=mask>0) 257 | #copy_img(ta_map, ta_map_patch, (x,y)) 258 | 259 | 260 | return target, ta_map 261 | 262 | def overlap_slices(overlap): 263 | """define slices for overlaps""" 264 | ovs = [np.s_[:,:overlap[0]],#left 265 | np.s_[:overlap[1],:],#top 266 | np.s_[:,-overlap[2]:],#right 267 | np.s_[-overlap[3]:,:]]#bottom 268 | return ovs 269 | 270 | def create_optimal_patch(pa, ta, overlaps): 271 | """ 272 | this function creates an optimal patch out of 1 tile with 273 | given number of border tiles where two overlapping regions 274 | are replaced by an optimal boundary from the other patches 275 | """ 276 | mask = np.ones(pa.shape[:2]) 277 | ovs = overlap_slices(overlaps) 278 | for ov, orient, sl, mirrored in zip(overlaps,["h","v","h","v"], ovs, 279 | [False,False,True,True]): 280 | if ov>0: #if this boundary is used 281 | m = minimum_error_boundary_cut((pa[sl],ta[sl]), orient) 282 | if mirrored: m = 1-m 283 | mask[sl] = np.minimum(m,mask[sl]) 284 | 285 | new_pa = mask_blend(mask, ta, pa) 286 | #TODO: only return mask 287 | return new_pa, mask 288 | 289 | def optimal_patch(ta_patch, example, res_patch, overlap, pos_ex, pos_ta): 290 | """ 291 | this creates optimal patches for reacangular patches with overlaps 292 | given in "overlap" with indics as follows: 293 | 2 294 | ----- 295 | 1| |3 296 | ----- 297 | 4 298 | 299 | the number of in "overlap" specifies the size of the overlap in pixels 300 | TODO: expand to arbitrarily sized boundaries 301 | TODO: make it possible to "move" the source patch a couple pixels 302 | to find a better fit 303 | """ 304 | #TODO use np.s_ as indices 305 | y,x = pos_ta 306 | y0,x0 = pos_ex 307 | pa = example[y0:y0+res_patch[0],x0:x0+res_patch[1]].copy() 308 | optimpatch, mask = create_optimal_patch(pa, ta_patch, overlap) 309 | return optimpatch, pa, ta_patch, mask 310 | 311 | def find_match(data, index, tol = 0.1, k=5): 312 | #get a horizonal overlap match 313 | e,idx = query_index(index,data,k) 314 | #e,idx = query_index(index,data,k) 315 | #TODO: keep an index with "weights" to make sure 316 | # that patches are selected equally oftern OR according 317 | # to a predefined distribution 318 | # TODO: make sure patches can get selected with an additional error-term 319 | # TODO: for very large textures it might make sense 320 | # to only query a close radius 321 | # TODO: implement theshold function 322 | #or choose ithin a certain tolerance 323 | if k>1: 324 | min_err = e[e>0].min() #find minimum error except for the exact matching pic 325 | #i2 = err.argmin() 326 | th = min_err * (1+tol) #threshold 327 | idx = [i for i,e in zip(idx,e) if e1: print(f"{len(idx)},",) 332 | 333 | @timing 334 | def synthesize_grid(example, res_patch, res_grid, overlap, tol = 0.1, k=5): 335 | """ 336 | synthesize a grid of patch indices 337 | by searching match pairs & triples in the 338 | overlaps database 339 | 340 | tolerance -> this basically controls the randomness of 341 | the algorithm 342 | """ 343 | 344 | res_patch = np.array(res_patch).astype(int) 345 | res_ex = example.shape[:2] 346 | #TODO: decrease overlap to mitigate memory_problems 347 | max_co = res_ex - res_patch 348 | ch = example.shape[-1] 349 | cellsize = example.itemsize 350 | #TODO: replace by a better KD Tree implementation or a Ball Tree 351 | cellsize = 8 #bytes (this is because of scikit-learns KDTree implementation 352 | # which will convert the supplied data into a float 353 | # representation) 354 | #ex_img_ratio = res_ex[0]/res_ex[1] 355 | 356 | #calculate maximum possible patch size for given number of patches and 357 | rp = res_patch 358 | pnum = max_co.prod() 359 | 360 | mem_limit = get_mem_limit() 361 | #check memory consumption for overlap database: 362 | #TODO: check memory consumption instructions from sklearn webpage 363 | #single_overlap_memory_horizontal = \ 364 | # overlap[0] * res_patch[1] * ch * cellsize #in byte 365 | #single_overlap_memory_vertical = \ 366 | # overlap[1] * res_patch[0] * ch * cellsize #in byte 367 | 368 | #factor 2 comes in here, because we have a 3rd database with the combined overlaps 369 | #totalmemoryGB = pnum * GB \ 370 | # * (2 * single_overlap_memory_vertical + 2 * single_overlap_memory_horizontal) 371 | #augmentation_multiplicator = 2 #(for mirroring, and rotating, the data gets 372 | #) multiplied 373 | data_memoryGB = check_memory_requirements(example,res_patch, maxmem=mem_limit, 374 | disable_safety_check=True) 375 | logger.info(f"using approx. {3*data_memoryGB:2f} GB in RAM.") 376 | 377 | #TODO: build my own search algorithm which doesn't consume as much memory 378 | # and can find things in an image much quicker 379 | #TODO: the "overlaps" are the same for left & right and top & bottom 380 | # patch generation can be completly left out as they can be 381 | # taken directly from the texture. For the search, only the 382 | # overlaps are important. 383 | #TODO: maybe save the coordinates of the overlaps in a second database 384 | #TODO: augmented patches(mirrored, ) 385 | #TODO: check memory consumption of patch overlaps 386 | #TODO: if memory consumption reaches a certain thresold, lower resolution 387 | # of overlap databases (this can be done in multiple ways: 388 | # - only take patches from every nth pixel, 389 | # - take overlaps with 1/k the resolution of the original in the database) 390 | # - use smaller patch sizes 391 | # - lower the reoslution of the original image but later keep the 392 | # original rsolution when stitching it back together 393 | # - take a random sample of patches from the original source image 394 | # TODO: add patch augmentation mirroring, rotaton 395 | # the problem here is: when augmenting horiz. or vert. 396 | # images, the "combined" would becom very large 397 | # basically squared. so if we mirror vert. overlaps, 398 | # only horizontally, we also have twice as many combined. 399 | # If we mirror vertically & horozontally we have 3 times as 400 | # many (all data). 401 | # If we also mirror horiz. overlaps in th same way we have 402 | # 3x3 = 9x as much data in combined and 3+3=6 times as much in 403 | # vert. or horiz. 404 | # we can find out whether something is mirrored, rotated or 405 | # whatever by analyzing the index module by the number of 406 | # augentations 407 | #lm = [] 408 | try: 409 | logger.info("init kdtree1") 410 | ld = create_patch_data(example, (rp[0], overlap[1]), max_co) 411 | l = init_ann_index(ld) 412 | logger.info("init kdtree2") 413 | td = create_patch_data(example, (overlap[0],rp[1]), max_co) 414 | t = init_ann_index(td) 415 | logger.info("init kdtree3") 416 | lt = init_ann_index(np.hstack((ld,td))) 417 | #TODO: check memory consumption of KDTrees 418 | #sklearn.neighbors.KDTree.valid_metrics 419 | #ov_db = [sklearn.neighbors.KDTree(i, metric='euclidean') for i in (ov_l, ov_t, ov_lt)] 420 | logger.info("KD-Tree initialization done") 421 | except MemoryError as err: 422 | logger.info(err) 423 | logger.info("example texture too large, algorithm needs " 424 | "too much RAM: {totalmemoryGB:.2f}GB") 425 | raise 426 | 427 | pg = np.full(shape=(*res_grid,2), fill_value=-1, dtype=int) 428 | pg[0,0] = idx2co(random.randrange(pnum), max_co) #get first patch 429 | #pg[0,0]=(0,0) 430 | 431 | #TODO: synthesize an arbitrary pic with the overlap database generated from example 432 | # but using a different source texture to "mix" two textures 433 | 434 | for i in tqdm(range(1,res_grid[1]),"find matches: first row"): 435 | y,x = pg[0,i-1] 436 | #patch = example[y:y+rp[1],x:x+rp[0]] 437 | #((patches[idx] - patch)**2).sum() == 0 #they have to be the same! 438 | ovl = example[y:y+rp[0],x:x+rp[1]][:,-overlap[0]:].flatten() 439 | #ov_idx = idx + (res_patch[0] - overlap[0])*max_co[1]#move towards the right by res_grid 440 | #(ovl-ov_l[ov_idx]) 441 | new_idx = find_match(ovl, l , tol=tol, k=k) 442 | #if new_idx 443 | pg[0,i] = idx2co(new_idx, max_co) 444 | 445 | for i in tqdm(range(1,res_grid[0]),"find matches: first column"): 446 | y,x = pg[i-1,0] 447 | ovt = example[y:y+rp[1],x:x+rp[0]][-overlap[1]:,:].flatten() 448 | pg[i,0] = idx2co(find_match(ovt, t, tol=tol, k=k), max_co) 449 | 450 | for ix,iy in tqdm(itertools.product(range(1,res_grid[1]), 451 | range(1,res_grid[0])), 452 | total = (res_grid[1]-1)*(res_grid[0]-1), 453 | desc = "find matches: complete grid"): 454 | y,x = pg[iy,ix-1] 455 | ovl = example[y:y+rp[0],x:x+rp[1]][:,-overlap[1]:].flatten() 456 | y,x = pg[iy-1,ix] 457 | ovt = example[y:y+rp[0],x:x+rp[1]][-overlap[0]:,:].flatten() 458 | ovlt = np.hstack((ovl, ovt)) 459 | pg[iy,ix] = idx2co(find_match(ovlt, lt, tol = tol, k=k), max_co) 460 | 461 | return pg 462 | 463 | def minimum_error_boundary_cut(overlaps, direction): 464 | """ 465 | create an optimal boundary cut from 466 | an error matrix calculated from overlaps 467 | """ 468 | #TODO: create minimum boundary for very small overlaps (for example just 1 pixel) 469 | ol1, ol2 = overlaps 470 | #calculate error and convert to grayscale 471 | err = ((ol1 - ol2)**2).mean(2) 472 | 473 | if direction == "v": err = err.T 474 | minIndex = [] 475 | E = [list(err[0])] 476 | for i in range(1, err.shape[0]): 477 | # Get min values and args, -1 = left, 0 = middle, 1 = right 478 | e = [np.inf] + E[-1] + [np.inf] 479 | e = np.array([e[:-2], e[1:-1], e[2:]]) 480 | # Get minIndex 481 | minArr = e.min(0) 482 | minArg = e.argmin(0) - 1 483 | minIndex.append(minArg) 484 | # Set Eij = e_ij + min_ 485 | Eij = err[i] + minArr 486 | E.append(list(Eij)) 487 | 488 | # Check the last element and backtrack to find path 489 | path = [] 490 | minArg = np.argmin(E[-1]) 491 | path.append(minArg) 492 | 493 | # Backtrack to min path 494 | for idx in minIndex[::-1]: 495 | minArg = minArg + idx[minArg] 496 | path.append(minArg) 497 | 498 | # Reverse to find full path 499 | path = path[::-1] 500 | m = np.zeros(err.shape, dtype=float)#define mask 501 | for i,pi in enumerate(path): 502 | #p1[i,-overlap[0] + pi,0]*=0.5 503 | m[i,pi:]=True 504 | m[i,pi]=0.5 #set a "smooth" boundary 505 | #err[i,pi]+=0.05 506 | 507 | if direction=="v": return m.T 508 | else: return m 509 | 510 | def create_patch_data(example, res_patch, max_co=None): 511 | """max_co is needed in the case where only overlap 512 | areas of patches are of interest. In this case we want 513 | the overlap areas to not extend beyond the area of 514 | a contiguos patch 515 | """ 516 | if max_co is None: max_co = np.array(example.shape[:2]) - res_patch 517 | rp = res_patch 518 | data = np.ascontiguousarray([example[y:y+rp[0],x:x+rp[1]].flatten() 519 | for y,x in tqdm(np.ndindex(*max_co), "create_patch_data")]) 520 | return data 521 | 522 | def idx2co(idx, max_co): 523 | yp = int(idx/max_co[1]) 524 | xp = idx - yp * max_co[1] 525 | return yp,xp 526 | 527 | #create patches 528 | def gen_patches(image, res_patch): 529 | res_img = np.array(image.shape[:2]) 530 | max_co = res_img - res_patch 531 | patches = np.array([image[y:y+res_patch[0],x:x+res_patch[1]] 532 | for y,x in np.ndindex(*max_co)]) 533 | return patches 534 | 535 | #create patches 536 | def gen_patches_from_mask(image, mask): 537 | res_img = np.array(image.shape[:2]) 538 | m_res = mask.shape[:2] 539 | max_co = res_img - m_res + (1,1) 540 | patches = np.array([image[y:y+m_res[0],x:x+m_res[1]][mask].flatten() 541 | for y,x in np.ndindex(*max_co)]) 542 | def idx2co(idx): 543 | yp = int(idx/max_co[1]) 544 | xp = idx - yp * max_co[1] 545 | return xp,yp 546 | 547 | return patches, max_co, idx2co 548 | 549 | #TODO: checkout the quality of the results for small image resolutions. 550 | # - it might make sense toreduce the resolution for images based 551 | # on certain criteria (for example patch size) anyways 552 | def build_gaussian_pyramid(example0, min_res=8): 553 | #build a gaussian image pyramid 554 | py = [im for im in skimage.transform.pyramid_gaussian(example0, multichannel=True)] 555 | #filter for minimum resolution 556 | #min_res = 8 557 | #py = [im for im in py if min(im.shape[:2]) >= min_res] 558 | return list(reversed(py)) 559 | 560 | 561 | def create_mask_tree(img, kind="causal5x3"): 562 | if kind == "causal5x3": #generate causal mask 563 | mask_res = (3,5) 564 | mask = np.ones(mask_res, dtype=bool) 565 | mask[2,2:]=False 566 | mask_center = (2,2) #starting index from 0 567 | elif kind == "noncausal5x5": #generate non-causal mask: 568 | mask_res = (5,5) 569 | mask = np.ones(mask_res, dtype=bool) 570 | mask_center = (2,2) #starting index from 0 571 | mask[mask_center]=False 572 | elif kind == "noncausal3x3": #generate non-causal mask: 573 | mask_res = (3,3) 574 | mask = np.ones(mask_res, dtype=bool) 575 | mask_center = (1,1) #starting index from 0 576 | else: raise ValueError(f"kind: {kind} is unknown") 577 | 578 | logger.info("generating patches") 579 | m_patches, max_co, idx2co = gen_patches_from_mask(img,mask) 580 | #build local neighbourdhood KDTree for pyramid level 581 | logger.info("generating tree from patches") 582 | index = init_ann_index(m_patches) 583 | return index, mask, mask_center, idx2co 584 | 585 | 586 | #generate image with noisy borders which later be cut off 587 | def local_neighbourhood_enhance(target, ex, mask, tree, 588 | mask_center, idx2co, initial=False, 589 | seed = 0): 590 | #TODO: make tileable texture by copying the right sections from the 591 | # texture to the other sides 592 | 593 | np.random.seed(seed) 594 | 595 | target_res=np.array(target.shape[:2]) 596 | m_res = np.array(mask.shape) 597 | image_range=((mask_center[0], mask_center[0] + target_res[0]), 598 | (mask_center[1], mask_center[0] + target_res[1])) 599 | mc = mask_center 600 | if initial: 601 | t = np.random.random((target_res[0] + m_res[0]-1, 602 | target_res[1] + m_res[1]-1,4)) 603 | t[...,3] = 1.0 604 | t[image_range[0][0]: image_range[0][1], 605 | image_range[1][0]: image_range[1][1]] = target 606 | else: 607 | t = np.random.random((target_res[0] + m_res[0]-1, 608 | target_res[1] + m_res[1]-1,4)) 609 | #target_tmp[:target_cut[0],:target_cut[1]//2] = target[] 610 | t[...,3] = 1.0 611 | t[image_range[0][0]: image_range[0][1], 612 | image_range[1][0]: image_range[1][1]] = target 613 | 614 | #skimage.io.imshow_collection([target, t]) 615 | #return 616 | 617 | if not initial: t2 = t.copy() 618 | else: t2 = t 619 | coords = itertools.product(range(image_range[0][0], image_range[0][1]), 620 | range(image_range[1][0], image_range[1][1])) 621 | #TODO: do some caching of similar mask queries to speed up the 622 | # process quiet a bit 623 | for y,x in tqdm(coords, "iterating over image", 624 | total = target_res.prod(), 625 | smoothing=0.01): 626 | p = t[y-mc[0]:y-mc[0]+m_res[0],x-mc[1]:x-mc[1]+m_res[1]] 627 | pf = p[mask].flatten() 628 | [[err]], [[idx]] = tree.query([pf], k=1) 629 | xp,yp = idx2co(idx) 630 | pixel = ex[yp+mc[0],xp+mc[1]] 631 | t2[y,x] = pixel 632 | 633 | if False: #if debugging 634 | if (x,y)==(4,4): 635 | #py[-1][yp:yp+3,xp:xp+5] 636 | p2 = ex[-1][yp:yp+m_res[0],xp:xp+m_res[1]].copy() 637 | p2[~mask]=(0,0,0,1) 638 | p3 = np.zeros((*mask.shape,4)) 639 | p3[mask] = m_patches[idx].reshape(-1,4) 640 | #skimage.io.imshow_collection([t]) 641 | skimage.io.imshow_collection([p,target, p2,p3, t]) 642 | break 643 | 644 | 645 | return t2[image_range[0][0]: image_range[0][1], 646 | image_range[1][0]: image_range[1][1]] 647 | 648 | 649 | def mask_synthesize_pixel(level, example, seed, pyramid, 650 | final_res, target = None): 651 | img = pyramid[level] 652 | levelscale = img.shape[:2]/np.array(example.shape[:2]) 653 | target_res = np.rint(final_res*levelscale).astype(int) 654 | if target is not None: 655 | target = skimage.transform.resize(target, target_res, anti_aliasing=False,order=0) 656 | kind = "noncausal5x5" 657 | else: 658 | target = np.zeros((*target_res,4), dtype=float) 659 | target[...,3] = 1.0 660 | kind = "causal5x3" 661 | 662 | 663 | tree, mask, mask_center, idx2co = create_mask_tree(img,kind) 664 | target = local_neighbourhood_enhance(target, img, mask, 665 | tree, mask_center, idx2co, 666 | initial=True, seed=seed) 667 | return target 668 | 669 | 670 | 671 | def pixel_synthesize_texture(final_res, scale = 1/2**3, seed = 15): 672 | #TODO: choose output resolution level with a dedicated (2D-)scale-factor 673 | # or a given resolution parameter (original texture will be 674 | # up/down-scaled) 675 | example = skimage.transform.rescale(example0, scale, preserve_range=False, 676 | multichannel=True, anti_aliasing=True) 677 | py = build_gaussian_pyramid(example) 678 | start_level=5 679 | 680 | tas=[] #save images for debug information 681 | 682 | target = mask_synthesize_pixel(start_level, example, seed, py, final_res) 683 | tas.append(target) 684 | for level in range(start_level+1,len(py)): 685 | logger.info(f"\nstarting next level calculation: {level}\n") 686 | target = mask_synthesize_pixel(level, example, seed, py, final_res, target) 687 | tas.append(target) 688 | 689 | return target, tas 690 | 691 | 692 | 693 | def calc_lib_scaling(res_ex, max_pixels): 694 | ex_pixels = np.prod(res_ex) 695 | px_ratio = ex_pixels/max_pixels 696 | scaling = 1/math.sqrt(px_ratio) 697 | if scaling<1.0: return scaling 698 | else: return 1.0 699 | 700 | def normalize_picture(example0, max_pixels = 256*256): 701 | """ 702 | scale picture down to a maximum number of pixels 703 | """ 704 | scaling = 1.0 705 | res_ex = example0.shape[:2] 706 | if max_pixels < np.prod(res_ex): 707 | #max_pixels basically defines whats possible with the avialable 708 | # memory & CPU power 256x256 has proved to be effective on modern systems 709 | scaling = calc_lib_scaling(res_ex, max_pixels) 710 | logger.info(f"resizing with scaling {scaling}") 711 | example = skimage.transform.rescale(example0, scaling, 712 | #example = skimage.transform.resize(example0, (256,256), 713 | anti_aliasing=True, 714 | multichannel=True, 715 | preserve_range=True)#.astype(np.uint8) 716 | #search_res = example.shape[:2] 717 | else: example = example0 718 | 719 | return example, scaling 720 | 721 | def create_patch_params2(res_ex, scaling, 722 | overlap_ratio, patch_ratio): 723 | """patch_ratio = #size of patches in comparison with original 724 | """ 725 | #TODO: define minimum size for patches 726 | res_patch0 = int(min(res_ex)*patch_ratio) 727 | res_patch0 = np.array([res_patch0]*2) 728 | res_patch = np.round(res_patch0*scaling).astype(int) 729 | overlap = np.ceil(res_patch*overlap_ratio).astype(int) 730 | #res_patch2 = np.round(np.array(res_patch)/scaling).astype(int) 731 | overlap0 = np.ceil(res_patch0*overlap_ratio).astype(int) 732 | 733 | return res_patch, res_patch0, overlap, overlap0 734 | 735 | def create_patch_params(example0, scaling, 736 | overlap_ratio = 1/6, patch_ratio = 0.05): 737 | return create_patch_params2(example0.shape[:2], scaling, 738 | overlap_ratio, patch_ratio) 739 | 740 | #create test-target to fill with mask: 741 | def generate_test_target_with_fill_mask(example): 742 | target = example.copy() 743 | target[...,3]=1.0 744 | 745 | verts = [np.array(((0.1,0.1),(0.4,0.15),(0.41,0.4),(0.2,0.38))), 746 | np.array(((0.5,0.55),(0.85,0.53),(0.8,0.7),(0.51,0.71)))] 747 | masks = [] 748 | pxverts = [] 749 | for v in verts: 750 | pxverts.append(v * target.shape[:2]) 751 | rr,cc = skimage.draw.polygon(*v.T) 752 | mask = np.zeros(target.shape[:2]) 753 | mask[rr,cc]=1.0 754 | target[rr,cc]=(1,0,0,1) 755 | masks.append(mask) 756 | 757 | return target, masks, pxverts 758 | 759 | 760 | def draw_polygon_mask(verts, size): 761 | rr,cc = skimage.draw.polygon(*verts.T) 762 | mask = np.zeros(size) 763 | mask[rr,cc]=1.0 764 | return mask 765 | 766 | def edge_distance(poly, x,y): 767 | d = poly.boundary.distance(shapely.geometry.Point(x,y)) 768 | if poly.contains(shapely.geometry.Point(x,y)): return d 769 | else: return -d 770 | 771 | 772 | def get_poly_levelset(verts, width=10): 773 | poly = shapely.geometry.Polygon(verts) 774 | poly_box = poly.buffer(+width) #add two pixels on the container 775 | bbox = poly_box.bounds 776 | miny, minx, maxy, maxx = bbox_px = np.round(np.array(bbox)).astype(int) 777 | w,h = maxx - minx, maxy-miny 778 | 779 | 780 | bbcoords = itertools.product(range(miny,maxy), range(minx, maxx)) 781 | levelset = np.array([edge_distance(poly,y,x) for y,x in bbcoords]).reshape(h,w) 782 | #normalize levelset: 783 | #levelset = np.maximum(levelset/levelset.max(),0.0) 784 | return levelset, bbox_px 785 | 786 | @timing 787 | def fill_area_with_texture(target, example0, ta_map_final=None, 788 | patch_ratio=0.1, libsize = 128*128, 789 | verts=None, mask = None, bounding_box = None): 790 | if bounding_box is None: 791 | area = shapely.geometry.Polygon(verts) 792 | ov = 1 #overlap 793 | y0,x0,y1,x1 = np.array(area.bounds).astype(int) + (-ov,-ov,ov,ov) 794 | else: 795 | y0,x0,y1,x1 = bounding_box 796 | #print("create levelset") 797 | #levelset, (minx, miny, maxx, maxy) = get_poly_levelset(verts, width=ov) 798 | bbox = target[y0:y1,x0:x1] 799 | #bmask = levelset>0 800 | if mask is None: 801 | mask = draw_polygon_mask(verts,target.shape[:2]) 802 | bmask = mask[y0:y1,x0:x1]>0 803 | else: 804 | bmask = mask 805 | #bmask2 = mask[y0:y1,x0:x1]>0 806 | #area.boundary.buffer(100) 807 | 808 | logger.info("synthesize texture") 809 | fill1, ta_map = synth_patch_tex(bbox, example0, k=1, 810 | patch_ratio=patch_ratio, 811 | libsize=libsize) 812 | copy_img(target, fill1, (x0,y0), bmask) 813 | #ta_map_final = np.full((*target.shape[:2],3),[0,0,0]) 814 | #ta_map_final = np.full([*target.shape[:2],3],0) 815 | if ta_map_final is None: ta_map_final = np.zeros([*target.shape[:2],3]) 816 | #copy_img(ta_map_final, ta_map, (x0,y0), bmask) 817 | copy_img(ta_map_final, ta_map, (x0,y0), bmask) 818 | #TODO: somehow the copy operation doesnt work here 819 | #import ipdb; ipdb.set_trace() # BREAKPOINT 820 | 821 | return target, ta_map_final 822 | 823 | def calculate_memory_consumption(res_ex, res_patch, 824 | ch_num, itemsize): 825 | patch_num=np.product(np.array(res_ex) - res_patch) 826 | #import ipdb; ipdb.set_trace() # BREAKPOINT 827 | data_memoryGB = patch_num*ch_num*itemsize*np.product(res_patch)*GB 828 | #print(f"using approx. {data_memoryGB:2f} GB in RAM.") 829 | return data_memoryGB 830 | 831 | def check_memory_requirements(example, res_patch, maxmem = 1.0, 832 | disable_safety_check=False): 833 | data_memoryGB = calculate_memory_consumption( 834 | np.array(example.shape[:2]),res_patch, 835 | ch_num = example.shape[2], 836 | itemsize = example.itemsize) 837 | logger.info(f"using {data_memoryGB:.4f} of {maxmem} GB for synthesis") 838 | if not disable_safety_check: 839 | if data_memoryGB > maxmem: 840 | raise MemoryError("the algorithm would exceed the " 841 | "maximum amount of Memory: " 842 | f"{data_memoryGB:2f} GB,; max: {maxmem}") 843 | 844 | return data_memoryGB 845 | 846 | @timing 847 | def prepare_tree(example0, lib_size, overlap_ratio, patch_ratio, 848 | mode=None): 849 | example, scaling = normalize_picture(example0, lib_size) 850 | res_patch, res_patch0, overlap, overlap0 = create_patch_params(example0, scaling, 851 | overlap_ratio, 852 | patch_ratio) 853 | max_co = np.array(example.shape[:2]) - res_patch 854 | 855 | data_memoryGB = check_memory_requirements(example,res_patch, 856 | maxmem=get_mem_limit(), 857 | disable_safety_check=True) 858 | logger.info(f"using approx. {data_memoryGB:2f} GB in RAM.") 859 | try: 860 | if mode==None: 861 | data = create_patch_data(example, res_patch, max_co) 862 | index = init_ann_index(data) 863 | else: 864 | if ('both' in mode) or ('horizontal' in mode): 865 | logger.info("init kdtree1") 866 | ld = create_patch_data(example, (res_patch[0], overlap[1]), max_co) 867 | l = init_ann_index(ld) 868 | if ('both' in mode) or ('vertical' in mode): 869 | logger.info("init kdtree2") 870 | td = create_patch_data(example, (overlap[0],res_patch[1]), max_co) 871 | t = init_ann_index(td) 872 | if 'both:': 873 | logger.info("init kdtree3") 874 | lt = init_ann_index(np.hstack((ld,td))) 875 | index = [l,t,lt] 876 | except MemoryError as err: 877 | logger.info(err) 878 | logger.info("example texture too large, algorithm needs " 879 | "too much RAM: {totalmemoryGB:.2f}GB") 880 | raise 881 | return index, res_patch, res_patch0, overlap, overlap0, max_co, scaling 882 | 883 | def gen_coordinate_grid(shape, flatten=False): 884 | x = np.arange(0,shape[1]) 885 | y = np.arange(0,shape[0]) 886 | #get coordinate grid 887 | #TODO: this might be more elegant using np.dstack 888 | grid = np.stack(np.meshgrid(y,x),axis = 2) 889 | if flatten: grid = grid.reshape(-1,2) 890 | return grid 891 | 892 | def gen_coordinate_map(shape): 893 | grid2ch=gen_coordinate_grid(shape) 894 | #TODO: this might be more elegant using np.dstack 895 | grid3ch=np.stack((grid2ch[...,0], 896 | grid2ch[...,1], 897 | np.zeros(shape)),axis=2)#,axis=2) 898 | return grid3ch 899 | 900 | """ 901 | #TODO: high quality optimization 902 | This function doesn't work yet... 903 | 904 | the main problem is that when changing the resolution of textures 905 | only for single patches, we get a different result for that area 906 | then if we would change it for the entire texture at once. The reason 907 | for this are the rounding errors when going to smaller resolution 908 | pictures: some of the pixels of the smaller sized image will include 909 | the values of multiple pixels also from the neighbouring patches. if 910 | we decrease the resolution of only a patch, this does not happen. 911 | 912 | This 913 | makes it impossible to incoporate 914 | @timing 915 | def synth_patch_tex(target0, example0, 916 | lib_size = 10000, 917 | k=1, 918 | patch_ratio=0.1, 919 | overlap_ratio = 1/6, 920 | tol=0.1 921 | ): 922 | ''' 923 | lib_size = 10000 924 | k=1 925 | patch_ratio=0.1 926 | overlap_ratio = 1/6 927 | tol=0.1 928 | ''' 929 | #TODO: merge this function with the "search" and the "optimal patch" 930 | # functionality to make it similar to the "synthesize_tex_patches" function 931 | chan = 3 932 | #target = target.copy() 933 | example0 = example0[...,:chan] 934 | target_map = target0.copy() 935 | (trees, res_patch, 936 | res_patch0, overlap, 937 | overlap0, max_co, 938 | scaling) = prepare_tree(example0, lib_size, overlap_ratio, patch_ratio, 939 | mode=["horizontal","vertical","both"]) 940 | logger.info(f"patch_size: {res_patch0}; initial scaling: {scaling}, ") 941 | 942 | #define search grid 943 | res_target0 = target0.shape[:2] 944 | rpg0 = np.array(res_patch0) - overlap0 945 | rpg = np.array(res_patch) - overlap 946 | res_grid0 = np.ceil((res_target0-overlap0)/rpg0).astype(int) 947 | 948 | co_map_base = gen_coordinate_map(res_patch0) 949 | 950 | #resize target to the same scale as the scaled example 951 | #this is actually important and can NOT be done "on the fly" for 952 | #each 953 | target = skimage.transform.rescale(target0, scaling, 954 | anti_aliasing=True, 955 | multichannel=True, 956 | preserve_range=True)#.astype(np.uint8) 957 | 958 | example = skimage.transform.rescale(example0, scaling, 959 | anti_aliasing=True, 960 | multichannel=True, 961 | preserve_range=True)#.astype(np.uint8) 962 | 963 | #left corner (0,0) 964 | for coords in tqdm(np.ndindex(*res_grid0),"iterate over image"): 965 | yp,xp=np.array(coords) * rpg 966 | search_area0 = target[yp:yp+res_patch[0], 967 | xp:xp+res_patch[1]].copy() 968 | if coords==(0,0): 969 | pa_coords_idx = pa_y,pa_x = np.array(idx2co( 970 | random.randrange(np.product(max_co)), 971 | max_co)) #get first patch 972 | 973 | #skimage.io.imshow_collection([patch, co_map/(*example.shape[:2],1)]) 974 | elif coords[0]==0: #first row 975 | ovl = search_area0[:,:overlap[1]] 976 | #ovl = skimage.transform.resize(ovl, 977 | # (res_patch[0],overlap[1]), 978 | # preserve_range=True) 979 | pa_idx = find_match(ovl.flatten(), trees[0] , tol=tol, k=k) 980 | pa_coords_idx = pa_y,pa_x = np.array(idx2co(pa_idx, max_co)) 981 | 982 | pa = example[pa_y:pa_y+res_patch[0],pa_x:pa_x+res_patch[1]] 983 | copy_img(target,pa,(xp,yp)) 984 | pa_y0,pa_x0 = np.round(pa_coords_idx / scaling).astype(int) 985 | pa0 = example0[pa_y0:pa_y0+res_patch0[0],pa_x0:pa_x0+res_patch0[1]] 986 | copy_img(target0,pa0,(xp,yp)) 987 | co_map = co_map_base + (pa_y0,pa_x0,0) 988 | copy_img(target_map,co_map,(xp,yp)) 989 | 990 | #ovl = example[y:y+rp[0],x:x+rp[1]][:,-overlap[0]:].flatten() 991 | 992 | #if False: 993 | if coords==(0,10): 994 | #patch_idx = trees[0].get_arrays()[0][pa_idx].reshape(res_patch[0],-1,3) 995 | patch_idx = trees[0]._raw_data[pa_idx].reshape(res_patch[0],-1,3) 996 | skimage.io.imshow_collection([ovl,search_area0, pa0, patch_idx]) 997 | skimage.io.imshow_collection([pa0, target_map/(*example0.shape[:2],1), target]) 998 | break 999 | 1000 | return target_map, target 1001 | """ 1002 | 1003 | 1004 | def synth_patch_tex(target, example0, k=1, patch_ratio=0.1, libsize = 256*256): 1005 | #TODO: merge this function with the "search" and the "optimal patch" 1006 | # functionality to make it similar to the "synthesize_tex_patches" function 1007 | example, scaling = normalize_picture(example0, libsize) 1008 | res_target = target.shape[:2] 1009 | res_patch, res_patch2, overlap, overlap2 = create_patch_params(example0, scaling, 1010 | patch_ratio=patch_ratio) 1011 | res_grid = np.ceil(res_target/(res_patch2 - overlap2)).astype(int) 1012 | 1013 | logger.info(f"patch_size: {res_patch2}; initial scaling: {scaling}, ") 1014 | 1015 | #time.sleep(10.0) 1016 | 1017 | pg = synthesize_grid(example, res_patch, res_grid, overlap, tol=.1, k=k) 1018 | 1019 | #transform pg coordinates into original source texture 1020 | pg2 = np.round(pg / scaling).astype(int) 1021 | pgimage = (pg2/example0.shape[:2])[...,::-1] 1022 | pgimage = np.dstack((pgimage,np.zeros(pgimage.shape[:2]))) 1023 | 1024 | target, ta_map = transform_patch_grid_to_tex(None, res_patch2, pg2, example0, 1025 | overlap2, 1026 | use_quilting=True) 1027 | 1028 | #target2, ta_map = transform_patch_grid_to_tex(None, res_patch, pg, example, 1029 | # overlap, 1030 | # use_quilting=True) 1031 | 1032 | return (target[:res_target[0],:res_target[1]], 1033 | ta_map[:res_target[0],:res_target[1]]) 1034 | 1035 | 1036 | 1037 | @timing 1038 | def synthesize_tex_patches(target0, example0, 1039 | lib_size = 200*200, 1040 | patch_ratio = 0.07, 1041 | overlap_ratio = 1/3): 1042 | """ 1043 | TODO: make target texture use the already synthesized patch as an option 1044 | --> this means to take a patch from the high-res image and scale it down 1045 | "on the fly". This way can avoid creating a scaled down version of the 1046 | target. It also makes the produced patch more "precise" as we are not 1047 | restricted to the target-pixels anymore 1048 | """ 1049 | 1050 | target_new = target0.copy() 1051 | (tree, res_patch, 1052 | res_patch0, overlap, 1053 | overlap0, max_co, 1054 | scaling) = prepare_tree(example0, lib_size, overlap_ratio, patch_ratio) 1055 | 1056 | res_target0 = target0.shape[:2] 1057 | rpg0 = np.array(res_patch0) - overlap0 1058 | res_grid0 = np.ceil((res_target0-overlap0)/rpg0).astype(int) 1059 | 1060 | rp = res_patch 1061 | #resize target to the same scale as the scaled example 1062 | #TODO: replace this with a "instant-scale-down" method (have a look 1063 | #at function documentation) 1064 | target = skimage.transform.rescale(target0, scaling, 1065 | anti_aliasing=True, 1066 | multichannel=True, 1067 | preserve_range=True)#.astype(np.uint8) 1068 | 1069 | for coords in tqdm(np.ndindex(*res_grid0),"iterate over image"): 1070 | #TODO: only iterate over "necessary" pixels indicated by a mask 1071 | #to get "whole" patches, we need the last row to have the same 1072 | #border as the target image thats why we use "minimum": 1073 | #print(coords) 1074 | yp0,xp0 = co_target0 = np.minimum(res_target0-res_patch0,np.array(coords) * rpg0) 1075 | yp,xp = np.round(co_target0*scaling).astype(int) 1076 | yp,xp = np.minimum(np.array(target.shape[:2])-res_patch, (yp,xp)) 1077 | 1078 | #TODO: replace with "real" target and scale down. This makes 1079 | # the search more precise as pixel rounding effects can be 1080 | # mitigated this way 1081 | search_area = target[yp:yp+rp[0],xp:xp+rp[1]].copy() 1082 | new_idx = find_match(search_area.flatten(), tree , tol=0.1, k=1) 1083 | #find patch from original image 1084 | co_p = np.array(idx2co(new_idx, max_co)) 1085 | co_p0 = np.round(co_p / scaling).astype(int) 1086 | ovs = np.r_[overlap0,0,0]#,overlap0] 1087 | pa, pa0, ta0, mask = optimal_patch(target_new, example0, res_patch0, 1088 | ovs, co_p0, co_target0) 1089 | copy_img(target_new, pa, co_target0[::-1]) 1090 | 1091 | return target_new 1092 | 1093 | def calculate_face_normal(f): 1094 | v1,v2 = f[1]-f[0], f[2]-f[1] 1095 | n = sign(np.cross(v1,v2)) 1096 | return n 1097 | 1098 | #@timing 1099 | def transfer_patch_pixelwise(target, search_area0, 1100 | yp,xp, 1101 | edge_info, 1102 | fromtarget, 1103 | face_source, 1104 | ta_map = None, 1105 | sub_pixels = 1, 1106 | mask = None, 1107 | pa = None, 1108 | ta_pa = None, 1109 | tol=0.0): 1110 | """This function takes an edge and pixel, and searches for the corresponding 1111 | pixel at another edge. 1112 | 1113 | TODO: cythonize or do other stuff with this function (nuitka, scipy, numba) 1114 | """ 1115 | e1,e2,v1,v2,e2_perp_left = edge_info 1116 | """ 1117 | TODO: the below version is still present, as it might 1118 | be preferable to do a loop-based version using cython 1119 | for patch_index in np.ndindex(search_area0.shape[:2]): 1120 | for sub_pix in np.ndindex(sub_pixels,sub_pixels): 1121 | sub_idx = np.array(patch_index) + np.array(sub_pix)/sub_pixels 1122 | coords = np.array((yp,xp)) + sub_idx - e1[0] 1123 | c = coords.dot(v1)/(v1.dot(v1))#orthogonal projection of pixel on edge 1124 | d = c*v1-coords #orthogonal distance from edge 1125 | #pixel on left or right side of edge? 1126 | isleft = (v1[1]*d[0]-d[1]*v1[0])>0 1127 | d_len = norm(d) 1128 | if isleft or (d_len0: 1140 | #copy pixel from generated patch back to target 1141 | target[tuple(tmp)] = pa[patch_index]""" 1142 | 1143 | y1,x1 = search_area0.shape[:2] 1144 | x = np.arange(0,x1,1.0/sub_pixels) 1145 | y = np.arange(0,y1,1.0/sub_pixels) 1146 | #get coordinate grid 1147 | sub_pix = np.stack(np.meshgrid(y,x),axis = 2).reshape(-1,2) 1148 | #vector from uv edge to sub_pixel in patch with corner yp,xp 1149 | coords_matrix = (yp,xp) + sub_pix - e1[0] 1150 | 1151 | 1152 | proj = proj = coords_matrix.dot(v1)/(v1.dot(v1)) #orthogonal projection of pixels on edge 1153 | orth_d = np.outer(proj,v1)-coords_matrix #orthogonal distance vector from edge 1154 | 1155 | isleft = (v1[1]*orth_d[:,0]-orth_d[:,1]*v1[0])>0 1156 | d_len = norm(orth_d)#distance of pixel from edge 1157 | 1158 | #calculate corresponding pixel at other edge in reverse direction 1159 | # (this is why we use (1-proj)) 1160 | p_e2s = np.outer((1-proj),(v2)) + e2[0] #projecton on corresponding edge 1161 | #import ipdb; ipdb.set_trace() # BREAKPOINT 1162 | px2_cos = p_e2s + (np.outer(d_len,e2_perp_left).T*(isleft * 2 - 1)).T#orthogonal pixel from that projection point on the edge 1163 | px2_cos = np.round(px2_cos).astype(int) 1164 | 1165 | #choose to transfer pixels from the left soude + a little overlap 1166 | #transfer_pixels = (isleft | (d_len0: 1190 | #copy pixel from generated patch back to target 1191 | #target[tuple(px2_coords)][:3] = pa[patch_index][:3]*mask[patch_index] 1192 | target[tuple(px2_coords)] = pa[patch_index] 1193 | if ta_map is not None: 1194 | ta_map[tuple(px2_coords)] = ta_pa[patch_index] 1195 | 1196 | def check_inside_convex_quadrilateral(corners, p, tol=0): 1197 | """be careful!, because this function is based 1198 | on whether the coordinate system is left- or right-handed""" 1199 | #TODO: vectorize this function or implement some cython-magic 1200 | sv = np.roll(corners, 1, 0) - corners 1201 | pv = p-corners 1202 | 1203 | #normalized normal vetors on all edges: 1204 | n = normalized(sv[:,::-1]*(1,-1)) 1205 | #orthogonal distance for all edges 1206 | orth_d = (pv*n).sum(axis=1) 1207 | #calculate distance from all edges 1208 | 1209 | return np.all(orth_d < tol) 1210 | # else: 1211 | # #check if point lies on left side of every side vector 1212 | #return np.all(sv[:,0] * pv[:,1] - sv[:,1] * pv[:,0] >= 0) # > 0 1213 | #return True 1214 | 1215 | #cache this function as it will be very similar for many points in the 1216 | #polygon. The function cache should be reset when the algorithm gets rerun though 1217 | def check_inside_face(polygon, point, tol=0.0): 1218 | face = shapely.geometry.Polygon(polygon).buffer(tol) 1219 | return face.contains(shapely.geometry.Point(*point)) 1220 | 1221 | @timing 1222 | def make_seamless_edge(edge1,edge2, target, example0, ta_map, 1223 | patch_ratio, 1224 | lib_size, debug_level=0, 1225 | tree_info = None): 1226 | #TODO: make sure that the "longer" edge is defined as 1227 | #"e1" --> this is so that we don't have huge undefined patch_sizes 1228 | #in the final result. Additionally, this make sure that we always have a 1229 | #"high-resolution"-edge where the patchesa re used. 1230 | 1231 | 1232 | (e1,verts1),(e2,verts2) = edge1,edge2 1233 | v1 = e1[1]-e1[0] 1234 | v2 = e2[1]-e2[0] 1235 | if norm(v1)0: #for debugging 1320 | search_area0[:,:,0:2]=(1,1) 1321 | pa0_r = pa0.copy() 1322 | pa0[:,:,0]*=0.5 #green -> "inside" 1323 | pa[:,:,1]*=0.5 #red -> "outside" 1324 | 1325 | #copy one side of the patch back to its respective face 1326 | #and also create a left/rght mask for masking the second part 1327 | #import ipdb; ipdb.set_trace() # BREAKPOINT 1328 | #print("copy back!") 1329 | transfer_patch_pixelwise(target_new, search_area0, 1330 | yp,xp, 1331 | edge_info = (e1,e2,v1,v2,e2_perp_left), 1332 | fromtarget = False, 1333 | face_source = verts2, 1334 | sub_pixels = 2, 1335 | mask = mask, 1336 | pa = pa, 1337 | ta_pa = ta_pa, 1338 | ta_map = ta_map, 1339 | tol=tol) 1340 | 1341 | 1342 | #TODO: copy only the part thats "inside" face 1 1343 | mask_inside = np.zeros(search_area0.shape[:2]) 1344 | for patch_index in np.ndindex(search_area0.shape[:2]): 1345 | coords = patch_index + np.array((yp,xp)) 1346 | mask_inside[patch_index] = check_inside_convex_quadrilateral(verts1,coords, tol=tol) 1347 | 1348 | #copy only the right side to its place 1349 | #mask_right_optimal = mask_sides==0 1350 | #mask_right_optimal = np.minimum(mask_sides==0, mask>0) 1351 | mask_right_optimal = np.minimum(mask>0, mask_inside>0) 1352 | copy_img(target_new, pa0, (xp,yp), mask_right_optimal) 1353 | 1354 | copy_img(ta_map, ta_pa, (xp,yp), mask_right_optimal) 1355 | #copy_img(target_new.copy(),) 1356 | 1357 | #if counter == 2: 1358 | if False:#debug_level>0:#counter == 2: 1359 | #patch_from_data = data[new_idx].reshape(*res_patch,4) 1360 | skimage.io.imshow_collection([search_area0, pa, mask, pa0, mask_inside]) 1361 | #skimage.io.imshow_collection([search_area0, search_area, target1, 1362 | # patch_from_data, pa,pa0,ta0]) 1363 | #break 1364 | 1365 | #import ipdb; ipdb.set_trace() # BREAKPOINT 1366 | return (target_new[res_patch0[0]:-res_patch0[0], 1367 | res_patch0[1]:-res_patch0[1]], 1368 | ta_map[res_patch0[0]:-res_patch0[0], 1369 | res_patch0[1]:-res_patch0[1]], 1370 | tree_info) 1371 | 1372 | if __name__ == "__main__": 1373 | logging.basicConfig(level=logging.INFO) 1374 | logger.setLevel(logging.INFO) 1375 | logging.getLogger('tex_synthesize').setLevel(logging.INFO) 1376 | #example0 = example = skimage.io.imread("textures/3.gif") #load example texture 1377 | example0 = skimage.io.imread("textures/rpitex.png") 1378 | example0 = example0/255 1379 | #example0 = skimage.transform.resize(example0, (500,1000)) 1380 | example0 = skimage.transform.rescale(example0, 0.3, multichannel=True) 1381 | #example0 = example = skimage.io.imread("RASP_03_05.png") #load example texture 1382 | #TODO: more sophisticated memory reduction techniques (such as 1383 | # a custom KDTree) This KDTree could be based on different, hierarchical 1384 | # image resolutions for the error-search 1385 | 1386 | #TODO: test "overhaul" of images with correspondance maps 1387 | 1388 | 1389 | #test image synthesis function 1390 | if True: 1391 | seed = 32 1392 | np.random.seed(seed) 1393 | random.seed(seed)#2992)#25 is chip + original img 1394 | 1395 | channels = 3 #TODO: prepare function for 1,3 and 4 channels 1396 | target0 = np.full((800,1000,channels),0.0) 1397 | target, ta_map = synth_patch_tex(target0,example0, 1398 | libsize=256*256, 1399 | patch_ratio=0.1) 1400 | ta_map = ta_map/np.array([*example0.shape[:2][::-1],1]) 1401 | skimage.io.imshow_collection([target, ta_map]) 1402 | 1403 | if False: 1404 | np.random.seed(10) 1405 | random.seed(50)#2992)#25 is chip + original img 1406 | target1, mask, verts = generate_test_target_with_fill_mask(example0) 1407 | skimage.io.imshow_collection([target1,mask]) 1408 | target1[:]=(0,0.5,0,1) 1409 | for v in verts: 1410 | y0,x0,y1,x1 = np.array(shapely.geometry.Polygon(v).bounds).astype(int) 1411 | #target1[y0:y1,x0:x1]*=(0.5,0.5,0.5,1)#mark bounding box for debugging 1412 | target1, fill1, fill2, pgimg, bmask = fill_area_with_texture(target1, example0, v) 1413 | 1414 | check_inside_face(verts[0],(55,100)) 1415 | 1416 | 1417 | #select two corresponding edges: 1418 | edges = ((verts[0][:2],verts[1][:2]), 1419 | (verts[0][1:3],verts[1][1:3])) 1420 | 1421 | target_new = target1 1422 | for e1,e2 in edges[:1]: 1423 | logger.info("alter next edge") 1424 | target_new = make_seamless_edge((e1,verts[0]),(e2,verts[1]), 1425 | target_new, example0) 1426 | 1427 | skimage.io.imshow_collection([target_new, target1]) 1428 | 1429 | #test image copying with "wrong" boundaries 1430 | if False: 1431 | tmp = np.full((100,100,4),(1.0,0,0,1)) 1432 | search_area0 = np.random.random((28,28,4)) 1433 | pos = (5,10) 1434 | copy_img(target=search_area0,src=tmp,pos=pos) 1435 | skimage.io.imshow_collection([search_area0,tmp]) 1436 | 1437 | #skimage.io.imshow_collection([target_new]) 1438 | #edge_area = np.array(edgebox.boundary.coords)[:-1] 1439 | #target1[skimage.draw.polygon(*x.boundary.xy)] *= (0.5,0.5,1.0,1.0) 1440 | #skimage.io.imshow_collection([target1]) 1441 | 1442 | #edgebox.buffer(15).bounds 1443 | 1444 | 1445 | #area around edge to get edge straight 1446 | #calculate area around edge: 1447 | 1448 | 1449 | #edge = 10 1450 | #pos = y0-edge,x0-edge 1451 | #target0 = target1[y0-edge:y1+edge,x0-edge:x1+edge].copy() 1452 | 1453 | #skimage.io.imshow_collection([target0]) 1454 | #TODO: cut out the stripes between the two uvs 1455 | 1456 | 1457 | 1458 | #skimage.io.imshow_collection([target0_start, fill1, fill2, pgimg])#,target0]) 1459 | 1460 | #targets=[] 1461 | #targets.append(synthesize_tex_patches(target0, example)) 1462 | #targets.append(synthesize_tex_patches(targets[-1], example,patch_ratio = 0.05)) 1463 | #targets.append(synthesize_tex_patches(targets[-1], example,patch_ratio = 0.04)) 1464 | 1465 | #copy_img(target1, targets[-1], pos[::-1]) 1466 | 1467 | #skimage.io.imshow_collection(targets) 1468 | #skimage.io.imshow_collection([target0, target]) 1469 | #skimage.io.imshow_collection([pgimg]) 1470 | #skimage.io.imshow_collection([target1]) 1471 | #skimage.io.imshow_collection([example]) 1472 | 1473 | #tree, mask, mask_center, idx2co = create_mask_tree(example0,"noncausal5x5") 1474 | 1475 | #skimage.io.imshow_collection([target, target2, pgimg, example0, target1, mask]) 1476 | #skimage.io.imshow_collection([target1, fill1, fill2, pgimg]) 1477 | #skimage.io.imshow_collection([target1]) 1478 | #skimage.io.imshow_collection([example0]) 1479 | #skimage.io.imsave("debug/synth.jpg", target0[...,:3]) 1480 | 1481 | #analyze resulting patchgrid (og): 1482 | #import pandas as pd 1483 | #allcells = pg.view([('f0', pg.dtype), ('f1', pg.dtype)]).squeeze(axis=-1) 1484 | #allcells = pd.DataFrame(np.unique(allcells.flatten(), return_counts=True)).T 1485 | #allcells.sort_values(1) 1486 | #pd.DataFrame.from_items(allcells) 1487 | 1488 | --------------------------------------------------------------------------------