prepare_textures.py 13.6 KB
Newer Older
1 2 3 4 5 6
# -*- coding: utf-8 -*-
"""
Created on Wed Mar 13 22:22:05 2013

@author: tom
"""
7
# The provides/requires mechanism could be improved. 
8
# Currently, RoofManager prepends the "provides" tags with the texture class.
9 10 11 12 13
# Find_matching will match only if "requires" is a subset of "provides".
# That is, there is no OR. All "requires" must be matched 
#
# ideally:
# Texture(rules='building.height > 15 
14
#                AND (roof.colour = black OR roof.colour = gray)
15 16 17
#                AND roof.shape = flat ')


18
import argparse
19
import enum
20 21
import logging
import os
22
import pickle
23
import sys
24
from typing import List
25

Thomas Albrecht's avatar
Thomas Albrecht committed
26
import img2np
27
import numpy as np
Thomas Albrecht's avatar
Thomas Albrecht committed
28
import parameters
29
import utils.utilities as util
30 31
from PIL import Image
from textures import atlas
32
from textures.texture import Texture, FacadeManager, RoofManager, SpecialManager
33

34 35
atlas_file_name = None

36 37
ROOFS_DEFAULT_FILE_NAME = "roofs_default.py"

38 39 40 41
# expose the three managers on module level
roofs = None  # RoofManager
facades = None  # FacadeManager
specials = None  # SpecialManager
42

43 44 45
# Hard-coded constants for the texture atlas. If they are changed, then maybe all sceneries in Terrasync need
# to be recreated -> therefore not configurable. Numbers are in pixels (need to be factor 2).
ATLAS_ROOFS_START = 0
46 47
ATLAS_FACADES_START = 18 * 256
ATLAS_HEIGHT = 64 * 256  # 16384
48
ATLAS_WIDTH = 256
49

50

51 52
def _make_texture_atlas(roofs_list: List[Texture], facades_list: List[Texture], specials_list: List[Texture],
                        atlas_filename: str, ext: str, pad_y: int=0) -> None:
53
    """
54
    Create texture atlas from all textures. Update all our item coordinates.
55
    """
56
    logging.info("Making texture atlas")
57 58

    if (len(facades_list) + len(roofs_list) + len(specials_list)) < 1:
59 60 61
        logging.error('Got an empty texture list. Check installation of tex.src/ folder!')
        sys.exit(-1)

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
    the_atlas = atlas.Atlas(0, 0, ATLAS_WIDTH, ATLAS_HEIGHT, 'Facades')

    _make_per_texture_type(roofs_list, the_atlas, pad_y)
    # reduce the available region based on roof slot
    the_atlas.regions = [atlas.Region(0, ATLAS_FACADES_START, ATLAS_WIDTH, ATLAS_HEIGHT - ATLAS_FACADES_START)]
    _make_per_texture_type(facades_list, the_atlas, pad_y)

    the_atlas.compute_nondim_tex_coords()
    the_atlas.write(atlas_filename + ext, 'im')

    # -- create LM atlas, using the coordinates of the main atlas
    light_map_atlas = atlas.Atlas(0, 0, ATLAS_WIDTH, ATLAS_HEIGHT, 'FacadesLM')
    for tex in roofs_list:
        light_map_atlas.pack_at_coords(tex, tex.ax, tex.ay)
    for tex in facades_list:
        light_map_atlas.pack_at_coords(tex, tex.ax, tex.ay)
    light_map_atlas.write(atlas_filename + '_LM' + ext, 'im_LM')

    for tex in (roofs_list + facades_list + specials_list):
        logging.debug('%s (%4.2f, %4.2f) (%4.2f, %4.2f)' % (tex.filename, tex.x0, tex.y0, tex.x1, tex.y1))
        del tex.im
        del tex.im_LM


def _make_per_texture_type(texture_list: List[Texture], the_atlas: atlas.Atlas, pad_y: int, ) -> None:
87
    keep_aspect = True  # FIXME: False won't work -- im.thumbnail seems to keep aspect no matter what
88 89 90

    next_y = 0

91
    # -- load and rotate images, store image data in RoofManager object
92 93 94
    #    append to either can_repeat or non_repeat list
    can_repeat_list = []
    non_repeat_list = []
95 96 97 98
    for tex in texture_list:
        filename = tex.filename
        tex.im = Image.open(filename)
        logging.debug("name %s size " % filename + str(tex.im.size))
99 100

        # light-map
101
        filename, file_extension = os.path.splitext(tex.filename)
102 103
        filename += '_LM' + file_extension
        try:
104
            tex.im_LM = Image.open(filename)
105
        except IOError:
106 107 108
            # assuming there is no light-map. Put a dark uniform light-map
            the_image = Image.new('RGB', tex.im.size, 'rgb({0},{0},{0})'.format(parameters.TEXTURES_EMPTY_LM_RGB_VALUE))
            tex.im_LM = the_image
109

110 111 112 113
        assert (tex.v_can_repeat + tex.h_can_repeat < 2)
        if tex.v_can_repeat:
            tex.rotated = True
            tex.im = tex.im.transpose(Image.ROTATE_270)
114

115
            tex.im_LM = tex.im_LM.transpose(Image.ROTATE_270)
116

117 118 119 120
        if tex.v_can_repeat or tex.h_can_repeat:
            can_repeat_list.append(tex)
        else:
            non_repeat_list.append(tex)
121

122
    # Work on not repeatable textures
123 124
    for tex in non_repeat_list:
        tex.width_px, tex.height_px = tex.im.size
125

126
        if not the_atlas.pack(tex):
127
            raise ValueError("No more space left and therefore failed to pack: %s" % str(tex))
128 129
    atlas_sy = the_atlas.cur_height()

130 131 132
    # Work on repeatable textures.
    # Scale each to full atlas width
    # Compute total height of repeatable section
133 134
    for tex in can_repeat_list:
        scale_x = 1. * ATLAS_WIDTH / tex.im.size[0]
135 136 137 138
        if keep_aspect:
            scale_y = scale_x
        else:
            scale_y = 1.
139 140
        org_size = tex.im.size

141 142
        nx = int(org_size[0] * scale_x)
        ny = int(org_size[1] * scale_y)
143 144 145 146 147 148
        tex.im = tex.im.resize((nx, ny), Image.ANTIALIAS)
        if tex.im_LM:
            tex.im_LM = tex.im_LM.resize((nx, ny), Image.ANTIALIAS)
        logging.debug("scale:" + str(org_size) + str(tex.im.size))
        atlas_sy += tex.im.size[1] + pad_y
        tex.width_px, tex.height_px = tex.im.size
149

150
    # Bake fake ambient occlusion. Multiply all channels of a facade texture by
151
    # 1. - param.BUILDING_FAKE_AMBIENT_OCCLUSION_VALUE * np.exp(-z / param.BUILDING_FAKE_AMBIENT_OCCLUSION_HEIGHT)
152 153
    #    where z is height above ground.
    # Has to be done after scaling of texture has happened
154
    if parameters.BUILDING_FAKE_AMBIENT_OCCLUSION:
155 156 157
        for tex in texture_list:
            if tex.cls == 'facade':
                R, G, B, A = img2np.img2RGBA(tex.im)
Thomas Albrecht's avatar
Thomas Albrecht committed
158 159
                height_px = R.shape[0]
                # reversed height
160 161 162 163
                Z = np.linspace(tex.v_size_meters, 0, height_px).reshape(height_px, 1)
                fac = 1. - parameters.BUILDING_FAKE_AMBIENT_OCCLUSION_VALUE * np.exp(
                    -Z / parameters.BUILDING_FAKE_AMBIENT_OCCLUSION_HEIGHT)
                tex.im = img2np.RGBA2img(R * fac, G * fac, B * fac)
164 165 166

    # -- paste, compute atlas coords
    #    lower left corner of texture is x0, y0
167 168 169 170 171 172 173
    for tex in can_repeat_list:
        tex.x0 = 0
        tex.x1 = float(tex.width_px) / ATLAS_WIDTH
        tex.y1 = 1 - float(next_y) / atlas_sy
        tex.y0 = 1 - float(next_y + tex.height_px) / atlas_sy
        tex.sy = float(tex.height_px) / atlas_sy
        tex.sx = 1.
174

175 176
        next_y += tex.height_px + pad_y
        if not the_atlas.pack(tex):
177 178
            #logging.debug("No more space left and therefore failed to pack: %s", str(tex))
            raise ValueError("No more space left and therefore failed to pack: %s" % str(tex))
179

180

181
def _check_missed_input_textures(tex_prefix: str, registered_textures: List[Texture]) -> None:
182 183 184 185 186 187 188 189
    """Find all .jpg and .png files in tex.src and compare with registered textures.

    If not found in registered textures, then log a warning"""
    for subdir, dirs, files in os.walk(tex_prefix, topdown=True):
        for filename in files:
            if filename[-4:] in [".jpg", ".png"]:
                if filename[-7:-4] in ["_LM", "_MA"]:
                    continue
190
                my_path = os.path.join(subdir, filename)
191 192 193 194 195 196 197 198 199
                found = False
                for registered in registered_textures:
                    if registered.filename == my_path:
                        found = True
                        break
                if not found:
                    logging.warning("Texture %s has not been registered", my_path)


200
def _append_facades(facade_manager: FacadeManager, tex_prefix: str) -> None:
201 202 203 204 205 206 207 208 209 210
    """Dynamically runs .py files in tex.src and sub-directories to add facades.

    For roofs see add_roofs(roofs)"""
    for subdir, dirs, files in os.walk(tex_prefix, topdown=True):
        for filename in files:
            if filename[-2:] != "py":
                continue
            elif filename == ROOFS_DEFAULT_FILE_NAME:
                continue

211
            my_path = os.path.join(subdir, filename)
212 213
            logging.info("Executing %s ", my_path)
            try:
214
                facade_manager.current_registered_in = my_path
215
                exec(compile(open(my_path).read(), my_path, 'exec'))
216 217 218 219
            except:
                logging.exception("Error while running %s" % filename)


220 221 222 223 224
def _append_roofs(roof_manager: RoofManager, tex_prefix: str) -> None:
    """Dynamically runs the content of a hard-coded file to fill the roofs texture list.

    Argument roof_manager is used dynamically in execfile
    ."""
225
    try:
226
        file_name = os.path.join(tex_prefix, ROOFS_DEFAULT_FILE_NAME)
227
        roof_manager.current_registered_in = file_name
228
        exec(compile(open(file_name).read(), file_name, 'exec'))
229
    except Exception as e:
230
        logging.exception("Unrecoverable error while loading roofs", e)
231 232 233
        sys.exit(1)


234 235 236 237 238 239 240 241 242
def _dump_all_provides_across_textures(texture_list: List[Texture]) -> None:
    provided_features_level_one = set()
    provided_features_level_two = set()
    provided_features_level_three = set()
    provided_features_level_four = set()

    for texture in texture_list:
        for feature in texture.provides:
            parts = feature.split(":")
243
            if parts:
244 245 246 247 248 249 250 251 252 253 254 255 256 257
                provided_features_level_one.add(parts[0])
            if len(parts) > 1:
                provided_features_level_two.add(parts[1])
            if len(parts) > 2:
                provided_features_level_three.add(parts[2])
            if len(parts) > 3:
                provided_features_level_four.add(parts[3])

    logging.debug("1st level provides: %s", provided_features_level_one)
    logging.debug("2nd level provides: %s", provided_features_level_two)
    logging.debug("3rd level provides: %s", provided_features_level_three)
    logging.debug("4th level provides: %s", provided_features_level_four)


258 259 260 261 262 263 264 265 266 267
@enum.unique
class InitMode(enum.IntEnum):
    read = 0
    create = 1  # create new texture atlas by reading from sources
    update = 2  # update existing texture atlas by reading from sources


def init(stats: util.Stats, mode: InitMode=InitMode.read) -> None:
    """Initializes the texture atlas based on the init mode of the process."""
    logging.debug("prepare_textures: init")
268
    global roofs
269 270
    global facades
    global specials
271
    global atlas_file_name
272

273 274
    atlas_file_name = os.path.join("tex", "atlas_facades")
    my_tex_prefix_src = os.path.join(parameters.PATH_TO_OSM2CITY_DATA, 'tex.src')
275
    Texture.tex_prefix = my_tex_prefix_src  # need to set static variable so managers get full path
276

277
    pkl_file_name = os.path.join(parameters.PATH_TO_OSM2CITY_DATA, "tex", "atlas_facades.pkl")
278
    
279
    if mode is InitMode.create:
280
        roofs = RoofManager('roof', stats)
281 282
        facades = FacadeManager('facade', stats)
        specials = SpecialManager('special', stats)
283

284
        # read registrations
285
        _append_roofs(roofs, my_tex_prefix_src)
286
        _append_facades(facades, my_tex_prefix_src)
287
        # FIXME: nothing to do yet for SpecialManager
288 289

        # warn for missed out textures
290
        _check_missed_input_textures(my_tex_prefix_src, roofs.get_list() + facades.get_list())
291 292

        # -- make texture atlas
293 294
        if parameters.ATLAS_SUFFIX:
            atlas_file_name += '_' + parameters.ATLAS_SUFFIX
295

296 297
        _make_texture_atlas(roofs.get_list(), facades.get_list(), specials.get_list(),
                            os.path.join(parameters.PATH_TO_OSM2CITY_DATA, atlas_file_name), '.png')
298
        
299 300
        params = dict()
        params['atlas_file_name'] = atlas_file_name
Thomas Albrecht's avatar
Thomas Albrecht committed
301

302 303 304
        logging.info("Saving %s", pkl_file_name)
        pickle_file = open(pkl_file_name, 'wb')
        pickle.dump(roofs, pickle_file, -1)
305 306
        pickle.dump(facades, pickle_file, -1)
        pickle.dump(specials, pickle_file, -1)
307 308
        pickle.dump(params, pickle_file, -1)
        pickle_file.close()
309 310

        logging.info(str(roofs))
311 312
        logging.info(str(facades))
        logging.info(str(specials))
Thomas Albrecht's avatar
Thomas Albrecht committed
313
    else:
314 315 316
        logging.info("Loading %s", pkl_file_name)
        pickle_file = open(pkl_file_name, 'rb')
        roofs = pickle.load(pickle_file)
317 318
        facades = pickle.load(pickle_file)
        specials = pickle.load(pickle_file)
319
        params = pickle.load(pickle_file)
320
        atlas_file_name = params['atlas_file_name']
321
        pickle_file.close()
322

323 324 325
    stats.textures_total = dict((filename, 0) for filename in map((lambda x: x.filename), roofs.get_list()))
    stats.textures_total.update(dict((filename, 0) for filename in map((lambda x: x.filename), facades.get_list())))
    stats.textures_total.update(dict((filename, 0) for filename in map((lambda x: x.filename), specials.get_list())))
326
    logging.info('Skipped textures: %d', stats.skipped_texture)
327 328 329


if __name__ == "__main__":
330 331 332
    parser = argparse.ArgumentParser(description="texture manager either reads existing texture atlas or creates new")
    parser.add_argument("-f", "--file", dest="filename",
                        help="read parameters from FILE (e.g. params.ini)", metavar="FILE", required=True)
333 334
    parser.add_argument("-l", "--loglevel", dest='loglevel',
                        help="set logging level. Valid levels are DEBUG, INFO (default), WARNING, ERROR, CRITICAL",
335
                        required=False)
336 337
    parser.add_argument("-u", "--update", dest="update", action="store_true",
                        help="update texture atlas instead of creating new", required=False)
338 339
    args = parser.parse_args()

340 341 342 343 344
    log_level = 'INFO'
    if args.loglevel:
        log_level = args.loglevel
    logging.getLogger().setLevel(log_level)

345 346
    if args.filename is not None:
        parameters.read_from_file(args.filename)
347 348 349 350 351
    parameters.show()

    init_mode = InitMode.create
    if args.update:
        init_mode = InitMode.update
352

353
    my_stats = util.Stats()
354
    init(my_stats, init_mode)