scrollstats 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scrollstats/__init__.py +39 -0
- scrollstats/_version.py +34 -0
- scrollstats/_version.pyi +4 -0
- scrollstats/delineation/__init__.py +13 -0
- scrollstats/delineation/array_types.py +31 -0
- scrollstats/delineation/line_smoother.py +122 -0
- scrollstats/delineation/raster_classifiers.py +537 -0
- scrollstats/delineation/raster_denoisers.py +68 -0
- scrollstats/delineation/ridge_area_raster.py +214 -0
- scrollstats/py.typed +0 -0
- scrollstats/ridge_metrics/__init__.py +18 -0
- scrollstats/ridge_metrics/calc_ridge_metrics.py +63 -0
- scrollstats/ridge_metrics/data_extractors.py +853 -0
- scrollstats/ridge_metrics/ridge_amplitude.py +226 -0
- scrollstats/transecting/__init__.py +8 -0
- scrollstats/transecting/transect.py +1331 -0
- scrollstats-0.1.1.dist-info/METADATA +165 -0
- scrollstats-0.1.1.dist-info/RECORD +20 -0
- scrollstats-0.1.1.dist-info/WHEEL +4 -0
- scrollstats-0.1.1.dist-info/licenses/LICENSE +21 -0
scrollstats/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2025 Andrew Vanderheiden. All rights reserved.
|
|
3
|
+
|
|
4
|
+
scrollstats: An open-source python library to calculate and extract morphometrics from scroll bar floodplains
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._version import version as __version__
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BendDataExtractor",
|
|
13
|
+
"LineSmoother",
|
|
14
|
+
"MultiTransect",
|
|
15
|
+
"RidgeDataExtractor",
|
|
16
|
+
"TransectDataExtractor",
|
|
17
|
+
"__version__",
|
|
18
|
+
"calc_ridge_amps",
|
|
19
|
+
"calculate_ridge_metrics",
|
|
20
|
+
"create_ridge_area_raster",
|
|
21
|
+
"create_ridge_area_raster_fs",
|
|
22
|
+
"create_transects",
|
|
23
|
+
"map_amp_values",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
from .delineation import (
|
|
27
|
+
LineSmoother,
|
|
28
|
+
create_ridge_area_raster,
|
|
29
|
+
create_ridge_area_raster_fs,
|
|
30
|
+
)
|
|
31
|
+
from .ridge_metrics import (
|
|
32
|
+
BendDataExtractor,
|
|
33
|
+
RidgeDataExtractor,
|
|
34
|
+
TransectDataExtractor,
|
|
35
|
+
calc_ridge_amps,
|
|
36
|
+
calculate_ridge_metrics,
|
|
37
|
+
map_amp_values,
|
|
38
|
+
)
|
|
39
|
+
from .transecting import MultiTransect, create_transects
|
scrollstats/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
scrollstats/_version.pyi
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"LineSmoother",
|
|
5
|
+
"create_ridge_area_raster",
|
|
6
|
+
"create_ridge_area_raster_fs",
|
|
7
|
+
"quadratic_profile_curvature",
|
|
8
|
+
"residual_topography",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
from .line_smoother import LineSmoother
|
|
12
|
+
from .raster_classifiers import quadratic_profile_curvature, residual_topography
|
|
13
|
+
from .ridge_area_raster import create_ridge_area_raster, create_ridge_area_raster_fs
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, TypeAlias
|
|
5
|
+
|
|
6
|
+
from nptyping import Bool, Float, NDArray, Shape
|
|
7
|
+
|
|
8
|
+
# Define array types
|
|
9
|
+
|
|
10
|
+
## Array2D: 2D array of any size of any dtype
|
|
11
|
+
Array2D: TypeAlias = NDArray[Shape["*, *"], Any] # noqa: F722 pylint: disable=C0103
|
|
12
|
+
|
|
13
|
+
## ElevationArray2D: 2D array of any size of dtype float
|
|
14
|
+
ElevationArray2D: TypeAlias = NDArray[Shape["*, *"], Float] # noqa: F722 pylint: disable=C0103
|
|
15
|
+
|
|
16
|
+
## BinaryArray2D: 2D array of any size of dtype bool
|
|
17
|
+
BinaryArray2D: TypeAlias = NDArray[Shape["*, *"], Bool] # noqa: F722 pylint: disable=C0103
|
|
18
|
+
|
|
19
|
+
# Define functions as interfaces
|
|
20
|
+
## `BinaryClassifierFn`s and `BinaryDenoiserFn`s use the following signature:
|
|
21
|
+
### They take a 2D array as input and output a binary 2D array
|
|
22
|
+
### Use partial functions from the `functools` library to create wrapper functions which contain all input arguments other than the input 2D array
|
|
23
|
+
|
|
24
|
+
# Mypy does not allow for the complex Callable typing that's currently used (a callable with one typed input and any number of other inputs of any type)
|
|
25
|
+
## So, the difference between a BinaryClassifierFn and BinaryDenoiserFn are in name only
|
|
26
|
+
|
|
27
|
+
# Takes an ElevationArray2D and kwargs as input
|
|
28
|
+
BinaryClassifierFn: TypeAlias = Callable[..., BinaryArray2D]
|
|
29
|
+
|
|
30
|
+
# Takes a BinaryArray2D and kwargs as input
|
|
31
|
+
BinaryDenoiserFn: TypeAlias = Callable[..., BinaryArray2D]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from geopandas import GeoDataFrame
|
|
5
|
+
from scipy.interpolate import CubicSpline
|
|
6
|
+
from shapely.geometry import LineString
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LineSmoother:
|
|
10
|
+
"""
|
|
11
|
+
Smooth and densify rough, manually drawn LineStrings.
|
|
12
|
+
|
|
13
|
+
Smoothing is accomplished with the use of a mean filter and densifying is accomplished with the use of a piecewise cubic spline.
|
|
14
|
+
The GeoDataFrame provided must only contain LineStrings. MuliLineStrings or other geometries are not supported.
|
|
15
|
+
The vertex count of any ridge cannot be lower than the window size for the mean filter
|
|
16
|
+
|
|
17
|
+
Values used for the Lower Brazos Ridges were:
|
|
18
|
+
window = 5 (vertices)
|
|
19
|
+
spacing = 1 (meters)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, lines: GeoDataFrame, spacing: int, window: int) -> None:
|
|
23
|
+
self.lines = lines
|
|
24
|
+
self.spacing = spacing
|
|
25
|
+
self.window = window
|
|
26
|
+
|
|
27
|
+
# Perform checks on inputs
|
|
28
|
+
self.check_geometry_type()
|
|
29
|
+
self.check_vertex_count()
|
|
30
|
+
|
|
31
|
+
def check_geometry_type(self) -> None:
|
|
32
|
+
"""Check that all geometries are of type LineString"""
|
|
33
|
+
if any(self.lines.geom_type != "LineString"):
|
|
34
|
+
err_msg = "Not all geometries are of type LineString. Remove the non-LineString geometry from `lines`"
|
|
35
|
+
raise ValueError(err_msg)
|
|
36
|
+
|
|
37
|
+
def check_vertex_count(self) -> None:
|
|
38
|
+
"""Check that all ridges have at least as many vertices as the smoothing window is long"""
|
|
39
|
+
if any(self.lines.geometry.apply(lambda x: len(x.coords)) < self.window):
|
|
40
|
+
err_msg = f"One or more ridges have fewer vertices than the smoothing window is long (window={self.window}). Remove these ridges or add more vertices to them."
|
|
41
|
+
raise ValueError(err_msg)
|
|
42
|
+
|
|
43
|
+
def meanfilt(self, line: LineString, w: int) -> LineString:
|
|
44
|
+
"""
|
|
45
|
+
Use a mean filter to smooth the xy points of the line.
|
|
46
|
+
This is done by passing a moving window with size `w` over the x and y coordinates separately and replacing the central value of the window with the mean value of the window.
|
|
47
|
+
This particular method appends the first and last coord to the new line to account for erosion via convolution
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
mode = "valid"
|
|
51
|
+
x, y = line.xy
|
|
52
|
+
|
|
53
|
+
xm = np.convolve(x, np.ones(w) * 1 / w, mode=mode)
|
|
54
|
+
xm = np.insert(xm, 0, x[0])
|
|
55
|
+
xm = np.append(xm, x[-1])
|
|
56
|
+
|
|
57
|
+
ym = np.convolve(y, np.ones(w) * 1 / w, mode=mode)
|
|
58
|
+
ym = np.insert(ym, 0, y[0])
|
|
59
|
+
ym = np.append(ym, y[-1])
|
|
60
|
+
|
|
61
|
+
return LineString(zip(xm, ym, strict=False))
|
|
62
|
+
|
|
63
|
+
def calc_dist(self, x: np.ndarray, y: np.ndarray) -> list[float]:
|
|
64
|
+
"""Calc distance along the line"""
|
|
65
|
+
xdiff = np.ediff1d(x)
|
|
66
|
+
ydiff = np.ediff1d(y)
|
|
67
|
+
|
|
68
|
+
return list(
|
|
69
|
+
np.cumsum(
|
|
70
|
+
np.insert(np.sqrt(np.add(np.power(xdiff, 2), np.power(ydiff, 2))), 0, 0)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def calc_cubic_spline(self, line: LineString, spacing: int) -> LineString:
|
|
75
|
+
"""
|
|
76
|
+
Fit a cubic spline function to a LineString then sample that function at the given `spacing`
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# Get x,y values from LineString
|
|
80
|
+
x, y = line.xy
|
|
81
|
+
|
|
82
|
+
# Calc distance along the line
|
|
83
|
+
s = self.calc_dist(x, y)
|
|
84
|
+
|
|
85
|
+
# Get the total length of the line
|
|
86
|
+
length = s[-1]
|
|
87
|
+
|
|
88
|
+
# Total number of output points
|
|
89
|
+
n = int(length // spacing)
|
|
90
|
+
|
|
91
|
+
# Interpolated distances for each output point
|
|
92
|
+
interp_dist = np.linspace(0, length, n + 1, endpoint=True)
|
|
93
|
+
|
|
94
|
+
# Create spline function of x and y
|
|
95
|
+
cx_func = CubicSpline(s, x)
|
|
96
|
+
cy_func = CubicSpline(s, y)
|
|
97
|
+
|
|
98
|
+
# Apply Spline
|
|
99
|
+
cx = cx_func(interp_dist)
|
|
100
|
+
cy = cy_func(interp_dist)
|
|
101
|
+
|
|
102
|
+
return LineString(zip(cx, cy, strict=False))
|
|
103
|
+
|
|
104
|
+
def execute(self) -> GeoDataFrame:
|
|
105
|
+
"""
|
|
106
|
+
Apply the mean filter and cubic spline to each line in the geodataframe.
|
|
107
|
+
Return a new geodataframe with the smooth lines
|
|
108
|
+
"""
|
|
109
|
+
# Create a copy of the lines GeoDataFrame
|
|
110
|
+
out_lines = self.lines.copy()
|
|
111
|
+
|
|
112
|
+
# Smooth points with mean filter
|
|
113
|
+
out_lines.geometry = out_lines.geometry.apply(
|
|
114
|
+
lambda x: self.meanfilt(x, self.window)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Densify points with cubic spline
|
|
118
|
+
out_lines.geometry = out_lines.geometry.apply(
|
|
119
|
+
lambda x: self.calc_cubic_spline(x, self.spacing)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return out_lines
|