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.
@@ -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
+ ]
@@ -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
@@ -0,0 +1,3 @@
1
+ """Video EDA module for VisEDA."""
2
+ from viseda.video.eda import VideoEDA, VideoRecord
3
+ __all__ = ["VideoEDA", "VideoRecord"]