import os from PIL import Image, ImageFilter, ImageEnhance, ImageFile from defines import * from layergen import * from log import * from functions import * from xp_normalmap import * ImageFile.LOAD_TRUNCATED_IMAGES = True # ------------------------------------------------------------------- # ORTHOGRAPHIC # Your personal aerial satellite. Always on. At any altitude.* # Developed by MarStrMind # License: Open Software License 3.0 # Up to date version always on marstr.online # ------------------------------------------------------------------- # photogen.py # The class that generates the photo tiles from previous layers, # in their correct order. # ------------------------------------------------------------------- class mstr_photogen: # Initializer doesn't need much def __init__ (self, lat, lng, ty, tx, maxlat, maxlng): self._lat = lat self._lng = lng self._ty = ty self._tx = tx self._maxlatlng = [ maxlat, maxlng ] # Define layer size depending on what is wanted self._imgsize = mstr_photores # Empty image where everything goes into self._tile = Image.new("RGBA", (self._imgsize, self._imgsize)) self._latlngfld = self.latlng_folder([lat,lng]) mstr_msg("photogen", "Photogen initialized") # Defines the order of layer names that were processed # Called by orthographic prior to starting the photo gen process def setLayerNames(self, names): self._lyrnames = names # This puts it all together. Bonus: AND saves it. def genphoto(self, layers, waterlayers, cpl): # Template for the file name which is always the same #root_filename = mstr_datafolder + "/_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_" # Correct layers mstr_msg("photogen", "Correcting layer order issues") layers = self.correctLayerIssues(layers) # First, we walk through all layers and blend them on top of each other, in order mstr_msg("photogen", "Merging layers") lyr=0 for l in layers: if self._lyrnames[lyr] != "building": self._tile.alpha_composite(l) lyr=lyr+1 # When we have run through this loop, we will end up with a sandwiched # image of all the other images, in their correct order. # However, since I have discovered that some areas in OSM simply do not # have any tag or information, it is possible that the final image will # have empty, alpha-transparent patches. # For this reason we need to check against these and fix that. # First, we will check if there is something to fix: emptyspace = self.checkForEmptySpace() mstr_msg("photogen", "Checked for empty patches") # If this check comes back as true, we need to perform # aforementioned fix: if emptyspace == True: mstr_msg("photogen", "Patching empty space") cmpl = Image.new("RGBA", (self._imgsize, self._imgsize)) edn = self.find_earthnavdata_number() edns = self.latlng_folder(edn) cmpl_ptc = Image.open(mstr_datafolder + "textures/tile/completion/ptc.png") cmpl_brd = Image.open(mstr_datafolder + "textures/tile/completion/brd.png") # Generate the source image for p in range(1, 201): rx = randrange(0 - int(cmpl_ptc.width / 2), ptc_full.width - int(cmpl_ptc.width / 2)) ry = randrange(0 - int(cmpl_ptc.height / 2), ptc_full.height - int(cmpl_ptc.height / 2)) cmpl.alpha_composite(cmpl_ptc, dest=(rx, ry)) cmpl.alpha_composite(cmpl_brd) # Patches to add from other sources. If they don't exist, we also need to make them masks = glob.glob(mstr_datafolder + "textures/tile/completion/*.png") amt = randrange(5, 16) patchtags = [ ["landuse", "meadow"], ["landuse", "grass"], ["natural", "heath"], ["natural", "scrub"] ] for i in range(1, amt + 1): pick = randrange(0, len(masks)) patchmask = Image.open(masks[pick]) patchmask = patchmask.rotate(randrange(0, 360), expand=True) # Make sure patch is within bounds if patchmask.width > self._tile.width or patchmask.height > self._tile.height: patchmask = patchmask.resize((mstr_photores, mstr_photores), Image.Resampling.BILINEAR) patchpix = patchmask.load() # Pick from possible tags and values for the patches numbers = list(range(1, 16)) src = random.sample(numbers, 5) patchpick = randrange(0, len(patchtags)) rg = mstr_resourcegen(patchtags[patchpick][0], patchtags[patchpick][1], src) rg.setLayerContrast(randrange(1, 4)) ptch = rg.gensource() # Generate a full size of the source ptc_full = Image.new("RGBA", (mstr_photores, mstr_photores)) # Generate the source image for p in range(1, 201): rx = randrange(0 - int(ptch[0].width / 2), ptc_full.width - int(ptch[0].width / 2)) ry = randrange(0 - int(ptch[0].height / 2), ptc_full.height - int(ptch[0].height / 2)) ptc_full.alpha_composite(ptch[0], dest=(rx, ry)) rg_img = ptc_full rg_pix = rg_img.load() # The patch to be used in the layer layerpatch = Image.new("RGBA", (patchmask.width, patchmask.height)) lp_pix = layerpatch.load() for y in range(0, patchmask.height): for x in range(0, patchmask.width): ptc_msk = patchpix[x, y] if ptc_msk[3] > 0: oc = rg_pix[x, y] nc = (oc[0], oc[1], oc[2], ptc_msk[3]) lp_pix[x, y] = nc #layerpatch = layerpatch.rotate(randrange(0, 360), expand=True) lx = randrange(0, self._imgsize - layerpatch.width) ly = randrange(0, self._imgsize - layerpatch.height) cmpl.alpha_composite(layerpatch, (lx, ly)) # Merge the images cmpl.alpha_composite(self._tile) # Make this the real one self._tile = cmpl # There may be some tiles that have a larger sea or even an ocean in them - these need to be # removed from the final tile ocean_pix = self._tile.load() for y in range(self._tile.width): for x in range(self._tile.height): p = ocean_pix[x,y] if p[0] == 255 and p[1] == 0 and p[2] == 255: t = (0,0,0,0) ocean_pix[x,y] = t # Alpha correction on final image corrpix = self._tile.load() for y in range(0, self._tile.height): for x in range(0, self._tile.width): c = corrpix[x,y] if c[3] > 0: nc = (c[0], c[1], c[2], 255) corrpix[x,y] = nc if c[3] == 0: corrpix[x,y] = (0,0,0,0) # One more thing... mstr_msg("photogen", "Adding features to layers") self.addTreesToFeatures(layers, waterlayers) # One final thing... mstr_msg("photogen", "Adding details to buildings") self.addDetailsToBuildings() # Throw missing buildings on top lyr = 0 for l in layers: if self._lyrnames[lyr] == "building": self._tile.alpha_composite(l) lyr = lyr + 1 # We are now in posession of the final image. # Contrast #self._tile = ImageEnhance.Contrast(self._tile).enhance(0.1) # This we can save accordingly. self._tile.save(mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + ".png") # Now we convert this into a DDS _tmpfn = mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) os.system(mstr_xp_ddstool + " --png2dxt1 " + _tmpfn + ".png " + _tmpfn + ".dds" ) os.remove(mstr_datafolder + "z_orthographic/orthos/" + self._latlngfld + "/" + str(self._ty) + "_" + str(self._tx) + ".png") # Now generate the normal map for this ortho. # But only if this is enabled. if mstr_xp_genscenery and mstr_xp_scn_normalmaps: # Generate the normal normal map first (hah) nrm = mstr_xp_normalmap() nrmimg = nrm.generate_normal_map_for_layer(self._tile, False) # Now we need to walk through the water layers and generate a combined normal map wtrlyr = Image.new("RGBA", (self._imgsize, self._imgsize)) for w in waterlayers: wtrlyr.alpha_composite(w) wtrlyr = wtrlyr.resize((int(mstr_photores/4), int(mstr_photores/4)), Image.Resampling.BILINEAR) wtrimg = nrm.generate_normal_map_for_layer(wtrlyr, True) # Blend nrmimg.alpha_composite(wtrimg) # Save nrmfln = mstr_datafolder + "z_orthographic/normals/" + self._latlngfld + "/" + str(self._ty) + "_" + str( self._tx) + ".png" nrmimg.save(nrmfln) # Generates some random tree. # We will now move away from using pre-made trees... # they didn't look so great def generate_tree(self, bccolor=None): sx = randrange(18, 31) sy = randrange(18, 31) treepoly = Image.new("RGBA", (sx, sy)) draw = ImageDraw.Draw(treepoly) draw.ellipse((4, 4, sx - 4, sy - 4), fill="black") tree = Image.new("RGBA", (sx, sy)) treepx = tree.load() maskpx = treepoly.load() # How many tree points do we want? treepts = 75 # How many of those have been drawn? ptsdrawn = 0 bc = [] bcp = 0 if bccolor == None: bc = [ (36, 50, 52), (30, 41, 39), (32, 45, 37), (32, 39, 49), (33, 34, 40), (44, 50, 53), (40, 46, 48), (14, 31, 38), (17, 41, 33), (39, 56, 35), (51, 51, 42), (12, 27, 31), (45, 59, 48), (37, 54, 29), (59, 50, 34), (59, 59, 35), (59, 51, 35), (70, 72, 45), (48, 59, 44), (29, 47, 23), (47, 61, 43), (29, 68, 15), (53, 77, 63), (20, 68, 40) ] bcp = randrange(0, len(bc)) else: bc = bccolor bcp = randrange(0, len(bc)) treedraw = ImageDraw.Draw(tree) while ptsdrawn < treepts + 1: rx = randrange(0, sx) ry = randrange(0, sy) mp = maskpx[rx, ry] if mp[3] > 0: d = randrange(0, 51) r = bc[bcp][0] g = bc[bcp][1] + d b = bc[bcp][2] + (d // 2) a = randrange(170, 256) c = (r, g, b, a) ex = randrange(2, 5) ey = randrange(2, 5) treedraw.ellipse((rx - (ex // 2), ry - (ey // 2), rx + (ey // 2), ry + (ey // 2)), fill=c) ptsdrawn = ptsdrawn + 1 for y in range(0, tree.height): for x in range(0, tree.width): tp = treepx[x, y] diff = randrange(0, 31) nc = (tp[0] - diff, tp[1] - diff, tp[2] - diff, tp[3]) treepx[x, y] = nc tree = tree.filter(ImageFilter.GaussianBlur(radius=0.5)) for y in range(0, tree.height): for x in range(0, tree.width): tp = treepx[x, y] diff = randrange(0, 51) nc = (tp[0] - diff, tp[1] - diff, tp[2] - diff, tp[3]) treepx[x, y] = nc return tree # This used to be in layergen and solves the problem of roads being rendered above trees. # It is the only solution that worked, after some research. def addTreesToFeatures(self, layers, wtr): # Generate a combined water layer. Order and appearance does not matter. wtrlyr = Image.new("RGBA", (self._imgsize, self._imgsize)) for l in wtr: wtrlyr.alpha_composite(l) # Preload the water layer for comparison wtrpix = wtrlyr.load() # To make trees blend better with the environment, we can determine # an average color from the forest layer. The pass this color as # base instead. forest = -1 curlyr = 0 for lyr in self._lyrnames: if lyr[0] == "landuse" and lyr[1] == "forest": forest = curlyr # Find the average color of the forest layer frstclr = [] frstpix = None if forest != -1: frstpix = layers[forest].load() for y in range(0, self._imgsize): for x in range(0, self._imgsize): frs = frstpix[x,y] if frs[3] > 0: c = ( frs[0]-40, frs[1]-40, frs[2]-40 ) frstclr.append(c) # Walk through list of layers to decide where to add the trees curlyr = 0 for lyr in self._lyrnames: # Add trees only in some features if (lyr[0] == "landuse" and lyr[1] == "cemetery") or ( lyr[0] == "landuse" and lyr[1] == "residential") or ( lyr[0] == "leisure" and lyr[1] == "park"): trees = Image.new("RGBA", (self._imgsize, self._imgsize)) amt = 4000 if lyr[1] == "cemetery": amt = 20000 lyrmask = layers[curlyr].load() # We can use the layer image as alpha mask for i in range(1, amt + 1): lx = randrange(0, self._imgsize) ly = randrange(0, self._imgsize) lp = lyrmask[lx,ly] wp = wtrpix[lx,ly] if lp[3] == 255 and wp[3] == 0: # Exclude water bodies from tree placement tree = None if len(frstclr) != 0: tree = self.generate_tree(bccolor=frstclr) else: tree = self.generate_tree() trees.alpha_composite(tree, (lx, ly)) tree_shadow = Image.new("RGBA", (self._imgsize, self._imgsize)) tree_pix = trees.load() shadow_pix = tree_shadow.load() for y in range(self._imgsize): for x in range(self._imgsize): tp = tree_pix[x, y] if tp[3] > 0: rndshd = randrange(5, 210) sc = (0, 0, 0, rndshd) if x + 8 < self._imgsize and y + 5 < self._imgsize: shadow_pix[x + 8, y + 5] = sc tree_shadow = tree_shadow.filter(ImageFilter.GaussianBlur(radius=2)) tree_shadow.alpha_composite(trees) self._tile.alpha_composite(tree_shadow) curlyr = curlyr + 1 # Reset for tree rows curlyr = 0 treerow = -1 for lyr in self._lyrnames: if lyr[0] == "natural" and lyr[1] == "tree_row": treerow = curlyr break curlyr = curlyr + 1 if treerow != -1: rowpx = layers[curlyr].load() for y in range(0, self._tile.height): for x in range(0, self._tile.width): tpx = rowpx[x,y] if tpx[3] == 1: tree = None if len(frstclr) == 0: tree = self.generate_tree() else: tree = self.generate_tree(bccolor=frstclr) self._tile.alpha_composite(tree, dest=(x, y)) # Reset curlyr = 0 bldg = [] tilepix = self._tile.load() for lyr in self._lyrnames: if lyr[0] == "building": bldg.append(curlyr) curlyr = curlyr + 1 for b in range(0, len(bldg)): bldg_lyr = layers[bldg[b]].load() shdw = Image.open(mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_" + self._lyrnames[bldg[b]][0] + "-" + self._lyrnames[bldg[b]][1] + "_layer_shadow.png") shdw_pix = shdw.load() for y in range(0, self._imgsize): for x in range(0, self._imgsize): bpix = bldg_lyr[x,y] spix = shdw_pix[x,y] if bpix[3] > 0 and spix[0] == 255: tilepix[x,y] = ( bpix[0], bpix[1], bpix[2], bpix[3] ) # Adds some intricate details to buildings def addDetailsToBuildings(self): curlyr = 0 bldg = [] for lyr in self._lyrnames: if lyr[0] == "building": bldg.append(curlyr) curlyr = curlyr + 1 for b in range(0, len(bldg)): shdw = Image.open(mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_" + self._lyrnames[bldg[b]][0] + "-" + self._lyrnames[bldg[b]][1] + "_layer_shadow.png") shdw_pix = shdw.load() det_image = Image.open(mstr_datafolder + "textures/building/details/"+str(randrange(1, 16))+".png") detpx = det_image.load() for y in range(0, shdw.height): for x in range(0, shdw.width): spx = shdw_pix[x,y] if spx[0] != 255: c = (0,0,0,0) detpx[x,y] = c self._tile.alpha_composite(det_image) # Correct some layer issues def correctLayerIssues(self, layers): # First the residential/forest dilemma residential = -1 forest = -1 curlyr = 0 for lyr in self._lyrnames: if lyr[0] == "landuse" and lyr[1] == "residential": residential=curlyr if lyr[0] == "landuse" and lyr[1] == "forest": forest = curlyr curlyr = curlyr+1 # Make sure we hit the correct layers with correct content! # Only do something if both are found. if residential != -1 and forest != -1: rsd_pix = layers[residential].load() frs_pix = layers[forest].load() rsd_adj = Image.new("RGBA", (self._imgsize, self._imgsize)) adj_pix = rsd_adj.load() for y in range(0, self._tile.height): for x in range(0, self._tile.width): fp = frs_pix[x,y] rp = rsd_pix[x,y] if rp[3] > 0 and fp[3] > 0: adj_pix[x,y] = (rp[0], rp[1], rp[2], rp[3]) layers[forest].alpha_composite(rsd_adj) return layers # This checks the final image for empty patches. Should one be # found, we will generate something to fill the gap. If this is # the case, we will also note this in the database for the tile, # under the special tag and value "tile", "completion". The same # conditions apply for edge testing and so on. def checkForEmptySpace(self): empty = False # Load photo layer_pix = self._tile.load() # Scan! for y in range(self._tile.width-1): for x in range(self._tile.height-1): p = layer_pix[x,y] if p[3] < 255: # <- Check for empty or non-complete alpha empty = True break # Tell about findings return empty # This returns a mask of the empty space to cover, should there be any def buildCompletionMask(self): mask = Image.new("RGBA", (self._imgsize, self._imgsize), (0,0,0,0)) mask_pix = mask.load() # Load photo layer_pix = self._tile.load() # Scan! for y in range(self._tile.width-1): for x in range(self._tile.height-1): p = layer_pix[x,y] if p[3] < 255: # <- Check for empty or non-complete alpha mask_pix[x,y] = (0,0,0,255) # We do not apply any blur or other effects here - we only want the # exact pixel positions. #mask.save( mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._ty) + "_" + str(self._lng) + "-" + str(self._tx) + "_tile-completion.png" ) mstr_msg("photogen", "Generated and saved empty space mask") return mask # Construct a folder name for latitude and longitude def latlng_folder(self, numbers): fstr = "" if numbers[0] >= 0: fstr = "+" if numbers[0] < 0: fstr = "-" if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) if numbers[1] >= 0: fstr = fstr + "+" if numbers[1] < 0: fstr = fstr + "-" if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) return fstr # Find the next "by-ten" numbers for the current latitude and longitude def find_earthnavdata_number(self): earthnavdata = [] lat = abs(int(self._lat / 10) * 10) lng = abs(int(self._lng / 10) * 10) earthnavdata.append(lat) earthnavdata.append(lng) return earthnavdata