viewinline 0.2.3__tar.gz → 0.3.0__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.2.3 → viewinline-0.3.0}/PKG-INFO +10 -4
- {viewinline-0.2.3 → viewinline-0.3.0}/README.md +9 -3
- {viewinline-0.2.3 → viewinline-0.3.0}/pyproject.toml +1 -1
- {viewinline-0.2.3 → viewinline-0.3.0}/src/viewinline/viewinline.py +205 -28
- {viewinline-0.2.3 → viewinline-0.3.0}/.github/FUNDING.yml +0 -0
- {viewinline-0.2.3 → viewinline-0.3.0}/.gitignore +0 -0
- {viewinline-0.2.3 → viewinline-0.3.0}/LICENSE +0 -0
- {viewinline-0.2.3 → viewinline-0.3.0}/src/viewinline/__init__.py +0 -0
- {viewinline-0.2.3 → viewinline-0.3.0}/viewinline_gif1.gif +0 -0
- {viewinline-0.2.3 → viewinline-0.3.0}/viewinline_gif2.gif +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viewinline
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
[](https://pypi.org/project/viewinline/)
|
|
38
38
|
|
|
39
39
|
**Quick-look geospatial viewer for compatible terminals.**
|
|
40
|
-
Displays rasters, vectors, and tabular data directly in the terminal with no GUI
|
|
40
|
+
Displays rasters, vectors, and tabular data directly in the terminal with no GUI.
|
|
41
41
|
|
|
42
42
|
<p align="center">
|
|
43
43
|
<a href="viewinline_gif1.gif"><img src="viewinline_gif1.gif" width="49%"></a>
|
|
@@ -64,6 +64,7 @@ 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
|
|
67
68
|
viewinline path/to/multiband.tif --rgb 3 2 1
|
|
68
69
|
viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
|
|
69
70
|
|
|
@@ -72,6 +73,7 @@ viewinline file.nc # list variables
|
|
|
72
73
|
viewinline file.nc --subset 2 # display variable 2
|
|
73
74
|
viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
|
|
74
75
|
viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
|
|
76
|
+
viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
|
|
75
77
|
|
|
76
78
|
# Vectors
|
|
77
79
|
viewinline path/to/vector.geojson
|
|
@@ -113,6 +115,7 @@ Install chafa once (it's a system binary, available across all conda/virtualenv
|
|
|
113
115
|
brew install chafa # macOS
|
|
114
116
|
sudo apt install chafa # Debian/Ubuntu
|
|
115
117
|
sudo dnf install chafa # Fedora
|
|
118
|
+
scoop install chafa # Windows
|
|
116
119
|
```
|
|
117
120
|
Without chafa, terminals outside the native list above show an info message instead of an image.
|
|
118
121
|
You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGINE=chafa`.
|
|
@@ -169,7 +172,9 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
|
|
|
169
172
|
|
|
170
173
|
**NetCDF/HDF notes:**
|
|
171
174
|
- viewinline lists only variables that can be displayed as 2D or 3D arrays
|
|
172
|
-
-
|
|
175
|
+
- 3D variables with time or known spatial dimensions are auto-handled (slices along the non-spatial axis)
|
|
176
|
+
- For 3D variables with non-standard dimensions (e.g., hyperspectral cubes like PICARD), viewinline auto-detects the band axis by smallest dimension. Use `--reduce DIM_NAME` to override.
|
|
177
|
+
- Variables with 4+ dimensions are not supported
|
|
173
178
|
- For a complete variable list, use `ncdump -h file.nc` or `viewtif`
|
|
174
179
|
|
|
175
180
|
## Dependencies
|
|
@@ -198,7 +203,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
|
|
|
198
203
|
|
|
199
204
|
**Note on HDF support:**
|
|
200
205
|
- **HDF5** (.h5, .hdf5): Supported via rasterio if GDAL has HDF5 support (most installations)
|
|
201
|
-
- **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (
|
|
206
|
+
- **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (the legacy format used by MODIS and older NASA products)
|
|
202
207
|
- **NetCDF** (.nc): Supported via rasterio (uses GDAL's NetCDF driver)
|
|
203
208
|
|
|
204
209
|
## Available options
|
|
@@ -210,6 +215,7 @@ Raster:
|
|
|
210
215
|
--band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
|
|
211
216
|
--timestep INTEGER Alias for --band when working with NetCDF files.
|
|
212
217
|
--subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
|
|
218
|
+
--reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
|
|
213
219
|
--colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
|
|
214
220
|
--rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
|
|
215
221
|
--rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://pypi.org/project/viewinline/)
|
|
5
5
|
|
|
6
6
|
**Quick-look geospatial viewer for compatible terminals.**
|
|
7
|
-
Displays rasters, vectors, and tabular data directly in the terminal with no GUI
|
|
7
|
+
Displays rasters, vectors, and tabular data directly in the terminal with no GUI.
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="viewinline_gif1.gif"><img src="viewinline_gif1.gif" width="49%"></a>
|
|
@@ -31,6 +31,7 @@ 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
|
|
34
35
|
viewinline path/to/multiband.tif --rgb 3 2 1
|
|
35
36
|
viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
|
|
36
37
|
|
|
@@ -39,6 +40,7 @@ viewinline file.nc # list variables
|
|
|
39
40
|
viewinline file.nc --subset 2 # display variable 2
|
|
40
41
|
viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
|
|
41
42
|
viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
|
|
43
|
+
viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
|
|
42
44
|
|
|
43
45
|
# Vectors
|
|
44
46
|
viewinline path/to/vector.geojson
|
|
@@ -80,6 +82,7 @@ Install chafa once (it's a system binary, available across all conda/virtualenv
|
|
|
80
82
|
brew install chafa # macOS
|
|
81
83
|
sudo apt install chafa # Debian/Ubuntu
|
|
82
84
|
sudo dnf install chafa # Fedora
|
|
85
|
+
scoop install chafa # Windows
|
|
83
86
|
```
|
|
84
87
|
Without chafa, terminals outside the native list above show an info message instead of an image.
|
|
85
88
|
You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGINE=chafa`.
|
|
@@ -136,7 +139,9 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
|
|
|
136
139
|
|
|
137
140
|
**NetCDF/HDF notes:**
|
|
138
141
|
- viewinline lists only variables that can be displayed as 2D or 3D arrays
|
|
139
|
-
-
|
|
142
|
+
- 3D variables with time or known spatial dimensions are auto-handled (slices along the non-spatial axis)
|
|
143
|
+
- For 3D variables with non-standard dimensions (e.g., hyperspectral cubes like PICARD), viewinline auto-detects the band axis by smallest dimension. Use `--reduce DIM_NAME` to override.
|
|
144
|
+
- Variables with 4+ dimensions are not supported
|
|
140
145
|
- For a complete variable list, use `ncdump -h file.nc` or `viewtif`
|
|
141
146
|
|
|
142
147
|
## Dependencies
|
|
@@ -165,7 +170,7 @@ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGI
|
|
|
165
170
|
|
|
166
171
|
**Note on HDF support:**
|
|
167
172
|
- **HDF5** (.h5, .hdf5): Supported via rasterio if GDAL has HDF5 support (most installations)
|
|
168
|
-
- **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (
|
|
173
|
+
- **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (the legacy format used by MODIS and older NASA products)
|
|
169
174
|
- **NetCDF** (.nc): Supported via rasterio (uses GDAL's NetCDF driver)
|
|
170
175
|
|
|
171
176
|
## Available options
|
|
@@ -177,6 +182,7 @@ Raster:
|
|
|
177
182
|
--band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
|
|
178
183
|
--timestep INTEGER Alias for --band when working with NetCDF files.
|
|
179
184
|
--subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
|
|
185
|
+
--reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
|
|
180
186
|
--colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
|
|
181
187
|
--rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
|
|
182
188
|
--rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
|
|
@@ -28,12 +28,19 @@ from matplotlib import colormaps
|
|
|
28
28
|
import matplotlib as mpl
|
|
29
29
|
import subprocess
|
|
30
30
|
|
|
31
|
+
try:
|
|
32
|
+
import netCDF4
|
|
33
|
+
HAS_NETCDF4 = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
HAS_NETCDF4 = False
|
|
36
|
+
|
|
31
37
|
import warnings
|
|
32
38
|
|
|
33
39
|
warnings.filterwarnings("ignore", message="More than one layer found", category=UserWarning)
|
|
34
40
|
warnings.filterwarnings("ignore", message="Dataset has no geotransform", category=UserWarning)
|
|
41
|
+
warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset attribute", category=UserWarning)
|
|
35
42
|
|
|
36
|
-
__version__ = "0.
|
|
43
|
+
__version__ = "0.3.0"
|
|
37
44
|
|
|
38
45
|
AVAILABLE_COLORMAPS = [
|
|
39
46
|
"viridis", "inferno", "magma", "plasma",
|
|
@@ -184,14 +191,7 @@ def show_inline_image(image_array: np.ndarray, display_scale = None, is_vector:
|
|
|
184
191
|
|
|
185
192
|
if _TERMINAL_SUPPORTS_IMAGES:
|
|
186
193
|
sys.stdout.write(f"\033]1337;File=inline=1;width={width_pct}%:{encoded}\a\n")
|
|
187
|
-
else:
|
|
188
|
-
# if is_chafa_available():
|
|
189
|
-
# chafa_output = subprocess.check_output(
|
|
190
|
-
# ["chafa", "-"],
|
|
191
|
-
# input=image_bytes
|
|
192
|
-
# ).decode()
|
|
193
|
-
|
|
194
|
-
# sys.stdout.write(f"\n{chafa_output}\a\n")
|
|
194
|
+
else:
|
|
195
195
|
if is_chafa_available():
|
|
196
196
|
# Inside tmux, force chafa to use block-art symbols instead of
|
|
197
197
|
# graphics protocols. Tmux mangles kitty graphics and sixel
|
|
@@ -227,9 +227,9 @@ def show_image_auto(img: np.ndarray, display_scale=None, is_vector: bool = False
|
|
|
227
227
|
try:
|
|
228
228
|
show_inline_image(img, display_scale, is_vector)
|
|
229
229
|
if _TERMINAL_SUPPORTS_IMAGES:
|
|
230
|
-
print("[VIEW]
|
|
230
|
+
print("[VIEW] Inline render complete")
|
|
231
231
|
elif is_chafa_available():
|
|
232
|
-
print("[VIEW]
|
|
232
|
+
print("[VIEW] Inline render complete via chafa")
|
|
233
233
|
# If neither path applies, show_inline_image already printed the info message
|
|
234
234
|
except Exception as e:
|
|
235
235
|
print(f"[ERROR] Failed to render image: {e}")
|
|
@@ -262,7 +262,6 @@ def load_csv_to_df(path: str) -> pd.DataFrame:
|
|
|
262
262
|
print(f"[ERROR] Failed to read CSV: {e}")
|
|
263
263
|
return pd.DataFrame()
|
|
264
264
|
|
|
265
|
-
|
|
266
265
|
# =============================================================
|
|
267
266
|
# Preview
|
|
268
267
|
# =============================================================
|
|
@@ -643,6 +642,177 @@ def render_simple_image(filepath: str, args) -> None:
|
|
|
643
642
|
except Exception as e:
|
|
644
643
|
print(f"[ERROR] Failed to load image: {e}")
|
|
645
644
|
|
|
645
|
+
def render_netcdf_via_netcdf4(path, args):
|
|
646
|
+
"""Read a NetCDF file via netCDF4 (bypassing GDAL). Handles hierarchical
|
|
647
|
+
groups and hyperspectral cubes where GDAL aborts or interprets axes wrong.
|
|
648
|
+
"""
|
|
649
|
+
if not HAS_NETCDF4:
|
|
650
|
+
print("[ERROR] netCDF4 not installed. Install with:")
|
|
651
|
+
print(" pip install netCDF4")
|
|
652
|
+
print(" or: pip install viewinline[netcdf]")
|
|
653
|
+
return
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
nc = netCDF4.Dataset(path)
|
|
657
|
+
except Exception as e:
|
|
658
|
+
print(f"[ERROR] Could not open NetCDF file: {e}")
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
# Recursively collect (path, variable) pairs across all groups
|
|
662
|
+
def collect_vars(group, prefix=""):
|
|
663
|
+
out = []
|
|
664
|
+
for name, var in group.variables.items():
|
|
665
|
+
full_name = f"{prefix}{name}"
|
|
666
|
+
out.append((full_name, var))
|
|
667
|
+
for sub_name, sub in group.groups.items():
|
|
668
|
+
out.extend(collect_vars(sub, f"{prefix}{sub_name}/"))
|
|
669
|
+
return out
|
|
670
|
+
|
|
671
|
+
all_vars = collect_vars(nc)
|
|
672
|
+
|
|
673
|
+
if not all_vars:
|
|
674
|
+
print("[ERROR] No variables found in file.")
|
|
675
|
+
nc.close()
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
# If no --subset, list all variables and exit
|
|
679
|
+
if not args.subset:
|
|
680
|
+
print(f"Found {len(all_vars)} variables in {os.path.basename(path)}:")
|
|
681
|
+
for i, (name, var) in enumerate(all_vars, 1):
|
|
682
|
+
shape_str = "x".join(str(s) for s in var.shape)
|
|
683
|
+
print(f" [{i}] {name} ({shape_str}, {var.dtype})")
|
|
684
|
+
print(f"\nUse --subset <N> to display a specific variable.")
|
|
685
|
+
nc.close()
|
|
686
|
+
return
|
|
687
|
+
|
|
688
|
+
# Validate --subset
|
|
689
|
+
if args.subset < 1 or args.subset > len(all_vars):
|
|
690
|
+
print(f"[ERROR] --subset must be between 1 and {len(all_vars)}")
|
|
691
|
+
nc.close()
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
var_name, var = all_vars[args.subset - 1]
|
|
695
|
+
print(f"[INFO] Displaying variable {args.subset}: {var_name}")
|
|
696
|
+
print(f"[DATA] Shape: {var.shape} dtype: {var.dtype} dims: {var.dimensions}")
|
|
697
|
+
|
|
698
|
+
# Detect dimensionality and read the right slice
|
|
699
|
+
if var.ndim == 2:
|
|
700
|
+
data = np.asarray(var[:, :], dtype=np.float64)
|
|
701
|
+
slice_info = "2D variable"
|
|
702
|
+
|
|
703
|
+
elif var.ndim == 3:
|
|
704
|
+
spatial_dims = {'lat', 'lon', 'latitude', 'longitude', 'y', 'x'}
|
|
705
|
+
|
|
706
|
+
spectral_axis = None
|
|
707
|
+
|
|
708
|
+
# 1. User override via --reduce
|
|
709
|
+
if args.reduce_dim is not None:
|
|
710
|
+
if args.reduce_dim in var.dimensions:
|
|
711
|
+
spectral_axis = list(var.dimensions).index(args.reduce_dim)
|
|
712
|
+
print(f"[INFO] Using user-specified --reduce '{args.reduce_dim}'")
|
|
713
|
+
else:
|
|
714
|
+
print(f"[ERROR] --reduce '{args.reduce_dim}' is not a dimension of this variable.")
|
|
715
|
+
print(f"[INFO] Available dimensions: {list(var.dimensions)}")
|
|
716
|
+
nc.close()
|
|
717
|
+
return
|
|
718
|
+
|
|
719
|
+
# 2. Standard convention: reduce along the non-spatial dim
|
|
720
|
+
if spectral_axis is None:
|
|
721
|
+
has_standard_spatial = any(d in spatial_dims for d in var.dimensions)
|
|
722
|
+
if has_standard_spatial:
|
|
723
|
+
for i, dim_name in enumerate(var.dimensions):
|
|
724
|
+
if dim_name not in spatial_dims:
|
|
725
|
+
spectral_axis = i
|
|
726
|
+
break
|
|
727
|
+
|
|
728
|
+
# 3. Fallback heuristic: smallest dim is typically the band axis
|
|
729
|
+
if spectral_axis is None:
|
|
730
|
+
sizes = [(i, var.shape[i]) for i in range(3)]
|
|
731
|
+
spectral_axis = min(sizes, key=lambda x: x[1])[0]
|
|
732
|
+
print(f"[INFO] Non-standard dimensions detected: {list(var.dimensions)}")
|
|
733
|
+
print(f"[INFO] Reducing along '{var.dimensions[spectral_axis]}' (size {var.shape[spectral_axis]}, assumed band/spectral axis)")
|
|
734
|
+
print(f"[INFO] If this is not correct, use --reduce DIM_NAME to override.")
|
|
735
|
+
|
|
736
|
+
# Slice along chosen axis
|
|
737
|
+
band_count = var.shape[spectral_axis]
|
|
738
|
+
band_num = args.band if args.band is not None else 1
|
|
739
|
+
band_idx = max(0, min(band_num - 1, band_count - 1))
|
|
740
|
+
slicer = [slice(None)] * 3
|
|
741
|
+
slicer[spectral_axis] = band_idx
|
|
742
|
+
data = np.asarray(var[tuple(slicer)], dtype=np.float64)
|
|
743
|
+
slice_info = f"slice along axis {spectral_axis} ({var.dimensions[spectral_axis]}), band {band_idx + 1} of {band_count}"
|
|
744
|
+
|
|
745
|
+
else:
|
|
746
|
+
print(f"[ERROR] viewinline only supports 2D or 3D variables. This one is {var.ndim}D.")
|
|
747
|
+
nc.close()
|
|
748
|
+
return
|
|
749
|
+
|
|
750
|
+
print(f"[DATA] {slice_info}")
|
|
751
|
+
# Apply fill value
|
|
752
|
+
fill = getattr(var, '_FillValue', None)
|
|
753
|
+
if fill is not None:
|
|
754
|
+
data = np.where(data == fill, np.nan, data)
|
|
755
|
+
# Flip vertically if data is stored south-to-north so north appears at top.
|
|
756
|
+
# Determine which dims remain after slicing — for the 2D result, figure out
|
|
757
|
+
# which axis (0 or 1) corresponds to latitude, and check that dim's coord values.
|
|
758
|
+
if var.ndim == 2:
|
|
759
|
+
remaining_dims = list(var.dimensions)
|
|
760
|
+
elif var.ndim == 3:
|
|
761
|
+
if spectral_axis is not None:
|
|
762
|
+
remaining_dims = [d for i, d in enumerate(var.dimensions) if i != spectral_axis]
|
|
763
|
+
else:
|
|
764
|
+
remaining_dims = list(var.dimensions[1:]) # axis 0 was reduced
|
|
765
|
+
else:
|
|
766
|
+
remaining_dims = []
|
|
767
|
+
lat_names = {'lat', 'latitude', 'y'}
|
|
768
|
+
for axis_in_2d, dim_name in enumerate(remaining_dims):
|
|
769
|
+
if dim_name in lat_names and dim_name in nc.variables:
|
|
770
|
+
lat_vals = nc[dim_name][:]
|
|
771
|
+
if len(lat_vals) > 1 and lat_vals[0] < lat_vals[-1]:
|
|
772
|
+
data = np.flip(data, axis=axis_in_2d)
|
|
773
|
+
print(f"[INFO] Flipped along '{dim_name}' for display (data stored south-to-north).")
|
|
774
|
+
break
|
|
775
|
+
nc.close()
|
|
776
|
+
|
|
777
|
+
# Normalize and display
|
|
778
|
+
band_u8 = normalize_to_uint8(data, vmin=args.vmin, vmax=args.vmax,
|
|
779
|
+
nodata=args.nodata)
|
|
780
|
+
|
|
781
|
+
if args.colormap:
|
|
782
|
+
cmap = colormaps[args.colormap]
|
|
783
|
+
colored = cmap(band_u8 / 255.0)
|
|
784
|
+
img = (colored[:, :, :3] * 255).astype(np.uint8)
|
|
785
|
+
print(f"[INFO] Applying colormap: {args.colormap}")
|
|
786
|
+
else:
|
|
787
|
+
img = np.stack([band_u8] * 3, axis=-1)
|
|
788
|
+
print("[INFO] Displaying grayscale")
|
|
789
|
+
|
|
790
|
+
# Resize to terminal
|
|
791
|
+
H, W = img.shape[:2]
|
|
792
|
+
if args.display:
|
|
793
|
+
new_w, new_h = max(1, int(W * args.display)), max(1, int(H * args.display))
|
|
794
|
+
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
795
|
+
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
|
+
else:
|
|
800
|
+
max_dim = 2000
|
|
801
|
+
if max(img.shape[:2]) > max_dim:
|
|
802
|
+
scale = max_dim / max(img.shape[:2])
|
|
803
|
+
new_w = int(img.shape[1] * scale)
|
|
804
|
+
new_h = int(img.shape[0] * scale)
|
|
805
|
+
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})")
|
|
807
|
+
print(f"[INFO] Use --display 1 for full resolution.")
|
|
808
|
+
else:
|
|
809
|
+
# (matches the width_pct logic in show_inline_image)
|
|
810
|
+
display_pct = args.display if args.display is not None else 0.33
|
|
811
|
+
|
|
812
|
+
print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
|
|
813
|
+
|
|
814
|
+
show_image_auto(img, getattr(args, "display", None), is_vector=False)
|
|
815
|
+
|
|
646
816
|
def render_raster(paths: list[str], args) -> None:
|
|
647
817
|
try:
|
|
648
818
|
import rasterio
|
|
@@ -655,8 +825,13 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
655
825
|
|
|
656
826
|
if len(paths) == 1:
|
|
657
827
|
path = paths[0]
|
|
828
|
+
|
|
829
|
+
if path.lower().endswith('.nc'):
|
|
830
|
+
render_netcdf_via_netcdf4(path, args)
|
|
831
|
+
return
|
|
658
832
|
|
|
659
833
|
# Handle NetCDF/HDF with subdatasets
|
|
834
|
+
|
|
660
835
|
if path.lower().endswith(('.nc', '.hdf', '.hdf5', '.h5')):
|
|
661
836
|
try:
|
|
662
837
|
|
|
@@ -732,7 +907,7 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
732
907
|
resampling=rasterio.enums.Resampling.bilinear
|
|
733
908
|
)
|
|
734
909
|
|
|
735
|
-
print(f"[
|
|
910
|
+
print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
|
|
736
911
|
else:
|
|
737
912
|
data = ds.read()
|
|
738
913
|
|
|
@@ -744,8 +919,11 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
744
919
|
print(f"[INFO] Multi-band raster detected ({band_count} bands)")
|
|
745
920
|
|
|
746
921
|
# MULTI BAND RGB (skip for NetCDF - treat as slices/timesteps, not RGB)
|
|
747
|
-
if band_count >= 3 and not paths[0].lower().endswith('.nc'):
|
|
748
|
-
|
|
922
|
+
# if band_count >= 3 and not paths[0].lower().endswith('.nc'):
|
|
923
|
+
# Auto-composite to RGB only when user didn't explicitly ask for a single band
|
|
924
|
+
# user_specified_band = args.band is not None and args.band != 1
|
|
925
|
+
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:
|
|
749
927
|
|
|
750
928
|
if getattr(args, "rgb", None):
|
|
751
929
|
try:
|
|
@@ -774,7 +952,8 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
774
952
|
# SINGLE BAND
|
|
775
953
|
else:
|
|
776
954
|
|
|
777
|
-
|
|
955
|
+
band_num = args.band if args.band is not None else 1
|
|
956
|
+
band_idx = max(0, min(band_num - 1, band_count - 1))
|
|
778
957
|
# print(f"[INFO] Displaying band {band_idx + 1} of {band_count}")
|
|
779
958
|
raw_band = data[band_idx].astype(float)
|
|
780
959
|
|
|
@@ -851,7 +1030,7 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
851
1030
|
print(f"[ERROR] Cannot display this variable.")
|
|
852
1031
|
print("[INFO] viewinline only supports 2D or 3D NetCDF variables")
|
|
853
1032
|
else:
|
|
854
|
-
print(f"[ERROR]
|
|
1033
|
+
print(f"[ERROR] Inline render failed: {e}")
|
|
855
1034
|
|
|
856
1035
|
|
|
857
1036
|
def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector=False) -> None:
|
|
@@ -956,13 +1135,6 @@ def render_vector(path, args):
|
|
|
956
1135
|
except Exception as e:
|
|
957
1136
|
print(f"[WARN] Could not list layers: {e}")
|
|
958
1137
|
|
|
959
|
-
# try:
|
|
960
|
-
# gdf = gpd.read_file(path, layer=getattr(args, "layer", None))
|
|
961
|
-
# print(f"[DATA] Vector loaded: {os.path.basename(path)} ({len(gdf)} features)")
|
|
962
|
-
# except Exception as e:
|
|
963
|
-
# print(f"[ERROR] Failed to read vector: {e}")
|
|
964
|
-
# return
|
|
965
|
-
|
|
966
1138
|
try:
|
|
967
1139
|
# Use read_parquet for parquet/geoparquet files
|
|
968
1140
|
if path.lower().endswith(('.parquet', '.geoparquet')):
|
|
@@ -1304,7 +1476,7 @@ def main() -> None:
|
|
|
1304
1476
|
|
|
1305
1477
|
# Raster options
|
|
1306
1478
|
parser.add_argument(
|
|
1307
|
-
"--band", type=int, default=
|
|
1479
|
+
"--band", type=int, default=None,
|
|
1308
1480
|
help="Band number to display (single raster case), or slice number for NetCDF."
|
|
1309
1481
|
)
|
|
1310
1482
|
parser.add_argument(
|
|
@@ -1320,6 +1492,10 @@ def main() -> None:
|
|
|
1320
1492
|
"--rgb", nargs=3, type=int, metavar=('R', 'G', 'B'), default=None,
|
|
1321
1493
|
help="Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3."
|
|
1322
1494
|
)
|
|
1495
|
+
parser.add_argument(
|
|
1496
|
+
"--rgbfiles", nargs=3, type=str, metavar=('R', 'G', 'B'),
|
|
1497
|
+
help="Three single-band rasters for RGB composite (e.g., --rgbfiles R.tif G.tif B.tif). Can also provide as positional arguments without the flag."
|
|
1498
|
+
)
|
|
1323
1499
|
parser.add_argument(
|
|
1324
1500
|
"--vmin", type=float, default=None,
|
|
1325
1501
|
help="Minimum pixel value for raster display scaling."
|
|
@@ -1341,8 +1517,9 @@ def main() -> None:
|
|
|
1341
1517
|
help="Variable index for NetCDF files (e.g. --subset 1)."
|
|
1342
1518
|
)
|
|
1343
1519
|
parser.add_argument(
|
|
1344
|
-
"--
|
|
1345
|
-
|
|
1520
|
+
"--reduce", dest="reduce_dim", type=str, default=None,
|
|
1521
|
+
metavar="DIM_NAME",
|
|
1522
|
+
help="For 3D NetCDF variables, specify which dimension to use as the band axis (auto-detected if omitted)."
|
|
1346
1523
|
)
|
|
1347
1524
|
|
|
1348
1525
|
# CSV options
|
|
@@ -1426,7 +1603,7 @@ def main() -> None:
|
|
|
1426
1603
|
parser.add_argument(
|
|
1427
1604
|
"--table", action="store_true",
|
|
1428
1605
|
help="Display vector/parquet file as tabular data instead of rendering geometry."
|
|
1429
|
-
)
|
|
1606
|
+
)
|
|
1430
1607
|
|
|
1431
1608
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
1432
1609
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|