viewinline 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {viewinline-0.3.0 → viewinline-0.3.2}/PKG-INFO +9 -5
- {viewinline-0.3.0 → viewinline-0.3.2}/README.md +8 -4
- {viewinline-0.3.0 → viewinline-0.3.2}/pyproject.toml +1 -1
- {viewinline-0.3.0 → viewinline-0.3.2}/src/viewinline/viewinline.py +335 -139
- {viewinline-0.3.0 → viewinline-0.3.2}/.github/FUNDING.yml +0 -0
- {viewinline-0.3.0 → viewinline-0.3.2}/.gitignore +0 -0
- {viewinline-0.3.0 → viewinline-0.3.2}/LICENSE +0 -0
- {viewinline-0.3.0 → viewinline-0.3.2}/src/viewinline/__init__.py +0 -0
- {viewinline-0.3.0 → viewinline-0.3.2}/viewinline_gif1.gif +0 -0
- {viewinline-0.3.0 → viewinline-0.3.2}/viewinline_gif2.gif +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viewinline
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Quick look geospatial viewer for the terminal, with inline image previews
|
|
5
5
|
Project-URL: Homepage, https://github.com/nkeikon/viewinline
|
|
6
6
|
Project-URL: Repository, https://github.com/nkeikon/viewinline
|
|
@@ -64,9 +64,9 @@ pip install viewinline
|
|
|
64
64
|
# Rasters
|
|
65
65
|
viewinline path/to/file.tif
|
|
66
66
|
viewinline R.tif G.tif B.tif # RGB composite (also works with --rgbfiles)
|
|
67
|
-
viewinline hyperspectral.nc --band 50
|
|
68
67
|
viewinline path/to/multiband.tif --rgb 3 2 1
|
|
69
|
-
viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
|
|
68
|
+
viewinline path/to/folder --gallery 4x3 # show image gallery of all files in the folder (e.g. 4x3 grid)
|
|
69
|
+
viewinline path/to/hyperspectral.tif --bands 10-50 # show image gallery of selected bands (also works with --bands 11,15,30,45; --gallery 5x5)
|
|
70
70
|
|
|
71
71
|
# NetCDF and HDF
|
|
72
72
|
viewinline file.nc # list variables
|
|
@@ -74,6 +74,8 @@ viewinline file.nc --subset 2 # display variable 2
|
|
|
74
74
|
viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
|
|
75
75
|
viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
|
|
76
76
|
viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
|
|
77
|
+
viewinline hyperspectral.nc --subset 22 --band 50 # show image gallery of selected bands
|
|
78
|
+
viewinline hyperspectral.nc --subset 22 --bands 10-54 --gallery 5x11
|
|
77
79
|
|
|
78
80
|
# Vectors
|
|
79
81
|
viewinline path/to/vector.geojson
|
|
@@ -169,6 +171,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
|
|
|
169
171
|
|
|
170
172
|
**Gallery view**
|
|
171
173
|
- Display all images in a folder with `--gallery 4x4`
|
|
174
|
+
- Display selected bands of a single raster as a grid with `--bands 101-120` or `--bands 11,12,45,55`. Works with GeoTIFF and NetCDF files.
|
|
172
175
|
|
|
173
176
|
**NetCDF/HDF notes:**
|
|
174
177
|
- viewinline lists only variables that can be displayed as 2D or 3D arrays
|
|
@@ -213,12 +216,13 @@ General:
|
|
|
213
216
|
|
|
214
217
|
Raster:
|
|
215
218
|
--band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
|
|
219
|
+
--bands BANDS Display multiple bands as a grid. Accepts ranges (30-40), lists (3,4,5), or mixed (1,5,10-15).
|
|
220
|
+
--rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
|
|
221
|
+
--rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
|
|
216
222
|
--timestep INTEGER Alias for --band when working with NetCDF files.
|
|
217
223
|
--subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
|
|
218
224
|
--reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
|
|
219
225
|
--colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
|
|
220
|
-
--rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
|
|
221
|
-
--rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
|
|
222
226
|
--vmin VMIN Minimum pixel value for raster display scaling.
|
|
223
227
|
--vmax VMAX Maximum pixel value for raster display scaling.
|
|
224
228
|
--nodata NODATA Override nodata value for rasters if dataset metadata is missing or incorrect.
|
|
@@ -31,9 +31,9 @@ pip install viewinline
|
|
|
31
31
|
# Rasters
|
|
32
32
|
viewinline path/to/file.tif
|
|
33
33
|
viewinline R.tif G.tif B.tif # RGB composite (also works with --rgbfiles)
|
|
34
|
-
viewinline hyperspectral.nc --band 50
|
|
35
34
|
viewinline path/to/multiband.tif --rgb 3 2 1
|
|
36
|
-
viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
|
|
35
|
+
viewinline path/to/folder --gallery 4x3 # show image gallery of all files in the folder (e.g. 4x3 grid)
|
|
36
|
+
viewinline path/to/hyperspectral.tif --bands 10-50 # show image gallery of selected bands (also works with --bands 11,15,30,45; --gallery 5x5)
|
|
37
37
|
|
|
38
38
|
# NetCDF and HDF
|
|
39
39
|
viewinline file.nc # list variables
|
|
@@ -41,6 +41,8 @@ viewinline file.nc --subset 2 # display variable 2
|
|
|
41
41
|
viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
|
|
42
42
|
viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
|
|
43
43
|
viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
|
|
44
|
+
viewinline hyperspectral.nc --subset 22 --band 50 # show image gallery of selected bands
|
|
45
|
+
viewinline hyperspectral.nc --subset 22 --bands 10-54 --gallery 5x11
|
|
44
46
|
|
|
45
47
|
# Vectors
|
|
46
48
|
viewinline path/to/vector.geojson
|
|
@@ -136,6 +138,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
|
|
|
136
138
|
|
|
137
139
|
**Gallery view**
|
|
138
140
|
- Display all images in a folder with `--gallery 4x4`
|
|
141
|
+
- Display selected bands of a single raster as a grid with `--bands 101-120` or `--bands 11,12,45,55`. Works with GeoTIFF and NetCDF files.
|
|
139
142
|
|
|
140
143
|
**NetCDF/HDF notes:**
|
|
141
144
|
- viewinline lists only variables that can be displayed as 2D or 3D arrays
|
|
@@ -180,12 +183,13 @@ General:
|
|
|
180
183
|
|
|
181
184
|
Raster:
|
|
182
185
|
--band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
|
|
186
|
+
--bands BANDS Display multiple bands as a grid. Accepts ranges (30-40), lists (3,4,5), or mixed (1,5,10-15).
|
|
187
|
+
--rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
|
|
188
|
+
--rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
|
|
183
189
|
--timestep INTEGER Alias for --band when working with NetCDF files.
|
|
184
190
|
--subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
|
|
185
191
|
--reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
|
|
186
192
|
--colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
|
|
187
|
-
--rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
|
|
188
|
-
--rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
|
|
189
193
|
--vmin VMIN Minimum pixel value for raster display scaling.
|
|
190
194
|
--vmax VMAX Maximum pixel value for raster display scaling.
|
|
191
195
|
--nodata NODATA Override nodata value for rasters if dataset metadata is missing or incorrect.
|
|
@@ -9,14 +9,11 @@ Supports:
|
|
|
9
9
|
|
|
10
10
|
Display:
|
|
11
11
|
Sends iTerm2-style inline image escape sequences. Works in terminals that support
|
|
12
|
-
the iTerm2 inline image protocol (iTerm2, WezTerm, Konsole, etc.).
|
|
13
|
-
|
|
12
|
+
the iTerm2 inline image protocol (iTerm2, WezTerm, Konsole, etc.). For others,
|
|
13
|
+
please see line 48-58.
|
|
14
14
|
|
|
15
15
|
Particularly useful on HPC systems and remote servers accessed via SSH — images
|
|
16
16
|
render on your local terminal without X11 forwarding, VNC, or file downloads.
|
|
17
|
-
|
|
18
|
-
No detection, no fallbacks. If images are not shown, it means that the terminal
|
|
19
|
-
is not compatible.
|
|
20
17
|
"""
|
|
21
18
|
|
|
22
19
|
import sys, os, base64, shutil, argparse
|
|
@@ -28,19 +25,13 @@ from matplotlib import colormaps
|
|
|
28
25
|
import matplotlib as mpl
|
|
29
26
|
import subprocess
|
|
30
27
|
|
|
31
|
-
try:
|
|
32
|
-
import netCDF4
|
|
33
|
-
HAS_NETCDF4 = True
|
|
34
|
-
except ImportError:
|
|
35
|
-
HAS_NETCDF4 = False
|
|
36
|
-
|
|
37
28
|
import warnings
|
|
38
29
|
|
|
39
30
|
warnings.filterwarnings("ignore", message="More than one layer found", category=UserWarning)
|
|
40
31
|
warnings.filterwarnings("ignore", message="Dataset has no geotransform", category=UserWarning)
|
|
41
32
|
warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset attribute", category=UserWarning)
|
|
42
33
|
|
|
43
|
-
__version__ = "0.3.
|
|
34
|
+
__version__ = "0.3.2"
|
|
44
35
|
|
|
45
36
|
AVAILABLE_COLORMAPS = [
|
|
46
37
|
"viridis", "inferno", "magma", "plasma",
|
|
@@ -248,6 +239,41 @@ def resize_to_terminal(img: np.ndarray) -> tuple[np.ndarray, float]:
|
|
|
248
239
|
pil_img = ImageOps.contain(pil_img, (new_w, new_h))
|
|
249
240
|
return np.array(pil_img), scale
|
|
250
241
|
|
|
242
|
+
def parse_bands(s: str) -> list[int]:
|
|
243
|
+
"""Parse --bands argument: accepts ranges (30-40), lists (3,4,5), or mixed (1,3,10-15)."""
|
|
244
|
+
bands = []
|
|
245
|
+
for part in s.split(","):
|
|
246
|
+
part = part.strip()
|
|
247
|
+
if "-" in part:
|
|
248
|
+
try:
|
|
249
|
+
start, end = map(int, part.split("-", 1))
|
|
250
|
+
bands.extend(range(start, end + 1))
|
|
251
|
+
except ValueError:
|
|
252
|
+
print(f"[WARN] Could not parse band range: {part}")
|
|
253
|
+
else:
|
|
254
|
+
try:
|
|
255
|
+
bands.append(int(part))
|
|
256
|
+
except ValueError:
|
|
257
|
+
print(f"[WARN] Could not parse band: {part}")
|
|
258
|
+
return sorted(set(bands))
|
|
259
|
+
|
|
260
|
+
def parse_rgb(values: list[str]) -> list[int]:
|
|
261
|
+
"""Parse --rgb: accepts '4 3 2' or '4,3,2'."""
|
|
262
|
+
if len(values) == 1:
|
|
263
|
+
# comma-separated: '4,3,2'
|
|
264
|
+
parts = values[0].split(",")
|
|
265
|
+
else:
|
|
266
|
+
# space-separated: '4' '3' '2'
|
|
267
|
+
parts = values
|
|
268
|
+
try:
|
|
269
|
+
result = [int(p.strip()) for p in parts]
|
|
270
|
+
if len(result) != 3:
|
|
271
|
+
raise ValueError
|
|
272
|
+
return result
|
|
273
|
+
except ValueError:
|
|
274
|
+
print("[WARN] --rgb requires exactly 3 band numbers. e.g. --rgb 4 3 2 or --rgb 4,3,2")
|
|
275
|
+
return None
|
|
276
|
+
|
|
251
277
|
# ---------------------------------------------------------------------
|
|
252
278
|
# CSV handling
|
|
253
279
|
# ---------------------------------------------------------------------
|
|
@@ -286,11 +312,18 @@ def preview_df(df, max_rows: int = 10, query_mode: bool = False, filename: str =
|
|
|
286
312
|
# -------------------------------------------------------------
|
|
287
313
|
if n_rows <= max_rows:
|
|
288
314
|
rows_to_show = df
|
|
315
|
+
elif query_mode:
|
|
316
|
+
ans = input(f"Filtered results: {n_rows} rows. Show first {max_rows} or all? [first/all]: ").strip().lower()
|
|
317
|
+
if ans == "all":
|
|
318
|
+
rows_to_show = df
|
|
319
|
+
else:
|
|
320
|
+
rows_to_show = df.head(max_rows)
|
|
289
321
|
else:
|
|
290
|
-
ans = input(f"
|
|
291
|
-
if ans
|
|
292
|
-
|
|
293
|
-
|
|
322
|
+
ans = input(f"Large file: {n_rows} rows. Show first {max_rows} or all? [first/all]: ").strip().lower()
|
|
323
|
+
if ans == "all":
|
|
324
|
+
rows_to_show = df
|
|
325
|
+
else:
|
|
326
|
+
rows_to_show = df.head(max_rows)
|
|
294
327
|
|
|
295
328
|
# -------------------------------------------------------------
|
|
296
329
|
# Build pretty table
|
|
@@ -592,8 +625,15 @@ def normalize_to_uint8(band: np.ndarray, vmin=None, vmax=None, nodata=None) -> n
|
|
|
592
625
|
valid_vals = band[valid]
|
|
593
626
|
|
|
594
627
|
# --- Manual scaling ---
|
|
595
|
-
if vmin is not None
|
|
596
|
-
|
|
628
|
+
if vmin is not None or vmax is not None:
|
|
629
|
+
# Use percentile for whichever end is not specified
|
|
630
|
+
if valid_vals.size < 1_000_000:
|
|
631
|
+
sample = valid_vals
|
|
632
|
+
else:
|
|
633
|
+
sample = np.random.choice(valid_vals, 1_000_000, replace=False)
|
|
634
|
+
p2, p98 = np.percentile(sample, (2, 98))
|
|
635
|
+
mn = vmin if vmin is not None else p2
|
|
636
|
+
mx = vmax if vmax is not None else p98
|
|
597
637
|
print(f"[VIEW] Using manual scaling: {mn} to {mx}")
|
|
598
638
|
|
|
599
639
|
# --- Percentile fallback ---
|
|
@@ -646,7 +686,9 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
646
686
|
"""Read a NetCDF file via netCDF4 (bypassing GDAL). Handles hierarchical
|
|
647
687
|
groups and hyperspectral cubes where GDAL aborts or interprets axes wrong.
|
|
648
688
|
"""
|
|
649
|
-
|
|
689
|
+
try:
|
|
690
|
+
import netCDF4
|
|
691
|
+
except ImportError:
|
|
650
692
|
print("[ERROR] netCDF4 not installed. Install with:")
|
|
651
693
|
print(" pip install netCDF4")
|
|
652
694
|
print(" or: pip install viewinline[netcdf]")
|
|
@@ -737,6 +779,66 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
737
779
|
band_count = var.shape[spectral_axis]
|
|
738
780
|
band_num = args.band if args.band is not None else 1
|
|
739
781
|
band_idx = max(0, min(band_num - 1, band_count - 1))
|
|
782
|
+
|
|
783
|
+
# BANDS GALLERY for NetCDF
|
|
784
|
+
if getattr(args, "bands", None):
|
|
785
|
+
band_list = parse_bands(args.bands)
|
|
786
|
+
# print(f"[DEBUG] band_count={band_count}, band_list={band_list}")
|
|
787
|
+
valid_bands = [b for b in band_list if 1 <= b <= band_count]
|
|
788
|
+
if not valid_bands:
|
|
789
|
+
print(f"[ERROR] No valid bands. Variable has {band_count} bands along '{var.dimensions[spectral_axis]}'.")
|
|
790
|
+
nc.close()
|
|
791
|
+
return
|
|
792
|
+
slices = []
|
|
793
|
+
for b in valid_bands:
|
|
794
|
+
slicer = [slice(None)] * 3
|
|
795
|
+
slicer[spectral_axis] = b - 1
|
|
796
|
+
slices.append(np.asarray(var[tuple(slicer)], dtype=np.float64))
|
|
797
|
+
nc.close()
|
|
798
|
+
colormap = args.colormap if args.colormap else "viridis"
|
|
799
|
+
render_bands_gallery(np.stack(slices, axis=0), valid_bands, band_count,
|
|
800
|
+
grid=getattr(args, "gallery", None),
|
|
801
|
+
display_scale=getattr(args, "display", None),
|
|
802
|
+
colormap=colormap)
|
|
803
|
+
return
|
|
804
|
+
|
|
805
|
+
# RGB COMPOSITE for NetCDF
|
|
806
|
+
if getattr(args, "rgb", None):
|
|
807
|
+
try:
|
|
808
|
+
# rgb_bands = args.rgb
|
|
809
|
+
rgb_bands = parse_rgb(args.rgb)
|
|
810
|
+
if rgb_bands is None:
|
|
811
|
+
return
|
|
812
|
+
if len(rgb_bands) != 3:
|
|
813
|
+
raise ValueError("exactly 3 bands required")
|
|
814
|
+
slices = []
|
|
815
|
+
for b in rgb_bands:
|
|
816
|
+
if b < 1 or b > band_count:
|
|
817
|
+
raise ValueError(f"band {b} out of range (1-{band_count})")
|
|
818
|
+
slicer = [slice(None)] * 3
|
|
819
|
+
slicer[spectral_axis] = b - 1
|
|
820
|
+
slices.append(np.asarray(var[tuple(slicer)], dtype=np.float64))
|
|
821
|
+
nc.close()
|
|
822
|
+
print(f"[INFO] Using RGB bands: {rgb_bands}")
|
|
823
|
+
img = np.stack([normalize_to_uint8(s, vmin=args.vmin, vmax=args.vmax, nodata=args.nodata)
|
|
824
|
+
for s in slices], axis=-1)
|
|
825
|
+
H, W = img.shape[:2]
|
|
826
|
+
if args.display:
|
|
827
|
+
new_w = max(1, int(W * args.display))
|
|
828
|
+
new_h = max(1, int(H * args.display))
|
|
829
|
+
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
830
|
+
else:
|
|
831
|
+
max_dim = 2000
|
|
832
|
+
if max(H, W) > max_dim:
|
|
833
|
+
scale = max_dim / max(H, W)
|
|
834
|
+
new_w, new_h = int(W * scale), int(H * scale)
|
|
835
|
+
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
836
|
+
show_image_auto(img, getattr(args, "display", None), is_vector=False)
|
|
837
|
+
return
|
|
838
|
+
except ValueError as e:
|
|
839
|
+
print(f"[WARN] Invalid --rgb: {e}. Falling back to band 1.")
|
|
840
|
+
|
|
841
|
+
# Single band slicer
|
|
740
842
|
slicer = [slice(None)] * 3
|
|
741
843
|
slicer[spectral_axis] = band_idx
|
|
742
844
|
data = np.asarray(var[tuple(slicer)], dtype=np.float64)
|
|
@@ -793,9 +895,6 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
793
895
|
new_w, new_h = max(1, int(W * args.display)), max(1, int(H * args.display))
|
|
794
896
|
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
795
897
|
print(f"[VIEW] Manual resize ×{args.display:.2f} → {new_w}×{new_h}px")
|
|
796
|
-
# else:
|
|
797
|
-
# img, scale = resize_to_terminal(img)
|
|
798
|
-
# print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
|
|
799
898
|
else:
|
|
800
899
|
max_dim = 2000
|
|
801
900
|
if max(img.shape[:2]) > max_dim:
|
|
@@ -803,7 +902,7 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
803
902
|
new_w = int(img.shape[1] * scale)
|
|
804
903
|
new_h = int(img.shape[0] * scale)
|
|
805
904
|
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
806
|
-
print(f"[VIEW] Downsampled from {W}×{H}px to {new_w}×{new_h}px (scale={scale:.2f})")
|
|
905
|
+
print(f"[VIEW] Downsampled from {W}×{H}px to {new_w}×{new_h}px (display scale={scale:.2f})")
|
|
807
906
|
print(f"[INFO] Use --display 1 for full resolution.")
|
|
808
907
|
else:
|
|
809
908
|
# (matches the width_pct logic in show_inline_image)
|
|
@@ -907,7 +1006,8 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
907
1006
|
resampling=rasterio.enums.Resampling.bilinear
|
|
908
1007
|
)
|
|
909
1008
|
|
|
910
|
-
print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
|
|
1009
|
+
print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (display scale={scale:.3f})")
|
|
1010
|
+
print(f"[INFO] Use --display 1 for full resolution.")
|
|
911
1011
|
else:
|
|
912
1012
|
data = ds.read()
|
|
913
1013
|
|
|
@@ -918,22 +1018,27 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
918
1018
|
else:
|
|
919
1019
|
print(f"[INFO] Multi-band raster detected ({band_count} bands)")
|
|
920
1020
|
|
|
921
|
-
#
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1021
|
+
# BANDS GALLERY
|
|
1022
|
+
if getattr(args, "bands", None):
|
|
1023
|
+
band_list = parse_bands(args.bands)
|
|
1024
|
+
colormap = args.colormap if args.colormap else "viridis"
|
|
1025
|
+
render_bands_gallery(data, band_list, band_count,
|
|
1026
|
+
grid=getattr(args, "gallery", None),
|
|
1027
|
+
display_scale=getattr(args, "display", None),
|
|
1028
|
+
colormap=colormap)
|
|
1029
|
+
return
|
|
1030
|
+
|
|
925
1031
|
user_specified_band = args.band is not None
|
|
926
|
-
if band_count >= 3 and not paths[0].lower().endswith('.nc') and not user_specified_band:
|
|
1032
|
+
if band_count >= 3 and not paths[0].lower().endswith('.nc') and not user_specified_band and getattr(args, 'rgb', None):
|
|
927
1033
|
|
|
928
1034
|
if getattr(args, "rgb", None):
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if len(rgb_idx) != 3:
|
|
932
|
-
raise ValueError
|
|
933
|
-
print(f"[INFO] Using RGB bands: {args.rgb}")
|
|
934
|
-
except Exception:
|
|
1035
|
+
rgb_parsed = parse_rgb(args.rgb)
|
|
1036
|
+
if rgb_parsed is None:
|
|
935
1037
|
print("[WARN] Invalid --rgb. Using default 1 2 3")
|
|
936
1038
|
rgb_idx = [0, 1, 2]
|
|
1039
|
+
else:
|
|
1040
|
+
rgb_idx = [b - 1 for b in rgb_parsed]
|
|
1041
|
+
print(f"[INFO] Using RGB bands: {rgb_parsed}")
|
|
937
1042
|
else:
|
|
938
1043
|
rgb_idx = [0, 1, 2]
|
|
939
1044
|
|
|
@@ -1045,26 +1150,30 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1045
1150
|
cols, rows = 4, 4
|
|
1046
1151
|
nmax = cols * rows
|
|
1047
1152
|
|
|
1048
|
-
# Collect files
|
|
1049
|
-
exts = (".png", ".jpg", ".jpeg", ".tif", ".tiff")
|
|
1153
|
+
# Collect all files, let the loader decide what's valid
|
|
1050
1154
|
files = [os.path.join(folder, f) for f in sorted(os.listdir(folder))
|
|
1051
|
-
if
|
|
1155
|
+
if os.path.isfile(os.path.join(folder, f))]
|
|
1052
1156
|
if not files:
|
|
1053
|
-
print(f"[WARN] No
|
|
1157
|
+
print(f"[WARN] No files found in {folder}")
|
|
1054
1158
|
return
|
|
1055
1159
|
|
|
1056
1160
|
files = files[:nmax]
|
|
1057
1161
|
|
|
1058
1162
|
# Load thumbnails
|
|
1059
1163
|
thumbs = []
|
|
1164
|
+
loaded_files = []
|
|
1060
1165
|
thumb_size = (128, 128)
|
|
1061
1166
|
for f in files:
|
|
1062
1167
|
try:
|
|
1063
1168
|
ext = os.path.splitext(f)[1].lower()
|
|
1064
1169
|
if ext in [".tif", ".tiff"]:
|
|
1065
1170
|
import rasterio
|
|
1171
|
+
from rasterio.enums import Resampling
|
|
1066
1172
|
with rasterio.open(f) as ds:
|
|
1067
|
-
arr = ds.read(
|
|
1173
|
+
arr = ds.read(
|
|
1174
|
+
out_shape=(ds.count, thumb_size[1], thumb_size[0]),
|
|
1175
|
+
resampling=Resampling.nearest
|
|
1176
|
+
)
|
|
1068
1177
|
if arr.shape[0] >= 3:
|
|
1069
1178
|
rgb = np.stack([normalize_to_uint8(arr[i]) for i in range(3)], axis=-1)
|
|
1070
1179
|
else:
|
|
@@ -1075,8 +1184,10 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1075
1184
|
img = Image.open(f).convert("RGB")
|
|
1076
1185
|
img.thumbnail(thumb_size)
|
|
1077
1186
|
thumbs.append(img)
|
|
1187
|
+
loaded_files.append(f)
|
|
1188
|
+
|
|
1078
1189
|
except Exception as e:
|
|
1079
|
-
print(f"[
|
|
1190
|
+
print(f"[SKIP] {os.path.basename(f)} — {e}")
|
|
1080
1191
|
|
|
1081
1192
|
if not thumbs:
|
|
1082
1193
|
print("[WARN] No valid images loaded.")
|
|
@@ -1099,11 +1210,103 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1099
1210
|
canvas.paste(img, (x, y))
|
|
1100
1211
|
|
|
1101
1212
|
print(f"[INFO] Displaying {n} images ({cols}×{rows} grid)")
|
|
1102
|
-
show_image_auto(np.array(canvas), display_scale, is_vector)
|
|
1103
1213
|
|
|
1214
|
+
for r in range(rows):
|
|
1215
|
+
row_files = loaded_files[r * cols:(r + 1) * cols]
|
|
1216
|
+
print(" ".join(f"{os.path.basename(f):<20}" for f in row_files))
|
|
1217
|
+
|
|
1218
|
+
show_image_auto(np.array(canvas), display_scale, is_vector)
|
|
1104
1219
|
except Exception as e:
|
|
1105
1220
|
print(f"[ERROR] Failed to render gallery: {e}")
|
|
1106
1221
|
|
|
1222
|
+
def render_bands_gallery(data: np.ndarray, band_list: list[int], band_count: int,
|
|
1223
|
+
grid: str = None, display_scale=None, colormap: str = "viridis") -> None:
|
|
1224
|
+
"""Render multiple bands from a single raster as a grid of thumbnails."""
|
|
1225
|
+
import math
|
|
1226
|
+
|
|
1227
|
+
# Validate bands
|
|
1228
|
+
valid_bands = [b for b in band_list if 1 <= b <= band_count]
|
|
1229
|
+
if not valid_bands:
|
|
1230
|
+
print(f"[ERROR] No valid bands. File has {band_count} bands.")
|
|
1231
|
+
return
|
|
1232
|
+
skipped = set(band_list) - set(valid_bands)
|
|
1233
|
+
if skipped:
|
|
1234
|
+
print(f"[WARN] Skipped out-of-range bands: {sorted(skipped)}")
|
|
1235
|
+
|
|
1236
|
+
n = len(valid_bands)
|
|
1237
|
+
|
|
1238
|
+
# Auto grid or user-specified
|
|
1239
|
+
if grid:
|
|
1240
|
+
try:
|
|
1241
|
+
cols, rows = map(int, grid.lower().split("x"))
|
|
1242
|
+
except Exception:
|
|
1243
|
+
cols = math.ceil(math.sqrt(n))
|
|
1244
|
+
rows = math.ceil(n / cols)
|
|
1245
|
+
else:
|
|
1246
|
+
cols = math.ceil(math.sqrt(n))
|
|
1247
|
+
rows = math.ceil(n / cols)
|
|
1248
|
+
|
|
1249
|
+
# Use fixed longest dimension to preserve aspect ratio
|
|
1250
|
+
max_thumb = 256
|
|
1251
|
+
label_height = 14 # space below each thumbnail for band label
|
|
1252
|
+
margin = 8
|
|
1253
|
+
cmap = colormaps[colormap]
|
|
1254
|
+
|
|
1255
|
+
thumbs = []
|
|
1256
|
+
thumb_w = thumb_h = max_thumb # will be updated from first image
|
|
1257
|
+
|
|
1258
|
+
for i, b in enumerate(valid_bands):
|
|
1259
|
+
arr = data[i].astype(float)
|
|
1260
|
+
normalized = normalize_to_uint8(arr)
|
|
1261
|
+
colored = cmap(normalized / 255.0)
|
|
1262
|
+
rgb = (colored[:, :, :3] * 255).astype(np.uint8)
|
|
1263
|
+
img = Image.fromarray(rgb)
|
|
1264
|
+
img.thumbnail((max_thumb, max_thumb), Image.LANCZOS)
|
|
1265
|
+
if i == 0:
|
|
1266
|
+
thumb_w, thumb_h = img.size
|
|
1267
|
+
thumbs.append(img)
|
|
1268
|
+
|
|
1269
|
+
# Build canvas with extra height per row for labels
|
|
1270
|
+
cols = min(cols, n)
|
|
1271
|
+
rows = math.ceil(n / cols)
|
|
1272
|
+
cell_w = thumb_w + margin
|
|
1273
|
+
cell_h = thumb_h + label_height + margin
|
|
1274
|
+
canvas_w = cols * cell_w + margin
|
|
1275
|
+
canvas_h = rows * cell_h + margin
|
|
1276
|
+
canvas = Image.new("RGB", (canvas_w, canvas_h), (220, 220, 220))
|
|
1277
|
+
draw = ImageDraw.Draw(canvas)
|
|
1278
|
+
from PIL import ImageFont
|
|
1279
|
+
try:
|
|
1280
|
+
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 10)
|
|
1281
|
+
except Exception:
|
|
1282
|
+
try:
|
|
1283
|
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
|
|
1284
|
+
except Exception:
|
|
1285
|
+
font = ImageFont.load_default()
|
|
1286
|
+
|
|
1287
|
+
for i, img in enumerate(thumbs):
|
|
1288
|
+
r, c = divmod(i, cols)
|
|
1289
|
+
x = margin + c * cell_w
|
|
1290
|
+
y = margin + r * cell_h
|
|
1291
|
+
canvas.paste(img, (x, y))
|
|
1292
|
+
if _TERMINAL_SUPPORTS_IMAGES:
|
|
1293
|
+
# Draw label on canvas background below the thumbnail
|
|
1294
|
+
label = f"B{valid_bands[i]}"
|
|
1295
|
+
lx = x + (thumb_w - len(label) * 6) // 2
|
|
1296
|
+
ly = y + thumb_h + 2
|
|
1297
|
+
draw.text((lx, ly), label, fill=(0, 0, 0), font=font)
|
|
1298
|
+
|
|
1299
|
+
print(f"[INFO] Displaying {n} bands ({cols}×{rows} grid, colormap: {colormap})")
|
|
1300
|
+
# print(f"[DEBUG] canvas size: {canvas_w}×{canvas_h}px")
|
|
1301
|
+
|
|
1302
|
+
if not _TERMINAL_SUPPORTS_IMAGES:
|
|
1303
|
+
# Print band labels as text grid
|
|
1304
|
+
for r in range(rows):
|
|
1305
|
+
row_bands = valid_bands[r * cols:(r + 1) * cols]
|
|
1306
|
+
print(" ".join(f"B{b:<4}" for b in row_bands))
|
|
1307
|
+
|
|
1308
|
+
show_image_auto(np.array(canvas), display_scale)
|
|
1309
|
+
|
|
1107
1310
|
# ---------------------------------------------------------------------
|
|
1108
1311
|
# Vector handling
|
|
1109
1312
|
# ---------------------------------------------------------------------
|
|
@@ -1463,147 +1666,140 @@ def main() -> None:
|
|
|
1463
1666
|
formatter_class=SmartDefaults
|
|
1464
1667
|
)
|
|
1465
1668
|
|
|
1466
|
-
|
|
1669
|
+
# File input
|
|
1467
1670
|
parser.add_argument(
|
|
1468
|
-
"paths", nargs="*",
|
|
1671
|
+
"paths", nargs="*",
|
|
1469
1672
|
help="Path to raster(s), vector, or CSV file. Provide 1 file or exactly 3 rasters for RGB (R G B)."
|
|
1470
1673
|
)
|
|
1471
|
-
|
|
1472
|
-
|
|
1674
|
+
|
|
1675
|
+
# General options
|
|
1676
|
+
general = parser.add_argument_group("General")
|
|
1677
|
+
general.add_argument(
|
|
1473
1678
|
"--display", type=float, default=None,
|
|
1474
1679
|
help="Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal."
|
|
1475
1680
|
)
|
|
1681
|
+
general.add_argument(
|
|
1682
|
+
"--gallery", nargs="?", const="4x4", metavar="GRID",
|
|
1683
|
+
help="Display all image files in a folder as thumbnails (e.g., --gallery 5x4). Incompatible files are skipped."
|
|
1684
|
+
)
|
|
1476
1685
|
|
|
1477
1686
|
# Raster options
|
|
1478
|
-
parser.
|
|
1687
|
+
raster = parser.add_argument_group("Raster")
|
|
1688
|
+
raster.add_argument(
|
|
1479
1689
|
"--band", type=int, default=None,
|
|
1480
|
-
help="Band number to display (single raster
|
|
1690
|
+
help="Band number to display (single raster), or slice number for NetCDF. (default: 1)"
|
|
1481
1691
|
)
|
|
1482
|
-
|
|
1692
|
+
raster.add_argument(
|
|
1693
|
+
"--bands", type=str,
|
|
1694
|
+
help="Display multiple bands as a grid. Accepts ranges (30-40), lists (3,4,5), or mixed (1,3,10-15)."
|
|
1695
|
+
)
|
|
1696
|
+
raster.add_argument(
|
|
1483
1697
|
"--timestep", type=int, default=None,
|
|
1484
|
-
help="Alias for --band when working with NetCDF files
|
|
1698
|
+
help="Alias for --band when working with NetCDF files."
|
|
1485
1699
|
)
|
|
1486
|
-
|
|
1700
|
+
raster.add_argument(
|
|
1701
|
+
"--subset", type=int, default=None,
|
|
1702
|
+
help="Variable index for NetCDF/HDF files (e.g., --subset 1)."
|
|
1703
|
+
)
|
|
1704
|
+
raster.add_argument(
|
|
1705
|
+
"--reduce", dest="reduce_dim", type=str, default=None, metavar="DIM_NAME",
|
|
1706
|
+
help="For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted."
|
|
1707
|
+
)
|
|
1708
|
+
raster.add_argument(
|
|
1487
1709
|
"--colormap", nargs="?", const="terrain",
|
|
1488
1710
|
choices=AVAILABLE_COLORMAPS, default=None,
|
|
1489
|
-
help="Apply colormap to single-band rasters
|
|
1711
|
+
help="Apply colormap to single-band rasters. Flag without value → 'terrain'."
|
|
1490
1712
|
)
|
|
1491
|
-
|
|
1492
|
-
"--rgb", nargs=
|
|
1493
|
-
help="Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3."
|
|
1713
|
+
raster.add_argument(
|
|
1714
|
+
"--rgb", nargs='+', type=str, metavar='BAND', default=None,
|
|
1715
|
+
help="Three band numbers for RGB display (e.g., --rgb 4 3 2 or --rgb 4,3,2). Overrides default 1 2 3."
|
|
1494
1716
|
)
|
|
1495
|
-
|
|
1717
|
+
raster.add_argument(
|
|
1496
1718
|
"--rgbfiles", nargs=3, type=str, metavar=('R', 'G', 'B'),
|
|
1497
|
-
help="Three single-band rasters for RGB composite
|
|
1719
|
+
help="Three single-band rasters for RGB composite. Can also provide as positional arguments."
|
|
1498
1720
|
)
|
|
1499
|
-
|
|
1721
|
+
raster.add_argument(
|
|
1500
1722
|
"--vmin", type=float, default=None,
|
|
1501
1723
|
help="Minimum pixel value for raster display scaling."
|
|
1502
1724
|
)
|
|
1503
|
-
|
|
1725
|
+
raster.add_argument(
|
|
1504
1726
|
"--vmax", type=float, default=None,
|
|
1505
1727
|
help="Maximum pixel value for raster display scaling."
|
|
1506
1728
|
)
|
|
1507
|
-
|
|
1729
|
+
raster.add_argument(
|
|
1508
1730
|
"--nodata", type=float, default=None,
|
|
1509
1731
|
help="Override nodata value for rasters if dataset metadata is missing or incorrect."
|
|
1510
1732
|
)
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
"
|
|
1517
|
-
help="Variable index for NetCDF files (e.g. --subset 1)."
|
|
1733
|
+
|
|
1734
|
+
# Vector options
|
|
1735
|
+
vector = parser.add_argument_group("Vector")
|
|
1736
|
+
vector.add_argument(
|
|
1737
|
+
"--color-by", type=str, default=None,
|
|
1738
|
+
help="Numeric column to color vector features by."
|
|
1518
1739
|
)
|
|
1519
|
-
|
|
1520
|
-
"--
|
|
1521
|
-
|
|
1522
|
-
|
|
1740
|
+
vector.add_argument(
|
|
1741
|
+
"--width", type=float, default=0.7,
|
|
1742
|
+
help="Line width for vector boundaries."
|
|
1743
|
+
)
|
|
1744
|
+
vector.add_argument(
|
|
1745
|
+
"--edgecolor", type=str, default="#F6FF00",
|
|
1746
|
+
help="Edge color for vector outlines (hex or named color)."
|
|
1747
|
+
)
|
|
1748
|
+
vector.add_argument(
|
|
1749
|
+
"--layer", type=str, default=None,
|
|
1750
|
+
help="Layer name for GeoPackage/multi-layer files, or variable name for NetCDF files."
|
|
1751
|
+
)
|
|
1752
|
+
vector.add_argument(
|
|
1753
|
+
"--table", action="store_true",
|
|
1754
|
+
help="Display vector/parquet file as tabular data instead of rendering geometry."
|
|
1523
1755
|
)
|
|
1524
1756
|
|
|
1525
|
-
#
|
|
1526
|
-
parser.
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
help="Show histograms for all numeric columns or specify one column name."
|
|
1757
|
+
# Tabular options
|
|
1758
|
+
tabular = parser.add_argument_group("Tabular")
|
|
1759
|
+
tabular.add_argument(
|
|
1760
|
+
"--hist", nargs="?", const=True,
|
|
1761
|
+
help="Show histograms for all numeric columns or specify one column name."
|
|
1531
1762
|
)
|
|
1532
|
-
|
|
1533
|
-
"--describe",
|
|
1534
|
-
nargs="?",
|
|
1535
|
-
const=True,
|
|
1763
|
+
tabular.add_argument(
|
|
1764
|
+
"--describe", nargs="?", const=True,
|
|
1536
1765
|
help="Show summary statistics for all numeric columns or specify one column name."
|
|
1537
1766
|
)
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1767
|
+
tabular.add_argument(
|
|
1768
|
+
"--bins", type=int, default=20,
|
|
1769
|
+
help="Number of bins for histograms (used with --hist)."
|
|
1541
1770
|
)
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1771
|
+
tabular.add_argument(
|
|
1772
|
+
"--scatter", nargs=2, metavar=("X", "Y"),
|
|
1773
|
+
help="Scatter plot of two numeric columns (e.g. --scatter area_km2 year)."
|
|
1545
1774
|
)
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
help="Show unique values for a categorical column and exit"
|
|
1775
|
+
tabular.add_argument(
|
|
1776
|
+
"--unique", metavar="COLUMN",
|
|
1777
|
+
help="Show unique values for a categorical column."
|
|
1550
1778
|
)
|
|
1551
|
-
|
|
1552
|
-
"--where",
|
|
1553
|
-
|
|
1554
|
-
default=None,
|
|
1555
|
-
help="Filter rows using SQL WHERE clause (DuckDB required). Example: --where \"year > 2010\""
|
|
1779
|
+
tabular.add_argument(
|
|
1780
|
+
"--where", type=str, default=None,
|
|
1781
|
+
help="Filter rows using SQL WHERE clause (e.g. --where \"year > 2010\")."
|
|
1556
1782
|
)
|
|
1557
|
-
|
|
1558
|
-
"--sort",
|
|
1559
|
-
|
|
1560
|
-
default=None,
|
|
1561
|
-
help="Sort rows by values in the specified column, ascending by default (e.g. --sort population). Use --desc to reverse."
|
|
1783
|
+
tabular.add_argument(
|
|
1784
|
+
"--sort", type=str, default=None,
|
|
1785
|
+
help="Sort rows by column, ascending by default. Use --desc to reverse."
|
|
1562
1786
|
)
|
|
1563
|
-
|
|
1564
|
-
"--desc",
|
|
1565
|
-
action="store_true",
|
|
1787
|
+
tabular.add_argument(
|
|
1788
|
+
"--desc", action="store_true",
|
|
1566
1789
|
help="Sort in descending order."
|
|
1567
1790
|
)
|
|
1568
|
-
|
|
1569
|
-
"--limit",
|
|
1570
|
-
type=int,
|
|
1571
|
-
default=None,
|
|
1791
|
+
tabular.add_argument(
|
|
1792
|
+
"--limit", type=int, default=None,
|
|
1572
1793
|
help="Limit number of rows shown (e.g. --limit 100)."
|
|
1573
1794
|
)
|
|
1574
|
-
|
|
1575
|
-
"--select",
|
|
1576
|
-
|
|
1577
|
-
help="Select specific columns (space separated) (e.g. --select Country City)"
|
|
1578
|
-
)
|
|
1579
|
-
parser.add_argument(
|
|
1580
|
-
"--sql",
|
|
1581
|
-
type=str,
|
|
1582
|
-
help="Execute full DuckDB SQL query against CSV (advanced mode)."
|
|
1583
|
-
)
|
|
1584
|
-
|
|
1585
|
-
# Vector options
|
|
1586
|
-
parser.add_argument(
|
|
1587
|
-
"--color-by", type=str, default=None,
|
|
1588
|
-
help="Numeric column to color vector features by (optional)."
|
|
1589
|
-
)
|
|
1590
|
-
parser.add_argument(
|
|
1591
|
-
"--width", type=float, default=0.7,
|
|
1592
|
-
help="Line width for vector boundaries"
|
|
1593
|
-
)
|
|
1594
|
-
parser.add_argument(
|
|
1595
|
-
"--edgecolor", type=str, default="#F6FF00",
|
|
1596
|
-
help="Edge color for vector outlines (hex or named color)."
|
|
1795
|
+
tabular.add_argument(
|
|
1796
|
+
"--select", nargs="+",
|
|
1797
|
+
help="Select specific columns (e.g. --select Country City)."
|
|
1597
1798
|
)
|
|
1598
|
-
|
|
1599
|
-
"--
|
|
1600
|
-
|
|
1601
|
-
help="Layer name for GeoPackage/multi-layer files, or variable name for NetCDF files."
|
|
1799
|
+
tabular.add_argument(
|
|
1800
|
+
"--sql", type=str,
|
|
1801
|
+
help="Execute full DuckDB SQL query against CSV/parquet (advanced mode)."
|
|
1602
1802
|
)
|
|
1603
|
-
parser.add_argument(
|
|
1604
|
-
"--table", action="store_true",
|
|
1605
|
-
help="Display vector/parquet file as tabular data instead of rendering geometry."
|
|
1606
|
-
)
|
|
1607
1803
|
|
|
1608
1804
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
1609
1805
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|