https://github.com/pecormiernocca/authoring-consistent-landscapes
Tip revision: 26fecbe9cf80fa54a1a82c7b57a763d413c45dcd authored by Pierre Ecormier-Nocca on 05 September 2021, 06:55:44 UTC
Merge pull request #1 from jiangzhongshi/patch-1
Merge pull request #1 from jiangzhongshi/patch-1
Tip revision: 26fecbe
fauna.py
import matplotlib.colors as mcols
import matplotlib.pyplot as plt
from scipy import interpolate
import numpy as np
import skimage
import skimage.morphology
import numba
import cv2
import sys
import pathlib
sys.path.append("./lib")
import QTree
import Animal
import utils
from Region import Region
sols_tautavel = ["J", "L"]
sol = "J"
resource_path = "resources/"
water_threshold = -2
density_threshold = 0.20
tree_obstacle_threshold = 0.3
terrain_cell_width = 1
map_exits = [[0, 1650], # Exits
[0, 2660],
[0, 3600],
[3060, 0],
[4096, 420],
[1957, 1121], # Fords
[1973, 1104],
[1201, 2578],
[1231, 2562],
[1356, 3106],
[1383, 3093]]
shrub_idx = [0, 2]
tree_idx = [1, 3, 8, 10, 11, 12, 14, 15]
plant_idx = [4, 5, 6, 7, 9, 13, 16, 17]
if sol == "J":
resource_path += "tautavel_J/"
estimated_pop = {"Bison": 2,
"Elk": 71,
"Deer": 59,
"Reindeer": 4,
"Mouflon": 10,
"Rhino": 4,
"Horse": 3}
estimated_carn = {"Bear": 2,
"Wolf": 3,
"Lion": 1,
"Lynx": 2,
"Fox": 2}
elif sol == "L":
resource_path += "tautavel_L/"
estimated_pop = {"Bison": 1,
"Elk": 12,
"Deer": 2,
"Reindeer": 78,
"Mouflon": 8,
"Rhino": 1,
"Horse": 1}
estimated_carn = {"Bear": 2,
"Wolf": 3,
"Lion": 1,
"Lynx": 1,
"Fox": 1}
pathlib.Path(resource_path + "visu").mkdir(parents=True, exist_ok=True)
###############################################################################
# Load all resources, including heightmap, water, plant densities, etc.
###############################################################################
utils.benchStart("Loading images")
smooth = cv2.imread(resource_path + "../raw/dem_filled.tif", cv2.IMREAD_UNCHANGED)
drain = cv2.imread(resource_path + "../raw/water.tif", cv2.IMREAD_UNCHANGED)
drain_log = np.log(drain)
idx = (drain_log > water_threshold)
water = np.zeros(drain.shape)
water[idx] = 1
bg_scale = 4
background = utils.makeBackground(smooth, water, ['darkgreen', 'gold', 'darkred', 'black'], bg_scale)
background_bw = utils.makeBackground(smooth, water, ['white', 'black'], bg_scale)
plant_density_count = max(max(shrub_idx), max(tree_idx), max(plant_idx))+1
species_herb = Animal.species_herb
species_carn = Animal.species_carn
plants = []
for i in range(plant_density_count):
plant = skimage.io.imread(resource_path + "flora/plant-%d.tif" % i).astype(np.float32)
plants.append(plant)
utils.benchEnd()
###############################################################################
# Extract chains of water pixels along rivers
# Similar to vectorizing the water bodies
###############################################################################
utils.benchStart("Extracting water chains")
def decimateChains(chains, max_err = 200):
"""
Decimate chains with Douglas-Peucker algorithm
max_err is the maximum distance allowed from a point to the simplification
"""
newchains = []
for chain in chains:
vs = chain - chain[0]
angles = np.arctan2(vs[:,1], vs[:,0])
vas = angles - angles[-1]
ds = np.linalg.norm(vs, axis=1)
errs = np.abs(np.sin(vas) * ds)
id_far = np.argmax(errs)
if errs[id_far] > max_err:
newchains += decimateChains([chain[:id_far+1], chain[id_far:]], max_err)
else:
newchains.append(chain)
return newchains
@numba.njit
def numbaFillChain(visited, init, pt):
chain = [init]
sx, sy = pt
while True:
chain.append([sx, sy])
if neighbs[sy, sx] != 2:
return np.array(chain)
visited[sy, sx] = True
for kidx in range(9):
dx = (kidx % 3) - 1
dy = int(kidx / 3) - 1
if dx == 0 and dy == 0:
continue
nsx = sx + dx
nsy = sy + dy
if nsx < 0 or nsx >= szw or nsy < 0 or nsy >= szh:
continue
if not ma[nsy, nsx]: # Not on the medial axis
continue
if visited[nsy, nsx]: # Already registered
continue
sx = nsx
sy = nsy
break
return np.array(chain)
def fillChain(junctions):
visited = np.zeros(ma.shape, np.bool8)
chains = []
# Loop through each junction (has neighbors but not 2)
for it in range(len(junctions)):
sx, sy = junctions[it]
visited[sy, sx] = True
# Find neighbors
for dx, dy in kernel_idx:
if sx+dx < 0 or sx+dx >= szw or sy+dy < 0 or sy+dy >= szh:
continue
if not ma[sy+dy, sx+dx]: # Not on the medial axis
continue
if visited[sy+dy, sx+dx]: # Already registered
continue
chain = numbaFillChain(visited, [sx, sy], [sx+dx, sy+dy])
if len(chain) > 2:
chains.append(chain)
return chains
szh, szw = water.shape
ma, dfield = skimage.morphology.medial_axis(water, return_distance=True)
# 8-connectivity kernel and relative indexes
kernel = np.ones((3,3), dtype=int)
kernel[1,1] = 0
kernel_idx = np.vstack(np.where(kernel)[::-1]).T-1
neighbs = np.zeros(ma.shape, dtype=int)
for x, y in kernel_idx:
neighbs[0+(y<0):szh-(y>0), 0+(x<0):szw-(x>0)] += ma[0+(y>0):szh-(y<0), 0+(x>0):szw-(x<0)]
neighbs *= ma
junctions = np.vstack(np.where((neighbs > 0) & (neighbs != 2))[::-1]).T
chains = fillChain(junctions)
chains = decimateChains(chains, 50)
utils.benchEnd()
###############################################################################
# Compute summarized density maps for vegetation
###############################################################################
utils.benchStart("Computing presence maps")
shrubs_presence = np.max([plants[i] for i in shrub_idx], axis=0) > density_threshold
trees_presence = np.max([plants[i] for i in tree_idx], axis=0) > density_threshold
plants_presence = np.max([plants[i] for i in plant_idx], axis=0) > density_threshold
trees_obstacles = np.max([plants[i] for i in tree_idx], axis=0) > tree_obstacle_threshold
presence_maps = [shrubs_presence, plants_presence, trees_presence]
presence_colors = ["red", "green", "purple"]
presence_names = ["Shrubs", "Plants", "Trees"]
presence_idx = [shrub_idx, plant_idx, tree_idx]
# Export merged density maps
cmap = plt.get_cmap("Greens")
percentile = 0.05
for i, idxs in enumerate([plant_idx, shrub_idx, tree_idx]):
plt.subplot(1, 3, i+1)
density = np.max([plants[i] for i in idxs], axis=0)
vmin, vmax = utils.computePercentileBounds(density, percentile)
norm = mcols.Normalize(vmin, vmax)
out = cmap(norm(density))[:,:,:3]
out[water == 1] = mcols.to_rgb("royalblue")
plt.imshow(out)
plt.title(presence_names[i])
plt.axis("off")
plt.tight_layout()
plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05)
plt.savefig(resource_path + "visu/plant_densities.png")
im = smooth / terrain_cell_width
us = np.zeros(im.shape)
vs = np.zeros(im.shape)
us[:,:-1] = im[:,1:] - im[:,:-1]
vs[:-1,:] = im[1:,:] - im[:-1,:]
ds = np.sqrt(us**2 + vs**2)
im2 = np.ones(im.shape)
im2[ds >= .5] = 0
im2[water != 0] = 0
im2[trees_obstacles != 0] = 0
utils.benchEnd()
###############################################################################
# Compute accessibility maps
###############################################################################
utils.benchStart("Extracting regions")
@numba.njit
def numbaApplyAlongAxis(func1d, axis, arr):
assert arr.ndim == 2
assert axis in [0, 1]
if axis == 0:
result = np.empty(arr.shape[1])
for i in range(len(result)):
result[i] = func1d(arr[:, i])
else:
result = np.empty(arr.shape[0])
for i in range(len(result)):
result[i] = func1d(arr[i, :])
return result
@numba.njit
def numbaMean(array, axis):
return numbaApplyAlongAxis(np.mean, axis, array)
@numba.njit
def numbaExtractRegions(lbls, nb_lbls, region_size_threshold, accessibility):
region_pxs = [np.zeros(1, np.int64)]
region_ellipses = [[0.0]]
access_regions = [0]
min_val = 0
big_lbls = np.ones(lbls.shape) * -1
vals = lbls.flatten()
nbins = nb_lbls+2
order = np.argsort(vals)
sep = np.sort(np.searchsorted(vals[order], np.arange(nb_lbls+2)))
for j in range(1, nb_lbls+1):
if sep[j+1] - sep[j] <= region_size_threshold:
continue
reg_ids = order[sep[j]:sep[j+1]]
region_x = reg_ids % lbls.shape[0]
region_y = (reg_ids / lbls.shape[0]).astype(np.int32)
if accessibility is not None:
region_pxs.append(reg_ids)
region_xy = np.vstack((region_x, region_y)).T
centers = numbaMean(region_xy, 0).astype(np.int32)
local_xy = region_xy - centers
cov = np.cov(local_xy, rowvar=False)
eigvals, eigvecs = np.linalg.eig(cov)
confidence = np.sqrt(eigvals * 4)
ellipse = [centers[0], centers[1],
2*confidence[0], 2*confidence[1],
np.arctan2(eigvecs[1,0], eigvecs[0,0])]
region_ellipses.append(ellipse)
res = accessibility[region_y[0], region_x[0]]
access_regions.append(res)
for idxi in range(len(region_y)):
big_lbls[region_y[idxi], region_x[idxi]] = min_val
min_val += 1
return big_lbls, region_ellipses, access_regions, region_pxs
def extractRegions(image, region_size_threshold=10000, accessibility=None):
"""
Extract contiguous regions with a flood-fill algorithm
"""
lbls, nb_lbls = skimage.measure.label(image, return_num=True, connectivity = 1)
bl, re, ar, pxs = numbaExtractRegions(lbls, nb_lbls, region_size_threshold, accessibility)
return bl, np.array(re[1:]), np.array(ar[1:]), pxs[1:]
accessibility, garb1, garb2, accessibility_px = extractRegions(im2)
nb_regions = int(accessibility.max() + 1)
vege_lbls = []
vege_ellipses = []
vege_regions = []
vege_pxs = []
reg_sz_thresh = 10000 / (terrain_cell_width ** 2)
for presence_map in presence_maps:
presence_map = presence_map.astype(np.int)
presence_map[(ds >= .5) | (water != 0) | (trees_obstacles != 0)] = 0
lbls, region_ellipses, access_regions, region_px = extractRegions(presence_map, region_size_threshold=reg_sz_thresh, accessibility=accessibility)
lbls[lbls == -1] = -np.inf
lbls[water != 0] = -1
vege_lbls.append(lbls)
vege_ellipses.append(region_ellipses)
vege_regions.append(access_regions)
vege_pxs.append(region_px)
utils.benchEnd()
###############################################################################
# Simplify water chains and convert them to nodes
###############################################################################
utils.benchStart("Mapping water nodes")
def chainsToEllipses(chains, dfield):
s0 = np.zeros((len(chains), 2))
s1 = np.zeros((len(chains), 2))
ellipses = np.zeros((len(chains), 5))
for i, chain in enumerate(chains):
v = np.array(chain[-1]) - chain[0]
xy = (v * 0.5) + chain[0]
ellipses[i, :2] = xy
ellipses[i, 2] = np.linalg.norm(v)
ellipses[i, 3] = 5 * 2 * np.mean(dfield[chain[:,1], chain[:,0]])
ellipses[i, 4] = np.arctan2(*v[::-1])
s0[i] = chain[0]
s1[i] = chain[-1]
return ellipses, (s0, s1)
def closestSegments(segments, pts):
"""
Returns the index of the closest segments to the given points
"""
mat = np.zeros((len(pts), len(segments[0])))
s0, s1 = segments
u = s1 - s0
l2 = np.sum(u ** 2, 1)
for i in range(len(s0)):
raw = np.dot(pts - s0[i], u[i]) / l2[i]
weight = (raw < 0) * (-raw) + (raw > 1) * (raw - 1)
ts = np.clip(raw, 0, 1)
projs = s0[i] + np.atleast_2d(ts).T * u[i]
mat[:,i] = np.sum((projs - pts) ** 2, 1) + weight
return np.argmin(mat, 1)
# Create a map of pixels on land and adjacent to water
drinks = np.zeros(accessibility.shape, dtype=int)
for i in range(9):
x, y = (i % 3)-1, int(i / 3)-1
if x == 0 and y == 0:
continue
drinks[0+(y<0):szh-(y>0), 0+(x<0):szw-(x>0)] += (water[0+(y>0):szh-(y<0), 0+(x>0):szw-(x<0)] != 0)
drinks *= (accessibility >= 0)
# Compute closest ellipses for each of the pixels near water
contact_where = np.where(drinks!=0)
contact = np.vstack(contact_where[::-1]).T
water_ellipses, water_segments = chainsToEllipses(chains, dfield)
dto = closestSegments(water_segments, contact)
# Store ellipses within in each region
region_vals = accessibility[contact_where]
nb_lbls = int(np.max(region_vals))
dtos = [None] * (nb_lbls+1)
nbins = nb_lbls+2
order = np.argsort(region_vals)
sep = np.sort(np.searchsorted(region_vals[order], np.arange(nb_lbls+2)))
for j in range(0, nb_lbls+1):
reg_ids = order[sep[j]:sep[j+1]]
dtos[j] = np.unique(dto[reg_ids]).astype(np.int)
utils.benchEnd()
###############################################################################
# Store nodes per region
###############################################################################
utils.benchStart("Organizing data per region")
regions = []
for reg_i in range(nb_regions):
veges = []
veges_px = []
for vege_i in range(len(presence_maps)):
idx = (vege_regions[vege_i] == reg_i)
sub_ells = vege_ellipses[vege_i][idx]
veges.append(sub_ells)
sub_pxs = [vege_pxs[vege_i][i] for i,take in enumerate(idx) if take]
veges_px.append(sub_pxs)
sub_water = np.zeros((0, 5))
water_px = []
if reg_i < len(dtos):
sub_water = water_ellipses[dtos[reg_i]]
for ellipse_i in dtos[reg_i]:
water_px.append(contact[(dto == ellipse_i) & (region_vals == reg_i)])
regions.append(Region(reg_i, veges, veges_px, sub_water, water_px))
utils.benchEnd()
total_pop = sum(estimated_pop.values()) + sum(estimated_carn.values())
estimated_density = {name:num/sum(estimated_pop.values()) for name,num in estimated_pop.items()}
carn_density = {name:num/total_pop for name,num in estimated_carn.items()}
###############################################################################
# Compute density maps for animals
###############################################################################
utils.benchStart("Competition algorithm for animals")
for region in regions:
region.resetMaxResources()
total = 0
instance_count = np.zeros(len(species_herb))
tgt_density = [estimated_density[animal.name] for animal in species_herb]
region_pop = np.zeros((len(species_herb), len(regions)))
region_herds = np.empty((len(species_herb), len(regions)), dtype=list)
for y in range(len(species_herb)):
for x in range(len(regions)):
region_herds[y, x] = []
while True:
idx, grp_sz = Animal.getCandidateId(instance_count, total, tgt_density, species_herb)
animal = species_herb[idx]
fit = Animal.fitness(regions, animal, grp_sz, cell_width_sq=terrain_cell_width**2)
region_idx = np.argmax(fit)
if int(fit[region_idx]) == 0:
break
region = regions[region_idx]
region.resources[0] -= animal.shrub_req * (Animal.YEARLY_RATIO / (terrain_cell_width ** 2)) * grp_sz
region.resources[1] -= animal.plant_req * (Animal.YEARLY_RATIO / (terrain_cell_width ** 2)) * grp_sz
region.resources[2] -= animal.tree_req * (Animal.YEARLY_RATIO / (terrain_cell_width ** 2)) * grp_sz
instance_count[idx] += grp_sz
total += grp_sz
region_pop[idx, region_idx] += grp_sz
region_herds[idx, region_idx].append(grp_sz)
tgt_carn_density = [carn_density[animal.name] for animal in species_carn]
region_surplus = (region_pop.T * [sp.surplus * np.mean(sp.mass) for sp in species_herb]).T / 365.0
carn_herds = np.empty((len(species_carn), len(regions)), dtype=list)
carn_pop = np.zeros((len(species_carn), len(regions)))
carn_count = np.zeros(len(species_carn))
for y in range(len(species_carn)):
for x in range(len(regions)):
carn_herds[y, x] = []
while True:
idx, grp_sz = Animal.getCandidateId(carn_count, total, tgt_carn_density, species_carn)
animal = species_carn[idx]
carn_fitness = Animal.fitness(regions, species_carn[idx], grp_sz, region_surplus, cell_width_sq=terrain_cell_width**2)
best_reg = np.argmax(carn_fitness)
if int(carn_fitness[best_reg]) == 0:
break
region_surplus[:,best_reg] -= animal.herbivores_req * grp_sz * region_surplus[:,best_reg] / region_surplus[:,best_reg].sum()
carn_count[idx] += grp_sz
total += grp_sz
carn_pop[idx, best_reg] += grp_sz
carn_herds[idx, best_reg].append(grp_sz)
utils.benchEnd()
utils.benchStart("Exporting animal density maps")
meat_map = np.zeros(accessibility.shape)
density_cmap = plt.get_cmap("Oranges")
resized_maps = []
sub_species = np.array([3, 1, 8])
for specie_i in np.arange(len(species_herb)):
specie = species_herb[specie_i]
density_map = np.zeros(accessibility.shape)
for region_i, reg in enumerate(regions):
pop = region_pop[specie_i, region_i]
for vege_i, veges_px in enumerate(reg.veges_px):
type_ratio = specie.reqs[vege_i] / specie.sum_reqs
if type_ratio == 0:
continue
szs = np.array([len(x) for x in veges_px])
ratios = type_ratio * (szs / np.sum(szs))
weights = pop * ratios
for idx, weight in enumerate(weights):
pxs = veges_px[idx]
ys = pxs // np.size(accessibility, 0)
xs = pxs % np.size(accessibility, 0)
density_map[ys, xs] += weight / szs[idx]
meat_map[ys, xs] += np.mean(species_herb[0].mass) * weight / szs[idx]
density_map = cv2.resize(density_map, background.shape[:2], interpolation=cv2.INTER_NEAREST)
resized_maps.append(density_map)
max_density = np.max([dmap.max() for dmap in resized_maps])
for i, density_map in enumerate(resized_maps):
if i not in sub_species:
continue
specie = species_herb[i]
out = background_bw.copy()
if density_map.max() > 0:
colored_density = density_cmap(density_map / max_density)
idxs = density_map > 0
out[idxs] = colored_density[idxs][:,:3]
plt.subplot(1, len(sub_species), np.where(sub_species == i)[0][0] + 1)
plt.title(specie.name)
plt.imshow(out)
plt.axis("off")
for specie_i in sub_species[sub_species > len(species_herb)]:
carn_i = specie_i - len(species_herb)
specie = species_carn[carn_i]
density_map = np.zeros(accessibility.shape)
for region_i, reg in enumerate(regions):
pop = carn_pop[carn_i, region_i]
if pop == 0:
continue
for vege_i, veges_px in enumerate(reg.veges_px):
type_ratio = specie.reqs[vege_i] / specie.sum_reqs
if type_ratio == 0:
continue
szs = np.array([len(x) for x in veges_px])
ratios = type_ratio * (szs / np.sum(szs))
weights = pop * ratios
for idx, weight in enumerate(weights):
pxs = veges_px[idx]
ys = pxs // np.size(accessibility, 0)
xs = pxs % np.size(accessibility, 0)
density_map[ys, xs] += weight / szs[idx]
type_ratio = specie.reqs[-1] / specie.sum_reqs
access_idxs = accessibility == region_i
density_map[access_idxs] += pop * meat_map[access_idxs] * type_ratio / np.sum(meat_map[access_idxs])
density_map = cv2.resize(density_map, background.shape[:2], interpolation=cv2.INTER_NEAREST)
colored_density = density_cmap(density_map / max_density)
idxs = density_map > 0
out = background_bw.copy()
out[idxs] = colored_density[idxs][:,:3]
plt.subplot(1, len(sub_species), np.where(sub_species == specie_i)[0][0] + 1)
plt.title(specie.name)
plt.imshow(out)
plt.axis("off")
utils.benchEnd()
plt.tight_layout()
plt.subplots_adjust(0.05, 0.05, 0.95, 0.95, 0.05, 0.05)
plt.savefig(resource_path + "visu/animal_densities.png")
###############################################################################
# Compute trail weights based on usage, animal count and weight
###############################################################################
utils.benchStart("Computing trail weights")
def getResourceWeight(res_ratio, res_type, specie):
"""
Resource weight = (ratio resource size / total resource in region) * (need for this resource / total needs)
"""
request = 0.2
if res_type == -2:
request = 0.2
if res_type >= 0:
request = (specie.reqs[res_type] / specie.sum_reqs)
return res_ratio * request
def getTrailWeight(type_a, ratio_a, type_b, ratio_b, region_pops):
"""
Trail weight = \sum_s Ps(A) * Ps(B) * N_animals(s) * mass(s)
"""
out = 0
for specie_i, specie in enumerate(species_herb):
animal_count = int(region_pops[specie_i])
if animal_count == 0:
continue
weight = getResourceWeight(ratio_a, type_a, specie)
weight *= getResourceWeight(ratio_b, type_b, specie)
weight *= animal_count * np.mean(specie.mass)
out += weight
return out
for region_i, reg in enumerate(regions):
weights = {}
ratios_water = reg.water[:,2] / np.sum(reg.water[:,2])
local_exits = [(x, y) for x, y in map_exits if accessibility[y, x] == reg.idx]
for vege_i, ells_i in enumerate(reg.veges):
if len(ells_i) == 0:
continue
areas_i = np.pi * (ells_i[:,2]*.5) * (ells_i[:,3]*.5)
ratios_i = areas_i / np.sum(areas_i)
for sub_i in range(len(reg.veges[vege_i])):
for vege_j in range(vege_i, len(reg.veges)):
ells_j = reg.veges[vege_j]
if len(ells_j) == 0:
continue
areas_j = np.pi * (ells_j[:,2]*.5) * (ells_j[:,3]*.5)
ratios_j = areas_j / np.sum(areas_j)
start_j = 0 if vege_j != vege_i else sub_i + 1
for sub_j in range(start_j, len(reg.veges[vege_j])):
weight = getTrailWeight(vege_i, ratios_i[sub_i], vege_j, ratios_j[sub_j], region_pop[:,region_i])
weight_id = (vege_i, sub_i, vege_j, sub_j)
if weight > 0:
weights[weight_id] = weight
for sub_j, ells_j in enumerate(reg.water):
weight = getTrailWeight(vege_i, ratios_i[sub_i], -1, ratios_water[sub_j], region_pop[:,region_i])
weight_id = (vege_i, sub_i, -1, sub_j)
if weight > 0:
weights[weight_id] = weight
for x, y in local_exits:
weight = getTrailWeight(vege_i, ratios_i[sub_i], -1, 1.0/len(local_exits), region_pop[:,region_i])
weight_id = (-2, utils.pt2idx(accessibility, (y, x)), vege_i, sub_i)
if weight > 0:
weights[weight_id] = weight
reg.weights = weights
utils.benchEnd()
###############################################################################
# Compute paths between resources
###############################################################################
def computeShortestPath(reg, qtree, minbounds, maxbounds, path_idx, path_weight):
xmin, ymin = minbounds
xmax, ymax = maxbounds
vege_i, sub_i, vege_j, sub_j = path_idx
qtree.reset()
if vege_i == -2:
sy, sx = utils.idx2pt(accessibility, sub_i)
else:
sidx = np.random.choice(reg.veges_px[vege_i][sub_i])
sy, sx = utils.idx2pt(accessibility, sidx)
start = qtree.findLeaf(sx - xmin, sy - ymin)
if start is None:
print("Error: could not find path:", path_id, path_weight, tgt_out)
return None
if vege_j == -1: # Water node
eidx = np.random.choice(np.arange(len(reg.water_px[sub_j])))
ex, ey = reg.water_px[sub_j][eidx]
else:
eidx = np.random.choice(reg.veges_px[vege_j][sub_j])
ey, ex = utils.idx2pt(accessibility, eidx)
end = qtree.findLeaf(ex - xmin, ey - ymin)
if start.aStar(end):
segs = []
curr = end
total_length = 0
while curr != start:
if len(segs) == 0:
segs.append((curr.x, curr.y, curr.sz))
segs.append((curr.previous.x, curr.previous.y, curr.previous.sz))
total_length += np.linalg.norm(curr.center - curr.previous.center)
curr = curr.previous
path = {"start":(sx, sy), "end":(int(ex), int(ey)), "weight":path_weight, "length": total_length, "segs":segs}
return path
return None
region_paths = []
max_id = int(accessibility.max()) + 1
for region_id in range(max_id):
py, px = np.where(accessibility == region_id)
xmin = np.min(px)
xmax = np.max(px)
ymin = np.min(py)
ymax = np.max(py)
sqszx = 2 << int(np.floor(np.log2(xmax - xmin)))
sqszy = 2 << int(np.floor(np.log2(ymax - ymin)))
sqsz = max(sqszx, sqszy)
im = np.ones((sqsz, sqsz), np.bool)
im[0:ymax+1-ymin, 0:xmax+1-xmin] = accessibility[ymin:ymax+1, xmin:xmax+1] != region_id
utils.benchStart("Computing QuadTree for region {}".format(region_id))
qtree = QTree.QTreeNode(im, im.shape[0])
utils.benchEnd()
utils.benchStart("Trails for region {}".format(region_id))
paths = {}
reg = regions[region_id]
reg_weight_items = list(reg.weights.items())
reg_weight_items.sort(key=lambda x: -x[1])
total_weight = sum([x[1] for x in reg_weight_items])
cum_weight = 0
for path_idx, path_weight in reg_weight_items:
print("\rPath {}/{}".format(len(paths)+1, len(reg.weights)), end="")
cum_weight += path_weight
if cum_weight / total_weight > 0.99:
break
path = computeShortestPath(reg, qtree, (xmin, ymin), (xmax, ymax), path_idx, path_weight)
if path is not None:
paths[path_idx] = path
print("")
region_paths.append({"id": region_id,
"qtree": qtree,
"min": (xmin, ymin),
"max": (xmax, ymax),
"paths": paths})
utils.benchEnd()
def registerEdge(dictionary, v1, v2):
def registerDirEdge(v1, v2):
if v1 not in dictionary:
sub = {}
dictionary[v1] = sub
else:
sub = dictionary[v1]
if v2 not in sub:
sub[v2] = 1
else:
sub[v2] += 1
registerDirEdge(v1, v2)
registerDirEdge(v2, v1)
def getMostUsedEdge(hist):
idx, count = max(hist.items(), key=lambda x:x[1])
cx, cy, csz, nx, ny, nsz = idx
return (cx, cy, csz), (nx, ny, nsz)
def segToIdx(v1, v2):
cx, cy, csz = v1
nx, ny, nsz = v2
item = (cx, cy, csz, nx, ny, nsz)
if nx < cx or (nx == cx and ny < cy):
item = (nx, ny, nsz, cx, cy, csz)
return item
def popEdge(hist, by_vert, v1, v2):
idx = segToIdx(v1, v2)
if idx in hist:
hist.pop(idx)
by_vert[v1].pop(v2)
by_vert[v2].pop(v1)
return idx
def followTrail(hist, by_vert, v1):
trail = [v1]
while True:
items = by_vert[v1].items()
if len(items) == 0:
break
v2 = max(items, key=lambda x:x[1])[0]
popEdge(hist, by_vert, v1, v2)
trail.append(v2)
v1 = v2
return trail
def cellCenter(cell):
cx, cy, csz = cell
return np.array([cx + (csz >> 1), cy + (csz >> 1)])
def prolongTrail(trails, first_appearance, t0, t1):
"""
Find extension of trail starting/ending at t0->t1 in previously extracted trails
Extension with lowest angle is kept
"""
v1 = cellCenter(t0)
v2 = cellCenter(t1)
seg = (v2 - v1) / np.linalg.norm(v2 - v1)
first_app = first_appearance[t0]
orig_trail = trails[first_app[0]]
candidates = []
if first_app[1] > 0:
candidates.append(orig_trail[first_app[1] - 1])
if first_app[1] < len(orig_trail) - 1:
candidates.append(orig_trail[first_app[1] + 1])
cand_centers = [cellCenter(candidate) for candidate in candidates]
cand_segs = [(v1 - v) / np.linalg.norm(v1 - v) for v in cand_centers]
cand_dot = [np.dot(cand_seg, seg) for cand_seg in cand_segs]
return candidates[np.argmax(cand_dot)]
###############################################################################
# Refine paths into trails
###############################################################################
utils.benchStart("Refining trails")
region_weights = region_pop.sum(0)
region_weights = region_weights / region_weights.max()
trail_map = np.zeros(tuple(np.array(accessibility.shape) // bg_scale), dtype=np.uint8)
for region_item in region_paths:
region_id = region_item["id"]
xmin, ymin = region_item["min"]
xmax, ymax = region_item["max"]
paths = region_item["paths"].values()
hist = {}
by_vert = {}
for i,path_obj in enumerate(paths):
segs = path_obj["segs"]
weight = path_obj["weight"]
for j in range(len(segs) - 1):
v1 = segs[j]
v2 = segs[j+1]
registerEdge(by_vert, v1, v2)
item = segToIdx(v1, v2)
if item not in hist:
hist[item] = weight
else:
hist[item] += weight
if len(hist.values()) == 0:
continue
weights_hist = hist.copy() # Make a copy since hist will be emptied
trails = []
first_appearance = {}
to_draw = []
while len(hist):
v1, v2 = getMostUsedEdge(hist)
start = popEdge(hist, by_vert, v1, v2)
trail = followTrail(hist, by_vert, v1)[::-1]
trail += followTrail(hist, by_vert, v2)
# Extend trail to previously instanciated ones for interpolation
if len(trail) > 1 and trail[0] in first_appearance:
trail = [prolongTrail(trails, first_appearance, trail[0], trail[1])] + trail
if len(trail) > 1 and trail[-1] in first_appearance:
trail = trail + [prolongTrail(trails, first_appearance, trail[-1], trail[-2])]
# Register first appearance of vertices to connect subpaths
for i, item in enumerate(trail):
if item not in first_appearance:
first_appearance[item] = (len(trails), i)
trails.append(trail)
xs = np.array([cx + (csz >> 1) for cx, cy, csz in trail])
ys = np.array([cy + (csz >> 1) for cx, cy, csz in trail])
# Extract/interpolate weights
wx = []
wy = []
maxusage = max(weights_hist.values())
seglens = np.sqrt((xs[1:]-xs[:-1])**2 + (ys[1:]-ys[:-1])**2)
cumlens = np.cumsum(seglens)
currlen = 0
for i in range(len(trail)-1):
usage = weights_hist[segToIdx(trail[i], trail[i+1])]
weight = usage / maxusage
if i == 0:
wx.append(0)
wy.append(weight)
wx.append(currlen + seglens[i] * 0.5)
wy.append(weight)
if i == len(trail)-2:
wx.append(currlen + seglens[i])
wy.append(weight)
currlen += seglens[i]
subdiv = int(max((wx[-1] - wx[0]) / 5, 5))
wx2 = np.linspace(wx[0], wx[-1], subdiv)
wx2 = wx2[:-1] + (wx2[1]-wx2[0]) * .5
spl = interpolate.interp1d(wx, wy)
wy2 = spl(wx2)
if len(xs) > 3:
fint, u = interpolate.splprep([xs, ys], s=0)
xint, yint = interpolate.splev(np.linspace(0, 1, subdiv), fint)
else:
xint = xs
yint = ys
for i in range(len(xint) - 1):
lx1 = int(xint[i] + xmin)
lx2 = int(xint[i+1] + xmin)
ly1 = int(yint[i] + ymin)
ly2 = int(yint[i+1] + ymin)
th = int(10 * (wy2[i]+1))
col = int(255 * (wy2[i] ** .9))
if wy2[i] < 0.01:
col = 0
to_draw.append(((lx1, ly1), (lx2, ly2), col, th))
to_draw.sort(key=lambda x: x[2])
for p1, p2, col, th in to_draw:
if th > 0 and col > 0:
p1 = tuple(np.array(p1) // bg_scale)
p2 = tuple(np.array(p2) // bg_scale)
cv2.line(trail_map, p1, p2, col, thickness=th // bg_scale)
alpha = trail_map / 256.0
alpha = np.dstack((alpha, alpha, alpha))
trails_full = background * (1.0 - alpha) + 1.0 * alpha
skimage.io.imsave(resource_path + 'visu/trails.png', np.uint8(trails_full * 256))
utils.benchEnd()
###############################################################################
# Compute daily plannings
###############################################################################
utils.benchStart("Computing daily plannings")
def getRandomResource(specie, reg):
cs = np.cumsum(np.array(specie.reqs) / specie.sum_reqs)
res_type = np.sum(np.random.random() >= cs)
if res_type < len(reg.veges_px):
cs_resource = np.cumsum([len(x) for x in reg.veges_px[res_type]])
res_id = np.sum((np.random.random() * cs_resource[-1]) >= cs_resource)
else:
res_type = -2
res_id = 0
return res_type, res_id
def sortedPathIdx(vg_i, sub_i, vg_j, sub_j):
if vg_i == -1 or (vg_j < vg_i and vg_j >= 0) or (vg_i == vg_j and sub_j < sub_i):
return (vg_j, sub_j, vg_i, sub_i)
return (vg_i, sub_i, vg_j, sub_j)
def getRegionPath(reg, vg_i, sub_i, vg_j, sub_j):
regpaths = region_paths[reg.idx]
paths = regpaths["paths"]
path_idx = sortedPathIdx(vg_i, sub_i, vg_j, sub_j)
if path_idx in paths:
return paths[path_idx]
# If the path was not already computed, compute it now
path = computeShortestPath(reg, regpaths["qtree"], regpaths["min"], regpaths["max"], path_idx, 0.0)
if path is not None:
paths[path_idx] = path
return path
return None
def getFullPlan(specie, reg, plan):
out = []
total = 0
for i in range(len(plan)):
p1 = plan[i]
out.append({"type":int(p1[0]), "nodeidx": int(p1[1]), "duration": int(p1[2])})
total += p1[2]
if i < len(plan) - 1:
p2 = plan[i + 1]
if p1[0] == -2 or p2[0] == -2:
out.append({"type":-1, "nodeidx":-1, "duration": 30})
else:
path = getRegionPath(reg, p1[0], p1[1], p2[0], p2[1])
if path is not None:
length = path["length"]
time = 60 * length / (specie.walk_speed*1000)
total += time
out.append({"type":-1, "nodeidx":-1, "duration": int(time)})
return out
herds_by_id = []
for sp_i, sp in enumerate(region_herds):
for reg_i, herds in enumerate(sp):
for herd in herds:
herds_by_id.append((reg_i, sp_i, herd))
for sp_i, sp in enumerate(carn_herds):
for reg_i, herds in enumerate(sp):
for herd in herds:
herds_by_id.append((reg_i, sp_i + len(species_herb), herd))
np.random.shuffle(herds_by_id)
plans_obj = []
for reg_i, sp_i, count in herds_by_id:
if sp_i < len(species_herb):
specie = species_herb[sp_i]
else:
specie = species_carn[sp_i - len(species_herb)]
reg = regions[reg_i]
full_duration = 0
plan = []
water_cs = np.cumsum(reg.water[:,2] / np.sum(reg.water[:,2]))
for i in range(specie.water_req):
if i > 0:
res_type, res_id = getRandomResource(specie, reg)
duration = np.random.rand() * 60 + 90
full_duration += duration
plan.append((res_type, res_id, duration))
duration = np.random.rand() * 60 + 90
full_duration += duration
plan.append((-1, np.sum(np.random.random() >= water_cs), duration))
while full_duration / 60 < 18:
res_type, res_id = getRandomResource(specie, reg)
duration = np.random.rand() * 60 + 90
full_duration += duration
plan.insert(np.random.randint(len(plan)+1), (res_type, res_id, duration))
full_plan = getFullPlan(specie, reg, plan)
plans_obj.append({"sp_i": sp_i, "reg_i": reg_i, "count": count, "plan": full_plan})
utils.benchEnd()