163 lines
6.0 KiB
Python
163 lines
6.0 KiB
Python
"""B03 원본 입력 파일 메타데이터 분석."""
|
|
|
|
import math
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import laspy
|
|
import numpy as np
|
|
import rasterio
|
|
from pyproj import CRS
|
|
|
|
|
|
def analyze_las_metadata(path: str | Path) -> dict[str, Any]:
|
|
"""LAS/LAZ 헤더와 분류 통계를 메모리에 전체 적재하지 않고 분석한다."""
|
|
source = Path(path)
|
|
with laspy.open(source) as las_file:
|
|
header = las_file.header
|
|
point_format = header.point_format
|
|
dimension_names = list(point_format.dimension_names)
|
|
point_count = int(header.point_count)
|
|
crs = header.parse_crs()
|
|
metadata: dict[str, Any] = {
|
|
"file": source.name,
|
|
"version": f"{header.version.major}.{header.version.minor}",
|
|
"point_format": {
|
|
"id": point_format.id,
|
|
"dimensions": dimension_names,
|
|
},
|
|
"point_count": point_count,
|
|
"bounds": {
|
|
"x": [float(header.mins[0]), float(header.maxs[0])],
|
|
"y": [float(header.mins[1]), float(header.maxs[1])],
|
|
"z": [float(header.mins[2]), float(header.maxs[2])],
|
|
},
|
|
"scale": [float(value) for value in header.scales],
|
|
"offset": [float(value) for value in header.offsets],
|
|
"has_crs": crs is not None,
|
|
"crs": crs.to_string() if crs else None,
|
|
"epsg": crs.to_epsg() if crs else None,
|
|
"has_classification": "classification" in dimension_names,
|
|
"has_rgb": all(name in dimension_names for name in ("red", "green", "blue")),
|
|
"has_intensity": "intensity" in dimension_names,
|
|
"has_return_number": "return_number" in dimension_names,
|
|
}
|
|
|
|
if metadata["has_classification"] and point_count > 0:
|
|
classification_counts: dict[int, int] = {}
|
|
for chunk in las_file.chunk_iterator(500_000):
|
|
values, counts = np.unique(
|
|
np.asarray(chunk.classification, dtype=np.uint8),
|
|
return_counts=True,
|
|
)
|
|
for value, count in zip(values.tolist(), counts.tolist(), strict=True):
|
|
classification_counts[value] = classification_counts.get(value, 0) + count
|
|
metadata["classification_summary"] = {
|
|
str(key): value for key, value in sorted(classification_counts.items())
|
|
}
|
|
|
|
return metadata
|
|
|
|
|
|
def analyze_prj_metadata(path: str | Path) -> dict[str, Any]:
|
|
"""PRJ WKT에서 좌표계 식별자와 명칭을 추출한다."""
|
|
source = Path(path)
|
|
text = source.read_text(encoding="utf-8", errors="replace").strip()
|
|
metadata: dict[str, Any] = {
|
|
"file": source.name,
|
|
"text_preview": text[:500],
|
|
"epsg": None,
|
|
"name": None,
|
|
"authority": None,
|
|
"is_valid": False,
|
|
}
|
|
if not text:
|
|
metadata["error"] = "PRJ 파일이 비어 있습니다."
|
|
return metadata
|
|
|
|
try:
|
|
crs = CRS.from_wkt(text)
|
|
except Exception as exc:
|
|
metadata["error"] = str(exc)
|
|
return metadata
|
|
|
|
metadata.update(
|
|
{
|
|
"epsg": crs.to_epsg(),
|
|
"name": crs.name,
|
|
"authority": crs.to_authority(),
|
|
"is_valid": True,
|
|
}
|
|
)
|
|
return metadata
|
|
|
|
|
|
def analyze_tfw_metadata(path: str | Path) -> dict[str, Any]:
|
|
"""TFW의 affine 변환 계수와 유효성을 분석한다."""
|
|
source = Path(path)
|
|
values = [
|
|
float(line.strip())
|
|
for line in source.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
if line.strip()
|
|
]
|
|
if any(not math.isfinite(value) for value in values):
|
|
raise ValueError("TFW 변환 계수는 유한한 숫자여야 합니다.")
|
|
|
|
return {
|
|
"file": source.name,
|
|
"values": values,
|
|
"pixel_size_x": values[0] if len(values) > 0 else None,
|
|
"rotation_y": values[1] if len(values) > 1 else None,
|
|
"rotation_x": values[2] if len(values) > 2 else None,
|
|
"pixel_size_y": values[3] if len(values) > 3 else None,
|
|
"origin_x": values[4] if len(values) > 4 else None,
|
|
"origin_y": values[5] if len(values) > 5 else None,
|
|
"is_valid": len(values) == 6,
|
|
}
|
|
|
|
|
|
def analyze_tif_metadata(path: str | Path) -> dict[str, Any]:
|
|
"""TIF/GeoTIFF 데이터셋의 공간 및 밴드 메타데이터를 분석한다."""
|
|
source = Path(path)
|
|
with rasterio.open(source) as dataset:
|
|
crs = dataset.crs
|
|
bounds = dataset.bounds
|
|
return {
|
|
"file": source.name,
|
|
"width": int(dataset.width),
|
|
"height": int(dataset.height),
|
|
"count": int(dataset.count),
|
|
"dtypes": list(dataset.dtypes),
|
|
"nodata": float(dataset.nodata) if dataset.nodata is not None else None,
|
|
"crs": crs.to_string() if crs else None,
|
|
"epsg": crs.to_epsg() if crs else None,
|
|
"bounds": {
|
|
"left": float(bounds.left),
|
|
"bottom": float(bounds.bottom),
|
|
"right": float(bounds.right),
|
|
"top": float(bounds.top),
|
|
},
|
|
"transform": [float(value) for value in list(dataset.transform)[:6]],
|
|
"resolution": [float(value) for value in dataset.res],
|
|
"likely_type": "dem" if dataset.count == 1 else "image",
|
|
}
|
|
|
|
|
|
def analyze_input_metadata(path: str | Path) -> dict[str, Any]:
|
|
"""입력 파일 확장자에 맞는 B03 메타데이터 분석 함수를 호출한다."""
|
|
source = Path(path)
|
|
extension = source.suffix.lower()
|
|
if extension in {".las", ".laz"}:
|
|
return analyze_las_metadata(source)
|
|
if extension == ".prj":
|
|
return analyze_prj_metadata(source)
|
|
if extension == ".tfw":
|
|
return analyze_tfw_metadata(source)
|
|
if extension in {".tif", ".tiff"}:
|
|
return analyze_tif_metadata(source)
|
|
return {
|
|
"file": source.name,
|
|
"extension": extension.lstrip("."),
|
|
"size_bytes": source.stat().st_size,
|
|
}
|