viseda 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- viseda/__init__.py +20 -0
- viseda/cli.py +144 -0
- viseda/core/__init__.py +3 -0
- viseda/core/base.py +69 -0
- viseda/hyperspectral/__init__.py +1 -0
- viseda/hyperspectral/eda.py +1849 -0
- viseda/image/__init__.py +1 -0
- viseda/image/eda.py +1840 -0
- viseda/pointcloud/__init__.py +3 -0
- viseda/pointcloud/eda.py +1167 -0
- viseda/report/__init__.py +3 -0
- viseda/report/html_report.py +213 -0
- viseda/text/__init__.py +3 -0
- viseda/text/eda.py +1613 -0
- viseda/utils/__init__.py +27 -0
- viseda/utils/helpers.py +120 -0
- viseda/video/__init__.py +3 -0
- viseda/video/eda.py +587 -0
- viseda-1.0.0.dist-info/METADATA +266 -0
- viseda-1.0.0.dist-info/RECORD +24 -0
- viseda-1.0.0.dist-info/WHEEL +5 -0
- viseda-1.0.0.dist-info/entry_points.txt +2 -0
- viseda-1.0.0.dist-info/licenses/LICENSE +21 -0
- viseda-1.0.0.dist-info/top_level.txt +1 -0
viseda/utils/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from viseda.utils.helpers import (
|
|
2
|
+
discover_files,
|
|
3
|
+
safe_divide,
|
|
4
|
+
percentile_clip,
|
|
5
|
+
entropy,
|
|
6
|
+
rgb_to_gray,
|
|
7
|
+
dominant_colors,
|
|
8
|
+
ensure_uint8,
|
|
9
|
+
resize_for_display,
|
|
10
|
+
IMAGE_EXTENSIONS,
|
|
11
|
+
HYPERSPECTRAL_EXTENSIONS,
|
|
12
|
+
POINTCLOUD_EXTENSIONS,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"discover_files",
|
|
17
|
+
"safe_divide",
|
|
18
|
+
"percentile_clip",
|
|
19
|
+
"entropy",
|
|
20
|
+
"rgb_to_gray",
|
|
21
|
+
"dominant_colors",
|
|
22
|
+
"ensure_uint8",
|
|
23
|
+
"resize_for_display",
|
|
24
|
+
"IMAGE_EXTENSIONS",
|
|
25
|
+
"HYPERSPECTRAL_EXTENSIONS",
|
|
26
|
+
"POINTCLOUD_EXTENSIONS",
|
|
27
|
+
]
|
viseda/utils/helpers.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viseda.utils
|
|
3
|
+
------------
|
|
4
|
+
Shared utility helpers used across all EDA modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# File discovery
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}
|
|
20
|
+
HYPERSPECTRAL_EXTENSIONS = {".hdr", ".bil", ".bip", ".bsq", ".envi", ".tif", ".tiff"}
|
|
21
|
+
POINTCLOUD_EXTENSIONS = {".las", ".laz", ".ply", ".pcd", ".xyz", ".txt", ".npy", ".npz"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def discover_files(
|
|
25
|
+
root: Union[str, Path],
|
|
26
|
+
extensions: set,
|
|
27
|
+
recursive: bool = True,
|
|
28
|
+
) -> List[Path]:
|
|
29
|
+
"""Walk *root* and return all files matching *extensions*."""
|
|
30
|
+
root = Path(root)
|
|
31
|
+
if not root.exists():
|
|
32
|
+
raise FileNotFoundError(f"Path not found: {root}")
|
|
33
|
+
if root.is_file():
|
|
34
|
+
return [root] if root.suffix.lower() in extensions else []
|
|
35
|
+
pattern = "**/*" if recursive else "*"
|
|
36
|
+
return sorted(
|
|
37
|
+
p for p in root.glob(pattern) if p.is_file() and p.suffix.lower() in extensions
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Numeric helpers
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def safe_divide(a: np.ndarray, b: np.ndarray, fill: float = 0.0) -> np.ndarray:
|
|
46
|
+
"""Element-wise division that replaces zero-denominator with *fill*."""
|
|
47
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
48
|
+
out = np.where(b != 0, a / b, fill)
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def percentile_clip(arr: np.ndarray, lo: float = 2, hi: float = 98) -> np.ndarray:
|
|
53
|
+
"""Clip array to [lo, hi] percentiles and normalise to [0, 1]."""
|
|
54
|
+
lo_val = np.percentile(arr, lo)
|
|
55
|
+
hi_val = np.percentile(arr, hi)
|
|
56
|
+
clipped = np.clip(arr, lo_val, hi_val)
|
|
57
|
+
return safe_divide(clipped - lo_val, hi_val - lo_val, fill=0.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def entropy(hist: np.ndarray) -> float:
|
|
61
|
+
"""Shannon entropy of a probability distribution."""
|
|
62
|
+
p = hist / (hist.sum() + 1e-12)
|
|
63
|
+
p = p[p > 0]
|
|
64
|
+
return float(-np.sum(p * np.log2(p)))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Colour helpers
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def rgb_to_gray(img: np.ndarray) -> np.ndarray:
|
|
72
|
+
"""Convert HxWx3 float/uint8 RGB to HxW grayscale (ITU-R BT.601)."""
|
|
73
|
+
if img.ndim == 2:
|
|
74
|
+
return img
|
|
75
|
+
return (0.299 * img[..., 0] + 0.587 * img[..., 1] + 0.114 * img[..., 2])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def dominant_colors(img: np.ndarray, k: int = 6) -> Tuple[np.ndarray, np.ndarray]:
|
|
79
|
+
"""
|
|
80
|
+
K-Means dominant colour extraction.
|
|
81
|
+
Returns (centers, percentages) both sorted by dominance descending.
|
|
82
|
+
"""
|
|
83
|
+
from sklearn.cluster import MiniBatchKMeans
|
|
84
|
+
|
|
85
|
+
pixels = img.reshape(-1, img.shape[-1]).astype(np.float32)
|
|
86
|
+
# subsample for speed on large images
|
|
87
|
+
if len(pixels) > 50_000:
|
|
88
|
+
idx = np.random.choice(len(pixels), 50_000, replace=False)
|
|
89
|
+
pixels = pixels[idx]
|
|
90
|
+
|
|
91
|
+
km = MiniBatchKMeans(n_clusters=k, random_state=42, n_init=3)
|
|
92
|
+
labels = km.fit_predict(pixels)
|
|
93
|
+
centers = km.cluster_centers_.astype(np.uint8)
|
|
94
|
+
counts = np.bincount(labels, minlength=k)
|
|
95
|
+
pct = counts / counts.sum()
|
|
96
|
+
order = np.argsort(-pct)
|
|
97
|
+
return centers[order], pct[order]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Image helpers
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def ensure_uint8(img: np.ndarray) -> np.ndarray:
|
|
105
|
+
"""Convert float images in [0,1] to uint8 [0,255]."""
|
|
106
|
+
if img.dtype in (np.float32, np.float64):
|
|
107
|
+
img = (np.clip(img, 0, 1) * 255).astype(np.uint8)
|
|
108
|
+
return img
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resize_for_display(img: np.ndarray, max_side: int = 512) -> np.ndarray:
|
|
112
|
+
"""Downscale large images for display purposes."""
|
|
113
|
+
import cv2 # lazy import
|
|
114
|
+
|
|
115
|
+
h, w = img.shape[:2]
|
|
116
|
+
scale = min(max_side / h, max_side / w, 1.0)
|
|
117
|
+
if scale < 1.0:
|
|
118
|
+
new_h, new_w = int(h * scale), int(w * scale)
|
|
119
|
+
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
|
120
|
+
return img
|
viseda/video/__init__.py
ADDED