viewinline 0.3.0__tar.gz → 0.3.1__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.1}/PKG-INFO +7 -3
- {viewinline-0.3.0 → viewinline-0.3.1}/README.md +6 -2
- {viewinline-0.3.0 → viewinline-0.3.1}/pyproject.toml +1 -1
- {viewinline-0.3.0 → viewinline-0.3.1}/src/viewinline/viewinline.py +151 -17
- {viewinline-0.3.0 → viewinline-0.3.1}/.github/FUNDING.yml +0 -0
- {viewinline-0.3.0 → viewinline-0.3.1}/.gitignore +0 -0
- {viewinline-0.3.0 → viewinline-0.3.1}/LICENSE +0 -0
- {viewinline-0.3.0 → viewinline-0.3.1}/src/viewinline/__init__.py +0 -0
- {viewinline-0.3.0 → viewinline-0.3.1}/viewinline_gif1.gif +0 -0
- {viewinline-0.3.0 → viewinline-0.3.1}/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.1
|
|
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,6 +216,7 @@ 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).
|
|
216
220
|
--timestep INTEGER Alias for --band when working with NetCDF files.
|
|
217
221
|
--subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
|
|
218
222
|
--reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
|
|
@@ -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,6 +183,7 @@ 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).
|
|
183
187
|
--timestep INTEGER Alias for --band when working with NetCDF files.
|
|
184
188
|
--subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
|
|
185
189
|
--reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
|
|
@@ -40,7 +40,7 @@ warnings.filterwarnings("ignore", message="More than one layer found", category=
|
|
|
40
40
|
warnings.filterwarnings("ignore", message="Dataset has no geotransform", category=UserWarning)
|
|
41
41
|
warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset attribute", category=UserWarning)
|
|
42
42
|
|
|
43
|
-
__version__ = "0.3.
|
|
43
|
+
__version__ = "0.3.1"
|
|
44
44
|
|
|
45
45
|
AVAILABLE_COLORMAPS = [
|
|
46
46
|
"viridis", "inferno", "magma", "plasma",
|
|
@@ -248,6 +248,24 @@ def resize_to_terminal(img: np.ndarray) -> tuple[np.ndarray, float]:
|
|
|
248
248
|
pil_img = ImageOps.contain(pil_img, (new_w, new_h))
|
|
249
249
|
return np.array(pil_img), scale
|
|
250
250
|
|
|
251
|
+
def parse_bands(s: str) -> list[int]:
|
|
252
|
+
"""Parse --bands argument: accepts ranges (30-40), lists (3,4,5), or mixed (1,3,10-15)."""
|
|
253
|
+
bands = []
|
|
254
|
+
for part in s.split(","):
|
|
255
|
+
part = part.strip()
|
|
256
|
+
if "-" in part:
|
|
257
|
+
try:
|
|
258
|
+
start, end = map(int, part.split("-", 1))
|
|
259
|
+
bands.extend(range(start, end + 1))
|
|
260
|
+
except ValueError:
|
|
261
|
+
print(f"[WARN] Could not parse band range: {part}")
|
|
262
|
+
else:
|
|
263
|
+
try:
|
|
264
|
+
bands.append(int(part))
|
|
265
|
+
except ValueError:
|
|
266
|
+
print(f"[WARN] Could not parse band: {part}")
|
|
267
|
+
return sorted(set(bands))
|
|
268
|
+
|
|
251
269
|
# ---------------------------------------------------------------------
|
|
252
270
|
# CSV handling
|
|
253
271
|
# ---------------------------------------------------------------------
|
|
@@ -737,6 +755,28 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
737
755
|
band_count = var.shape[spectral_axis]
|
|
738
756
|
band_num = args.band if args.band is not None else 1
|
|
739
757
|
band_idx = max(0, min(band_num - 1, band_count - 1))
|
|
758
|
+
|
|
759
|
+
# BANDS GALLERY for NetCDF
|
|
760
|
+
if getattr(args, "bands", None):
|
|
761
|
+
band_list = parse_bands(args.bands)
|
|
762
|
+
print(f"[DEBUG] band_count={band_count}, band_list={band_list}")
|
|
763
|
+
valid_bands = [b for b in band_list if 1 <= b <= band_count]
|
|
764
|
+
if not valid_bands:
|
|
765
|
+
print(f"[ERROR] No valid bands. Variable has {band_count} bands along '{var.dimensions[spectral_axis]}'.")
|
|
766
|
+
nc.close()
|
|
767
|
+
return
|
|
768
|
+
slices = []
|
|
769
|
+
for b in valid_bands:
|
|
770
|
+
slicer = [slice(None)] * 3
|
|
771
|
+
slicer[spectral_axis] = b - 1
|
|
772
|
+
slices.append(np.asarray(var[tuple(slicer)], dtype=np.float64))
|
|
773
|
+
nc.close()
|
|
774
|
+
colormap = args.colormap if args.colormap else "viridis"
|
|
775
|
+
render_bands_gallery(np.stack(slices, axis=0), valid_bands, band_count,
|
|
776
|
+
display_scale=getattr(args, "display", None),
|
|
777
|
+
colormap=colormap)
|
|
778
|
+
return
|
|
779
|
+
|
|
740
780
|
slicer = [slice(None)] * 3
|
|
741
781
|
slicer[spectral_axis] = band_idx
|
|
742
782
|
data = np.asarray(var[tuple(slicer)], dtype=np.float64)
|
|
@@ -793,9 +833,6 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
793
833
|
new_w, new_h = max(1, int(W * args.display)), max(1, int(H * args.display))
|
|
794
834
|
img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
|
|
795
835
|
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
836
|
else:
|
|
800
837
|
max_dim = 2000
|
|
801
838
|
if max(img.shape[:2]) > max_dim:
|
|
@@ -803,7 +840,7 @@ def render_netcdf_via_netcdf4(path, args):
|
|
|
803
840
|
new_w = int(img.shape[1] * scale)
|
|
804
841
|
new_h = int(img.shape[0] * scale)
|
|
805
842
|
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})")
|
|
843
|
+
print(f"[VIEW] Downsampled from {W}×{H}px to {new_w}×{new_h}px (display scale={scale:.2f})")
|
|
807
844
|
print(f"[INFO] Use --display 1 for full resolution.")
|
|
808
845
|
else:
|
|
809
846
|
# (matches the width_pct logic in show_inline_image)
|
|
@@ -907,7 +944,8 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
907
944
|
resampling=rasterio.enums.Resampling.bilinear
|
|
908
945
|
)
|
|
909
946
|
|
|
910
|
-
print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
|
|
947
|
+
print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (display scale={scale:.3f})")
|
|
948
|
+
print(f"[INFO] Use --display 1 for full resolution.")
|
|
911
949
|
else:
|
|
912
950
|
data = ds.read()
|
|
913
951
|
|
|
@@ -918,12 +956,18 @@ def render_raster(paths: list[str], args) -> None:
|
|
|
918
956
|
else:
|
|
919
957
|
print(f"[INFO] Multi-band raster detected ({band_count} bands)")
|
|
920
958
|
|
|
921
|
-
#
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
959
|
+
# BANDS GALLERY
|
|
960
|
+
if getattr(args, "bands", None):
|
|
961
|
+
band_list = parse_bands(args.bands)
|
|
962
|
+
colormap = args.colormap if args.colormap else "viridis"
|
|
963
|
+
render_bands_gallery(data, band_list, band_count,
|
|
964
|
+
grid=getattr(args, "gallery", None),
|
|
965
|
+
display_scale=getattr(args, "display", None),
|
|
966
|
+
colormap=colormap)
|
|
967
|
+
return
|
|
968
|
+
|
|
925
969
|
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:
|
|
970
|
+
if band_count >= 3 and not paths[0].lower().endswith('.nc') and not user_specified_band and getattr(args, 'rgb', None):
|
|
927
971
|
|
|
928
972
|
if getattr(args, "rgb", None):
|
|
929
973
|
try:
|
|
@@ -1045,12 +1089,11 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1045
1089
|
cols, rows = 4, 4
|
|
1046
1090
|
nmax = cols * rows
|
|
1047
1091
|
|
|
1048
|
-
# Collect files
|
|
1049
|
-
exts = (".png", ".jpg", ".jpeg", ".tif", ".tiff")
|
|
1092
|
+
# Collect all files, let the loader decide what's valid
|
|
1050
1093
|
files = [os.path.join(folder, f) for f in sorted(os.listdir(folder))
|
|
1051
|
-
if
|
|
1094
|
+
if os.path.isfile(os.path.join(folder, f))]
|
|
1052
1095
|
if not files:
|
|
1053
|
-
print(f"[WARN] No
|
|
1096
|
+
print(f"[WARN] No files found in {folder}")
|
|
1054
1097
|
return
|
|
1055
1098
|
|
|
1056
1099
|
files = files[:nmax]
|
|
@@ -1063,8 +1106,12 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1063
1106
|
ext = os.path.splitext(f)[1].lower()
|
|
1064
1107
|
if ext in [".tif", ".tiff"]:
|
|
1065
1108
|
import rasterio
|
|
1109
|
+
from rasterio.enums import Resampling
|
|
1066
1110
|
with rasterio.open(f) as ds:
|
|
1067
|
-
arr = ds.read(
|
|
1111
|
+
arr = ds.read(
|
|
1112
|
+
out_shape=(ds.count, thumb_size[1], thumb_size[0]),
|
|
1113
|
+
resampling=Resampling.nearest
|
|
1114
|
+
)
|
|
1068
1115
|
if arr.shape[0] >= 3:
|
|
1069
1116
|
rgb = np.stack([normalize_to_uint8(arr[i]) for i in range(3)], axis=-1)
|
|
1070
1117
|
else:
|
|
@@ -1075,8 +1122,9 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1075
1122
|
img = Image.open(f).convert("RGB")
|
|
1076
1123
|
img.thumbnail(thumb_size)
|
|
1077
1124
|
thumbs.append(img)
|
|
1125
|
+
|
|
1078
1126
|
except Exception as e:
|
|
1079
|
-
print(f"[
|
|
1127
|
+
print(f"[SKIP] {os.path.basename(f)} — {e}")
|
|
1080
1128
|
|
|
1081
1129
|
if not thumbs:
|
|
1082
1130
|
print("[WARN] No valid images loaded.")
|
|
@@ -1104,6 +1152,87 @@ def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector
|
|
|
1104
1152
|
except Exception as e:
|
|
1105
1153
|
print(f"[ERROR] Failed to render gallery: {e}")
|
|
1106
1154
|
|
|
1155
|
+
def render_bands_gallery(data: np.ndarray, band_list: list[int], band_count: int,
|
|
1156
|
+
grid: str = None, display_scale=None, colormap: str = "viridis") -> None:
|
|
1157
|
+
"""Render multiple bands from a single raster as a grid of thumbnails."""
|
|
1158
|
+
import math
|
|
1159
|
+
from PIL import ImageDraw
|
|
1160
|
+
|
|
1161
|
+
# Validate bands
|
|
1162
|
+
valid_bands = [b for b in band_list if 1 <= b <= band_count]
|
|
1163
|
+
if not valid_bands:
|
|
1164
|
+
print(f"[ERROR] No valid bands. File has {band_count} bands.")
|
|
1165
|
+
return
|
|
1166
|
+
skipped = set(band_list) - set(valid_bands)
|
|
1167
|
+
if skipped:
|
|
1168
|
+
print(f"[WARN] Skipped out-of-range bands: {sorted(skipped)}")
|
|
1169
|
+
|
|
1170
|
+
n = len(valid_bands)
|
|
1171
|
+
|
|
1172
|
+
# Auto grid or user-specified
|
|
1173
|
+
if grid:
|
|
1174
|
+
try:
|
|
1175
|
+
cols, rows = map(int, grid.lower().split("x"))
|
|
1176
|
+
except Exception:
|
|
1177
|
+
cols = math.ceil(math.sqrt(n))
|
|
1178
|
+
rows = math.ceil(n / cols)
|
|
1179
|
+
else:
|
|
1180
|
+
cols = math.ceil(math.sqrt(n))
|
|
1181
|
+
rows = math.ceil(n / cols)
|
|
1182
|
+
|
|
1183
|
+
# Use fixed longest dimension to preserve aspect ratio
|
|
1184
|
+
max_thumb = 256
|
|
1185
|
+
label_height = 14 # space below each thumbnail for band label
|
|
1186
|
+
margin = 8
|
|
1187
|
+
cmap = colormaps[colormap]
|
|
1188
|
+
|
|
1189
|
+
thumbs = []
|
|
1190
|
+
thumb_w = thumb_h = max_thumb # will be updated from first image
|
|
1191
|
+
|
|
1192
|
+
for i, b in enumerate(valid_bands):
|
|
1193
|
+
arr = data[i].astype(float)
|
|
1194
|
+
normalized = normalize_to_uint8(arr)
|
|
1195
|
+
colored = cmap(normalized / 255.0)
|
|
1196
|
+
rgb = (colored[:, :, :3] * 255).astype(np.uint8)
|
|
1197
|
+
img = Image.fromarray(rgb)
|
|
1198
|
+
img.thumbnail((max_thumb, max_thumb), Image.LANCZOS)
|
|
1199
|
+
if i == 0:
|
|
1200
|
+
thumb_w, thumb_h = img.size
|
|
1201
|
+
thumbs.append(img)
|
|
1202
|
+
|
|
1203
|
+
# Build canvas with extra height per row for labels
|
|
1204
|
+
cols = min(cols, n)
|
|
1205
|
+
rows = math.ceil(n / cols)
|
|
1206
|
+
cell_w = thumb_w + margin
|
|
1207
|
+
cell_h = thumb_h + label_height + margin
|
|
1208
|
+
canvas_w = cols * cell_w + margin
|
|
1209
|
+
canvas_h = rows * cell_h + margin
|
|
1210
|
+
canvas = Image.new("RGB", (canvas_w, canvas_h), (220, 220, 220))
|
|
1211
|
+
draw = ImageDraw.Draw(canvas)
|
|
1212
|
+
from PIL import ImageFont
|
|
1213
|
+
try:
|
|
1214
|
+
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 10)
|
|
1215
|
+
except Exception:
|
|
1216
|
+
try:
|
|
1217
|
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
|
|
1218
|
+
except Exception:
|
|
1219
|
+
font = ImageFont.load_default()
|
|
1220
|
+
|
|
1221
|
+
for i, img in enumerate(thumbs):
|
|
1222
|
+
r, c = divmod(i, cols)
|
|
1223
|
+
x = margin + c * cell_w
|
|
1224
|
+
y = margin + r * cell_h
|
|
1225
|
+
canvas.paste(img, (x, y))
|
|
1226
|
+
# Draw label on canvas background below the thumbnail
|
|
1227
|
+
label = f"B{valid_bands[i]}"
|
|
1228
|
+
lx = x + (thumb_w - len(label) * 6) // 2
|
|
1229
|
+
ly = y + thumb_h + 2
|
|
1230
|
+
draw.text((lx, ly), label, fill=(0, 0, 0), font=font)
|
|
1231
|
+
|
|
1232
|
+
print(f"[INFO] Displaying {n} bands ({cols}×{rows} grid, colormap: {colormap})")
|
|
1233
|
+
# print(f"[DEBUG] canvas size: {canvas_w}×{canvas_h}px")
|
|
1234
|
+
show_image_auto(np.array(canvas), display_scale)
|
|
1235
|
+
|
|
1107
1236
|
# ---------------------------------------------------------------------
|
|
1108
1237
|
# Vector handling
|
|
1109
1238
|
# ---------------------------------------------------------------------
|
|
@@ -1521,6 +1650,11 @@ def main() -> None:
|
|
|
1521
1650
|
metavar="DIM_NAME",
|
|
1522
1651
|
help="For 3D NetCDF variables, specify which dimension to use as the band axis (auto-detected if omitted)."
|
|
1523
1652
|
)
|
|
1653
|
+
parser.add_argument(
|
|
1654
|
+
"--bands",
|
|
1655
|
+
type=str,
|
|
1656
|
+
help="Display multiple bands as a grid. Accepts ranges (30-40), lists (3,4,5), or mixed (1,3,10-15)."
|
|
1657
|
+
)
|
|
1524
1658
|
|
|
1525
1659
|
# CSV options
|
|
1526
1660
|
parser.add_argument(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|