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