Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: allow passing format ratio instead of make_sqaure to cover more use cases #14

Merged
merged 3 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:

- repo: https://github.com/psf/black
rev: 21.10b0
rev: 22.1.0
hooks:
- id: black

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Options:
model. Defaults to 4.0. Is not influenced by z-scale.
--z-scale FLOAT Value to be multiplied to the z-axis elevation data to
scale up the height of the model. Defaults to 1.0.
--demo Converts a demo tif of Hawaii into a STL file.
--demo Converts a demo tiff of Hawaii into a STL file.
--make-square If the input tiff is a rectangle and not a square, cut
the longer side to make the output STL file a square.
--version Show the version and exit.
Expand All @@ -80,9 +80,9 @@ Options:
### 3. Using `mapa` as python library
In case you are building your own application you can simply use `mapa`'s functionality as a within your application by importing the modules functions.
```python
from mapa import convert_tif_to_stl
from mapa import convert_tiff_to_stl

path_to_stl = convert_tif_to_stl(...)
path_to_stl = convert_tiff_to_stl(...)
```

## Changelog
Expand Down
24 changes: 13 additions & 11 deletions mapa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mapa.geometry import compute_all_triangles, reduce_resolution
from mapa.raster import (
clip_tiff_to_bbox,
cut_array_to_square,
cut_array_to_format,
determine_elevation_scale,
merge_tiffs,
remove_empty_first_and_last_rows_and_cols,
Expand Down Expand Up @@ -59,15 +59,15 @@ def convert_array_to_stl(
max_res: bool,
z_offset: float,
z_scale: float,
make_square: bool,
cut_to_format_ratio: Union[None, float],
elevation_scale: float,
output_file: Path,
) -> Path:
# drop higher dimension to get 2-dimensional (x * y) array
array = array[0, :, :]
array = remove_empty_first_and_last_rows_and_cols(array)
if make_square:
array = cut_array_to_square(array)
if cut_to_format_ratio:
array = cut_array_to_format(array, cut_to_format_ratio)

x, y = array.shape
if max_res:
Expand All @@ -82,22 +82,24 @@ def convert_array_to_stl(
click.echo(f"{'🔍 reducing image resolution...':<50s}", nl=False)
array = reduce_resolution(array, bin_factor=bin_fac)
combined_z_scale = z_scale * elevation_scale
triangles = compute_all_triangles(array, model_size, z_offset, combined_z_scale)

triangles = compute_all_triangles(array, model_size, z_offset, combined_z_scale, cut_to_format_ratio)
click.echo(f"{'💾 saving data to stl file...':<50s}", nl=False)

output_file = _save_to_stl_file(triangles, output_file, as_ascii)
click.echo(f"\n🎉 successfully generated STL file: {Path(output_file).absolute()}")
return Path(output_file)


def convert_tif_to_stl(
def convert_tiff_to_stl(
input_file: str,
as_ascii: bool,
model_size: int,
output_file: Union[str, None],
max_res: bool,
z_offset: float,
z_scale: float,
make_square: bool,
cut_to_format_ratio: Union[None, float],
) -> Path:
_verify_input_is_valid(input_file)
if output_file is None:
Expand All @@ -115,7 +117,7 @@ def convert_tif_to_stl(
max_res=max_res,
z_offset=z_offset,
z_scale=z_scale,
make_square=make_square,
cut_to_format_ratio=cut_to_format_ratio,
elevation_scale=elevation_scale,
output_file=output_file,
)
Expand Down Expand Up @@ -147,7 +149,7 @@ def convert_bbox_to_stl(
max_res: bool = False,
z_offset: float = 0.0,
z_scale: float = 1.0,
make_square: bool = False,
cut_to_format_ratio: Union[None, float] = None,
allow_caching: bool = True,
) -> Path:
if bbox_geometry is None:
Expand All @@ -157,14 +159,14 @@ def convert_bbox_to_stl(
click.echo("⏳ converting bounding box to STL file... \n")

tiff = _get_tiff_for_bbox(bbox_geometry, allow_caching)
output_file = convert_tif_to_stl(
output_file = convert_tiff_to_stl(
input_file=tiff,
as_ascii=as_ascii,
model_size=model_size,
output_file=output_file,
max_res=max_res,
z_offset=z_offset,
z_scale=z_scale,
make_square=make_square,
cut_to_format_ratio=cut_to_format_ratio,
)
return output_file
20 changes: 13 additions & 7 deletions mapa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import click

from mapa import conf, convert_tif_to_stl
from mapa import conf, convert_tiff_to_stl


@click.command(help="🌍 Convert DEM data into STL files 🌏")
Expand Down Expand Up @@ -45,9 +45,15 @@
)
@click.option("--demo", is_flag=True, help="Converts a demo tif of Hawaii into a STL file.")
@click.option(
"--make-square",
is_flag=True,
help="If the input tiff is a rectangle and not a square, cut the longer side to make the output STL file a square.",
"--cut-to-format-ratio",
default=None,
help=(
"Cut the input tiff file to a specified format. Set to `1` if you want the output model to be squared. Set to "
"`0.5` if you want one side to be half the length of the other side. Omit this flag to keep the input format. "
"This option is particularly useful when an exact output format ratio is required for example when planning to "
"put the 3d printed model into a picture frame. Using this option will always try to cut the shorter side of "
"the input tiff."
),
)
@click.version_option()
def dem2stl(
Expand All @@ -59,7 +65,7 @@ def dem2stl(
demo: bool = False,
as_ascii: bool = False,
model_size: int = conf.DEFAULT_MODEL_OUTPUT_SIZE_IN_MM,
make_square: bool = False,
cut_to_format_ratio: Union[None, float] = None,
) -> None:
if demo is False and input is None:
click.echo("💥 Either of --input or --demo is required, try --help.")
Expand All @@ -68,11 +74,11 @@ def dem2stl(
click.echo("💥 Only one of --input or --demo is allowed, try --help.")
exit(1)
if demo:
input = conf.DEMO_TIF_PATH
input = conf.DEMO_TIFF_PATH
max_res = True
z_scale = 2.5

convert_tif_to_stl(input, as_ascii, model_size, output, max_res, z_offset, z_scale, make_square)
convert_tiff_to_stl(input, as_ascii, model_size, output, max_res, z_offset, z_scale, cut_to_format_ratio)


@click.command(help="🗺 Draw a bounding box on a map and turn it into a STL file 🗺")
Expand Down
2 changes: 1 addition & 1 deletion mapa/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
DEFAULT_Z_SCALE = 1.0

# path to demo tiff
DEMO_TIF_PATH = Path(__file__).parent.parent / "tests" / "tif" / "hawaii.tif"
DEMO_TIFF_PATH = Path(__file__).parent.parent / "tests" / "tiff" / "hawaii.tiff"

# stac catalogue
PLANETARY_COMPUTER_API_URL = "https://planetarycomputer.microsoft.com/api/stac/v1"
Expand Down
22 changes: 15 additions & 7 deletions mapa/geometry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Tuple
from typing import Tuple, Union

import click
import numba as nb
Expand Down Expand Up @@ -224,11 +224,19 @@ def compute_all_triangles(
target_size: int,
z_offset: float,
z_scale: float,
cut_to_format_ratio: Union[float, None],
) -> np.ndarray:

# determine scales
max_x, max_y = array.shape
xy_scale = target_size / max_x
x_scale = target_size / max_x
if cut_to_format_ratio:
if cut_to_format_ratio > 1.0:
# ensure ratio is between 0.0 and 1.0 and transpose ratio
cut_to_format_ratio = cut_to_format_ratio**-1
y_scale = target_size * cut_to_format_ratio / max_y
else:
y_scale = target_size / max_x

# create raster
click.echo(f"{'🗺 creating base raster for tiff...':<50s}", nl=False)
Expand All @@ -252,8 +260,8 @@ def compute_all_triangles(
array=array,
max_x=max_x,
max_y=max_y,
x_scale=xy_scale,
y_scale=xy_scale,
x_scale=x_scale,
y_scale=y_scale,
z_scale=z_scale,
z_offset=z_offset,
)
Expand All @@ -262,12 +270,12 @@ def compute_all_triangles(
raster=raster,
max_x=max_x,
max_y=max_y,
x_scale=xy_scale,
y_scale=xy_scale,
x_scale=x_scale,
y_scale=y_scale,
z_scale=z_scale,
z_offset=z_offset,
)
bottom_triangles = _compute_triangles_of_bottom(max_x=max_x, max_y=max_y, x_scale=xy_scale, y_scale=xy_scale)
bottom_triangles = _compute_triangles_of_bottom(max_x=max_x, max_y=max_y, x_scale=x_scale, y_scale=y_scale)
return np.vstack((dem_triangles, side_triangles, bottom_triangles))


Expand Down
23 changes: 10 additions & 13 deletions mapa/mapa.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "e29f8fff53b4431c93fc194ba59f7140",
"model_id": "12b74c148640406da3beafa16c706101",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -66,7 +66,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 11,
"id": "39c8fef3",
"metadata": {
"scrolled": true
Expand All @@ -78,17 +78,14 @@
"text": [
"⏳ converting bounding box to STL file... \n",
"\n",
"⬇️ fetching 2 stac items...\n",
"🚀 using cached stac item ALPSMLC30_N046E009_DSM ✅ (0.0s)\n",
"🚀 using cached stac item ALPSMLC30_N045E009_DSM ✅ (0.0s)\n",
"🔪 clipping region of interest... ✅ (0.1s) \n",
"🔍 reducing image resolution... ✅ (0.0s) \n",
"🗺 creating base raster for tiff... ✅ (0.2s) \n",
"⛰ computing triangles of 3d surface... ✅ (0.1s) \n",
"📐 computing triangles of body sides... ✅ (0.2s) \n",
"💾 saving data to stl file... ✅ (0.7s) \n",
"🚀 using cached tiff... ✅ (0.0s)\n",
"🔍 reducing image resolution... ✅ (0.1s) \n",
"🗺 creating base raster for tiff... ✅ (0.0s) \n",
"⛰ computing triangles of 3d surface... ✅ (0.2s) \n",
"📐 computing triangles of body sides... ✅ (0.3s) \n",
"💾 saving data to stl file... ✅ (1.2s) \n",
"\n",
"🎉 successfully generated STL file: /Users/ppqw/dev/geospacial/mapa/mapa/output.stl\n"
"🎉 successfully generated STL file: /Users/ppqw/dev/geospacial/mapa/mapa/output_orig.stl\n"
]
}
],
Expand All @@ -102,7 +99,7 @@
" z_offset=3.0, # in millimeter\n",
" z_scale=3.0, # factor to scale z-axis with\n",
" max_res=False, # whether to use maximum resolution, `True` might consume a lot of time and memory\n",
" make_square=False, # cuts longer side of the input bbox to get a square\n",
" cut_to_format_ratio=None, # cuts longer side of the input bbox to achieve desired format ratio of the model\n",
")"
]
}
Expand Down
35 changes: 33 additions & 2 deletions mapa/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,50 @@ def remove_empty_first_and_last_rows_and_cols(array: npt.ArrayLike) -> np.ndarra
return array


def cut_array_to_square(array: npt.ArrayLike) -> np.ndarray:
def _cut_array_to_square(array: npt.ArrayLike) -> np.ndarray:
rows, cols = array.shape
if rows > cols:
diff = rows - cols
# remove last n=diff rows
return array[:-diff, :]
elif cols > rows:
diff = cols - rows
return np.delete(array, np.s_[-diff:], axis=1)
return array[:, :-diff]
else:
return array


def _cut_array_to_rectangular_shape(array: npt.ArrayLike, cut_to_format_ratio: float) -> np.ndarray:
rows, cols = array.shape
if rows / cols == cut_to_format_ratio or cols / rows == cut_to_format_ratio:
# input array has already desired format ratio
return array
elif rows > cols:
# cut cols
desired_n_cols = int(rows * cut_to_format_ratio)
return array[:, :desired_n_cols]
elif cols > rows:
# cut rows
desired_n_rows = int(cols * cut_to_format_ratio)
return array[:desired_n_rows, :]
else:
# cut cols anyway
desired_n_cols = int(rows * cut_to_format_ratio)
return array[:, :desired_n_cols]


def cut_array_to_format(array: npt.ArrayLike, cut_to_format_ratio: float) -> np.ndarray:
if cut_to_format_ratio == 1.0:
return _cut_array_to_square(array)
if cut_to_format_ratio == 0.0:
raise ValueError("Cannot cut array to format with ratio 0.0. Choose a format ratio between 0.0 and 1.0")
else:
if cut_to_format_ratio > 1.0:
# ensure ratio is between 0.0 and 1.0 and transpose ratio
cut_to_format_ratio = cut_to_format_ratio**-1
return _cut_array_to_rectangular_shape(array, cut_to_format_ratio)


def _get_coordinate_of_pixel(row: int, col: int, tiff) -> Tuple[float]:
meta = tiff.meta
window = Window(0, 0, meta["width"], meta["height"])
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ def test_stl_ascii():

@pytest.fixture
def test_tiff():
yield Path(__file__).parent / "tif" / "hawaii_low_res.tif"
yield Path(__file__).parent / "tiff" / "hawaii_low_res.tiff"


@pytest.fixture
def clipped_tiff():
yield Path(__file__).parent / "tif" / "clipped.tif"
yield Path(__file__).parent / "tiff" / "clipped.tiff"


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion tests/regenerate_test_stls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

if __name__ == "__main__":
cli = CliRunner()
test_tiff = Path(__file__).parent / "tif" / "hawaii_low_res.tif"
test_tiff = Path(__file__).parent / "tiff" / "hawaii_low_res.tiff"

ascii_output = Path(__file__).parent / "stl" / "hawaii_ascii.stl"
print(f"regenerating ascii stl file: {ascii_output}")
Expand Down
Loading