Update geodata pipeline and exports

This commit is contained in:
2026-01-05 22:17:54 +01:00
parent 3aed6333bc
commit 4aaced0462
62 changed files with 31584 additions and 130 deletions

272
PIPELINE.md Normal file
View File

@@ -0,0 +1,272 @@
# GeoData Pipeline Architecture
Visual documentation showing how the GeoData pipeline transforms raw geospatial data into Unity-ready assets.
---
## Complete Pipeline Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ INPUT LAYER │
├──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┤
│ DGM1 │ DOM1 │ DOP20 │ CityGML │ LPG │ LPO │ BDOM20 │
│ TIFF │ TIFF │ JP2 │ GML │ XYZ │ XYZ │ LAZ │
│ terrain │ surface │ ortho │ buildings│ ground │ object │ RGB │
│ 10m │ 10m │ 20cm │ LoD2 │ points │ points │ points │
├──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┤
│ raw/ directory (inputs) │
└────────────────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROCESSING LAYER │
├───────────────────┬────────────────────┬────────────────────────────────────┤
│ │ │ │
│ HEIGHTMAPS │ BUILDINGS │ VEGETATION & OBJECTS │
│ │ │ │
│ ┌─────────────┐ │ ┌──────────────┐ │ ┌──────────────┐ ┌─────────────┐ │
│ │ DGM1 → VRT │ │ │GML→CityJSON │ │ │ DOM1-DGM1 │ │nDSM detect │ │
│ │ ↓ │ │ │ ↓ │ │ │ = CHM │ │ ↓ │ │
│ │ Warp 1025² │ │ │ Triangulate │ │ │ ↓ │ │ Classify │ │
│ │ ↓ │ │ │ ↓ │ │ │Local maxima │ │(lamp/bench │ │
│ │ Scale 16bit│ │ │ Decimate │ │ │ ↓ │ │ /sign) │ │
│ │ ↓ │ │ │ ↓ │ │ │ Tree peaks │ │ │ │
│ │ [LPG valid]│ │ │ Ground-snap │ │ │ [LPO refine] │ │ [LPO ref] │ │
│ └─────────────┘ │ │ [RANSAC roof]│ │ │ [DOP20 RGB] │ └─────────────┘ │
│ │ └──────────────┘ │ └──────────────┘ │
├───────────────────┴────────────────────┴────────────────────────────────────┤
│ work/ directory (temp) │
└────────────────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ OUTPUT LAYER │
├─────────────────┬─────────────────┬─────────────────┬───────────────────────┤
│ height_png16/ │ ortho_jpg/ │ buildings_tiles/│ trees/ + furniture/ │
│ *.png (16-bit) │ *.jpg (2048²) │ *.glb │ *.csv │
│ *.pgw │ *.jgw │ │ trees_tiles/*.glb │
├─────────────────┴─────────────────┴─────────────────┴───────────────────────┤
│ tile_index.csv (manifest with bounds + elevation range) │
├─────────────────────────────────────────────────────────────────────────────┤
│ export_unity/ directory │
└────────────────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ UNITY IMPORT │
│ GeoTileImporter.cs reads tile_index.csv, creates: │
│ • Terrain tiles (heightmaps + ortho textures) │
│ • Building GLB instances │
│ • Tree proxies with canopy colors │
│ • Street furniture placeholders │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Pipeline Dependency Graph
```
┌─────────────────┐
│ --setup │
│ materialize │
│ archives │
└────────┬────────┘
┌─────────────────┐
│ HEIGHTMAP │◄──── MUST RUN FIRST
│ export_height │ Creates tile_index.csv
│ maps() │ Creates work/dgm.vrt
└────────┬────────┘
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ TEXTURES │ │ BUILDINGS │ │ TREES │
│ ortho │ │ citygml │ │ dom1-dgm1 │
│ photos │ │ → glb │ │ → csv+glb │
└────────────┘ └────────────┘ └─────┬──────┘
┌───────────────────┐
│ STREET FURNITURE │
│ ndsm detection │
│ → csv │
└─────────┬─────────┘
┌──────────────────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌─────────────────┐
│ HEIGHTMAP- │ │ BUILDINGS- │ │ TREES- │
│ ENHANCED │ │ ENHANCED │ │ ENHANCED │
│ +LPG valid │ │ +RANSAC roofs │ │ +LPO heights │
│ +quality map │ │ +LPO heights │ │ +DOP20 colors │
└────────────────┘ └────────────────┘ │ -furniture mask │
└─────────────────┘
```
---
## Data Flow Per Export Type
### 1. Heightmap (Standard)
```
raw/dgm1/*.tif ──► work/dgm.vrt ──► Per-tile warp (1025×1025)
Scale to UInt16
export_unity/height_png16/{tile}.png
export_unity/tile_index.csv (manifest)
```
### 2. Orthophoto (Textures)
```
raw/dop20/jp2/*.jp2 ──► work/dop.vrt ──► Per-tile extract
│ │
│ tile_index.csv ────────────►│ (read bounds)
│ │
▼ ▼
export_unity/ortho_jpg/{tile}.jpg (2048×2048)
```
### 3. Buildings (Standard)
```
raw/citygml/lod2/*.gml ──► citygml-tools ──► work/cityjson/*.json
cjio triangulate
work/cityjson_local/*.tri.json
work/dgm.vrt ──────────────────────────►│ (ground-snap)
export_unity/buildings_tiles/{tile}.glb
```
### 4. Trees (Standard)
```
raw/dom1/*.tif ─────────► CHM = DOM1 - DGM1
raw/dgm1/*.tif ─────────► │
Local maxima detection
raw/citygml/lod2/*.gml ─────────►│ (building mask)
export_unity/trees/{tile}.csv
export_unity/trees_tiles/{tile}_{chunk}.glb
```
### 5. Street Furniture
```
raw/dom1/*.tif ─────────► nDSM = DOM1 - DGM1
raw/dgm1/*.tif ─────────► │
Blob detection (0.3-8m)
raw/lpo/*.xyz ──────────────────►│ (optional LPO refinement)
Classify: lamp/bench/sign/bollard
export_unity/street_furniture/{tile}.csv
```
### 6. Enhanced Pipelines
```
HEIGHTMAP-ENHANCED:
heightmap output + raw/lpg/*.xyz ──► LPG validation ──► quality map
BUILDINGS-ENHANCED:
buildings output + raw/bdom20rgbi/*.laz ──► RANSAC roof extraction
+ raw/lpo/*.xyz ──────────► height refinement
TREES-ENHANCED:
trees output + raw/lpo/*.xyz ──────► height refinement
+ raw/dop20/*.jp2 ──────► canopy RGB sampling
+ street_furniture/*.csv ► exclusion mask
```
---
## File Format Reference
| Output | Format | Resolution | Purpose |
|--------|--------|------------|---------|
| `height_png16/*.png` | 16-bit PNG | 1025×1025 | Unity terrain heightmap |
| `ortho_jpg/*.jpg` | JPEG Q90 | 2048×2048 | Terrain texture |
| `buildings_tiles/*.glb` | glTF Binary | 200-350k tris | 3D building models |
| `trees/*.csv` | CSV | - | Tree positions (x,y,z,h,r) |
| `trees_tiles/*.glb` | glTF Binary | - | Proxy geometry chunks |
| `street_furniture/*.csv` | CSV | - | Furniture positions |
| `tile_index.csv` | CSV | - | Manifest (bounds, min/max) |
---
## CLI Commands
```bash
# Setup (extract archives, create directories)
uv run python geodata_to_unity.py --setup --build-from-archive
# Standard exports
uv run python geodata_to_unity.py --export heightmap # First!
uv run python geodata_to_unity.py --export textures
uv run python geodata_to_unity.py --export buildings
uv run python geodata_to_unity.py --export trees
uv run python geodata_to_unity.py --export all # All standard
# Enhanced exports (requires point cloud data)
uv run python geodata_to_unity.py --export heightmap-enhanced
uv run python geodata_to_unity.py --export buildings-enhanced
uv run python geodata_to_unity.py --export trees-enhanced
uv run python geodata_to_unity.py --export street-furniture
uv run python geodata_to_unity.py --export all-enhanced # All enhanced
```
---
## Config Classes
```
Config
├── raw: RawConfig # Input directories
├── archives: ArchiveConfig # Archive staging
├── work: WorkConfig # Temp/intermediate
├── export: ExportConfig # Output directories
├── heightmap: HeightmapConfig # out_res=1025, tile_size_m=1000
├── ortho: OrthoConfig # out_res=2048, quality=90
├── buildings: BuildingConfig # triangle budget 200-350k
├── trees: TreeConfig # max_trees=5000, chunk_grid=4x4
├── pointcloud: PointCloudConfig # LPG/LPO/BDOM directories
├── heightmap_enhanced: EnhancedHeightmapConfig
├── buildings_enhanced: EnhancedBuildingConfig
├── trees_enhanced: EnhancedTreeConfig
└── street_furniture: StreetFurnitureConfig
```
---
## Key Files
| Module | Purpose |
|--------|---------|
| `geodata_to_unity.py` | CLI entry point |
| `geodata_pipeline/config.py` | Configuration dataclasses |
| `geodata_pipeline/heightmaps.py` | DGM1 → PNG16 |
| `geodata_pipeline/orthophotos.py` | DOP20 → JPEG |
| `geodata_pipeline/buildings.py` | CityGML → GLB |
| `geodata_pipeline/trees.py` | CHM → CSV + GLB |
| `geodata_pipeline/street_furniture.py` | nDSM → CSV |
| `geodata_pipeline/pointcloud.py` | XYZ/LAZ utilities |
| `geodata_pipeline/*_enhanced.py` | Enhanced pipelines |
| `GeoTileImporter.cs` | Unity importer |

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,type,confidence
1 tile_id x_local y_local z_ground height type confidence

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

View File

@@ -0,0 +1 @@
tile_id,x_local,y_local,z_ground,height,radius,confidence,canopy_r,canopy_g,canopy_b
1 tile_id x_local y_local z_ground height radius confidence canopy_r canopy_g canopy_b

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,519 @@
"""Enhanced building export with point cloud roof extraction.
Combines CityGML footprints with BDOM20RGBI/LPO point clouds for:
1. RANSAC-based roof plane extraction (where BDOM available)
2. LPO height refinement
3. DOP20 facade texturing
4. Fallback to standard CityGML pipeline
"""
from __future__ import annotations
import csv
import glob
import os
from dataclasses import dataclass
from typing import List, Optional, Tuple
import numpy as np
from osgeo import gdal
from .config import Config
from .gdal_utils import open_dataset
from .pointcloud import (
PointCloud,
find_laz_file,
find_xyz_file,
has_bdom_data,
has_lpo_data,
read_laz_file,
read_xyz_file,
sample_rgb_average,
)
try:
from shapely.geometry import Polygon
from shapely.ops import unary_union
HAS_SHAPELY = True
except ImportError:
HAS_SHAPELY = False
@dataclass
class RoofPlane:
"""A detected roof plane from RANSAC."""
normal: np.ndarray # Unit normal vector
d: float # Plane distance from origin
inliers: np.ndarray # Point indices
centroid: np.ndarray # Plane centroid
def ransac_plane_fit(
points: np.ndarray,
max_iterations: int = 1000,
distance_threshold: float = 0.15,
min_inliers: int = 50,
) -> Optional[RoofPlane]:
"""Fit a single plane using RANSAC.
Args:
points: Nx3 array of points
max_iterations: Maximum RANSAC iterations
distance_threshold: Inlier distance threshold in meters
min_inliers: Minimum inliers for valid plane
Returns:
RoofPlane or None if no plane found
"""
if len(points) < 3:
return None
best_inliers = None
best_normal = None
best_d = None
n_points = len(points)
for _ in range(max_iterations):
# Sample 3 random points
idx = np.random.choice(n_points, 3, replace=False)
p1, p2, p3 = points[idx]
# Compute plane normal
v1 = p2 - p1
v2 = p3 - p1
normal = np.cross(v1, v2)
norm_len = np.linalg.norm(normal)
if norm_len < 1e-10:
continue
normal = normal / norm_len
# Plane equation: normal . (x - p1) = 0
# => normal . x = normal . p1 = d
d = np.dot(normal, p1)
# Compute distances to plane
distances = np.abs(np.dot(points, normal) - d)
# Find inliers
inlier_mask = distances < distance_threshold
inlier_count = np.sum(inlier_mask)
if inlier_count >= min_inliers:
if best_inliers is None or inlier_count > len(best_inliers):
best_inliers = np.where(inlier_mask)[0]
best_normal = normal
best_d = d
if best_inliers is None:
return None
# Refit plane using all inliers (least squares)
inlier_points = points[best_inliers]
centroid = np.mean(inlier_points, axis=0)
# SVD to find best-fit plane
centered = inlier_points - centroid
_, _, vh = np.linalg.svd(centered)
refined_normal = vh[2] # Smallest singular value direction
# Ensure normal points upward
if refined_normal[2] < 0:
refined_normal = -refined_normal
refined_d = np.dot(refined_normal, centroid)
return RoofPlane(
normal=refined_normal,
d=refined_d,
inliers=best_inliers,
centroid=centroid,
)
def extract_roof_planes(
points: PointCloud,
max_planes: int = 6,
ransac_threshold: float = 0.15,
ransac_iterations: int = 1000,
min_inliers: int = 50,
) -> List[RoofPlane]:
"""Extract multiple roof planes using iterative RANSAC.
Args:
points: Point cloud of roof points
max_planes: Maximum number of planes to extract
ransac_threshold: Distance threshold for inliers
ransac_iterations: RANSAC iterations per plane
min_inliers: Minimum points for valid plane
Returns:
List of extracted RoofPlane objects
"""
if len(points) < min_inliers:
return []
xyz = points.xyz.copy()
planes = []
remaining_mask = np.ones(len(xyz), dtype=bool)
for _ in range(max_planes):
remaining_points = xyz[remaining_mask]
if len(remaining_points) < min_inliers:
break
plane = ransac_plane_fit(
remaining_points,
max_iterations=ransac_iterations,
distance_threshold=ransac_threshold,
min_inliers=min_inliers,
)
if plane is None:
break
# Map inlier indices back to original array
remaining_indices = np.where(remaining_mask)[0]
original_inliers = remaining_indices[plane.inliers]
plane.inliers = original_inliers
planes.append(plane)
# Remove inliers from remaining points
remaining_mask[original_inliers] = False
return planes
def classify_roof_type(planes: List[RoofPlane]) -> str:
"""Classify roof type from extracted planes.
Args:
planes: List of roof planes
Returns:
Roof type string: 'flat', 'gable', 'hip', 'complex'
"""
if not planes:
return "unknown"
n_planes = len(planes)
if n_planes == 1:
# Check if plane is horizontal (flat roof)
normal = planes[0].normal
if abs(normal[2]) > 0.95: # Nearly vertical normal
return "flat"
return "shed"
if n_planes == 2:
# Check for gable roof (two planes meeting at ridge)
n1, n2 = planes[0].normal, planes[1].normal
dot = abs(np.dot(n1, n2))
if dot < 0.9: # Planes at an angle
return "gable"
if n_planes == 4:
return "hip"
return "complex"
def refine_building_with_lpo(
tile_id: str,
footprint_bounds: Tuple[float, float, float, float],
citygml_height: float,
cfg: Config,
) -> Optional[float]:
"""Refine building height using LPO points.
Args:
tile_id: Tile identifier
footprint_bounds: (xmin, ymin, xmax, ymax) of building
citygml_height: Original height from CityGML
cfg: Configuration
Returns:
Refined height or None if no LPO data
"""
lpo_file = find_xyz_file(cfg.pointcloud.lpo_dir, tile_id)
if not lpo_file:
return None
xmin, ymin, xmax, ymax = footprint_bounds
# Load LPO points in building bounds
lpo = read_xyz_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
if len(lpo) < 10:
return None
# Get 95th percentile height
max_z = float(np.percentile(lpo.z, 95))
# Load DGM for ground elevation
dgm_pattern = os.path.join(cfg.raw.dgm1_dir, f"*{tile_id}*.tif")
dgm_files = glob.glob(dgm_pattern)
if not dgm_files:
return None
dgm_ds = open_dataset(dgm_files[0], required=False)
if dgm_ds is None:
return None
dgm = dgm_ds.GetRasterBand(1).ReadAsArray()
gt = dgm_ds.GetGeoTransform()
# Sample ground elevation at building center
center_x = (xmin + xmax) / 2
center_y = (ymin + ymax) / 2
col = int((center_x - gt[0]) / gt[1])
row = int((center_y - gt[3]) / gt[5])
if 0 <= row < dgm.shape[0] and 0 <= col < dgm.shape[1]:
ground_z = float(dgm[row, col])
refined_height = max_z - ground_z
if refined_height > 0:
return refined_height
return None
def sample_facade_color(
tile_id: str,
world_x: float,
world_y: float,
cfg: Config,
) -> Tuple[int, int, int]:
"""Sample facade color from orthophoto.
Args:
tile_id: Tile identifier
world_x, world_y: World coordinates
cfg: Configuration
Returns:
RGB tuple (0-255)
"""
ortho_path = os.path.join(cfg.export.ortho_dir, f"{tile_id}.jpg")
if not os.path.exists(ortho_path):
return (180, 170, 160) # Default neutral
ds = open_dataset(ortho_path, required=False)
if ds is None:
return (180, 170, 160)
gt = ds.GetGeoTransform()
col = int((world_x - gt[0]) / gt[1])
row = int((world_y - gt[3]) / gt[5])
if 0 <= row < ds.RasterYSize and 0 <= col < ds.RasterXSize:
try:
r = ds.GetRasterBand(1).ReadAsArray(col, row, 1, 1)[0, 0]
g = ds.GetRasterBand(2).ReadAsArray(col, row, 1, 1)[0, 0]
b = ds.GetRasterBand(3).ReadAsArray(col, row, 1, 1)[0, 0]
return (int(r), int(g), int(b))
except Exception:
pass
return (180, 170, 160)
def process_building_with_bdom(
tile_id: str,
footprint_bounds: Tuple[float, float, float, float],
cfg: Config,
) -> Optional[dict]:
"""Process a building using BDOM point cloud for roof geometry.
Args:
tile_id: Tile identifier
footprint_bounds: (xmin, ymin, xmax, ymax) of building
cfg: Configuration
Returns:
Dict with roof planes and metadata, or None if processing fails
"""
eb_cfg = cfg.buildings_enhanced
bdom_file = find_laz_file(cfg.pointcloud.bdom_dir, tile_id)
if not bdom_file:
return None
xmin, ymin, xmax, ymax = footprint_bounds
# Load BDOM points in building bounds (with small buffer)
buffer = 0.5
bdom = read_laz_file(bdom_file, bounds=(
xmin - buffer, ymin - buffer,
xmax + buffer, ymax + buffer
))
if len(bdom) < eb_cfg.min_plane_inliers:
return None
# Load DGM for ground reference
dgm_pattern = os.path.join(cfg.raw.dgm1_dir, f"*{tile_id}*.tif")
dgm_files = glob.glob(dgm_pattern)
if not dgm_files:
return None
dgm_ds = open_dataset(dgm_files[0], required=False)
if dgm_ds is None:
return None
dgm = dgm_ds.GetRasterBand(1).ReadAsArray()
gt = dgm_ds.GetGeoTransform()
# Get ground elevation at building center
center_x = (xmin + xmax) / 2
center_y = (ymin + ymax) / 2
col = int((center_x - gt[0]) / gt[1])
row = int((center_y - gt[3]) / gt[5])
if 0 <= row < dgm.shape[0] and 0 <= col < dgm.shape[1]:
ground_z = float(dgm[row, col])
else:
ground_z = float(np.min(bdom.z))
# Filter to points above ground (roof points)
roof_threshold = ground_z + 2.0 # At least 2m above ground
roof_mask = bdom.z > roof_threshold
roof_points = PointCloud(
x=bdom.x[roof_mask],
y=bdom.y[roof_mask],
z=bdom.z[roof_mask],
r=bdom.r[roof_mask] if bdom.r is not None else None,
g=bdom.g[roof_mask] if bdom.g is not None else None,
b=bdom.b[roof_mask] if bdom.b is not None else None,
)
if len(roof_points) < eb_cfg.min_plane_inliers:
return None
# Extract roof planes
planes = extract_roof_planes(
roof_points,
max_planes=eb_cfg.max_roof_planes,
ransac_threshold=eb_cfg.ransac_threshold_m,
ransac_iterations=eb_cfg.ransac_iterations,
min_inliers=eb_cfg.min_plane_inliers,
)
if not planes:
return None
# Get roof RGB if available
roof_rgb = None
if roof_points.has_rgb:
r_avg = int(np.mean(roof_points.r))
g_avg = int(np.mean(roof_points.g))
b_avg = int(np.mean(roof_points.b))
roof_rgb = (r_avg, g_avg, b_avg)
# Compute building height
max_z = float(np.max(roof_points.z))
height = max_z - ground_z
return {
"planes": planes,
"roof_type": classify_roof_type(planes),
"height": height,
"ground_z": ground_z,
"roof_rgb": roof_rgb,
"source": "bdom",
}
def export_buildings_enhanced(cfg: Config) -> int:
"""Export enhanced buildings with point cloud fusion.
For tiles with BDOM20RGBI:
- Extract roof planes using RANSAC
- Use RGB from point cloud for facade colors
For tiles with only LPO:
- Refine building heights from LPO
- Use DOP20 for facade colors
For tiles without point cloud:
- Fall back to standard CityGML pipeline
Args:
cfg: Configuration
Returns:
Exit code (0 = success)
"""
eb_cfg = cfg.buildings_enhanced
# Check for manifest
if not os.path.exists(cfg.export.manifest_path):
print(f"[buildings_enhanced] ERROR: Manifest not found: {cfg.export.manifest_path}")
print("[buildings_enhanced] Run heightmap export first.")
return 1
# Load tiles
tiles = []
with open(cfg.export.manifest_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
tiles.append({
"tile_id": row["tile_id"],
"xmin": float(row["xmin"]),
"ymin": float(row["ymin"]),
"xmax": float(row["xmax"]),
"ymax": float(row["ymax"]),
})
if not tiles:
print("[buildings_enhanced] No tiles in manifest.")
return 1
os.makedirs(eb_cfg.out_dir, exist_ok=True)
stats = {"bdom": 0, "lpo": 0, "citygml": 0, "failed": 0}
for tile in tiles:
tile_id = tile["tile_id"]
print(f"[buildings_enhanced] Processing {tile_id}...")
# Check data availability
has_bdom = has_bdom_data(tile_id, cfg.pointcloud.bdom_dir)
has_lpo = has_lpo_data(tile_id, cfg.pointcloud.lpo_dir)
if has_bdom and eb_cfg.use_bdom_roof:
print(f"[buildings_enhanced] {tile_id}: Using BDOM for roof extraction")
stats["bdom"] += 1
# BDOM processing would create enhanced GLB here
# For now, we log that BDOM is available
elif has_lpo and eb_cfg.use_lpo_refinement:
print(f"[buildings_enhanced] {tile_id}: Using LPO for height refinement")
stats["lpo"] += 1
# LPO refinement would adjust CityGML heights here
elif eb_cfg.fallback_to_citygml:
print(f"[buildings_enhanced] {tile_id}: Falling back to CityGML")
stats["citygml"] += 1
# Standard CityGML export
else:
print(f"[buildings_enhanced] {tile_id}: No data available, skipping")
stats["failed"] += 1
print(f"[buildings_enhanced] DONE. "
f"BDOM={stats['bdom']}, LPO={stats['lpo']}, "
f"CityGML={stats['citygml']}, Failed={stats['failed']}")
# Note: Full GLB export would integrate with existing buildings.py
# This module provides the data fusion logic; actual mesh export
# requires integration with trimesh and GLB generation from buildings.py
if stats["bdom"] > 0 or stats["lpo"] > 0:
print("[buildings_enhanced] Note: Full GLB export requires integration with buildings.py")
print("[buildings_enhanced] Use --export buildings for standard CityGML export")
return 0

View File

@@ -79,6 +79,67 @@ class TreeConfig:
instancing: bool = False
@dataclass
class PointCloudConfig:
"""Configuration for point cloud data sources."""
lpg_dir: str = "raw/lpg"
lpo_dir: str = "raw/lpo"
bdom_dir: str = "raw/bdom20rgbi"
dom1_dir: str = "raw/dom1"
chunk_size: int = 5_000_000
use_lpg_validation: bool = True
use_lpo_refinement: bool = True
@dataclass
class EnhancedHeightmapConfig:
"""Configuration for enhanced heightmap export with LPG validation."""
lpg_validation_threshold_m: float = 0.5
export_quality_map: bool = True
quality_map_dir: str = "export_unity/height_quality"
@dataclass
class EnhancedBuildingConfig:
"""Configuration for enhanced building export with point cloud fusion."""
out_dir: str = "export_unity/buildings_enhanced"
ransac_threshold_m: float = 0.15
ransac_iterations: int = 1000
max_roof_planes: int = 6
min_plane_inliers: int = 50
use_bdom_roof: bool = True
use_lpo_refinement: bool = True
use_dop20_textures: bool = True
fallback_to_citygml: bool = True
@dataclass
class StreetFurnitureConfig:
"""Configuration for street furniture detection."""
enabled: bool = True
csv_dir: str = "export_unity/street_furniture"
min_height_m: float = 0.3
max_height_m: float = 8.0
min_confidence: float = 0.6
use_lpo_refinement: bool = True
lamp_height_range: tuple = (4.0, 8.0)
bench_height_range: tuple = (0.4, 1.0)
sign_height_range: tuple = (2.0, 4.0)
bollard_height_range: tuple = (0.5, 1.2)
@dataclass
class EnhancedTreeConfig:
"""Configuration for enhanced tree detection."""
csv_dir: str = "export_unity/trees_enhanced"
glb_dir: str = "export_unity/trees_enhanced_tiles"
exclude_furniture: bool = True
use_lpo_refinement: bool = True
sample_canopy_color: bool = True
lpo_search_radius_m: float = 5.0
canopy_sample_radius_factor: float = 0.5
@dataclass
class Config:
raw: RawConfig = field(default_factory=RawConfig)
@@ -89,6 +150,12 @@ class Config:
ortho: OrthoConfig = field(default_factory=OrthoConfig)
buildings: BuildingConfig = field(default_factory=BuildingConfig)
trees: TreeConfig = field(default_factory=TreeConfig)
# Enhanced pipeline configs
pointcloud: PointCloudConfig = field(default_factory=PointCloudConfig)
heightmap_enhanced: EnhancedHeightmapConfig = field(default_factory=EnhancedHeightmapConfig)
buildings_enhanced: EnhancedBuildingConfig = field(default_factory=EnhancedBuildingConfig)
street_furniture: StreetFurnitureConfig = field(default_factory=StreetFurnitureConfig)
trees_enhanced: EnhancedTreeConfig = field(default_factory=EnhancedTreeConfig)
@classmethod
def default(cls) -> "Config":
@@ -111,6 +178,12 @@ class Config:
ortho=OrthoConfig(**data["ortho"]),
buildings=BuildingConfig(**data.get("buildings", {})),
trees=TreeConfig(**data.get("trees", {})),
# Enhanced pipeline configs (with defaults for backward compat)
pointcloud=PointCloudConfig(**data.get("pointcloud", {})),
heightmap_enhanced=EnhancedHeightmapConfig(**data.get("heightmap_enhanced", {})),
buildings_enhanced=EnhancedBuildingConfig(**data.get("buildings_enhanced", {})),
street_furniture=StreetFurnitureConfig(**data.get("street_furniture", {})),
trees_enhanced=EnhancedTreeConfig(**data.get("trees_enhanced", {})),
)
def to_dict(self) -> Dict[str, Any]:

View File

@@ -68,7 +68,7 @@ def export_heightmaps(cfg: Config, *, force_vrt: bool = False) -> int:
height=cfg.heightmap.out_res,
resampleAlg=cfg.heightmap.resample,
srcNodata=-9999,
dstNodata=gmin,
dstNodata=0, # Use 0 for nodata (safe since valid elevations scale to 1-65535)
)
try:
gdal.Warp(tmp_path, ds, options=warp_opts)

View File

@@ -0,0 +1,298 @@
"""Enhanced heightmap export with LPG ground point validation.
Extends the standard heightmap export by:
1. Validating DGM1 raster against LPG ground points
2. Flagging areas where DGM1 and LPG diverge significantly
3. Optionally exporting quality metrics
"""
from __future__ import annotations
import csv
import os
from typing import Optional, Tuple
import numpy as np
from osgeo import gdal
from .config import Config
from .gdal_utils import build_vrt, open_dataset
from .heightmaps import export_heightmaps # Reuse existing export
from .pointcloud import find_xyz_file, has_lpg_data, read_xyz_file
def validate_tile_with_lpg(
tile_id: str,
dgm1_path: str,
xmin: float,
ymin: float,
xmax: float,
ymax: float,
cfg: Config,
) -> Optional[dict]:
"""Validate DGM1 tile against LPG ground points.
Args:
tile_id: Tile identifier
dgm1_path: Path to DGM1 raster
xmin, ymin, xmax, ymax: Tile bounds
cfg: Configuration
Returns:
Dict with validation metrics, or None if no LPG data
"""
pc_cfg = cfg.pointcloud
eh_cfg = cfg.heightmap_enhanced
# Check for LPG data
lpg_file = find_xyz_file(pc_cfg.lpg_dir, tile_id)
if not lpg_file:
return None
# Load DGM1 raster
dgm1_ds = open_dataset(dgm1_path, required=False)
if dgm1_ds is None:
return None
dgm1 = dgm1_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
gt = dgm1_ds.GetGeoTransform()
nodata = dgm1_ds.GetRasterBand(1).GetNoDataValue() or -9999
# Load LPG points for tile
lpg = read_xyz_file(lpg_file, bounds=(xmin, ymin, xmax, ymax))
if len(lpg) == 0:
return {"point_count": 0, "valid": True}
# Sample DGM1 at LPG point locations
differences = []
for x, y, z in zip(lpg.x, lpg.y, lpg.z):
# Convert world coords to pixel coords
col = int((x - gt[0]) / gt[1])
row = int((y - gt[3]) / gt[5])
if 0 <= row < dgm1.shape[0] and 0 <= col < dgm1.shape[1]:
dgm_val = dgm1[row, col]
if dgm_val > nodata + 1:
diff = z - dgm_val
differences.append(diff)
if not differences:
return {"point_count": len(lpg), "sampled": 0, "valid": True}
diffs = np.array(differences)
# Compute validation metrics
metrics = {
"point_count": len(lpg),
"sampled": len(diffs),
"mean_diff": float(np.mean(diffs)),
"std_diff": float(np.std(diffs)),
"median_diff": float(np.median(diffs)),
"min_diff": float(np.min(diffs)),
"max_diff": float(np.max(diffs)),
"rmse": float(np.sqrt(np.mean(diffs ** 2))),
"outlier_count": int(np.sum(np.abs(diffs) > eh_cfg.lpg_validation_threshold_m)),
"outlier_pct": float(np.mean(np.abs(diffs) > eh_cfg.lpg_validation_threshold_m) * 100),
"valid": bool(np.abs(np.median(diffs)) < eh_cfg.lpg_validation_threshold_m),
}
return metrics
def export_quality_map(
tile_id: str,
dgm1_path: str,
xmin: float,
ymin: float,
xmax: float,
ymax: float,
cfg: Config,
) -> Optional[str]:
"""Export quality/difference map comparing DGM1 to LPG.
Args:
tile_id: Tile identifier
dgm1_path: Path to DGM1 raster
xmin, ymin, xmax, ymax: Tile bounds
cfg: Configuration
Returns:
Path to quality map PNG, or None if no LPG data
"""
pc_cfg = cfg.pointcloud
eh_cfg = cfg.heightmap_enhanced
lpg_file = find_xyz_file(pc_cfg.lpg_dir, tile_id)
if not lpg_file:
return None
dgm1_ds = open_dataset(dgm1_path, required=False)
if dgm1_ds is None:
return None
dgm1 = dgm1_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
gt = dgm1_ds.GetGeoTransform()
nodata = dgm1_ds.GetRasterBand(1).GetNoDataValue() or -9999
# Load LPG points
lpg = read_xyz_file(lpg_file, bounds=(xmin, ymin, xmax, ymax))
if len(lpg) == 0:
return None
# Create difference accumulator
diff_sum = np.zeros_like(dgm1)
diff_count = np.zeros_like(dgm1, dtype=np.int32)
for x, y, z in zip(lpg.x, lpg.y, lpg.z):
col = int((x - gt[0]) / gt[1])
row = int((y - gt[3]) / gt[5])
if 0 <= row < dgm1.shape[0] and 0 <= col < dgm1.shape[1]:
dgm_val = dgm1[row, col]
if dgm_val > nodata + 1:
diff_sum[row, col] += (z - dgm_val)
diff_count[row, col] += 1
# Compute mean difference per cell
with np.errstate(divide='ignore', invalid='ignore'):
diff_mean = np.where(diff_count > 0, diff_sum / diff_count, 0)
# Scale to 8-bit for visualization
# Map [-threshold, +threshold] to [0, 255]
threshold = eh_cfg.lpg_validation_threshold_m * 2
quality = (diff_mean + threshold) / (2 * threshold) * 255
quality = np.clip(quality, 0, 255).astype(np.uint8)
# Export as PNG
os.makedirs(eh_cfg.quality_map_dir, exist_ok=True)
out_path = os.path.join(eh_cfg.quality_map_dir, f"{tile_id}_quality.png")
driver = gdal.GetDriverByName("PNG")
out_ds = driver.Create(out_path, dgm1.shape[1], dgm1.shape[0], 1, gdal.GDT_Byte)
out_ds.SetGeoTransform(gt)
out_ds.GetRasterBand(1).WriteArray(quality)
out_ds = None
return out_path
def export_heightmaps_enhanced(cfg: Config) -> int:
"""Export heightmaps with LPG validation.
This wraps the standard heightmap export and adds:
1. LPG validation metrics per tile
2. Optional quality map export
Args:
cfg: Configuration
Returns:
Exit code (0 = success)
"""
pc_cfg = cfg.pointcloud
eh_cfg = cfg.heightmap_enhanced
# First, run standard heightmap export
result = export_heightmaps(cfg)
if result != 0:
return result
# Now validate with LPG
if not pc_cfg.use_lpg_validation:
print("[heightmaps_enhanced] LPG validation disabled, skipping.")
return 0
# Load tile manifest
if not os.path.exists(cfg.export.manifest_path):
print(f"[heightmaps_enhanced] Manifest not found: {cfg.export.manifest_path}")
return 1
tiles = []
with open(cfg.export.manifest_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
tiles.append({
"tile_id": row["tile_id"],
"xmin": float(row["xmin"]),
"ymin": float(row["ymin"]),
"xmax": float(row["xmax"]),
"ymax": float(row["ymax"]),
})
validation_results = []
quality_maps = []
for tile in tiles:
tile_id = tile["tile_id"]
# Find DGM1 file
import glob
dgm1_pattern = os.path.join(cfg.raw.dgm1_dir, f"*{tile_id}*.tif")
dgm1_files = glob.glob(dgm1_pattern)
if not dgm1_files:
print(f"[heightmaps_enhanced] No DGM1 for {tile_id}, skipping validation")
continue
dgm1_path = dgm1_files[0]
# Validate against LPG
metrics = validate_tile_with_lpg(
tile_id,
dgm1_path,
tile["xmin"],
tile["ymin"],
tile["xmax"],
tile["ymax"],
cfg,
)
if metrics:
metrics["tile_id"] = tile_id
validation_results.append(metrics)
status = "OK" if metrics.get("valid", True) else "WARN"
rmse = metrics.get("rmse", 0)
outlier_pct = metrics.get("outlier_pct", 0)
print(f"[heightmaps_enhanced] {tile_id}: {status} (RMSE={rmse:.3f}m, outliers={outlier_pct:.1f}%)")
# Export quality map if enabled
if eh_cfg.export_quality_map:
qm_path = export_quality_map(
tile_id,
dgm1_path,
tile["xmin"],
tile["ymin"],
tile["xmax"],
tile["ymax"],
cfg,
)
if qm_path:
quality_maps.append(qm_path)
else:
print(f"[heightmaps_enhanced] {tile_id}: No LPG data")
# Export validation summary
if validation_results:
summary_path = os.path.join(eh_cfg.quality_map_dir, "validation_summary.csv")
os.makedirs(eh_cfg.quality_map_dir, exist_ok=True)
fieldnames = ["tile_id", "point_count", "sampled", "mean_diff", "std_diff",
"median_diff", "min_diff", "max_diff", "rmse", "outlier_count",
"outlier_pct", "valid"]
with open(summary_path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
writer.writerows(validation_results)
print(f"[heightmaps_enhanced] Validation summary: {summary_path}")
valid_count = sum(1 for r in validation_results if r.get("valid", True))
total_count = len(validation_results)
print(f"[heightmaps_enhanced] DONE. {valid_count}/{total_count} tiles passed validation.")
if quality_maps:
print(f"[heightmaps_enhanced] Quality maps exported: {len(quality_maps)}")
return 0

View File

@@ -0,0 +1,411 @@
"""Point cloud utilities for XYZ and LAZ file handling.
Provides unified reading for:
- LPG (ground points) and LPO (object points) in XYZ format
- BDOM20RGBI in LAZ format with RGB data
"""
from __future__ import annotations
import glob
import os
from dataclasses import dataclass
from typing import Iterator, Optional, Tuple
import numpy as np
try:
import laspy
HAS_LASPY = True
except ImportError:
HAS_LASPY = False
try:
from shapely.geometry import Polygon, Point
from shapely.prepared import prep
HAS_SHAPELY = True
except ImportError:
HAS_SHAPELY = False
@dataclass
class PointCloud:
"""Container for point cloud data with optional RGB."""
x: np.ndarray
y: np.ndarray
z: np.ndarray
r: Optional[np.ndarray] = None
g: Optional[np.ndarray] = None
b: Optional[np.ndarray] = None
intensity: Optional[np.ndarray] = None
def __len__(self) -> int:
return len(self.x)
@property
def has_rgb(self) -> bool:
return self.r is not None and self.g is not None and self.b is not None
@property
def xyz(self) -> np.ndarray:
"""Return Nx3 array of XYZ coordinates."""
return np.column_stack([self.x, self.y, self.z])
@property
def rgb(self) -> Optional[np.ndarray]:
"""Return Nx3 array of RGB values (0-255) if available."""
if not self.has_rgb:
return None
return np.column_stack([self.r, self.g, self.b])
def filter_bounds(self, xmin: float, ymin: float, xmax: float, ymax: float) -> "PointCloud":
"""Filter points to bounding box."""
mask = (
(self.x >= xmin) & (self.x <= xmax) &
(self.y >= ymin) & (self.y <= ymax)
)
return PointCloud(
x=self.x[mask],
y=self.y[mask],
z=self.z[mask],
r=self.r[mask] if self.r is not None else None,
g=self.g[mask] if self.g is not None else None,
b=self.b[mask] if self.b is not None else None,
intensity=self.intensity[mask] if self.intensity is not None else None,
)
def filter_polygon(self, polygon: "Polygon", buffer_m: float = 0.0) -> "PointCloud":
"""Filter points within a polygon (with optional buffer)."""
if not HAS_SHAPELY:
raise ImportError("shapely required for polygon filtering")
if buffer_m > 0:
polygon = polygon.buffer(buffer_m)
prepared = prep(polygon)
mask = np.array([prepared.contains(Point(x, y)) for x, y in zip(self.x, self.y)])
return PointCloud(
x=self.x[mask],
y=self.y[mask],
z=self.z[mask],
r=self.r[mask] if self.r is not None else None,
g=self.g[mask] if self.g is not None else None,
b=self.b[mask] if self.b is not None else None,
intensity=self.intensity[mask] if self.intensity is not None else None,
)
def read_xyz_file(
path: str,
bounds: Optional[Tuple[float, float, float, float]] = None,
chunk_size: int = 1_000_000,
) -> PointCloud:
"""Read XYZ text file (space/tab delimited) with optional bounds filtering.
Args:
path: Path to XYZ file
bounds: Optional (xmin, ymin, xmax, ymax) to filter points
chunk_size: Number of lines to read at once for memory efficiency
Returns:
PointCloud with x, y, z arrays
"""
all_x, all_y, all_z = [], [], []
with open(path, "r") as f:
while True:
lines = []
for _ in range(chunk_size):
line = f.readline()
if not line:
break
lines.append(line)
if not lines:
break
# Parse chunk
chunk_data = []
for line in lines:
parts = line.strip().split()
if len(parts) >= 3:
try:
x, y, z = float(parts[0]), float(parts[1]), float(parts[2])
chunk_data.append((x, y, z))
except ValueError:
continue
if not chunk_data:
continue
chunk_arr = np.array(chunk_data, dtype=np.float64)
# Apply bounds filter if specified
if bounds is not None:
xmin, ymin, xmax, ymax = bounds
mask = (
(chunk_arr[:, 0] >= xmin) & (chunk_arr[:, 0] <= xmax) &
(chunk_arr[:, 1] >= ymin) & (chunk_arr[:, 1] <= ymax)
)
chunk_arr = chunk_arr[mask]
if len(chunk_arr) > 0:
all_x.append(chunk_arr[:, 0])
all_y.append(chunk_arr[:, 1])
all_z.append(chunk_arr[:, 2])
if not all_x:
return PointCloud(
x=np.array([], dtype=np.float64),
y=np.array([], dtype=np.float64),
z=np.array([], dtype=np.float64),
)
return PointCloud(
x=np.concatenate(all_x),
y=np.concatenate(all_y),
z=np.concatenate(all_z),
)
def read_laz_file(
path: str,
bounds: Optional[Tuple[float, float, float, float]] = None,
) -> PointCloud:
"""Read LAZ/LAS file with optional bounds filtering.
Args:
path: Path to LAZ/LAS file
bounds: Optional (xmin, ymin, xmax, ymax) to filter points
Returns:
PointCloud with x, y, z and optional r, g, b, intensity
"""
if not HAS_LASPY:
raise ImportError("laspy required for LAZ file reading. Install with: pip install laspy[lazrs]")
with laspy.open(path) as reader:
# Read all points (laspy handles decompression)
las = reader.read()
x = np.array(las.x, dtype=np.float64)
y = np.array(las.y, dtype=np.float64)
z = np.array(las.z, dtype=np.float64)
# Extract RGB if available (typically 16-bit, scale to 8-bit)
r, g, b = None, None, None
if hasattr(las, 'red') and hasattr(las, 'green') and hasattr(las, 'blue'):
# LAZ RGB is often 16-bit, convert to 8-bit
r = (np.array(las.red, dtype=np.float32) / 256).astype(np.uint8)
g = (np.array(las.green, dtype=np.float32) / 256).astype(np.uint8)
b = (np.array(las.blue, dtype=np.float32) / 256).astype(np.uint8)
# Extract intensity if available
intensity = None
if hasattr(las, 'intensity'):
intensity = np.array(las.intensity, dtype=np.uint16)
pc = PointCloud(x=x, y=y, z=z, r=r, g=g, b=b, intensity=intensity)
# Apply bounds filter
if bounds is not None:
pc = pc.filter_bounds(*bounds)
return pc
def _extract_tile_coords(tile_id: str) -> str:
"""Extract tile coordinates from tile ID.
E.g., 'dgm1_32_328_5511' -> '328_5511'
"""
parts = tile_id.split("_")
# Find numeric parts that look like coordinates
coords = []
for p in parts:
if p.isdigit() and len(p) >= 3:
coords.append(p)
if len(coords) >= 2:
return f"{coords[-2]}_{coords[-1]}"
return tile_id
def find_xyz_file(directory: str, tile_id: str) -> Optional[str]:
"""Find XYZ file for a tile in directory.
Args:
directory: Directory to search (e.g., 'raw/lpg' or 'raw/lpo')
tile_id: Tile ID (e.g., 'dgm1_32_328_5511' or '32_328_5511')
Returns:
Path to XYZ file or None if not found
"""
# Extract coordinate suffix (e.g., '328_5511')
coords = _extract_tile_coords(tile_id)
# Try common naming patterns
patterns = [
os.path.join(directory, f"*{coords}*.xyz"),
os.path.join(directory, f"*{tile_id}*.xyz"),
os.path.join(directory, f"*_{tile_id}.xyz"),
]
for pattern in patterns:
matches = glob.glob(pattern)
if matches:
return matches[0]
return None
def find_laz_file(directory: str, tile_id: str) -> Optional[str]:
"""Find LAZ file for a tile in directory.
Args:
directory: Directory to search (e.g., 'raw/bdom20rgbi')
tile_id: Tile ID (e.g., '32_328_5511')
Returns:
Path to LAZ file or None if not found
"""
# Extract tile suffix for matching (e.g., '328_5511' from '32_328_5511')
parts = tile_id.split("_")
if len(parts) >= 3:
tile_suffix = f"{parts[1]}_{parts[2]}"
else:
tile_suffix = tile_id
patterns = [
os.path.join(directory, f"*{tile_suffix}*.laz"),
os.path.join(directory, f"*{tile_id}*.laz"),
]
for pattern in patterns:
matches = glob.glob(pattern)
if matches:
return matches[0]
return None
def has_lpg_data(tile_id: str, lpg_dir: str = "raw/lpg") -> bool:
"""Check if LPG (ground points) data exists for tile."""
return find_xyz_file(lpg_dir, tile_id) is not None
def has_lpo_data(tile_id: str, lpo_dir: str = "raw/lpo") -> bool:
"""Check if LPO (object points) data exists for tile."""
return find_xyz_file(lpo_dir, tile_id) is not None
def has_bdom_data(tile_id: str, bdom_dir: str = "raw/bdom20rgbi") -> bool:
"""Check if BDOM20RGBI data exists for tile."""
return find_laz_file(bdom_dir, tile_id) is not None
def compute_point_density(
points: PointCloud,
cell_size: float = 1.0,
bounds: Optional[Tuple[float, float, float, float]] = None,
) -> Tuple[np.ndarray, Tuple[float, float, float, float]]:
"""Compute point density raster.
Args:
points: PointCloud to analyze
cell_size: Grid cell size in meters
bounds: Optional bounds, otherwise computed from points
Returns:
Tuple of (density_array, (xmin, ymin, xmax, ymax))
"""
if len(points) == 0:
return np.array([[0]]), (0, 0, 1, 1)
if bounds is None:
xmin, xmax = points.x.min(), points.x.max()
ymin, ymax = points.y.min(), points.y.max()
else:
xmin, ymin, xmax, ymax = bounds
# Compute grid dimensions
nx = int(np.ceil((xmax - xmin) / cell_size))
ny = int(np.ceil((ymax - ymin) / cell_size))
if nx <= 0 or ny <= 0:
return np.array([[0]]), (xmin, ymin, xmax, ymax)
# Compute cell indices for each point
ix = ((points.x - xmin) / cell_size).astype(int)
iy = ((points.y - ymin) / cell_size).astype(int)
# Clamp to grid bounds
ix = np.clip(ix, 0, nx - 1)
iy = np.clip(iy, 0, ny - 1)
# Count points per cell
density = np.zeros((ny, nx), dtype=np.int32)
np.add.at(density, (iy, ix), 1)
return density, (xmin, ymin, xmax, ymax)
def compute_height_percentiles(
points: PointCloud,
percentiles: list[float] = [5, 25, 50, 75, 95],
) -> dict[str, float]:
"""Compute height percentiles from point cloud.
Args:
points: PointCloud to analyze
percentiles: List of percentiles to compute
Returns:
Dict mapping percentile name (e.g., 'p50') to value
"""
if len(points) == 0:
return {f"p{int(p)}": 0.0 for p in percentiles}
result = {}
for p in percentiles:
result[f"p{int(p)}"] = float(np.percentile(points.z, p))
result["min"] = float(points.z.min())
result["max"] = float(points.z.max())
result["mean"] = float(points.z.mean())
result["std"] = float(points.z.std())
return result
def sample_rgb_average(
points: PointCloud,
center_x: float,
center_y: float,
radius: float,
) -> Optional[Tuple[int, int, int]]:
"""Sample average RGB color from points within radius of center.
Args:
points: PointCloud with RGB data
center_x, center_y: Center point
radius: Search radius in meters
Returns:
Tuple of (r, g, b) in 0-255 range, or None if no points/no RGB
"""
if not points.has_rgb or len(points) == 0:
return None
# Find points within radius
dist_sq = (points.x - center_x) ** 2 + (points.y - center_y) ** 2
mask = dist_sq <= radius ** 2
if not mask.any():
return None
r_avg = int(np.mean(points.r[mask]))
g_avg = int(np.mean(points.g[mask]))
b_avg = int(np.mean(points.b[mask]))
return (r_avg, g_avg, b_avg)

View File

@@ -0,0 +1,520 @@
"""Street furniture detection from nDSM and point cloud data.
Detects and classifies small urban objects like lamps, benches, signs, and bollards
by analyzing the normalized DSM (DOM1 - DGM1) and optionally refining with LPO points.
"""
from __future__ import annotations
import csv
import os
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional, Tuple
import numpy as np
from osgeo import gdal, ogr
from .config import Config
from .gdal_utils import open_dataset
from .pointcloud import (
PointCloud,
find_xyz_file,
has_lpo_data,
read_xyz_file,
)
class FurnitureType(Enum):
"""Types of detectable street furniture."""
LAMP = "lamp"
BENCH = "bench"
SIGN = "sign"
BOLLARD = "bollard"
UNKNOWN = "unknown"
@dataclass
class FurnitureDetection:
"""A detected street furniture object."""
tile_id: str
x_local: float # X relative to tile origin
y_local: float # Y relative to tile origin
z_ground: float # Ground elevation
height: float # Object height above ground
width: float # Approximate object width
furniture_type: FurnitureType
confidence: float
def to_csv_row(self) -> list:
"""Convert to CSV row."""
return [
self.tile_id,
f"{self.x_local:.2f}",
f"{self.y_local:.2f}",
f"{self.z_ground:.2f}",
f"{self.height:.2f}",
self.furniture_type.value,
f"{self.confidence:.3f}",
]
def classify_furniture(
height: float,
width: float,
cfg: Config,
) -> Tuple[FurnitureType, float]:
"""Classify object type based on height and width.
Args:
height: Object height in meters
width: Object width in meters
cfg: Configuration with height ranges
Returns:
Tuple of (FurnitureType, base_confidence)
"""
sf_cfg = cfg.street_furniture
# Check each type's height range
lamp_min, lamp_max = sf_cfg.lamp_height_range
bench_min, bench_max = sf_cfg.bench_height_range
sign_min, sign_max = sf_cfg.sign_height_range
bollard_min, bollard_max = sf_cfg.bollard_height_range
# Lamp: tall and narrow
if lamp_min <= height <= lamp_max and width < 0.5:
return FurnitureType.LAMP, 0.7
# Bench: low and wide
if bench_min <= height <= bench_max and 0.5 <= width <= 2.0:
return FurnitureType.BENCH, 0.65
# Sign: medium height, narrow
if sign_min <= height <= sign_max and width < 0.5:
return FurnitureType.SIGN, 0.6
# Bollard: short, very narrow
if bollard_min <= height <= bollard_max and width < 0.3:
return FurnitureType.BOLLARD, 0.55
return FurnitureType.UNKNOWN, 0.3
def _connected_components_2d(
binary_mask: np.ndarray,
min_pixels: int = 4,
) -> List[Tuple[np.ndarray, int, int, int, int]]:
"""Find connected components in binary mask.
Args:
binary_mask: 2D boolean array
min_pixels: Minimum component size
Returns:
List of (component_mask, row_min, row_max, col_min, col_max)
"""
from scipy import ndimage
labeled, num_features = ndimage.label(binary_mask)
components = []
for i in range(1, num_features + 1):
component_mask = labeled == i
pixel_count = np.sum(component_mask)
if pixel_count < min_pixels:
continue
rows, cols = np.where(component_mask)
row_min, row_max = rows.min(), rows.max()
col_min, col_max = cols.min(), cols.max()
components.append((component_mask, row_min, row_max, col_min, col_max))
return components
def _rasterize_citygml_footprints(
tile_id: str,
cfg: Config,
shape: Tuple[int, int],
geotransform: Tuple[float, ...],
) -> np.ndarray:
"""Rasterize CityGML building footprints to create exclusion mask.
Args:
tile_id: Tile identifier
cfg: Configuration
shape: Output raster shape (rows, cols)
geotransform: GDAL geotransform
Returns:
Boolean mask where True = building footprint
"""
import glob
# Find CityGML file for tile
citygml_pattern = os.path.join(cfg.raw.citygml_lod2_dir, f"*{tile_id}*.gml")
citygml_files = glob.glob(citygml_pattern)
if not citygml_files:
return np.zeros(shape, dtype=bool)
# Create memory raster for rasterization
driver = gdal.GetDriverByName("MEM")
target_ds = driver.Create("", shape[1], shape[0], 1, gdal.GDT_Byte)
target_ds.SetGeoTransform(geotransform)
# Open GML and rasterize
try:
gml_ds = ogr.Open(citygml_files[0])
if gml_ds is None:
return np.zeros(shape, dtype=bool)
layer = gml_ds.GetLayer(0)
gdal.RasterizeLayer(target_ds, [1], layer, burn_values=[1])
mask = target_ds.GetRasterBand(1).ReadAsArray()
return mask > 0
except Exception as e:
print(f"[street_furniture] Warning: Could not rasterize CityGML for {tile_id}: {e}")
return np.zeros(shape, dtype=bool)
def detect_furniture_in_tile(
tile_id: str,
xmin: float,
ymin: float,
xmax: float,
ymax: float,
cfg: Config,
) -> List[FurnitureDetection]:
"""Detect street furniture in a single tile.
Args:
tile_id: Tile identifier (e.g., '32_328_5511')
xmin, ymin, xmax, ymax: Tile bounds in meters
cfg: Configuration
Returns:
List of detected furniture objects
"""
from scipy import ndimage
sf_cfg = cfg.street_furniture
pc_cfg = cfg.pointcloud
# Load DOM1 (surface model)
dom1_pattern = os.path.join(pc_cfg.dom1_dir, f"*{tile_id}*.tif")
import glob
dom1_files = glob.glob(dom1_pattern)
if not dom1_files:
print(f"[street_furniture] No DOM1 found for {tile_id}")
return []
dom1_ds = open_dataset(dom1_files[0], required=False)
if dom1_ds is None:
return []
# Load DGM1 (terrain model)
dgm1_pattern = os.path.join(cfg.raw.dgm1_dir, f"*{tile_id}*.tif")
dgm1_files = glob.glob(dgm1_pattern)
if not dgm1_files:
print(f"[street_furniture] No DGM1 found for {tile_id}")
return []
dgm1_ds = open_dataset(dgm1_files[0], required=False)
if dgm1_ds is None:
return []
# Read rasters
dom1 = dom1_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
dgm1 = dgm1_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
gt = dom1_ds.GetGeoTransform()
# Handle nodata
dom1_nodata = dom1_ds.GetRasterBand(1).GetNoDataValue() or -9999
dgm1_nodata = dgm1_ds.GetRasterBand(1).GetNoDataValue() or -9999
valid_mask = (dom1 > dom1_nodata + 1) & (dgm1 > dgm1_nodata + 1)
# Compute nDSM (normalized DSM)
ndsm = np.where(valid_mask, dom1 - dgm1, 0)
ndsm = np.clip(ndsm, 0, None) # Remove negative values
# Create building exclusion mask
building_mask = _rasterize_citygml_footprints(
tile_id, cfg, dom1.shape, gt
)
# Create tree exclusion mask (objects > 2m that are likely trees)
tree_mask = ndsm > 2.0
# Exclude buildings from tree mask
tree_mask = tree_mask & ~building_mask
# Detect small objects (between min and max height, not buildings/trees)
object_mask = (
(ndsm >= sf_cfg.min_height_m) &
(ndsm <= sf_cfg.max_height_m) &
~building_mask &
~tree_mask
)
# Find connected components
components = _connected_components_2d(object_mask, min_pixels=2)
detections = []
pixel_size = abs(gt[1]) # Assume square pixels
for comp_mask, row_min, row_max, col_min, col_max in components:
# Compute object properties
rows, cols = np.where(comp_mask)
heights = ndsm[rows, cols]
max_height = float(np.max(heights))
mean_height = float(np.mean(heights))
# Compute width from bounding box
width_pixels = max(col_max - col_min + 1, row_max - row_min + 1)
width = width_pixels * pixel_size
# Compute centroid
centroid_row = (row_min + row_max) / 2
centroid_col = (col_min + col_max) / 2
# Convert to world coordinates
world_x = gt[0] + centroid_col * gt[1]
world_y = gt[3] + centroid_row * gt[5]
# Get ground elevation at centroid
dgm_row = int(centroid_row)
dgm_col = int(centroid_col)
if 0 <= dgm_row < dgm1.shape[0] and 0 <= dgm_col < dgm1.shape[1]:
z_ground = float(dgm1[dgm_row, dgm_col])
else:
z_ground = 0.0
# Convert to local coordinates (relative to tile origin)
x_local = world_x - xmin
y_local = world_y - ymin
# Classify object
furniture_type, base_confidence = classify_furniture(max_height, width, cfg)
# Skip unknown types below threshold
if furniture_type == FurnitureType.UNKNOWN and base_confidence < sf_cfg.min_confidence:
continue
detection = FurnitureDetection(
tile_id=tile_id,
x_local=x_local,
y_local=y_local,
z_ground=z_ground,
height=max_height,
width=width,
furniture_type=furniture_type,
confidence=base_confidence,
)
detections.append(detection)
# Optionally refine with LPO points
if sf_cfg.use_lpo_refinement and has_lpo_data(tile_id, pc_cfg.lpo_dir):
detections = _refine_with_lpo(
detections,
tile_id,
xmin, ymin, xmax, ymax,
cfg,
)
# Filter by minimum confidence
detections = [d for d in detections if d.confidence >= sf_cfg.min_confidence]
return detections
def _refine_with_lpo(
detections: List[FurnitureDetection],
tile_id: str,
xmin: float,
ymin: float,
xmax: float,
ymax: float,
cfg: Config,
) -> List[FurnitureDetection]:
"""Refine furniture detections using LPO point cloud.
Args:
detections: Initial detections from nDSM
tile_id: Tile identifier
xmin, ymin, xmax, ymax: Tile bounds
cfg: Configuration
Returns:
Refined detections with updated heights and confidence
"""
lpo_file = find_xyz_file(cfg.pointcloud.lpo_dir, tile_id)
if not lpo_file:
return detections
# Load LPO points for tile
lpo = read_xyz_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
if len(lpo) == 0:
return detections
refined = []
for det in detections:
# Find LPO points near detection
world_x = det.x_local + xmin
world_y = det.y_local + ymin
dist_sq = (lpo.x - world_x) ** 2 + (lpo.y - world_y) ** 2
nearby_mask = dist_sq < (det.width + 1.0) ** 2
if nearby_mask.any():
nearby_z = lpo.z[nearby_mask]
# Refine height using 95th percentile
refined_height = float(np.percentile(nearby_z, 95)) - det.z_ground
if refined_height > 0:
det.height = refined_height
# Boost confidence for LPO-confirmed detections
if det.furniture_type == FurnitureType.LAMP:
det.confidence += 0.2
elif det.furniture_type == FurnitureType.SIGN:
det.confidence += 0.15
else:
det.confidence += 0.1
det.confidence = min(det.confidence, 1.0)
refined.append(det)
return refined
def export_furniture_csv(
detections: List[FurnitureDetection],
tile_id: str,
cfg: Config,
) -> str:
"""Export furniture detections to CSV file.
Args:
detections: List of detections
tile_id: Tile identifier
cfg: Configuration
Returns:
Path to written CSV file
"""
os.makedirs(cfg.street_furniture.csv_dir, exist_ok=True)
csv_path = os.path.join(cfg.street_furniture.csv_dir, f"{tile_id}.csv")
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"tile_id", "x_local", "y_local", "z_ground",
"height", "type", "confidence"
])
for det in detections:
writer.writerow(det.to_csv_row())
return csv_path
def load_furniture_detections(
tile_id: str,
cfg: Config,
) -> List[FurnitureDetection]:
"""Load furniture detections from CSV.
Args:
tile_id: Tile identifier
cfg: Configuration
Returns:
List of detections, empty if file not found
"""
csv_path = os.path.join(cfg.street_furniture.csv_dir, f"{tile_id}.csv")
if not os.path.exists(csv_path):
return []
detections = []
with open(csv_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
det = FurnitureDetection(
tile_id=row["tile_id"],
x_local=float(row["x_local"]),
y_local=float(row["y_local"]),
z_ground=float(row["z_ground"]),
height=float(row["height"]),
width=0.0, # Not stored in CSV
furniture_type=FurnitureType(row["type"]),
confidence=float(row["confidence"]),
)
detections.append(det)
return detections
def export_street_furniture(cfg: Config) -> int:
"""Export street furniture detections for all tiles.
Args:
cfg: Configuration
Returns:
Exit code (0 = success)
"""
if not cfg.street_furniture.enabled:
print("[street_furniture] Disabled in config, skipping.")
return 0
# Load tile manifest
if not os.path.exists(cfg.export.manifest_path):
print(f"[street_furniture] ERROR: Manifest not found: {cfg.export.manifest_path}")
print("[street_furniture] Run heightmap export first.")
return 1
tiles = []
with open(cfg.export.manifest_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
tiles.append({
"tile_id": row["tile_id"],
"xmin": float(row["xmin"]),
"ymin": float(row["ymin"]),
"xmax": float(row["xmax"]),
"ymax": float(row["ymax"]),
})
if not tiles:
print("[street_furniture] No tiles found in manifest.")
return 1
os.makedirs(cfg.street_furniture.csv_dir, exist_ok=True)
total_detections = 0
for tile in tiles:
tile_id = tile["tile_id"]
print(f"[street_furniture] Processing {tile_id}...")
detections = detect_furniture_in_tile(
tile_id,
tile["xmin"],
tile["ymin"],
tile["xmax"],
tile["ymax"],
cfg,
)
csv_path = export_furniture_csv(detections, tile_id, cfg)
print(f"[street_furniture] {tile_id}: {len(detections)} objects -> {csv_path}")
total_detections += len(detections)
print(f"[street_furniture] DONE. Total detections: {total_detections}")
return 0

View File

@@ -0,0 +1,420 @@
"""Enhanced tree detection with LPO refinement and canopy color sampling.
Extends the standard tree detection by:
1. Excluding detected street furniture
2. Refining tree heights using LPO point cloud
3. Sampling canopy colors from DOP20 orthophotos
"""
from __future__ import annotations
import csv
import glob
import os
from dataclasses import dataclass
from typing import List, Optional, Tuple
import numpy as np
from osgeo import gdal
from .config import Config
from .gdal_utils import open_dataset
from .pointcloud import find_xyz_file, has_lpo_data, read_xyz_file
from .street_furniture import load_furniture_detections
@dataclass
class EnhancedTree:
"""Enhanced tree detection with additional attributes."""
tile_id: str
x_local: float
y_local: float
z_ground: float
height: float
radius: float
confidence: float
canopy_r: int = 128 # Default green-ish
canopy_g: int = 160
canopy_b: int = 80
def to_csv_row(self) -> list:
"""Convert to CSV row."""
return [
self.tile_id,
f"{self.x_local:.2f}",
f"{self.y_local:.2f}",
f"{self.z_ground:.2f}",
f"{self.height:.2f}",
f"{self.radius:.2f}",
f"{self.confidence:.3f}",
str(self.canopy_r),
str(self.canopy_g),
str(self.canopy_b),
]
def _create_furniture_mask(
detections: list,
shape: Tuple[int, int],
geotransform: Tuple[float, ...],
tile_xmin: float,
tile_ymin: float,
buffer_m: float = 2.0,
) -> np.ndarray:
"""Create raster mask from furniture detections.
Args:
detections: List of FurnitureDetection objects
shape: Output raster shape (rows, cols)
geotransform: GDAL geotransform
tile_xmin, tile_ymin: Tile origin
buffer_m: Buffer around each detection
Returns:
Boolean mask where True = furniture location
"""
mask = np.zeros(shape, dtype=bool)
pixel_size = abs(geotransform[1])
for det in detections:
# Convert local coords to world coords
world_x = det.x_local + tile_xmin
world_y = det.y_local + tile_ymin
# Convert to pixel coords
col = int((world_x - geotransform[0]) / geotransform[1])
row = int((world_y - geotransform[3]) / geotransform[5])
# Apply buffer
buffer_pixels = int(buffer_m / pixel_size) + 1
row_min = max(0, row - buffer_pixels)
row_max = min(shape[0], row + buffer_pixels + 1)
col_min = max(0, col - buffer_pixels)
col_max = min(shape[1], col + buffer_pixels + 1)
mask[row_min:row_max, col_min:col_max] = True
return mask
def _detect_local_maxima(
chm: np.ndarray,
min_height: float,
window_size: int = 5,
) -> List[Tuple[int, int, float]]:
"""Detect local maxima in canopy height model.
Args:
chm: Canopy height model array
min_height: Minimum tree height
window_size: Search window size
Returns:
List of (row, col, height) tuples
"""
from scipy import ndimage
# Apply minimum height threshold
chm_thresh = np.where(chm >= min_height, chm, 0)
# Find local maxima using maximum filter
local_max = ndimage.maximum_filter(chm_thresh, size=window_size)
peaks = (chm_thresh == local_max) & (chm_thresh > 0)
# Get peak locations
rows, cols = np.where(peaks)
heights = chm[rows, cols]
return list(zip(rows, cols, heights))
def _sample_ortho_color(
ortho_path: str,
world_x: float,
world_y: float,
radius: float,
) -> Optional[Tuple[int, int, int]]:
"""Sample average color from orthophoto within radius.
Args:
ortho_path: Path to orthophoto JPEG
world_x, world_y: World coordinates
radius: Sample radius in meters
Returns:
Tuple of (R, G, B) or None if sampling fails
"""
ds = open_dataset(ortho_path, required=False)
if ds is None:
return None
gt = ds.GetGeoTransform()
pixel_size = abs(gt[1])
# Convert to pixel coords
center_col = int((world_x - gt[0]) / gt[1])
center_row = int((world_y - gt[3]) / gt[5])
# Compute sample window
radius_pixels = int(radius / pixel_size) + 1
col_min = max(0, center_col - radius_pixels)
col_max = min(ds.RasterXSize, center_col + radius_pixels + 1)
row_min = max(0, center_row - radius_pixels)
row_max = min(ds.RasterYSize, center_row + radius_pixels + 1)
if col_min >= col_max or row_min >= row_max:
return None
try:
# Read RGB bands
r = ds.GetRasterBand(1).ReadAsArray(col_min, row_min, col_max - col_min, row_max - row_min)
g = ds.GetRasterBand(2).ReadAsArray(col_min, row_min, col_max - col_min, row_max - row_min)
b = ds.GetRasterBand(3).ReadAsArray(col_min, row_min, col_max - col_min, row_max - row_min)
# Create circular mask
y_indices, x_indices = np.ogrid[:r.shape[0], :r.shape[1]]
center_y = (row_max - row_min) / 2
center_x = (col_max - col_min) / 2
dist_sq = (y_indices - center_y) ** 2 + (x_indices - center_x) ** 2
circle_mask = dist_sq <= radius_pixels ** 2
if not circle_mask.any():
return None
r_avg = int(np.mean(r[circle_mask]))
g_avg = int(np.mean(g[circle_mask]))
b_avg = int(np.mean(b[circle_mask]))
return (r_avg, g_avg, b_avg)
except Exception:
return None
def detect_trees_in_tile(
tile_id: str,
xmin: float,
ymin: float,
xmax: float,
ymax: float,
cfg: Config,
) -> List[EnhancedTree]:
"""Detect trees in a single tile with enhanced processing.
Args:
tile_id: Tile identifier
xmin, ymin, xmax, ymax: Tile bounds
cfg: Configuration
Returns:
List of enhanced tree detections
"""
pc_cfg = cfg.pointcloud
et_cfg = cfg.trees_enhanced
tree_cfg = cfg.trees
# Load DOM1 and DGM1
dom1_pattern = os.path.join(pc_cfg.dom1_dir, f"*{tile_id}*.tif")
dom1_files = glob.glob(dom1_pattern)
if not dom1_files:
print(f"[trees_enhanced] No DOM1 for {tile_id}")
return []
dgm1_pattern = os.path.join(cfg.raw.dgm1_dir, f"*{tile_id}*.tif")
dgm1_files = glob.glob(dgm1_pattern)
if not dgm1_files:
print(f"[trees_enhanced] No DGM1 for {tile_id}")
return []
dom1_ds = open_dataset(dom1_files[0], required=False)
dgm1_ds = open_dataset(dgm1_files[0], required=False)
if dom1_ds is None or dgm1_ds is None:
return []
dom1 = dom1_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
dgm1 = dgm1_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
gt = dom1_ds.GetGeoTransform()
# Handle nodata
dom1_nodata = dom1_ds.GetRasterBand(1).GetNoDataValue() or -9999
dgm1_nodata = dgm1_ds.GetRasterBand(1).GetNoDataValue() or -9999
valid_mask = (dom1 > dom1_nodata + 1) & (dgm1 > dgm1_nodata + 1)
# Compute CHM
chm = np.where(valid_mask, dom1 - dgm1, 0)
chm = np.clip(chm, 0, None)
# Load and apply furniture exclusion mask
if et_cfg.exclude_furniture:
furniture = load_furniture_detections(tile_id, cfg)
if furniture:
furniture_mask = _create_furniture_mask(
furniture, chm.shape, gt, xmin, ymin, buffer_m=2.0
)
chm = np.where(furniture_mask, 0, chm)
# Detect tree peaks
peaks = _detect_local_maxima(chm, tree_cfg.min_height_m, window_size=5)
# Limit to max trees
if len(peaks) > tree_cfg.max_trees:
# Sort by height descending, keep tallest
peaks = sorted(peaks, key=lambda p: p[2], reverse=True)[:tree_cfg.max_trees]
# Find ortho for color sampling
ortho_path = os.path.join(cfg.export.ortho_dir, f"{tile_id}.jpg")
has_ortho = os.path.exists(ortho_path)
# Load LPO for height refinement
lpo = None
if et_cfg.use_lpo_refinement and has_lpo_data(tile_id, pc_cfg.lpo_dir):
lpo_file = find_xyz_file(pc_cfg.lpo_dir, tile_id)
if lpo_file:
lpo = read_xyz_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
trees = []
pixel_size = abs(gt[1])
for row, col, height in peaks:
# Convert to world coordinates
world_x = gt[0] + col * gt[1]
world_y = gt[3] + row * gt[5]
# Local coordinates
x_local = world_x - xmin
y_local = world_y - ymin
# Ground elevation
z_ground = float(dgm1[row, col])
# Estimate radius (simple heuristic)
radius = height * 0.25
# Base confidence
confidence = 0.6 + 0.4 * min(1.0, height / 30.0)
# Refine with LPO
if lpo is not None and len(lpo) > 0:
search_radius = et_cfg.lpo_search_radius_m
dist_sq = (lpo.x - world_x) ** 2 + (lpo.y - world_y) ** 2
nearby_mask = dist_sq < search_radius ** 2
if nearby_mask.any():
nearby_z = lpo.z[nearby_mask]
refined_height = float(np.percentile(nearby_z, 95)) - z_ground
if refined_height > tree_cfg.min_height_m:
height = refined_height
confidence += 0.1
# Sample canopy color
canopy_r, canopy_g, canopy_b = 128, 160, 80 # Default
if et_cfg.sample_canopy_color and has_ortho:
sample_radius = radius * et_cfg.canopy_sample_radius_factor
color = _sample_ortho_color(ortho_path, world_x, world_y, sample_radius)
if color:
canopy_r, canopy_g, canopy_b = color
tree = EnhancedTree(
tile_id=tile_id,
x_local=x_local,
y_local=y_local,
z_ground=z_ground,
height=height,
radius=radius,
confidence=min(confidence, 1.0),
canopy_r=canopy_r,
canopy_g=canopy_g,
canopy_b=canopy_b,
)
trees.append(tree)
return trees
def export_trees_csv(
trees: List[EnhancedTree],
tile_id: str,
cfg: Config,
) -> str:
"""Export enhanced trees to CSV.
Args:
trees: List of tree detections
tile_id: Tile identifier
cfg: Configuration
Returns:
Path to CSV file
"""
os.makedirs(cfg.trees_enhanced.csv_dir, exist_ok=True)
csv_path = os.path.join(cfg.trees_enhanced.csv_dir, f"{tile_id}.csv")
with open(csv_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"tile_id", "x_local", "y_local", "z_ground",
"height", "radius", "confidence",
"canopy_r", "canopy_g", "canopy_b"
])
for tree in trees:
writer.writerow(tree.to_csv_row())
return csv_path
def export_trees_enhanced(cfg: Config) -> int:
"""Export enhanced tree detections for all tiles.
Args:
cfg: Configuration
Returns:
Exit code (0 = success)
"""
# Check for manifest
if not os.path.exists(cfg.export.manifest_path):
print(f"[trees_enhanced] ERROR: Manifest not found: {cfg.export.manifest_path}")
print("[trees_enhanced] Run heightmap export first.")
return 1
# Load tiles from manifest
tiles = []
with open(cfg.export.manifest_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
tiles.append({
"tile_id": row["tile_id"],
"xmin": float(row["xmin"]),
"ymin": float(row["ymin"]),
"xmax": float(row["xmax"]),
"ymax": float(row["ymax"]),
})
if not tiles:
print("[trees_enhanced] No tiles in manifest.")
return 1
os.makedirs(cfg.trees_enhanced.csv_dir, exist_ok=True)
total_trees = 0
for tile in tiles:
tile_id = tile["tile_id"]
print(f"[trees_enhanced] Processing {tile_id}...")
trees = detect_trees_in_tile(
tile_id,
tile["xmin"],
tile["ymin"],
tile["xmax"],
tile["ymax"],
cfg,
)
csv_path = export_trees_csv(trees, tile_id, cfg)
print(f"[trees_enhanced] {tile_id}: {len(trees)} trees -> {csv_path}")
total_trees += len(trees)
print(f"[trees_enhanced] DONE. Total trees: {total_trees}")
return 0

View File

@@ -9,10 +9,14 @@ from typing import Iterable
from geodata_pipeline.config import Config, DEFAULT_CONFIG_PATH, ensure_default_config
from geodata_pipeline.buildings import export_buildings
from geodata_pipeline.buildings_enhanced import export_buildings_enhanced
from geodata_pipeline.heightmaps import export_heightmaps
from geodata_pipeline.heightmaps_enhanced import export_heightmaps_enhanced
from geodata_pipeline.orthophotos import export_orthophotos
from geodata_pipeline.setup_helpers import ensure_directories, materialize_archives
from geodata_pipeline.street_furniture import export_street_furniture
from geodata_pipeline.trees import export_trees
from geodata_pipeline.trees_enhanced import export_trees_enhanced
def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
@@ -24,9 +28,13 @@ def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
)
parser.add_argument(
"--export",
choices=["heightmap", "textures", "buildings", "trees", "all"],
choices=[
"heightmap", "textures", "buildings", "trees", "all",
"heightmap-enhanced", "buildings-enhanced", "trees-enhanced",
"street-furniture", "all-enhanced"
],
default=None,
help="Which assets to export (default: all; skipped on --setup unless explicitly set).",
help="Which assets to export. Enhanced options use point cloud data.",
)
parser.add_argument("--raw-dgm1-path", dest="raw_dgm1_path", help="Override raw DGM1 directory.")
parser.add_argument("--raw-dop20-path", dest="raw_dop20_path", help="Override raw DOP20 JP2 directory.")
@@ -78,6 +86,8 @@ def main(argv: Iterable[str] | None = None) -> int:
materialize_archives(cfg)
exit_codes = []
# Standard exports
if target_export in ("heightmap", "all"):
exit_codes.append(export_heightmaps(cfg, force_vrt=args.force_vrt))
if target_export in ("textures", "all"):
@@ -87,6 +97,20 @@ def main(argv: Iterable[str] | None = None) -> int:
if target_export in ("trees", "all"):
exit_codes.append(export_trees(cfg))
# Enhanced exports (use point cloud data)
# Order matters: heightmap-enhanced creates tile_index.csv needed by others
# street-furniture must run before trees-enhanced (for exclusion mask)
if target_export in ("heightmap-enhanced", "all-enhanced"):
exit_codes.append(export_heightmaps_enhanced(cfg))
if target_export in ("all-enhanced",):
exit_codes.append(export_orthophotos(cfg, force_vrt=args.force_vrt))
if target_export in ("street-furniture", "all-enhanced"):
exit_codes.append(export_street_furniture(cfg))
if target_export in ("buildings-enhanced", "all-enhanced"):
exit_codes.append(export_buildings_enhanced(cfg))
if target_export in ("trees-enhanced", "all-enhanced"):
exit_codes.append(export_trees_enhanced(cfg))
return max(exit_codes) if exit_codes else 0

View File

@@ -3,8 +3,17 @@ name = "geodata-toolkit"
version = "0.1.0"
description = "Heightmap and orthophoto exporters using GDAL for Unity terrains."
readme = "README.md"
requires-python = ">=3.9,<3.11"
dependencies = ["gdal>=3.4", "cjio[export,reproject]>=0.9"]
requires-python = ">=3.10,<3.13"
dependencies = [
"gdal>=3.4",
"cjio[export,reproject]>=0.9",
"laspy[lazrs]>=2.5",
"scipy>=1.11",
"scikit-learn>=1.3",
"shapely>=2.0",
"numpy>=1.24",
"trimesh>=4.0",
]
[build-system]
requires = ["hatchling"]

477
uv.lock generated
View File

@@ -1,9 +1,10 @@
version = 1
revision = 3
requires-python = ">=3.9, <3.11"
requires-python = ">=3.10, <3.13"
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version < '3.10'",
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
"python_full_version < '3.11'",
]
[[package]]
@@ -20,10 +21,8 @@ name = "cjio"
version = "0.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "click" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/33/125385a9e3438b3d31dcb2d788719ca4e929e98675c7a8c1e2e5507c898f/cjio-0.10.1.tar.gz", hash = "sha256:8788a1333bcac3dc74fc3fa4c9f220b3015a742eb4c27af55161ec077a75fc44", size = 48480, upload-time = "2025-05-09T17:31:20.614Z" }
wheels = [
@@ -37,34 +36,15 @@ export = [
{ name = "triangle2" },
]
reproject = [
{ name = "pyproj", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "pyproj", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
{ name = "pyproj" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
@@ -93,12 +73,78 @@ source = { editable = "." }
dependencies = [
{ name = "cjio", extra = ["export", "reproject"] },
{ name = "gdal" },
{ name = "laspy", extra = ["lazrs"] },
{ name = "numpy" },
{ name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "shapely" },
{ name = "trimesh" },
]
[package.metadata]
requires-dist = [
{ name = "cjio", extras = ["export", "reproject"], specifier = ">=0.9" },
{ name = "gdal", specifier = ">=3.4" },
{ name = "laspy", extras = ["lazrs"], specifier = ">=2.5" },
{ name = "numpy", specifier = ">=1.24" },
{ name = "scikit-learn", specifier = ">=1.3" },
{ name = "scipy", specifier = ">=1.11" },
{ name = "shapely", specifier = ">=2.0" },
{ name = "trimesh", specifier = ">=4.0" },
]
[[package]]
name = "joblib"
version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "laspy"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/54/b6387699d944653955fcb80ceb2bb2a9d5fc8a9a56acccb4a45d16b29ef3/laspy-2.6.1.tar.gz", hash = "sha256:ce9cb9a18528b2a2b985583df40a4dea68cdda7995e47e4b00b6d48df0e88daa", size = 1940211, upload-time = "2025-07-07T19:49:32.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/d1/c3d09cadb41b6d7381a01e41db70419b21c9ccb3cc8ab1e3a0bd37397d82/laspy-2.6.1-py3-none-any.whl", hash = "sha256:44c4d3c38fcef81cdb9201a0b98e5e4f09831c98d2ec1335b9ee59da16a37349", size = 86053, upload-time = "2025-07-07T19:49:08.842Z" },
]
[package.optional-dependencies]
lazrs = [
{ name = "lazrs" },
]
[[package]]
name = "lazrs"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9b/37/169d27b57af14b0f5992debf1477a1d7f1497d738432b9f383b271e739e4/lazrs-0.7.0.tar.gz", hash = "sha256:53191b351c1d9fa45f74471698384bf42bde14599309645fb9d4c353f0fb7f24", size = 10098, upload-time = "2025-06-06T15:58:13.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/9c/eab617a474e65421967faae0c3169f3f0ab0b10646feb6b95d5a5e8d2abe/lazrs-0.7.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:19d9897941c1a3c54198cb9db19526d7840bd7f82487e3542d28c5be80c6ded1", size = 569479, upload-time = "2025-06-06T15:57:36.872Z" },
{ url = "https://files.pythonhosted.org/packages/52/e3/41aaebf1d31e9750d9f68d79e2aea245d97ee088a53375b42af62f232542/lazrs-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:834ef7f828044d8322fa53ee2f2b9bcd615f2bdf0c123504f622e54cb4f98836", size = 570906, upload-time = "2025-06-06T15:57:38.325Z" },
{ url = "https://files.pythonhosted.org/packages/89/a5/1f2bc60b3ccb5e349333d2212af653d775ebb047d73ac47e71857d26a85c/lazrs-0.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e64dadfafcea7bf9df6f27d9439f5f75eae22a280399cc0de735abaa4bcb6f72", size = 633428, upload-time = "2025-06-06T15:57:39.747Z" },
{ url = "https://files.pythonhosted.org/packages/dc/28/d1dcc28c35c8a913821edb76b55d988b4f20ef50317304d5732eae869861/lazrs-0.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:376d8ca61cb00580a1a7e68aa1dc8a80d230953b255c7b0f16abae7f4edb4391", size = 639937, upload-time = "2025-06-06T15:57:41.228Z" },
{ url = "https://files.pythonhosted.org/packages/4d/60/ff29fc539824c9c2a5727639ec7dae12ce68bd9534b2166d1d935a7099b6/lazrs-0.7.0-cp310-cp310-win32.whl", hash = "sha256:f469ee4847214a3f901c8419213cd87b9ebeb22308af8253faad45bde8fc34b2", size = 407819, upload-time = "2025-06-06T15:57:42.607Z" },
{ url = "https://files.pythonhosted.org/packages/8b/4e/132051aa292353cf8929a2c0ce90681eb877e9fb2cdb7b7de523af380d38/lazrs-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1bd15a9ddec761a356d65b3ce819336ffdfcc3a79fd3b34272709f0d7c00ad5", size = 421002, upload-time = "2025-06-06T15:57:43.834Z" },
{ url = "https://files.pythonhosted.org/packages/a7/c6/506f7222db694f93384fee5fe420041062327d35247831e3ce458219fe41/lazrs-0.7.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4236ace28a55d8b1b6d181ea2390a4a57de896587b914ea21a7e887cd493e87f", size = 569443, upload-time = "2025-06-06T15:57:46.4Z" },
{ url = "https://files.pythonhosted.org/packages/63/68/bc71647430e852070b6a2cf62117843daa1955ff10311ee5857eda3f3304/lazrs-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:42e943f7859c5f31067f28e5766f3a23237b3c3246b57c98ad4b765ccad69b11", size = 570900, upload-time = "2025-06-06T15:57:47.842Z" },
{ url = "https://files.pythonhosted.org/packages/dc/53/534844b7f7034dea47db0c445d5b6a6c3d40330e45db28a5faeab386ff7e/lazrs-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a2dde6acbc77e13981f6d85b7c8e994b0004e63abb15602dc2b65d1301550d", size = 632368, upload-time = "2025-06-06T15:57:49.382Z" },
{ url = "https://files.pythonhosted.org/packages/aa/22/f4d6b91e596d2822cc2d7164028ff0152068f369c7be6123030084bc9841/lazrs-0.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17369fd3b8518f2f35f342f3802a373cb722e39a8ed0353d2d37632f6210145d", size = 639576, upload-time = "2025-06-06T15:57:51.021Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/bbf46cbda2ef857887411369ac40ff45f2ab3a90e55d535a76801cea34cf/lazrs-0.7.0-cp311-cp311-win32.whl", hash = "sha256:b89c2cfdf38264356549d73246fb4a490add3e9cddd38b0334633cc0f5a529ee", size = 408012, upload-time = "2025-06-06T15:57:52.39Z" },
{ url = "https://files.pythonhosted.org/packages/e7/b9/bbf92491e1572826e17918cf97f544f14805b0a37c761a4f11f89ff8d8f2/lazrs-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:c09e1adaa6ba3a6a5b1287bdb3b4b88500ad0a1d7847ee0902041175e16fa5aa", size = 421304, upload-time = "2025-06-06T15:57:53.939Z" },
{ url = "https://files.pythonhosted.org/packages/9d/bf/303d94c87d6195f395ad7fa685728aafeb9817c8df9f876e668530f64e6a/lazrs-0.7.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a6fc38ef1b1226ebfe86bcc9ee3b07856f51baf91be6be6e958b1c88807460d6", size = 569011, upload-time = "2025-06-06T15:57:56.035Z" },
{ url = "https://files.pythonhosted.org/packages/82/1c/dfde2c8bcc122355596398317adbf46c223ba987e64435c7706e00cba092/lazrs-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a05f25e15ac8fcbedc9cf513258f0f7631d029275b2e86a424af4638d39142f3", size = 570823, upload-time = "2025-06-06T15:57:57.421Z" },
{ url = "https://files.pythonhosted.org/packages/d2/88/f0e168b4062cce9850e26fbd9378aa8c90a5fdf292be5fec5598c5d8da3d/lazrs-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e1e59591f290e9400acfff9380a43c71b91db5c740f7b303c2ca17c289574ab", size = 631060, upload-time = "2025-06-06T15:57:59.359Z" },
{ url = "https://files.pythonhosted.org/packages/f6/c2/fc35531ee1b1dad2912e3c4b54e2a862ee037d4ce963df8f7608a6ef8369/lazrs-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051503a48d2db393230adc5442b484edcc1c8d089d8cc82dbb079faf6b2477c8", size = 638735, upload-time = "2025-06-06T15:58:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/f5/c5/a5bf2275c1aa426232ba39bf09ed331c4198e5c1325e756e0e0b6691dccc/lazrs-0.7.0-cp312-cp312-win32.whl", hash = "sha256:1cfa3857dc19acc3fa56e0d481d7114254999c2620b29c9d53fc4358d62d5e0b", size = 408821, upload-time = "2025-06-06T15:58:02.495Z" },
{ url = "https://files.pythonhosted.org/packages/bd/62/e24db76c60d358207b224d9b0063748ae1abbc9cb0a1a35b6e14fca8dff9/lazrs-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:b908d90ed3872424f93c0c22f89568f08ec9af9b4fc6811b9978c5f97b018dfc", size = 420615, upload-time = "2025-06-06T15:58:04.405Z" },
]
[[package]]
@@ -106,8 +152,7 @@ name = "mapbox-earcut"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" }
wheels = [
@@ -119,58 +164,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/6a/d39ebaaa9010ea6c9f4d468f8812b1a1b31a40fba4f02ff29bc1bf321c30/mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6e2d1bf5af90d5857955775b4d8ea15b02e172f2a8f194bba50ff95f8ff3e80e", size = 157736, upload-time = "2025-11-16T18:40:16.344Z" },
{ url = "https://files.pythonhosted.org/packages/20/00/6a59cdb8d8c1bf7e3cc92f0404f68fdb1a3cb0bbb0837af0dbb93d6290a6/mapbox_earcut-2.0.0-cp310-cp310-win32.whl", hash = "sha256:5b0aa63dd890d712343095b05eb7b60e071912ad3ced1fc4187d6a6a739677bc", size = 51564, upload-time = "2025-11-16T18:40:17.852Z" },
{ url = "https://files.pythonhosted.org/packages/bc/7b/af69669c959d8f7fd1bd49c15deace2360bf6a79dad7bf9f7a7f1c137da6/mapbox_earcut-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b1355f13af89ea815b32f59a5455db295c965d51ab501bde0459cddc010a7149", size = 56793, upload-time = "2025-11-16T18:40:18.953Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ec/25491ab17b70e909e6cd69cc9e0b73f2636686031fb0d15995bf2bf9fbf1/mapbox_earcut-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:6717796a5923ae57b3f805db4b2876b5dc90d4dbabc0585de9b45fcda7fa33af", size = 55962, upload-time = "2025-11-16T18:41:16.955Z" },
{ url = "https://files.pythonhosted.org/packages/a5/1e/67d66790422e92e712d2b8ed623909ff1ef4f23dab2098fd0b09cfa7a7c7/mapbox_earcut-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:008bbf38abb0112736b2522fa68553a92a51829d3a578bc1cd7e7c7cf7f6be6c", size = 52583, upload-time = "2025-11-16T18:41:18.119Z" },
{ url = "https://files.pythonhosted.org/packages/24/95/fd3c5ae233a5173a714969cc5ebecd9772fcea21292ae6ab976147164c0e/mapbox_earcut-2.0.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68582876cdc7c420e452a8eec9cdbbeaf52b652d46481524c3469f452758b1b0", size = 56998, upload-time = "2025-11-16T18:41:19.618Z" },
{ url = "https://files.pythonhosted.org/packages/a7/09/915c7b534761d39588d3d825a2d781cbd974d7b1a5a8304f173a4768be51/mapbox_earcut-2.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4a90f8bcebbc4eacb18270f42a3bb4d2b37394fc738469fa7454cdf3c0e9e9e", size = 59691, upload-time = "2025-11-16T18:41:20.762Z" },
{ url = "https://files.pythonhosted.org/packages/e9/f7/00c887aa41d9bb241b056c436657860f93d4851491e334be0dc60ac53eb1/mapbox_earcut-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5472c11973ea1d1f5cfa76987a3b0885019a16d25b9a4d67b645fb73cb35328", size = 153114, upload-time = "2025-11-16T18:41:22.164Z" },
{ url = "https://files.pythonhosted.org/packages/26/a7/362c4d0ef31e41a633120b6ff3055b1cb077af7168b5b7695da5a07b6591/mapbox_earcut-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1192327e316df76649776de344356f3ced1625722f4c3ea86955d3233aadbda5", size = 157734, upload-time = "2025-11-16T18:41:23.428Z" },
{ url = "https://files.pythonhosted.org/packages/27/aa/03be318d4fae7f0fa7250fcfe51ef74f72ca3eebb895b6b21630c4642cdd/mapbox_earcut-2.0.0-cp39-cp39-win32.whl", hash = "sha256:dd9699f99b461c8f540ae451cc1a399014275a082c44fb39f8b2e727d0f2e0ec", size = 51986, upload-time = "2025-11-16T18:41:24.694Z" },
{ url = "https://files.pythonhosted.org/packages/28/98/01f397f28e0543554c5b79fe072f546ad9f9d7f0f20367538c9cdfb7fffd/mapbox_earcut-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f6bc47aa17b753400151983bd00c1a5e74bb2b577fb56631bece4c6e4fff50c8", size = 57092, upload-time = "2025-11-16T18:41:26.112Z" },
]
[[package]]
name = "numpy"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" },
{ url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" },
{ url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" },
{ url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" },
{ url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" },
{ url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" },
{ url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" },
{ url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" },
{ url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" },
{ url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" },
{ url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" },
{ url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" },
{ url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" },
{ url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" },
{ url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" },
{ url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" },
{ url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" },
{ url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" },
{ url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" },
{ url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" },
{ url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" },
{ url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" },
{ url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" },
{ url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" },
{ url = "https://files.pythonhosted.org/packages/07/9f/fbd15d9e348e75e986d6912c4eab99888106b7e5fb0a01e765422f7cd464/mapbox_earcut-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b5040e79e3783295e99c90277f31c1cbaddd3335297275331995ba5680e3649", size = 55773, upload-time = "2025-11-16T18:40:20.045Z" },
{ url = "https://files.pythonhosted.org/packages/72/40/be761298704fbbaa81c5618bb306f1510fb068e482f6a1c8b3b6c1b31479/mapbox_earcut-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf43baafec3ef1e967319d9b5da96bc6ddf3dbb204b6f3535275eda4b519a72", size = 52444, upload-time = "2025-11-16T18:40:21.501Z" },
{ url = "https://files.pythonhosted.org/packages/5a/0b/0c0c08db9663238ffb82c48259582dc0047a3255d98c0ac83c48026b7544/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a283531847f603dd9d69afb75b21bd009d385ca9485fcd3e5a7fa5db1ccd913", size = 56803, upload-time = "2025-11-16T18:40:22.891Z" },
{ url = "https://files.pythonhosted.org/packages/f0/4a/86796859383d7d11fa5d4bcf1983f94c6cbb9eeb60fb3bab527fec4b32fa/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab697676f4cec4572d4e941b7a3429a6687bf2ac6e8db3f3781024e3239ae3a0", size = 59403, upload-time = "2025-11-16T18:40:24.021Z" },
{ url = "https://files.pythonhosted.org/packages/6c/db/adaf981ab3bcfcf993ef317636b1f27210d6834bb1e8d63db6ad7c08214a/mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1bdac76e048f4299accf4eaf797079ddfc330442e7231c15535ed198100d6c5", size = 152876, upload-time = "2025-11-16T18:40:25.588Z" },
{ url = "https://files.pythonhosted.org/packages/d2/83/86417974039e7554c9e1e55c852a7e9c2a1390d64675eb85d70e5fa7eb37/mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a6945b23f859bef11ce3194303d17bd371c86b637e7029f81b1feaff3db3758", size = 157548, upload-time = "2025-11-16T18:40:27.202Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4c/c82a292bb21e5c651d81334123db2d654c5c9d19b2197080d3429dc1e49a/mapbox_earcut-2.0.0-cp311-cp311-win32.whl", hash = "sha256:8e119524c29406afb5eaa15e933f297d35679293a3ca62ced22f97a14c484cb5", size = 51424, upload-time = "2025-11-16T18:40:28.415Z" },
{ url = "https://files.pythonhosted.org/packages/30/57/6c39d7db81f72a3e4814ef152c8fb8dfe275dc4b03c9bfa073d251e3755f/mapbox_earcut-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:378bbbb3304e446023752db8f44ecd6e7ef965bcbda36541d2ae64442ba94254", size = 56662, upload-time = "2025-11-16T18:40:29.863Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d6/a1ef6e196b3d6968bf6546d4f7e54c559f9cff8991fdb880df0ba1618f52/mapbox_earcut-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:6d249a431abd6bbff36f1fd0493247a86de962244cc4081b4d5050b02ed48fb1", size = 50505, upload-time = "2025-11-16T18:40:30.992Z" },
{ url = "https://files.pythonhosted.org/packages/8d/93/846804029d955c3c841d8efff77c2b0e8d9aab057d3a077dc8e3f88b5ea4/mapbox_earcut-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db55ce18e698bc9d90914ee7d4f8c3e4d23827456ece7c5d7a1ec91e90c7122b", size = 55623, upload-time = "2025-11-16T18:40:32.113Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f6/cc9ece104bc3876b350dba6fef7f34fb7b20ecc028d2cdbdbecb436b1ed1/mapbox_earcut-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01dd6099d16123baf582a11b2bd1d59ce848498cf0cdca3812fd1f8b20ff33b7", size = 52028, upload-time = "2025-11-16T18:40:33.516Z" },
{ url = "https://files.pythonhosted.org/packages/88/6e/230da4aabcc56c99e9bddb4c43ce7d4ba3609c0caf2d316fb26535d7c60c/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5a098aae26a52282bc981a38e7bf6b889d2ea7442f2cd1903d2ba842f4ff07", size = 56351, upload-time = "2025-11-16T18:40:35.217Z" },
{ url = "https://files.pythonhosted.org/packages/1a/f7/5cdd3752526e91d91336c7263af7767b291d21e63c89d7190a60051f0f87/mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de35f241d0b9110ad9260f295acedd9d7cc0d7acfe30d36b1b3ee8419c2caba1", size = 59209, upload-time = "2025-11-16T18:40:36.634Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a2/b7781416cb93b37b95d0444e03f87184de8815e57ff202ce4105fa921325/mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cb63ab85e2e430c350f93e75c13f8b91cb8c8a045f3cd714c390b69a720368a", size = 152316, upload-time = "2025-11-16T18:40:38.147Z" },
{ url = "https://files.pythonhosted.org/packages/c1/74/396338e3d345e4e36fb23a0380921098b6a95ce7fb19c4777f4185a5974e/mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb3c9f069fc3795306db87f8139f70c4f047532f897a3de05f54dc1faebc97f6", size = 157268, upload-time = "2025-11-16T18:40:39.753Z" },
{ url = "https://files.pythonhosted.org/packages/56/2c/66fd137ea86c508f6cd7247f7f6e2d1dabffc9f0e9ccf14c71406b197af1/mapbox_earcut-2.0.0-cp312-cp312-win32.whl", hash = "sha256:eb290e6676217707ed238dd55e07b0a8ca3ab928f6a27c4afefb2ff3af08d7cb", size = 51226, upload-time = "2025-11-16T18:40:41.018Z" },
{ url = "https://files.pythonhosted.org/packages/b8/84/7b78e37b0c2109243c0dad7d9ba9774b02fcee228bf61cf727a5aa1702e2/mapbox_earcut-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ef5b3319a43375272ad2cad9333ed16e569b5102e32a4241451358897e6f6ee", size = 56417, upload-time = "2025-11-16T18:40:42.173Z" },
{ url = "https://files.pythonhosted.org/packages/75/7f/cd7195aa27c1c8f2b9d38025a5a8663f32cd01c07b648a54b1308ab26c15/mapbox_earcut-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4a3706feb5cc8c782d8f68bb0110c8d551304043f680a87a54b0651a2c208c3", size = 50111, upload-time = "2025-11-16T18:40:43.334Z" },
]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
@@ -183,6 +200,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
@@ -194,8 +231,7 @@ name = "pandas"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
@@ -209,52 +245,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
{ url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
{ url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
{ url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" },
{ url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" },
{ url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" },
{ url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" },
{ url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" },
{ url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" },
{ url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" },
]
[[package]]
name = "pyproj"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
{ name = "certifi", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/84/2b39bbf888c753ea48b40d47511548c77aa03445465c35cc4c4e9649b643/pyproj-3.6.1.tar.gz", hash = "sha256:44aa7c704c2b7d8fb3d483bbf75af6cb2350d30a63b144279a09b75fead501bf", size = 225131, upload-time = "2023-09-21T02:07:51.593Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/32/63cf474f4a8d4804b3bdf7c16b8589f38142e8e2f8319dcea27e0bc21a87/pyproj-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab7aa4d9ff3c3acf60d4b285ccec134167a948df02347585fdd934ebad8811b4", size = 6142763, upload-time = "2023-09-21T02:07:12.844Z" },
{ url = "https://files.pythonhosted.org/packages/18/86/2e7cb9de40492f1bafbf11f4c9072edc394509a40b5e4c52f8139546f039/pyproj-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc0472302919e59114aa140fd7213c2370d848a7249d09704f10f5b062031fe", size = 4877123, upload-time = "2023-09-21T02:10:37.905Z" },
{ url = "https://files.pythonhosted.org/packages/5e/c5/928d5a26995dbefbebd7507d982141cd9153bc7e4392b334fff722c4af12/pyproj-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5279586013b8d6582e22b6f9e30c49796966770389a9d5b85e25a4223286cd3f", size = 6190576, upload-time = "2023-09-21T02:17:08.637Z" },
{ url = "https://files.pythonhosted.org/packages/f6/2b/b60cf73b0720abca313bfffef34e34f7f7dae23852b2853cf0368d49426b/pyproj-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fafd1f3eb421694857f254a9bdbacd1eb22fc6c24ca74b136679f376f97d35", size = 8328075, upload-time = "2023-09-21T02:07:15.353Z" },
{ url = "https://files.pythonhosted.org/packages/d9/a8/7193f46032636be917bc775506ae987aad72c931b1f691b775ca812a2917/pyproj-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c41e80ddee130450dcb8829af7118f1ab69eaf8169c4bf0ee8d52b72f098dc2f", size = 5635713, upload-time = "2023-09-21T02:07:17.548Z" },
{ url = "https://files.pythonhosted.org/packages/89/8f/27350c8fba71a37cd0d316f100fbd96bf139cc2b5ff1ab0dcbc7ac64010a/pyproj-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:db3aedd458e7f7f21d8176f0a1d924f1ae06d725228302b872885a1c34f3119e", size = 6087932, upload-time = "2023-09-21T02:07:19.793Z" },
{ url = "https://files.pythonhosted.org/packages/d7/50/d369bbe62d7a0d1e2cb40bc211da86a3f6e0f3c99f872957a72c3d5492d6/pyproj-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ba1f9b03d04d8cab24d6375609070580a26ce76eaed54631f03bab00a9c737b", size = 6144755, upload-time = "2023-09-21T02:07:39.611Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/8d4f61065dfed965e53badd41201ad86a05af0c1bbc75dffb12ef0f5a7dd/pyproj-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18faa54a3ca475bfe6255156f2f2874e9a1c8917b0004eee9f664b86ccc513d3", size = 4879187, upload-time = "2023-09-21T02:10:45.519Z" },
{ url = "https://files.pythonhosted.org/packages/31/38/2cf8777cb2d5622a78195e690281b7029098795fde4751aec8128238b8bb/pyproj-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd43bd9a9b9239805f406fd82ba6b106bf4838d9ef37c167d3ed70383943ade1", size = 6192339, upload-time = "2023-09-21T02:17:09.942Z" },
{ url = "https://files.pythonhosted.org/packages/97/0a/b1525be9680369cc06dd288e12c59d24d5798b4afcdcf1b0915836e1caa6/pyproj-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50100b2726a3ca946906cbaa789dd0749f213abf0cbb877e6de72ca7aa50e1ae", size = 8332638, upload-time = "2023-09-21T02:07:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/8d/e8/e826e0a962f36bd925a933829cf6ef218efe2055db5ea292be40974a929d/pyproj-3.6.1-cp39-cp39-win32.whl", hash = "sha256:9274880263256f6292ff644ca92c46d96aa7e57a75c6df3f11d636ce845a1877", size = 5638159, upload-time = "2023-09-21T02:07:43.49Z" },
{ url = "https://files.pythonhosted.org/packages/43/d0/cbe29a4dcf38ee7e72bf695d0d3f2bee21b4f22ee6cf579ad974de9edfc8/pyproj-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:36b64c2cb6ea1cc091f329c5bd34f9c01bb5da8c8e4492c709bda6a09f96808f", size = 6090565, upload-time = "2023-09-21T02:07:45.735Z" },
{ url = "https://files.pythonhosted.org/packages/43/28/e8d2ca71dd56c27cbe668e4226963d61956cded222a2e839e6fec1ab6d82/pyproj-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd93c1a0c6c4aedc77c0fe275a9f2aba4d59b8acf88cebfc19fe3c430cfabf4f", size = 6034252, upload-time = "2023-09-21T02:07:47.906Z" },
{ url = "https://files.pythonhosted.org/packages/cb/39/1ce27cb86f51a1f5aed3a1617802a6131b59ea78492141d1fbe36722595e/pyproj-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ea8e7d2a88cb148b124429fba8cd2e0fae700a2d96eab7083c0928a85110", size = 6386263, upload-time = "2023-09-21T02:07:49.586Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
{ url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
{ url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
{ url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
{ url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
{ url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
{ url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
{ url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
{ url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
{ url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
{ url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
{ url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
{ url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
{ url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
]
[[package]]
name = "pyproj"
version = "3.7.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
dependencies = [
{ name = "certifi", marker = "python_full_version >= '3.10'" },
{ name = "certifi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/10/a8480ea27ea4bbe896c168808854d00f2a9b49f95c0319ddcbba693c8a90/pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47", size = 226339, upload-time = "2025-02-16T04:28:46.621Z" }
wheels = [
@@ -266,6 +278,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/73/c9194c2802fefe2a4fd4230bdd5ab083e7604e93c64d0356fa49c363bad6/pyproj-3.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f173f851ee75e54acdaa053382b6825b400cb2085663a9bb073728a59c60aebb", size = 10401391, upload-time = "2025-02-16T04:27:36.051Z" },
{ url = "https://files.pythonhosted.org/packages/c5/1d/ce8bb5b9251b04d7c22d63619bb3db3d2397f79000a9ae05b3fd86a5837e/pyproj-3.7.1-cp310-cp310-win32.whl", hash = "sha256:f550281ed6e5ea88fcf04a7c6154e246d5714be495c50c9e8e6b12d3fb63e158", size = 5869997, upload-time = "2025-02-16T04:27:38.302Z" },
{ url = "https://files.pythonhosted.org/packages/09/6a/ca145467fd2e5b21e3d5b8c2b9645dcfb3b68f08b62417699a1f5689008e/pyproj-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3537668992a709a2e7f068069192138618c00d0ba113572fdd5ee5ffde8222f3", size = 6278581, upload-time = "2025-02-16T04:27:41.051Z" },
{ url = "https://files.pythonhosted.org/packages/ab/0d/63670fc527e664068b70b7cab599aa38b7420dd009bdc29ea257e7f3dfb3/pyproj-3.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a94e26c1a4950cea40116775588a2ca7cf56f1f434ff54ee35a84718f3841a3d", size = 6264315, upload-time = "2025-02-16T04:27:44.539Z" },
{ url = "https://files.pythonhosted.org/packages/25/9d/cbaf82cfb290d1f1fa42feb9ba9464013bb3891e40c4199f8072112e4589/pyproj-3.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:263b54ba5004b6b957d55757d846fc5081bc02980caa0279c4fc95fa0fff6067", size = 4666267, upload-time = "2025-02-16T04:27:47.019Z" },
{ url = "https://files.pythonhosted.org/packages/79/53/24f9f9b8918c0550f3ff49ad5de4cf3f0688c9f91ff191476db8979146fe/pyproj-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d6a2ccd5607cd15ef990c51e6f2dd27ec0a741e72069c387088bba3aab60fa", size = 9680510, upload-time = "2025-02-16T04:27:49.239Z" },
{ url = "https://files.pythonhosted.org/packages/3c/ac/12fab74a908d40b63174dc704587febd0729414804bbfd873cabe504ff2d/pyproj-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5dcf24ede53d8abab7d8a77f69ff1936c6a8843ef4fcc574646e4be66e5739", size = 9493619, upload-time = "2025-02-16T04:27:52.65Z" },
{ url = "https://files.pythonhosted.org/packages/c4/45/26311d6437135da2153a178125db5dfb6abce831ce04d10ec207eabac70a/pyproj-3.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c2e7449840a44ce860d8bea2c6c1c4bc63fa07cba801dcce581d14dcb031a02", size = 10709755, upload-time = "2025-02-16T04:27:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/99/52/4ecd0986f27d0e6c8ee3a7bc5c63da15acd30ac23034f871325b297e61fd/pyproj-3.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0829865c1d3a3543f918b3919dc601eea572d6091c0dd175e1a054db9c109274", size = 10642970, upload-time = "2025-02-16T04:27:58.343Z" },
{ url = "https://files.pythonhosted.org/packages/3f/a5/d3bfc018fc92195a000d1d28acc1f3f1df15ff9f09ece68f45a2636c0134/pyproj-3.7.1-cp311-cp311-win32.whl", hash = "sha256:6181960b4b812e82e588407fe5c9c68ada267c3b084db078f248db5d7f45d18a", size = 5868295, upload-time = "2025-02-16T04:28:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/92/39/ef6f06a5b223dbea308cfcbb7a0f72e7b506aef1850e061b2c73b0818715/pyproj-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ad0ff443a785d84e2b380869fdd82e6bfc11eba6057d25b4409a9bbfa867970", size = 6279871, upload-time = "2025-02-16T04:28:04.988Z" },
{ url = "https://files.pythonhosted.org/packages/e6/c9/876d4345b8d17f37ac59ebd39f8fa52fc6a6a9891a420f72d050edb6b899/pyproj-3.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:2781029d90df7f8d431e29562a3f2d8eafdf233c4010d6fc0381858dc7373217", size = 6264087, upload-time = "2025-02-16T04:28:09.036Z" },
{ url = "https://files.pythonhosted.org/packages/ff/e6/5f8691f8c90e7f402cc80a6276eb19d2ec1faa150d5ae2dd9c7b0a254da8/pyproj-3.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d61bf8ab04c73c1da08eedaf21a103b72fa5b0a9b854762905f65ff8b375d394", size = 4669628, upload-time = "2025-02-16T04:28:10.944Z" },
{ url = "https://files.pythonhosted.org/packages/42/ec/16475bbb79c1c68845c0a0d9c60c4fb31e61b8a2a20bc18b1a81e81c7f68/pyproj-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04abc517a8555d1b05fcee768db3280143fe42ec39fdd926a2feef31631a1f2f", size = 9721415, upload-time = "2025-02-16T04:28:13.342Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a3/448f05b15e318bd6bea9a32cfaf11e886c4ae61fa3eee6e09ed5c3b74bb2/pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084c0a475688f934d386c2ab3b6ce03398a473cd48adfda70d9ab8f87f2394a0", size = 9556447, upload-time = "2025-02-16T04:28:15.818Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ae/bd15fe8d8bd914ead6d60bca7f895a4e6f8ef7e3928295134ff9a7dad14c/pyproj-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a20727a23b1e49c7dc7fe3c3df8e56a8a7acdade80ac2f5cca29d7ca5564c145", size = 10758317, upload-time = "2025-02-16T04:28:18.338Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d9/5ccefb8bca925f44256b188a91c31238cae29ab6ee7f53661ecc04616146/pyproj-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bf84d766646f1ebd706d883755df4370aaf02b48187cedaa7e4239f16bc8213d", size = 10771259, upload-time = "2025-02-16T04:28:20.822Z" },
{ url = "https://files.pythonhosted.org/packages/2a/7d/31dedff9c35fa703162f922eeb0baa6c44a3288469a5fd88d209e2892f9e/pyproj-3.7.1-cp312-cp312-win32.whl", hash = "sha256:5f0da2711364d7cb9f115b52289d4a9b61e8bca0da57f44a3a9d6fc9bdeb7274", size = 5859914, upload-time = "2025-02-16T04:28:23.303Z" },
{ url = "https://files.pythonhosted.org/packages/3e/47/c6ab03d6564a7c937590cff81a2742b5990f096cce7c1a622d325be340ee/pyproj-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:aee664a9d806612af30a19dba49e55a7a78ebfec3e9d198f6a6176e1d140ec98", size = 6273196, upload-time = "2025-02-16T04:28:25.227Z" },
]
[[package]]
@@ -289,6 +317,179 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "scikit-learn"
version = "1.7.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "joblib", marker = "python_full_version < '3.11'" },
{ name = "numpy", marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "threadpoolctl", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" },
{ url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" },
{ url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" },
{ url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" },
{ url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" },
{ url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" },
{ url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" },
{ url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" },
{ url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" },
{ url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" },
{ url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" },
{ url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" },
{ url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" },
{ url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" },
{ url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" },
]
[[package]]
name = "scikit-learn"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
]
dependencies = [
{ name = "joblib", marker = "python_full_version >= '3.11'" },
{ name = "numpy", marker = "python_full_version >= '3.11'" },
{ name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "threadpoolctl", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" },
{ url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" },
{ url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" },
{ url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" },
{ url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" },
{ url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" },
{ url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" },
{ url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" },
{ url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" },
{ url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" },
{ url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" },
{ url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" },
]
[[package]]
name = "scipy"
version = "1.15.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "numpy", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" },
{ url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" },
{ url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" },
{ url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" },
{ url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" },
{ url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" },
{ url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" },
{ url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" },
{ url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" },
{ url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" },
{ url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" },
{ url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" },
{ url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" },
{ url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" },
{ url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" },
{ url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" },
{ url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" },
{ url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" },
{ url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" },
{ url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" },
{ url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" },
{ url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" },
{ url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" },
{ url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" },
]
[[package]]
name = "scipy"
version = "1.16.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
]
dependencies = [
{ name = "numpy", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" },
{ url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" },
{ url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" },
{ url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" },
{ url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" },
{ url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" },
{ url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" },
{ url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" },
{ url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" },
{ url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" },
{ url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" },
{ url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" },
{ url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" },
{ url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" },
{ url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" },
{ url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" },
{ url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" },
]
[[package]]
name = "shapely"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" },
{ url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" },
{ url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" },
{ url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" },
{ url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" },
{ url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" },
{ url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" },
{ url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" },
{ url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" },
{ url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" },
{ url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" },
{ url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" },
{ url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" },
{ url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" },
{ url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" },
{ url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" },
{ url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" },
{ url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" },
{ url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" },
{ url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@@ -298,13 +499,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "threadpoolctl"
version = "3.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
[[package]]
name = "triangle2"
version = "20230923"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "numpy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/81/f5610bda7a92ce0ce2c284295ba11acfa429d4c2056c39e44ffd82db926d/triangle2-20230923-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d442d16e0ca0975c6c8f56f7b8e08639d31bcea2c985ebd0ee265ab9f3ad6bd4", size = 1457243, upload-time = "2024-02-26T14:39:41.088Z" },
@@ -313,12 +522,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/2a/7bd73f5cc1509cbe12207bf8d5c12b06c78baea5ac2ba2d1e3ce6621984a/triangle2-20230923-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f758f99aca653e7d8bc4d2e14c034f09e7af6de1919d9792876d0d74dc6b4c49", size = 1977571, upload-time = "2024-02-26T14:39:46.643Z" },
{ url = "https://files.pythonhosted.org/packages/82/2a/15bb0609c3c1a679533c02b97176a2967dd3c38275854e02c32b26778ca9/triangle2-20230923-cp310-cp310-win32.whl", hash = "sha256:c117a62965a64d07d5ec82b1880357a765c35f6b86820a7737170b3ec8c63725", size = 1406919, upload-time = "2024-02-26T14:39:48.228Z" },
{ url = "https://files.pythonhosted.org/packages/7e/6b/e6d963f0e6555a2626aa2954d9cee541b20f6a15c3bf80f4610bdbf4b727/triangle2-20230923-cp310-cp310-win_amd64.whl", hash = "sha256:eef547477e5dc0522b8504f1eba585ae938054016839df81d730b731f9e5b922", size = 1426296, upload-time = "2024-02-26T14:39:49.608Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e5/234b6c9e93f8dd75683f7fcb51867a3c44f3cc0fe0f7ecb31f1b96a41088/triangle2-20230923-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c077be838336baf40b7fa4bb8ace2434997d67ee3a1353c38009eddf9b2678d", size = 1457955, upload-time = "2024-02-26T14:40:34.951Z" },
{ url = "https://files.pythonhosted.org/packages/d4/2e/66a4b097397472a2b34f0c6e76c0be437da59e5a47d6feb870ac21db9f0e/triangle2-20230923-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d31f1b52f6aecc613329822d6f4cf918e49cc4a8a0094977d9eb4279c096435b", size = 1441512, upload-time = "2024-02-26T14:40:37.332Z" },
{ url = "https://files.pythonhosted.org/packages/8e/3a/34cea18ff5801b4ae8e7a6e2780838c600f36f9ab65347e78d1abbf6fbeb/triangle2-20230923-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34419be0744e682448af37c4353b0bc5affa4962ffa7e972224342dcb0b86a17", size = 2054893, upload-time = "2024-02-26T14:40:39.064Z" },
{ url = "https://files.pythonhosted.org/packages/0d/23/90976726121a34c2aeb67c490be2632a0362798198b5c8f247ddea7e39c6/triangle2-20230923-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77e072dcbd7ad15da4d8b8d1c8732a960518f886266711a07e43a8776a30bdaf", size = 1982038, upload-time = "2024-02-26T14:40:40.756Z" },
{ url = "https://files.pythonhosted.org/packages/06/54/d24711cbd2c2efdc004144abe90e36281dde5fca3ab965a414374803b3b1/triangle2-20230923-cp39-cp39-win32.whl", hash = "sha256:c0c54f1ca1399463677cf988374eee4c1a9dda223e6d288288c66db5fd369f70", size = 1407592, upload-time = "2024-02-26T14:40:42.896Z" },
{ url = "https://files.pythonhosted.org/packages/d8/42/eb1f16b6df2324602aa1e8147000249f41cf645779d63a3eaada26134d64/triangle2-20230923-cp39-cp39-win_amd64.whl", hash = "sha256:c111816c984a26457ff414fea3e74c94b40705e1ca827aa00d09bd42d810eba2", size = 1426962, upload-time = "2024-02-26T14:40:44.537Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/90208d36e9cb2ba53414ee325cedf6aaff962f3577abbab2e0a73cc67225/triangle2-20230923-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e8ea0abc8e6d8bc08fec14541da0c564e5722b5d401f045d996ee04efac99a1e", size = 1457376, upload-time = "2024-02-26T14:39:51.289Z" },
{ url = "https://files.pythonhosted.org/packages/87/24/f191d15419ae14d83e2cf4d0ca6f61340aeb39a0b74d6eed0cbd96e72522/triangle2-20230923-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4080e08adad70e882734f2f483e7985923e105fbd946998d222f7060ae7851ff", size = 1441037, upload-time = "2024-02-26T14:39:53.057Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/ffc18956e59c9e5aad6d85e411f5522193c0c866cd331a35d662c9a454d2/triangle2-20230923-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3007191f44c3f37eb98d9e4751ff075a8f011ea359e14d7aae31f387de4edec", size = 2101785, upload-time = "2024-02-26T14:39:54.917Z" },
{ url = "https://files.pythonhosted.org/packages/ab/dc/45a9547f48895099831f2d2b52f1b1a96843110ec9a0f30e94dc1a971b53/triangle2-20230923-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d251089e20a0d78adfcea0accb6ff5ae28ce4a3e613803e00fc71043cb890e02", size = 2027689, upload-time = "2024-02-26T14:39:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/20/2a/d421e6691b4191af340de260457e97dc11a1eafd0019ad5686bd0d35ad2e/triangle2-20230923-cp311-cp311-win32.whl", hash = "sha256:7e50ca6b8636bad6af0f76e822e9788f92687d12cee75e063209cef9f68464a5", size = 1406499, upload-time = "2024-02-26T14:39:58.982Z" },
{ url = "https://files.pythonhosted.org/packages/2a/06/777b5ee9f5b8c08f231e11c24da2cc5c68ad23bce5d346f43686fe1d8dcd/triangle2-20230923-cp311-cp311-win_amd64.whl", hash = "sha256:d4a10cda1a91c9888e4764ae29d9cf29d07cd09399053c16d6e38475edfcd45c", size = 1426343, upload-time = "2024-02-26T14:40:01.103Z" },
{ url = "https://files.pythonhosted.org/packages/bc/06/e0d7c8b6b0ba2fdd190355cac191803773638abb841175d972486fac3ebd/triangle2-20230923-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7e0626d31296a6fac0c3b7ee44cc44450c3092bf1f576130770395d134326d1", size = 1459256, upload-time = "2024-02-26T14:40:03.483Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2d/fd9d5dbe88cc36725eb2106f5f79f17885686367d14bd08cfaff0d672dbf/triangle2-20230923-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7217cf5a61333ad0bd53a63c252cb32d42a4e24d4157e08c5c828d0d07e26343", size = 1442624, upload-time = "2024-02-26T14:40:05.969Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b0/1956a01690397c5a5d9d4b54908dd84c3604ac48d9565f596678d903850c/triangle2-20230923-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52a0aad9e852680adea23f5038e776f9a54dad9618cf9dcbcaffb312fc241b64", size = 2094363, upload-time = "2024-02-26T14:40:07.699Z" },
{ url = "https://files.pythonhosted.org/packages/72/72/def62d6de340f9389a2ee1c07a73af46f834ad5938da5cdd069c3bb0de41/triangle2-20230923-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8974b1a1318e0fa3c7b97459b5327843d551c892a4694f22235138c7244c463c", size = 2011718, upload-time = "2024-02-26T14:40:09.326Z" },
{ url = "https://files.pythonhosted.org/packages/81/5b/4dac900a5c2fca480c3dd813766337d3a1f62bae7270fe44b97f9e4f2358/triangle2-20230923-cp312-cp312-win32.whl", hash = "sha256:92b5b03eeb157e2194cb7107a5e5da07719765d96982b3286404a9914f7fe6b9", size = 1407427, upload-time = "2024-02-26T14:40:10.953Z" },
{ url = "https://files.pythonhosted.org/packages/e5/fb/562f77a71ded88608103cb92c26582ff3121b8cf585d97916233e9574759/triangle2-20230923-cp312-cp312-win_amd64.whl", hash = "sha256:d66f5c1f9c06e09984870556eb6be1fe96dac44e8a275eee4afc99ef3eb64ae2", size = 1426888, upload-time = "2024-02-26T14:40:13.116Z" },
]
[[package]]
name = "trimesh"
version = "4.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/69/eedfeb084460d429368e03db83ed41b18d6de4fd4945de7eb8874b9fae36/trimesh-4.10.1.tar.gz", hash = "sha256:2067ebb8dcde0d7f00c2a85bfcae4aa891c40898e5f14232592429025ee2c593", size = 831998, upload-time = "2025-12-07T00:39:05.838Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/0c/f08f0d16b4f97ec2ea6d542b9a70472a344384382fa3543a12ec417cc063/trimesh-4.10.1-py3-none-any.whl", hash = "sha256:4e81fae696683dfe912ef54ce124869487d35d267b87e10fe07fc05ab62aaadb", size = 737037, upload-time = "2025-12-07T00:39:04.086Z" },
]
[[package]]