initial
This commit is contained in:
commit
7ebd03ec98
14 changed files with 288 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
**/*.egg-info
|
||||||
|
/dist
|
0
README.md
Normal file
0
README.md
Normal file
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
[project]
|
||||||
|
name = "pyskyline"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = [
|
||||||
|
{ name="Alexandre FOUCHER", email="foucher@univ-ubs.fr" },
|
||||||
|
]
|
||||||
|
description = "A small example package"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
|
||||||
|
# [project.urls]
|
||||||
|
# Homepage = "https://github.com/pypa/sampleproject"
|
||||||
|
# Issues = "https://github.com/pypa/sampleproject/issues"
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
perlin-numpy @ git+https://github.com/pvigier/perlin-numpy
|
||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
opencv-python
|
BIN
resources/mask.jpg
Executable file
BIN
resources/mask.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
18
src/pyskyline/__init__.py
Normal file
18
src/pyskyline/__init__.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 University of South Brittany, Lab-STICC UMR 6285 All Rights Reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .raycast import bresenham_line
|
||||||
|
from .terrain import Terrain
|
BIN
src/pyskyline/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
src/pyskyline/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
src/pyskyline/__pycache__/raycast.cpython-38.pyc
Normal file
BIN
src/pyskyline/__pycache__/raycast.cpython-38.pyc
Normal file
Binary file not shown.
BIN
src/pyskyline/__pycache__/skyline.cpython-38.pyc
Normal file
BIN
src/pyskyline/__pycache__/skyline.cpython-38.pyc
Normal file
Binary file not shown.
BIN
src/pyskyline/__pycache__/terrain.cpython-38.pyc
Normal file
BIN
src/pyskyline/__pycache__/terrain.cpython-38.pyc
Normal file
Binary file not shown.
26
src/pyskyline/localization/mosse_correlation.py
Normal file
26
src/pyskyline/localization/mosse_correlation.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import numpy as np
|
||||||
|
import math
|
||||||
|
from scipy.fft import fft, ifft
|
||||||
|
from scipy.signal import gaussian
|
||||||
|
|
||||||
|
g = gaussian(256, std=2.25,sym=True)
|
||||||
|
G = fft(g)
|
||||||
|
|
||||||
|
def compute_score(F : np.ndarray, h : np.ndarray) -> float:
|
||||||
|
""" Compute the correlation score of the two skyline based on a MOSSE correlation filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
F (np.ndarray): Real skyline in fourier domain.
|
||||||
|
h (np.ndarray): Simulated skyline in time domain.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Correlation score, higher is the score higher the correlation is.
|
||||||
|
"""
|
||||||
|
|
||||||
|
H = fft(h)
|
||||||
|
r = abs(ifft(H*G/F))
|
||||||
|
shift = np.argmax(r)
|
||||||
|
peak = r[shift]
|
||||||
|
r[shift-5:shift+5] = 0
|
||||||
|
score = peak/math.sqrt(np.sum(np.power(r,2)))
|
||||||
|
return math.log(score)
|
57
src/pyskyline/raycast.py
Normal file
57
src/pyskyline/raycast.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 University of South Brittany, Lab-STICC UMR 6285 All Rights Reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def bresenham_line(x:int, y:int, dist:float, angle:float, limit:int) -> list:
|
||||||
|
"""Generate list of points of all grid cell where raycast hit.
|
||||||
|
see https://zingl.github.io/bresenham.html
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (int): X coordinate.
|
||||||
|
y (int): Y coordinate.
|
||||||
|
dist (float): Ray distance.
|
||||||
|
angle (float): Ray angle.
|
||||||
|
limit (int): coordinate limit.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of points [(x,y),...].
|
||||||
|
"""
|
||||||
|
points = []
|
||||||
|
|
||||||
|
x1 = int(x + np.cos(angle)* dist)
|
||||||
|
y1 = int(y + np.sin(angle)* dist)
|
||||||
|
dx = abs(x1 - x)
|
||||||
|
sx = 1 if x < x1 else -1
|
||||||
|
dy = -abs(y1 - y)
|
||||||
|
sy = 1 if y < y1 else -1
|
||||||
|
error = dx + dy
|
||||||
|
|
||||||
|
while (x != x1 and y != y1) and (0 <= x < limit and 0 <= y < limit):
|
||||||
|
points.append((x,y))
|
||||||
|
|
||||||
|
e2 = 2 * error
|
||||||
|
if e2 >= dy:
|
||||||
|
error += dy
|
||||||
|
x += sx
|
||||||
|
if e2 <= dx:
|
||||||
|
error += dx
|
||||||
|
y += sy
|
||||||
|
|
||||||
|
|
||||||
|
return points
|
147
src/pyskyline/terrain.py
Normal file
147
src/pyskyline/terrain.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 University of South Brittany, Lab-STICC UMR 6285 All Rights Reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
import cv2 as cv
|
||||||
|
import perlin_numpy as perlin
|
||||||
|
from .raycast import bresenham_line
|
||||||
|
|
||||||
|
print(__file__)
|
||||||
|
|
||||||
|
class Terrain:
|
||||||
|
DEFAULT_MASK = os.path.dirname(__file__) + '/../../resources/mask.jpg'
|
||||||
|
DEFAULT_RAYCOUNT = 256
|
||||||
|
DEFAULT_RAYDIST = 1000.0
|
||||||
|
DEFAULT_LAYERS = {
|
||||||
|
-1 : (169, 166, 97), # Water
|
||||||
|
18 : (175, 214, 238), # Sand
|
||||||
|
36 : ( 34, 139, 34), # Grass
|
||||||
|
82 : ( 20, 100, 20) # Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, seed:int, size:int, height:float, scale:float) -> None:
|
||||||
|
self.seed = seed
|
||||||
|
self.size = size
|
||||||
|
self.height = height
|
||||||
|
self.scale = scale
|
||||||
|
self.elevation_map = None
|
||||||
|
self.color_map = None
|
||||||
|
self.skylines = None
|
||||||
|
|
||||||
|
def generate_elevation_map(self, mask_path:str) -> None:
|
||||||
|
""" Generate grayscale elevation map from 2D perlin noise
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask_path (str): Terrain grayscale mask path.
|
||||||
|
"""
|
||||||
|
np.random.seed(self.seed)
|
||||||
|
perlin_noise = perlin.generate_fractal_noise_2d(
|
||||||
|
shape = (512,)*2,
|
||||||
|
res = (2**3,)*2,
|
||||||
|
octaves = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set noise in [0;1]
|
||||||
|
p_min = np.min(perlin_noise)
|
||||||
|
p_max = np.max(perlin_noise)
|
||||||
|
perlin_noise = (perlin_noise - p_min) / (p_max - p_min)
|
||||||
|
|
||||||
|
if self.size != 512:
|
||||||
|
perlin_noise = cv.resize(perlin_noise, (self.size,)*2, interpolation=cv.INTER_CUBIC)
|
||||||
|
|
||||||
|
# Open and rescale mask
|
||||||
|
mask = cv.imread(mask_path, cv.IMREAD_GRAYSCALE)
|
||||||
|
mask = cv.resize(mask, (self.size,)*2, interpolation=cv.INTER_CUBIC)
|
||||||
|
|
||||||
|
elevation_map = perlin_noise * mask
|
||||||
|
|
||||||
|
self.elevation_map = elevation_map.astype(dtype=np.uint8)
|
||||||
|
|
||||||
|
def generate_colored_map(self, layers:'dict|None'=None) -> None:
|
||||||
|
""" Generate RGB colored map based on elevation map.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layers (dict): Layer dictionary where the key is the height and value the color in BGR.
|
||||||
|
"""
|
||||||
|
if layers is None:
|
||||||
|
layers = self.DEFAULT_LAYERS
|
||||||
|
|
||||||
|
h,w = self.elevation_map.shape
|
||||||
|
color_map = np.full((h,w,3), layers[-1], dtype=np.uint8)
|
||||||
|
|
||||||
|
for height, color in list(layers.items())[1:]:
|
||||||
|
mask = self.elevation_map > height
|
||||||
|
color_map[mask] = color
|
||||||
|
|
||||||
|
self.color_map = cv.cvtColor(color_map, cv.COLOR_BGR2RGB)
|
||||||
|
|
||||||
|
def compute_skyline(self, x:int, y:int, ray_count:int=256, ray_dist:float=1000.0) -> np.ndarray:
|
||||||
|
"""_summary_
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (int): X coordinate
|
||||||
|
y (int): Y coordinate
|
||||||
|
ray_count (int, optional): Raycast count. Defaults to 256.
|
||||||
|
ray_dist (float, optional): Raycast distance in meter. Defaults to 1000.0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: skyline array.
|
||||||
|
"""
|
||||||
|
ray_step = 2 * np.pi / (ray_count-1)
|
||||||
|
line = np.zeros((ray_count), dtype=np.float16)
|
||||||
|
|
||||||
|
for i in range(ray_count):
|
||||||
|
max_height = -10
|
||||||
|
max_v_angle = 0
|
||||||
|
points = bresenham_line(x, y, ray_dist/self.scale, ray_step*i, self.size)
|
||||||
|
for x0, y0 in points:
|
||||||
|
height = self.elevation_map[y0,x0]
|
||||||
|
if height > max_height:
|
||||||
|
max_height = height
|
||||||
|
dist = math.sqrt((x0-x)**2+(y0-y)**2) * self.scale
|
||||||
|
if dist != 0.0:
|
||||||
|
v_angle = np.arctan((height * self.height/255)/dist)
|
||||||
|
if v_angle > max_v_angle:
|
||||||
|
max_v_angle = v_angle
|
||||||
|
|
||||||
|
line[i] = np.rad2deg(max_v_angle)
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate(seed:int=0x7B16, size:int=512, height:float=25.0, scale:float=1.0,
|
||||||
|
mask_path:str=DEFAULT_MASK) -> 'Terrain':
|
||||||
|
""" Generate a procedural terrain using seed and mask, and precompute all horizon lines
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seed (int, optional): Random seed for the terrain generation. Defaults to 0x7B16.
|
||||||
|
size (int, optional): Size of the generated elevation map in pixel. Defaults to 512.
|
||||||
|
height (float, optional): Maximum altitude of the elevation map. Defaults to 40.0.
|
||||||
|
scale (float, optional): Scale of a pixel in meter. Defaults to 1.0.
|
||||||
|
mask_path (str, optional): Terrain mask path. Defaults to DEFAULT_MASK.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Terrain: _description_
|
||||||
|
"""
|
||||||
|
terrain = Terrain(seed,size,height,scale)
|
||||||
|
terrain.generate_elevation_map(mask_path)
|
||||||
|
terrain.generate_colored_map()
|
||||||
|
|
||||||
|
return terrain
|
13
test/test_terrain.py
Normal file
13
test/test_terrain.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import pyskyline
|
||||||
|
|
||||||
|
terrain = pyskyline.Terrain.generate()
|
||||||
|
h = terrain.compute_skyline(330,330,360)
|
||||||
|
|
||||||
|
plt.figure()
|
||||||
|
plt.imshow(terrain.elevation_map)
|
||||||
|
plt.figure()
|
||||||
|
plt.plot(np.arange(0,360,360/h.shape[0]), h)
|
||||||
|
plt.show()
|
Loading…
Reference in a new issue