viewtif 0.2.4__py3-none-any.whl → 0.2.6__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.
- viewtif/__init__.py +0 -0
- viewtif/tif_viewer.py +310 -370
- {viewtif-0.2.4.dist-info → viewtif-0.2.6.dist-info}/METADATA +45 -25
- viewtif-0.2.6.dist-info/RECORD +7 -0
- viewtif-0.2.6.dist-info/licenses/LICENSE +21 -0
- viewtif-0.2.4.dist-info/RECORD +0 -5
- {viewtif-0.2.4.dist-info → viewtif-0.2.6.dist-info}/WHEEL +0 -0
- {viewtif-0.2.4.dist-info → viewtif-0.2.6.dist-info}/entry_points.txt +0 -0
viewtif/__init__.py
ADDED
|
File without changes
|
viewtif/tif_viewer.py
CHANGED
|
@@ -15,7 +15,7 @@ Controls
|
|
|
15
15
|
Arrow keys or WASD : pan
|
|
16
16
|
C / V : increase/decrease contrast (works in RGB and single-band)
|
|
17
17
|
G / H : increase/decrease gamma (works in RGB and single-band)
|
|
18
|
-
M : toggle colormap
|
|
18
|
+
M : toggle colormap. Single-band: viridis/magma. NetCDF: RdBu_r/viridis/magma.
|
|
19
19
|
[ / ] : previous / next band (or time step) (single-band)
|
|
20
20
|
R : reset view
|
|
21
21
|
|
|
@@ -27,13 +27,12 @@ Examples
|
|
|
27
27
|
|
|
28
28
|
import sys
|
|
29
29
|
import os
|
|
30
|
-
import argparse
|
|
31
30
|
import numpy as np
|
|
32
31
|
import rasterio
|
|
33
32
|
from rasterio.transform import Affine
|
|
34
33
|
from PySide6.QtWidgets import (
|
|
35
34
|
QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
|
|
36
|
-
QScrollBar, QGraphicsPathItem, QVBoxLayout,
|
|
35
|
+
QScrollBar, QGraphicsPathItem, QVBoxLayout, QWidget, QStatusBar
|
|
37
36
|
)
|
|
38
37
|
from PySide6.QtGui import QImage, QPixmap, QPainter, QPen, QColor, QPainterPath
|
|
39
38
|
from PySide6.QtCore import Qt
|
|
@@ -41,8 +40,10 @@ from PySide6.QtCore import Qt
|
|
|
41
40
|
import matplotlib.cm as cm
|
|
42
41
|
import warnings
|
|
43
42
|
warnings.filterwarnings("ignore", category=RuntimeWarning, module="shapely")
|
|
43
|
+
warnings.filterwarnings("ignore", category=UserWarning, module="rasterio")
|
|
44
|
+
warnings.filterwarnings("ignore", category=UserWarning, module="xarray")
|
|
44
45
|
|
|
45
|
-
__version__ = "0.2.
|
|
46
|
+
__version__ = "0.2.6"
|
|
46
47
|
|
|
47
48
|
# Optional overlay deps
|
|
48
49
|
try:
|
|
@@ -166,9 +167,13 @@ class TiffViewer(QMainWindow):
|
|
|
166
167
|
rgb: list[int] | None = None,
|
|
167
168
|
rgbfiles: list[str] | None = None,
|
|
168
169
|
shapefiles: list[str] | None = None,
|
|
169
|
-
shp_color: str = "
|
|
170
|
+
shp_color: str = "cyan",
|
|
170
171
|
shp_width: float = 2,
|
|
171
172
|
subset: int | None = None,
|
|
173
|
+
vmin: float | None = None,
|
|
174
|
+
vmax: float | None = None,
|
|
175
|
+
cartopy: str = "on",
|
|
176
|
+
timestep: int | None = None,
|
|
172
177
|
):
|
|
173
178
|
super().__init__()
|
|
174
179
|
|
|
@@ -177,6 +182,13 @@ class TiffViewer(QMainWindow):
|
|
|
177
182
|
self.band = int(band)
|
|
178
183
|
self.rgb = rgb
|
|
179
184
|
self.rgbfiles = rgbfiles
|
|
185
|
+
self._user_vmin = vmin
|
|
186
|
+
self._user_vmax = vmax
|
|
187
|
+
self.cartopy_mode = cartopy.lower()
|
|
188
|
+
|
|
189
|
+
if not tif_path and not rgbfiles:
|
|
190
|
+
print("Usage: viewtif <file.tif>")
|
|
191
|
+
sys.exit(1)
|
|
180
192
|
|
|
181
193
|
self._scale_arg = max(1, int(scale or 1))
|
|
182
194
|
self._transform: Affine | None = None
|
|
@@ -206,20 +218,53 @@ class TiffViewer(QMainWindow):
|
|
|
206
218
|
self.data = arr
|
|
207
219
|
self.band_count = 3
|
|
208
220
|
self.rgb = [os.path.basename(red), os.path.basename(green), os.path.basename(blue)]
|
|
209
|
-
# Use common prefix for title if tif_path not passed
|
|
210
221
|
self.tif_path = self.tif_path or (os.path.commonprefix([red, green, blue]) or red)
|
|
211
222
|
|
|
212
223
|
elif tif_path:
|
|
213
|
-
# --- Warn for large files before loading ---
|
|
214
|
-
warn_if_large(tif_path, scale=self._scale_arg)
|
|
215
224
|
|
|
216
|
-
#
|
|
217
|
-
if tif_path
|
|
225
|
+
# ---------------- Handle File Geodatabase (.gdb) ---------------- #
|
|
226
|
+
if tif_path.lower().endswith(".gdb") and ":" not in tif_path:
|
|
227
|
+
|
|
228
|
+
import re, subprocess
|
|
229
|
+
gdb_path = tif_path
|
|
230
|
+
|
|
218
231
|
try:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
232
|
+
out = subprocess.check_output(
|
|
233
|
+
["gdalinfo", "-norat", gdb_path],
|
|
234
|
+
text=True
|
|
235
|
+
)
|
|
236
|
+
rasters = re.findall(r"RASTER_DATASET=(\S+)", out)
|
|
237
|
+
|
|
238
|
+
if not rasters:
|
|
239
|
+
print(f"[WARN] No raster datasets found in {os.path.basename(gdb_path)}.")
|
|
240
|
+
sys.exit(0)
|
|
241
|
+
|
|
242
|
+
print(f"Found {len(rasters)} raster dataset{'s' if len(rasters) > 1 else ''}:")
|
|
243
|
+
for i, r in enumerate(rasters):
|
|
244
|
+
print(f"[{i}] {r}")
|
|
245
|
+
|
|
246
|
+
print("\nUse one of these names to open. For example, to open the first raster:")
|
|
247
|
+
print(f'viewtif "OpenFileGDB:{gdb_path}:{rasters[0]}"')
|
|
248
|
+
sys.exit(0)
|
|
249
|
+
|
|
250
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
251
|
+
print("[ERROR] This file requires full GDAL support.")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
# Warn for large files
|
|
255
|
+
warn_if_large(tif_path, scale=self._scale_arg)
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------
|
|
258
|
+
# Detect NetCDF
|
|
259
|
+
# ---------------------------------------------------------------
|
|
260
|
+
if tif_path.lower().endswith((".nc", ".netcdf")):
|
|
261
|
+
try:
|
|
262
|
+
import xarray as xr
|
|
263
|
+
except ModuleNotFoundError:
|
|
264
|
+
print("NetCDF support requires extra dependencies.")
|
|
265
|
+
print("Install them with: pip install viewtif[netcdf]")
|
|
266
|
+
sys.exit(0)
|
|
267
|
+
|
|
223
268
|
# Open the NetCDF file
|
|
224
269
|
ds = xr.open_dataset(tif_path)
|
|
225
270
|
|
|
@@ -249,11 +294,11 @@ class TiffViewer(QMainWindow):
|
|
|
249
294
|
|
|
250
295
|
# Get coordinate info if available
|
|
251
296
|
self._has_geo_coords = False
|
|
252
|
-
if
|
|
297
|
+
if "lon" in ds.coords and "lat" in ds.coords:
|
|
253
298
|
self._has_geo_coords = True
|
|
254
299
|
self._lon_data = ds.lon.values
|
|
255
300
|
self._lat_data = ds.lat.values
|
|
256
|
-
elif
|
|
301
|
+
elif "longitude" in ds.coords and "latitude" in ds.coords:
|
|
257
302
|
self._has_geo_coords = True
|
|
258
303
|
self._lon_data = ds.longitude.values
|
|
259
304
|
self._lat_data = ds.latitude.values
|
|
@@ -261,29 +306,18 @@ class TiffViewer(QMainWindow):
|
|
|
261
306
|
# Handle time or other index dimension if present
|
|
262
307
|
self._has_time_dim = False
|
|
263
308
|
self._time_dim_name = None
|
|
264
|
-
time_index = 0
|
|
265
309
|
|
|
266
310
|
# Look for a time dimension first
|
|
267
311
|
if 'time' in var_data.dims:
|
|
268
312
|
self._has_time_dim = True
|
|
269
|
-
self._time_dim_name =
|
|
270
|
-
self._time_values = ds[
|
|
313
|
+
self._time_dim_name = "time"
|
|
314
|
+
self._time_values = ds["time"].values
|
|
271
315
|
self._time_index = 0
|
|
272
316
|
print(f"NetCDF time dimension detected: {len(self._time_values)} steps")
|
|
273
|
-
|
|
274
|
-
self.band_count = var_data.sizes['time']
|
|
317
|
+
self.band_count = var_data.sizes["time"]
|
|
275
318
|
self.band_index = 0
|
|
276
|
-
|
|
319
|
+
var_data = var_data.isel(time=0)
|
|
277
320
|
|
|
278
|
-
# Try to format time values for better display
|
|
279
|
-
time_units = getattr(ds.time, 'units', None)
|
|
280
|
-
time_calendar = getattr(ds.time, 'calendar', 'standard')
|
|
281
|
-
|
|
282
|
-
# Select first time step by default
|
|
283
|
-
var_data = var_data.isel(time=time_index)
|
|
284
|
-
|
|
285
|
-
# If no time dimension but variable has multiple dimensions,
|
|
286
|
-
# use the first non-spatial dimension as a "time" dimension
|
|
287
321
|
elif len(var_data.dims) > 2:
|
|
288
322
|
# Try to find a dimension that's not lat/lon
|
|
289
323
|
spatial_dims = ['lat', 'lon', 'latitude', 'longitude', 'y', 'x']
|
|
@@ -292,111 +326,73 @@ class TiffViewer(QMainWindow):
|
|
|
292
326
|
self._has_time_dim = True
|
|
293
327
|
self._time_dim_name = dim
|
|
294
328
|
self._time_values = ds[dim].values
|
|
295
|
-
self._time_index =
|
|
296
|
-
|
|
297
|
-
# Select first index by default
|
|
298
|
-
var_data = var_data.isel({dim: time_index})
|
|
329
|
+
self._time_index = 0
|
|
330
|
+
var_data = var_data.isel({dim: 0})
|
|
299
331
|
break
|
|
300
|
-
|
|
301
|
-
# Convert to numpy array
|
|
332
|
+
|
|
302
333
|
arr = var_data.values.astype(np.float32)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
arr = np.squeeze(arr)
|
|
335
|
+
|
|
336
|
+
# --------------------------------------------------------
|
|
337
|
+
# Apply timestep jump after base array is created
|
|
338
|
+
# --------------------------------------------------------
|
|
339
|
+
if timestep is not None and self._has_time_dim:
|
|
340
|
+
ts = max(1, min(timestep, self.band_count))
|
|
341
|
+
self.band_index = ts - 1
|
|
342
|
+
print(f"[INFO] Jumping to timestep {ts}/{self.band_count}")
|
|
343
|
+
|
|
344
|
+
# Replace arr with the correct slice
|
|
345
|
+
frame = self._nc_var_data.isel({self._time_dim_name: self.band_index})
|
|
346
|
+
arr = np.squeeze(frame.values.astype(np.float32))
|
|
347
|
+
|
|
310
348
|
if arr.ndim >= 2:
|
|
311
349
|
h, w = arr.shape[:2]
|
|
312
350
|
if h * w > 4_000_000:
|
|
313
351
|
step = max(2, int((h * w / 4_000_000) ** 0.5))
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
else:
|
|
317
|
-
arr = arr[::step, ::step, :]
|
|
318
|
-
|
|
319
|
-
# --- Final assignments ---
|
|
352
|
+
arr = arr[::step, ::step]
|
|
353
|
+
|
|
320
354
|
self.data = arr
|
|
321
355
|
|
|
322
356
|
# Try to extract CRS from CF conventions
|
|
323
357
|
self._transform = None
|
|
324
358
|
self._crs = None
|
|
325
|
-
|
|
359
|
+
|
|
360
|
+
if "crs" in ds.variables:
|
|
326
361
|
try:
|
|
327
362
|
import rasterio.crs
|
|
328
|
-
crs_var = ds.variables[
|
|
329
|
-
if hasattr(crs_var,
|
|
363
|
+
crs_var = ds.variables["crs"]
|
|
364
|
+
if hasattr(crs_var, "spatial_ref"):
|
|
330
365
|
self._crs = rasterio.crs.CRS.from_wkt(crs_var.spatial_ref)
|
|
331
366
|
except Exception as e:
|
|
332
367
|
print(f"Could not parse CRS: {e}")
|
|
333
|
-
|
|
334
|
-
#
|
|
335
|
-
if
|
|
336
|
-
self.band_count = arr.shape[2]
|
|
337
|
-
else:
|
|
368
|
+
|
|
369
|
+
# Preserve time dimension if detected earlier
|
|
370
|
+
if not self._has_time_dim:
|
|
338
371
|
self.band_count = 1
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
self.band_index = 0
|
|
373
|
+
|
|
341
374
|
self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
375
|
+
|
|
376
|
+
if self._user_vmin is not None:
|
|
377
|
+
self.vmin = self._user_vmin
|
|
378
|
+
if self._user_vmax is not None:
|
|
379
|
+
self.vmax = self._user_vmax
|
|
380
|
+
|
|
348
381
|
self._use_cartopy = HAVE_CARTOPY and self._has_geo_coords
|
|
349
|
-
|
|
350
|
-
except ImportError as e:
|
|
351
|
-
if "xarray" in str(e) or "netCDF4" in str(e):
|
|
352
|
-
raise RuntimeError(
|
|
353
|
-
f"NetCDF support requires additional dependencies.\n"
|
|
354
|
-
f"Install them with: pip install viewtif[netcdf]\n"
|
|
355
|
-
f"Original error: {str(e)}"
|
|
356
|
-
)
|
|
357
|
-
else:
|
|
358
|
-
raise RuntimeError(f"Error reading NetCDF file: {str(e)}")
|
|
359
|
-
except Exception as e:
|
|
360
|
-
raise RuntimeError(f"Error reading NetCDF file: {str(e)}")
|
|
361
|
-
|
|
362
|
-
# ---------------- Handle File Geodatabase (.gdb) ---------------- #
|
|
363
|
-
if tif_path and tif_path.lower().endswith(".gdb") and ":" not in tif_path:
|
|
364
|
-
import re, subprocess
|
|
365
|
-
gdb_path = tif_path # use full path to .gdb
|
|
366
|
-
try:
|
|
367
|
-
out = subprocess.check_output(["gdalinfo", "-norat", gdb_path], text=True)
|
|
368
|
-
rasters = re.findall(r"RASTER_DATASET=(\S+)", out)
|
|
369
|
-
if not rasters:
|
|
370
|
-
print(f"[WARN] No raster datasets found in {os.path.basename(gdb_path)}.")
|
|
371
|
-
sys.exit(0)
|
|
372
|
-
else:
|
|
373
|
-
print(f"Found {len(rasters)} raster dataset{'s' if len(rasters) > 1 else ''}:")
|
|
374
|
-
for i, r in enumerate(rasters):
|
|
375
|
-
print(f"[{i}] {r}")
|
|
376
|
-
print("\nUse one of these names to open. For example, to open the first raster:")
|
|
377
|
-
print(f'viewtif "OpenFileGDB:{gdb_path}:{rasters[0]}"')
|
|
378
|
-
sys.exit(0)
|
|
379
|
-
except subprocess.CalledProcessError as e:
|
|
380
|
-
print(f"[WARN] Could not inspect FileGDB: {e}")
|
|
381
|
-
sys.exit(0)
|
|
382
382
|
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
pass
|
|
388
|
-
# --------------------- Detect HDF/HDF5 --------------------- #
|
|
389
|
-
elif tif_path and tif_path.lower().endswith((".hdf", ".h5", ".hdf5")):
|
|
383
|
+
# ---------------------------------------------------------------
|
|
384
|
+
# Detect HDF or HDF5
|
|
385
|
+
# ---------------------------------------------------------------
|
|
386
|
+
elif tif_path.lower().endswith((".hdf", ".h5", ".hdf5")):
|
|
390
387
|
try:
|
|
391
|
-
# Try GDAL first (best support for HDF subdatasets)
|
|
392
388
|
from osgeo import gdal
|
|
393
|
-
gdal.UseExceptions()
|
|
389
|
+
# gdal.UseExceptions()
|
|
394
390
|
|
|
395
391
|
ds = gdal.Open(tif_path)
|
|
396
392
|
subs = ds.GetSubDatasets()
|
|
397
393
|
|
|
398
394
|
if not subs:
|
|
399
|
-
raise ValueError("No subdatasets found in HDF
|
|
395
|
+
raise ValueError("No subdatasets found in HDF file.")
|
|
400
396
|
|
|
401
397
|
# Only list subsets if --subset not given
|
|
402
398
|
if subset is None:
|
|
@@ -408,18 +404,15 @@ class TiffViewer(QMainWindow):
|
|
|
408
404
|
|
|
409
405
|
# Validate subset index
|
|
410
406
|
if subset < 0 or subset >= len(subs):
|
|
411
|
-
raise ValueError(f"Invalid subset index {subset}.
|
|
407
|
+
raise ValueError(f"Invalid subset index {subset}.")
|
|
412
408
|
|
|
413
409
|
sub_name, desc = subs[subset]
|
|
414
410
|
print(f"\nOpening subdataset [{subset}]: {desc}")
|
|
415
411
|
sub_ds = gdal.Open(sub_name)
|
|
416
412
|
|
|
417
|
-
# --- Read once ---
|
|
418
413
|
arr = sub_ds.ReadAsArray().astype(np.float32)
|
|
419
|
-
#print(f"Raw array shape from GDAL: {arr.shape} (ndim={arr.ndim})")
|
|
420
|
-
|
|
421
|
-
# --- Normalize shape ---
|
|
422
414
|
arr = np.squeeze(arr)
|
|
415
|
+
|
|
423
416
|
if arr.ndim == 3:
|
|
424
417
|
# Convert from (bands, rows, cols) → (rows, cols, bands)
|
|
425
418
|
arr = np.transpose(arr, (1, 2, 0))
|
|
@@ -435,93 +428,46 @@ class TiffViewer(QMainWindow):
|
|
|
435
428
|
step = max(2, int((h * w / 4_000_000) ** 0.5))
|
|
436
429
|
arr = arr[::step, ::step] if arr.ndim == 2 else arr[::step, ::step, :]
|
|
437
430
|
|
|
438
|
-
# --- Final assignments ---
|
|
439
431
|
self.data = arr
|
|
440
432
|
self._transform = None
|
|
441
433
|
self._crs = None
|
|
442
434
|
self.band_count = arr.shape[2] if arr.ndim == 3 else 1
|
|
443
435
|
self.band_index = 0
|
|
444
436
|
self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
|
|
437
|
+
if getattr(self, "_scale_arg", 1) > 1:
|
|
438
|
+
print(f"[INFO] Value range (scaled): {self.vmin:.3f} -> {self.vmax:.3f}")
|
|
439
|
+
else:
|
|
440
|
+
print(f"[INFO] Value range: {self.vmin:.3f} -> {self.vmax:.3f}")
|
|
445
441
|
|
|
446
|
-
|
|
447
|
-
|
|
442
|
+
except ImportError as e:
|
|
443
|
+
if "osgeo" in str(e):
|
|
444
|
+
print("[ERROR] This file requires full GDAL support.")
|
|
445
|
+
# print("Install GDAL with:")
|
|
446
|
+
# print(" conda install -c conda-forge gdal")
|
|
447
|
+
sys.exit(1)
|
|
448
448
|
else:
|
|
449
|
-
print("
|
|
449
|
+
print(f"Error reading HDF file: {e}")
|
|
450
|
+
sys.exit(1)
|
|
450
451
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
except Exception as e:
|
|
453
|
+
print(f"Error reading HDF file: {e}")
|
|
454
|
+
sys.exit(1)
|
|
454
455
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
try:
|
|
459
|
-
import rasterio as rio
|
|
460
|
-
with rio.open(tif_path) as src:
|
|
461
|
-
print(f"[INFO] NetCDF file opened via rasterio")
|
|
462
|
-
print(f"[INFO] Data shape: {src.height} x {src.width} x {src.count} bands")
|
|
463
|
-
|
|
464
|
-
if src.count == 0:
|
|
465
|
-
raise ValueError("No bands found in NetCDF file.")
|
|
466
|
-
|
|
467
|
-
# Determine which band(s) to read
|
|
468
|
-
if self.band and self.band <= src.count:
|
|
469
|
-
band_indices = [self.band]
|
|
470
|
-
print(f"Opening band {self.band}/{src.count}")
|
|
471
|
-
elif rgb and all(b <= src.count for b in rgb):
|
|
472
|
-
band_indices = rgb
|
|
473
|
-
print(f"Opening bands {rgb} as RGB")
|
|
474
|
-
else:
|
|
475
|
-
band_indices = list(range(1, min(src.count + 1, 4))) # Read up to 3 bands
|
|
476
|
-
print(f"Opening bands {band_indices}")
|
|
477
|
-
|
|
478
|
-
# Read selected bands
|
|
479
|
-
bands = []
|
|
480
|
-
for b in band_indices:
|
|
481
|
-
band_data = src.read(b, out_shape=(src.height // self._scale_arg, src.width // self._scale_arg))
|
|
482
|
-
bands.append(band_data)
|
|
483
|
-
|
|
484
|
-
# Stack into array
|
|
485
|
-
arr = np.stack(bands, axis=-1).astype(np.float32) if len(bands) > 1 else bands[0].astype(np.float32)
|
|
486
|
-
|
|
487
|
-
# Handle no-data values
|
|
488
|
-
nd = src.nodata
|
|
489
|
-
if nd is not None:
|
|
490
|
-
if arr.ndim == 3:
|
|
491
|
-
arr[arr == nd] = np.nan
|
|
492
|
-
else:
|
|
493
|
-
arr[arr == nd] = np.nan
|
|
494
|
-
|
|
495
|
-
# Final assignments
|
|
496
|
-
self.data = arr
|
|
497
|
-
self._transform = src.transform
|
|
498
|
-
self._crs = src.crs
|
|
499
|
-
self.band_count = arr.shape[2] if arr.ndim == 3 else 1
|
|
500
|
-
self.band_index = 0
|
|
501
|
-
self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
|
|
502
|
-
|
|
503
|
-
if self.band_count > 1:
|
|
504
|
-
print(f"Loaded {self.band_count} bands — switch with [ and ] keys.")
|
|
505
|
-
else:
|
|
506
|
-
print("Loaded 1 band.")
|
|
507
|
-
except Exception as e:
|
|
508
|
-
raise RuntimeError(
|
|
509
|
-
f"Failed to read HDF/NetCDF file: {e}\n"
|
|
510
|
-
"For full HDF support, install GDAL: pip install GDAL"
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
# --------------------- Regular GeoTIFF --------------------- #
|
|
456
|
+
# ---------------------------------------------------------------
|
|
457
|
+
# Regular TIFF
|
|
458
|
+
# ---------------------------------------------------------------
|
|
514
459
|
else:
|
|
515
|
-
if tif_path and os.path.dirname(tif_path).endswith(".gdb"):
|
|
516
|
-
tif_path = f"OpenFileGDB:{os.path.dirname(tif_path)}:{os.path.basename(tif_path)}"
|
|
517
|
-
|
|
518
460
|
import rasterio as rio_module
|
|
519
461
|
with rio_module.open(tif_path) as src:
|
|
520
462
|
self._transform = src.transform
|
|
521
463
|
self._crs = src.crs
|
|
464
|
+
|
|
522
465
|
if rgb is not None:
|
|
523
|
-
bands = [
|
|
524
|
-
|
|
466
|
+
bands = [
|
|
467
|
+
src.read(b, out_shape=(src.height // self._scale_arg, src.width // self._scale_arg))
|
|
468
|
+
for b in rgb
|
|
469
|
+
]
|
|
470
|
+
|
|
525
471
|
arr = np.stack(bands, axis=-1).astype(np.float32)
|
|
526
472
|
nd = src.nodata
|
|
527
473
|
if nd is not None:
|
|
@@ -539,7 +485,14 @@ class TiffViewer(QMainWindow):
|
|
|
539
485
|
self.data = arr
|
|
540
486
|
self.band_count = src.count
|
|
541
487
|
|
|
542
|
-
|
|
488
|
+
if self.band_count == 1:
|
|
489
|
+
print("[INFO] This TIFF has 1 band.")
|
|
490
|
+
else:
|
|
491
|
+
print(
|
|
492
|
+
f"[INFO] This TIFF has {self.band_count} bands. "
|
|
493
|
+
"Use [ and ] to switch bands, or use --rgb R G B."
|
|
494
|
+
)
|
|
495
|
+
|
|
543
496
|
try:
|
|
544
497
|
stats = src.stats(self.band)
|
|
545
498
|
if stats and stats.min is not None and stats.max is not None:
|
|
@@ -548,9 +501,10 @@ class TiffViewer(QMainWindow):
|
|
|
548
501
|
raise ValueError("No stats in file")
|
|
549
502
|
except Exception:
|
|
550
503
|
self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
504
|
+
if getattr(self, "_scale_arg", 1) > 1:
|
|
505
|
+
print(f"[INFO] Value range (scaled): {self.vmin:.3f} -> {self.vmax:.3f}")
|
|
506
|
+
else:
|
|
507
|
+
print(f"[INFO] Value range: {self.vmin:.3f} -> {self.vmax:.3f}")
|
|
554
508
|
|
|
555
509
|
# Window title
|
|
556
510
|
self.update_title()
|
|
@@ -560,7 +514,6 @@ class TiffViewer(QMainWindow):
|
|
|
560
514
|
self.gamma = 1.0
|
|
561
515
|
|
|
562
516
|
# Colormap (single-band)
|
|
563
|
-
# For NetCDF temperature data, have three colormaps in rotation
|
|
564
517
|
if tif_path and tif_path.lower().endswith(('.nc', '.netcdf')):
|
|
565
518
|
self.cmap_names = ["RdBu_r", "viridis", "magma"] # three colormaps for NetCDF
|
|
566
519
|
self.cmap_index = 0 # start with RdBu_r
|
|
@@ -593,7 +546,9 @@ class TiffViewer(QMainWindow):
|
|
|
593
546
|
self._last_rgb = None
|
|
594
547
|
|
|
595
548
|
# --- Initial render ---
|
|
549
|
+
self._suppress_scale_print = True # Need for NetCDF
|
|
596
550
|
self.update_pixmap()
|
|
551
|
+
self._suppress_scale_print = False # Need for NetCDF
|
|
597
552
|
|
|
598
553
|
# Overlays (if any)
|
|
599
554
|
if self._shapefiles:
|
|
@@ -604,9 +559,32 @@ class TiffViewer(QMainWindow):
|
|
|
604
559
|
if self.pixmap_item is not None:
|
|
605
560
|
rect = self.pixmap_item.boundingRect()
|
|
606
561
|
self.scene.setSceneRect(rect)
|
|
562
|
+
|
|
563
|
+
# Fit first
|
|
607
564
|
self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatioByExpanding)
|
|
608
|
-
|
|
565
|
+
|
|
566
|
+
# ----------------------------
|
|
567
|
+
# NetCDF needs a different scaling (appears smaller)
|
|
568
|
+
# ----------------------------
|
|
569
|
+
if hasattr(self, "_nc_var_name"):
|
|
570
|
+
# NetCDF view adjustment
|
|
571
|
+
self.view.scale(11.0, 11.0)
|
|
572
|
+
else:
|
|
573
|
+
# Default behavior for TIFF/HDF imagery
|
|
574
|
+
self.view.scale(7.0, 7.0)
|
|
575
|
+
|
|
609
576
|
self.view.centerOn(self.pixmap_item)
|
|
577
|
+
|
|
578
|
+
# Previous version below
|
|
579
|
+
# # --- Initial render ---
|
|
580
|
+
# self.update_pixmap()
|
|
581
|
+
# self.resize(1200, 800)
|
|
582
|
+
# if self.pixmap_item is not None:
|
|
583
|
+
# rect = self.pixmap_item.boundingRect()
|
|
584
|
+
# self.scene.setSceneRect(rect)
|
|
585
|
+
# self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatioByExpanding)
|
|
586
|
+
# self.view.scale(5, 5)
|
|
587
|
+
# self.view.centerOn(self.pixmap_item)
|
|
610
588
|
|
|
611
589
|
# ---------------------------- Overlays ---------------------------- #
|
|
612
590
|
def _geo_to_pixel(self, x: float, y: float):
|
|
@@ -730,23 +708,40 @@ class TiffViewer(QMainWindow):
|
|
|
730
708
|
|
|
731
709
|
# ----------------------- Title / Rendering ----------------------- #
|
|
732
710
|
def update_title(self):
|
|
733
|
-
"""
|
|
711
|
+
"""Add band before the title."""
|
|
734
712
|
import os
|
|
713
|
+
file_name = os.path.basename(self.tif_path)
|
|
735
714
|
|
|
736
715
|
if hasattr(self, "_has_time_dim") and self._has_time_dim:
|
|
737
|
-
nc_name = getattr(self, "_nc_var_name", "")
|
|
738
|
-
|
|
716
|
+
# nc_name = getattr(self, "_nc_var_name", "")
|
|
717
|
+
|
|
739
718
|
title = f"Time step {self.band_index + 1}/{self.band_count} — {file_name}"
|
|
719
|
+
|
|
740
720
|
|
|
741
721
|
elif hasattr(self, "band_index"):
|
|
742
|
-
title = f"Band {self.band_index + 1}/{self.band_count} — {
|
|
722
|
+
title = f"Band {self.band_index + 1}/{self.band_count} — {file_name}"
|
|
723
|
+
|
|
724
|
+
elif self.rgb_mode:
|
|
725
|
+
|
|
726
|
+
# Case 1: --rgbfiles → filenames
|
|
727
|
+
if self.rgbfiles:
|
|
728
|
+
files = [os.path.basename(p) for p in self.rgbfiles]
|
|
729
|
+
title = f"RGB ({files[0]}, {files[1]}, {files[2]})"
|
|
730
|
+
|
|
731
|
+
# Case 2: --rgb → band numbers
|
|
732
|
+
elif self.rgb:
|
|
733
|
+
r, g, b = self.rgb
|
|
734
|
+
title = f"RGB ({r}, {g}, {b}) — {file_name}"
|
|
735
|
+
|
|
736
|
+
else:
|
|
737
|
+
title = f"RGB — {file_name}"
|
|
743
738
|
|
|
744
|
-
elif self.rgb_mode
|
|
745
|
-
#
|
|
746
|
-
title = f"
|
|
739
|
+
elif not self.rgb_mode:
|
|
740
|
+
# TIFF uses self.band
|
|
741
|
+
title = f"Band {self.band}/{self.band_count} — {file_name}"
|
|
747
742
|
|
|
748
743
|
else:
|
|
749
|
-
title =
|
|
744
|
+
title = {file_name}
|
|
750
745
|
|
|
751
746
|
print(f"Title: {title}")
|
|
752
747
|
self.setWindowTitle(title)
|
|
@@ -793,7 +788,8 @@ class TiffViewer(QMainWindow):
|
|
|
793
788
|
return frame
|
|
794
789
|
|
|
795
790
|
step = int(self._scale_arg)
|
|
796
|
-
|
|
791
|
+
if not hasattr(self, "_suppress_scale_print"):
|
|
792
|
+
print(f"Applying scale factor {self._scale_arg} to current frame")
|
|
797
793
|
|
|
798
794
|
# Downsample the frame
|
|
799
795
|
frame = frame[::step, ::step]
|
|
@@ -828,10 +824,6 @@ class TiffViewer(QMainWindow):
|
|
|
828
824
|
if hasattr(frame, "values"):
|
|
829
825
|
frame = frame.values
|
|
830
826
|
|
|
831
|
-
# Apply same scaling factor (if any)
|
|
832
|
-
if hasattr(self, "_scale_arg") and self._scale_arg > 1:
|
|
833
|
-
step = int(self._scale_arg)
|
|
834
|
-
|
|
835
827
|
return frame.astype(np.float32)
|
|
836
828
|
|
|
837
829
|
def format_time_value(self, time_value):
|
|
@@ -858,118 +850,6 @@ class TiffViewer(QMainWindow):
|
|
|
858
850
|
pass
|
|
859
851
|
|
|
860
852
|
return time_str
|
|
861
|
-
|
|
862
|
-
# def update_time_label(self):
|
|
863
|
-
# """Update the time label with the current time value"""
|
|
864
|
-
# if hasattr(self, '_has_time_dim') and self._has_time_dim:
|
|
865
|
-
# try:
|
|
866
|
-
# time_value = self._time_values[self._time_index]
|
|
867
|
-
# time_str = self.format_time_value(time_value)
|
|
868
|
-
|
|
869
|
-
# # Update time label if it exists
|
|
870
|
-
# if hasattr(self, 'time_label'):
|
|
871
|
-
# self.time_label.setText(f"Time: {time_str}")
|
|
872
|
-
|
|
873
|
-
# # Create a progress bar style display of time position
|
|
874
|
-
# total = len(self._time_values)
|
|
875
|
-
# position = self._time_index + 1
|
|
876
|
-
# bar_width = 20 # Width of the progress bar
|
|
877
|
-
# filled = int(bar_width * position / total)
|
|
878
|
-
# bar = "[" + "#" * filled + "-" * (bar_width - filled) + "]"
|
|
879
|
-
|
|
880
|
-
# # Show time info in status bar
|
|
881
|
-
# step_info = f"Time step: {position}/{total} {bar} {self.format_time_value(self._time_values[self._time_index])}"
|
|
882
|
-
|
|
883
|
-
# # Update status bar if it exists
|
|
884
|
-
# if hasattr(self, 'statusBar') and callable(self.statusBar):
|
|
885
|
-
# self.statusBar().showMessage(step_info)
|
|
886
|
-
# else:
|
|
887
|
-
# print(step_info)
|
|
888
|
-
# except Exception as e:
|
|
889
|
-
# print(f"Error updating time label: {e}")
|
|
890
|
-
|
|
891
|
-
# def toggle_play_pause(self):
|
|
892
|
-
# """Toggle play/pause animation of time steps"""
|
|
893
|
-
# if self._is_playing:
|
|
894
|
-
# self.stop_animation()
|
|
895
|
-
# else:
|
|
896
|
-
# self.start_animation()
|
|
897
|
-
|
|
898
|
-
# def start_animation(self):
|
|
899
|
-
# """Start the time animation"""
|
|
900
|
-
# from PySide6.QtCore import QTimer
|
|
901
|
-
|
|
902
|
-
# if not hasattr(self, '_play_timer') or self._play_timer is None:
|
|
903
|
-
# self._play_timer = QTimer(self)
|
|
904
|
-
# self._play_timer.timeout.connect(self.animation_step)
|
|
905
|
-
|
|
906
|
-
# # Set animation speed (milliseconds between frames)
|
|
907
|
-
# animation_speed = 500 # 0.5 seconds between frames
|
|
908
|
-
# self._play_timer.start(animation_speed)
|
|
909
|
-
|
|
910
|
-
# self._is_playing = True
|
|
911
|
-
# self.play_button.setText("⏸") # Pause symbol
|
|
912
|
-
# self.play_button.setToolTip("Pause animation")
|
|
913
|
-
|
|
914
|
-
# def stop_animation(self):
|
|
915
|
-
# """Stop the time animation"""
|
|
916
|
-
# if hasattr(self, '_play_timer') and self._play_timer is not None:
|
|
917
|
-
# self._play_timer.stop()
|
|
918
|
-
|
|
919
|
-
# self._is_playing = False
|
|
920
|
-
# self.play_button.setText("▶") # Play symbol
|
|
921
|
-
# self.play_button.setToolTip("Play animation")
|
|
922
|
-
|
|
923
|
-
# def animation_step(self):
|
|
924
|
-
# """Advance one frame in the animation"""
|
|
925
|
-
# # Go to next time step
|
|
926
|
-
# next_time = (self._time_index + 1) % len(self._time_values)
|
|
927
|
-
# self.time_slider.setValue(next_time)
|
|
928
|
-
|
|
929
|
-
# def closeEvent(self, event):
|
|
930
|
-
# """Clean up resources when the window is closed"""
|
|
931
|
-
# # Stop animation timer if it's running
|
|
932
|
-
# if hasattr(self, '_is_playing') and self._is_playing:
|
|
933
|
-
# self.stop_animation()
|
|
934
|
-
|
|
935
|
-
# # Call the parent class closeEvent
|
|
936
|
-
# super().closeEvent(event)
|
|
937
|
-
|
|
938
|
-
# def populate_date_combo(self):
|
|
939
|
-
# """Populate the date combo box with time values"""
|
|
940
|
-
# if hasattr(self, '_has_time_dim') and self._has_time_dim and hasattr(self, 'date_combo'):
|
|
941
|
-
# try:
|
|
942
|
-
# self.date_combo.clear()
|
|
943
|
-
|
|
944
|
-
# # Add a reasonable subset of dates if there are too many
|
|
945
|
-
# max_items = 100 # Maximum number of items to show in dropdown
|
|
946
|
-
|
|
947
|
-
# if len(self._time_values) <= max_items:
|
|
948
|
-
# # Add all time values
|
|
949
|
-
# for i, time_value in enumerate(self._time_values):
|
|
950
|
-
# time_str = self.format_time_value(time_value)
|
|
951
|
-
# self.date_combo.addItem(time_str, i)
|
|
952
|
-
# else:
|
|
953
|
-
# # Add a subset of time values
|
|
954
|
-
# step = len(self._time_values) // max_items
|
|
955
|
-
|
|
956
|
-
# # Always include first and last
|
|
957
|
-
# indices = list(range(0, len(self._time_values), step))
|
|
958
|
-
# if (len(self._time_values) - 1) not in indices:
|
|
959
|
-
# indices.append(len(self._time_values) - 1)
|
|
960
|
-
|
|
961
|
-
# for i in indices:
|
|
962
|
-
# time_str = self.format_time_value(self._time_values[i])
|
|
963
|
-
# self.date_combo.addItem(f"{time_str} [{i+1}/{len(self._time_values)}]", i)
|
|
964
|
-
# except Exception as e:
|
|
965
|
-
# print(f"Error populating date combo: {e}")
|
|
966
|
-
|
|
967
|
-
# def date_combo_changed(self, index):
|
|
968
|
-
# """Handle date combo box selection change"""
|
|
969
|
-
# if index >= 0:
|
|
970
|
-
# time_index = self.date_combo.itemData(index)
|
|
971
|
-
# if time_index is not None:
|
|
972
|
-
# self.time_slider.setValue(time_index)
|
|
973
853
|
|
|
974
854
|
def _render_rgb(self):
|
|
975
855
|
if self.rgb_mode:
|
|
@@ -999,7 +879,7 @@ class TiffViewer(QMainWindow):
|
|
|
999
879
|
return rgb
|
|
1000
880
|
|
|
1001
881
|
def _render_cartopy_map(self, data):
|
|
1002
|
-
"""
|
|
882
|
+
""" Use cartopy for better visualization"""
|
|
1003
883
|
import matplotlib.pyplot as plt
|
|
1004
884
|
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
1005
885
|
import cartopy.crs as ccrs
|
|
@@ -1014,7 +894,6 @@ class TiffViewer(QMainWindow):
|
|
|
1014
894
|
lats = self._lat_data
|
|
1015
895
|
|
|
1016
896
|
# Create contour plot
|
|
1017
|
-
levels = 20
|
|
1018
897
|
if hasattr(plt.cm, self.cmap_name):
|
|
1019
898
|
cmap = getattr(plt.cm, self.cmap_name)
|
|
1020
899
|
else:
|
|
@@ -1033,8 +912,6 @@ class TiffViewer(QMainWindow):
|
|
|
1033
912
|
norm_data = np.power(norm_data, self.gamma)
|
|
1034
913
|
norm_data = norm_data * rng + vmin
|
|
1035
914
|
|
|
1036
|
-
# Downsample coordinates to match downsampled data shape
|
|
1037
|
-
# --- Align coordinates with data shape (no stepping assumptions) ---
|
|
1038
915
|
# Downsample coordinates to match downsampled data shape
|
|
1039
916
|
data_height, data_width = data.shape[:2]
|
|
1040
917
|
lat_samples = len(lats)
|
|
@@ -1057,27 +934,37 @@ class TiffViewer(QMainWindow):
|
|
|
1057
934
|
# print("[DEBUG] 2D lat grid ascending → flip lats_downsampled vertically")
|
|
1058
935
|
lats_downsampled = np.flipud(lats_downsampled)
|
|
1059
936
|
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
lons_downsampled = ((lons_downsampled + 180) % 360) - 180
|
|
937
|
+
# ---- Fix longitude and sort correctly ----
|
|
938
|
+
lons_ds = lons_downsampled.copy()
|
|
1063
939
|
|
|
940
|
+
# Convert 0–360 → -180–180 only once
|
|
941
|
+
if lons_ds.max() > 180:
|
|
942
|
+
lons_ds = ((lons_ds + 180) % 360) - 180
|
|
1064
943
|
|
|
1065
|
-
#
|
|
1066
|
-
|
|
944
|
+
# Sort and reorder data
|
|
945
|
+
sort_idx = np.argsort(lons_ds)
|
|
946
|
+
lons_ds = lons_ds[sort_idx]
|
|
947
|
+
data = data[:, sort_idx]
|
|
1067
948
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
shading="auto"
|
|
949
|
+
extent = (
|
|
950
|
+
float(lons_ds[0]),
|
|
951
|
+
float(lons_ds[-1]),
|
|
952
|
+
float(lats_downsampled[-1]),
|
|
953
|
+
float(lats_downsampled[0])
|
|
1074
954
|
)
|
|
1075
955
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
956
|
+
vmin = self.vmin if self._user_vmin is not None else np.nanmin(data)
|
|
957
|
+
vmax = self.vmax if self._user_vmax is not None else np.nanmax(data)
|
|
958
|
+
|
|
959
|
+
# Changed from pcolormesh to imshow to prevent artefacts when used with cartopy
|
|
960
|
+
img = ax.imshow(
|
|
961
|
+
data,
|
|
962
|
+
extent=extent,
|
|
963
|
+
transform=ccrs.PlateCarree(),
|
|
964
|
+
cmap=cmap,
|
|
965
|
+
interpolation="nearest",
|
|
966
|
+
vmin=vmin,
|
|
967
|
+
vmax=vmax
|
|
1081
968
|
)
|
|
1082
969
|
|
|
1083
970
|
# Add map features
|
|
@@ -1115,43 +1002,65 @@ class TiffViewer(QMainWindow):
|
|
|
1115
1002
|
|
|
1116
1003
|
# Close figure to prevent memory leak
|
|
1117
1004
|
plt.close(fig)
|
|
1005
|
+
del fig
|
|
1118
1006
|
|
|
1119
1007
|
return rgb
|
|
1120
|
-
|
|
1008
|
+
|
|
1121
1009
|
def update_pixmap(self):
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1010
|
+
# ------------------------------------------------------------------
|
|
1011
|
+
# Select respective data (a = single-band 2D, rgb = RGB array)
|
|
1012
|
+
# ------------------------------------------------------------------
|
|
1013
|
+
|
|
1014
|
+
rgb = None # ensure defined
|
|
1015
|
+
|
|
1016
|
+
# Case 1: RGB override (GeoTIFF or RGB-files)
|
|
1017
|
+
if self.rgb_mode:
|
|
1018
|
+
rgb = self.data
|
|
1019
|
+
a = None
|
|
1020
|
+
|
|
1021
|
+
# Case 2: Scientific multi-band (NetCDF/HDF)
|
|
1022
|
+
elif hasattr(self, "band_index"):
|
|
1023
|
+
# Always get consistent per-frame 2D data
|
|
1024
|
+
a = self.get_current_frame()
|
|
1025
|
+
|
|
1026
|
+
# Case 3: Regular GeoTIFF single-band
|
|
1130
1027
|
else:
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
rgb = self.data
|
|
1134
|
-
a = None
|
|
1135
|
-
else:
|
|
1136
|
-
a = self.data
|
|
1137
|
-
rgb = None
|
|
1138
|
-
# ----------------------------
|
|
1028
|
+
rgb = None
|
|
1029
|
+
a = self.data
|
|
1139
1030
|
|
|
1140
1031
|
# --- Render image ---
|
|
1141
|
-
#
|
|
1032
|
+
# Cartopy is only relevant for NetCDF
|
|
1142
1033
|
use_cartopy = False
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1034
|
+
|
|
1035
|
+
if hasattr(self, "_nc_var_name"):
|
|
1036
|
+
use_cartopy = (
|
|
1037
|
+
self.cartopy_mode == "on"
|
|
1038
|
+
and HAVE_CARTOPY
|
|
1039
|
+
and getattr(self, "_use_cartopy", False)
|
|
1040
|
+
and getattr(self, "_has_geo_coords", False)
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
# Inform user when cartopy was requested but cannot be used
|
|
1044
|
+
if self.cartopy_mode == "on" and not use_cartopy:
|
|
1045
|
+
if not HAVE_CARTOPY:
|
|
1046
|
+
print("[INFO] Cartopy not installed — using standard scientific rendering.")
|
|
1047
|
+
elif not getattr(self, "_use_cartopy", False):
|
|
1048
|
+
print("[INFO] This file lacks geospatial coordinates — cartopy disabled.")
|
|
1049
|
+
elif not getattr(self, "_has_geo_coords", False):
|
|
1050
|
+
print("[INFO] No lat/lon coordinates found — cartopy disabled.")
|
|
1051
|
+
|
|
1147
1052
|
if use_cartopy:
|
|
1148
|
-
# Render with cartopy for better geographic visualization
|
|
1149
1053
|
rgb = self._render_cartopy_map(a)
|
|
1150
1054
|
elif rgb is None:
|
|
1151
|
-
# Standard grayscale rendering for single-band
|
|
1055
|
+
# Standard grayscale rendering for single-band data
|
|
1152
1056
|
finite = np.isfinite(a)
|
|
1153
|
-
|
|
1057
|
+
|
|
1058
|
+
# Respect user-specified limits
|
|
1059
|
+
vmin = self._user_vmin if self._user_vmin is not None else np.nanmin(a)
|
|
1060
|
+
vmax = self._user_vmax if self._user_vmax is not None else np.nanmax(a)
|
|
1061
|
+
|
|
1154
1062
|
rng = max(vmax - vmin, 1e-12)
|
|
1063
|
+
|
|
1155
1064
|
norm = np.zeros_like(a, dtype=np.float32)
|
|
1156
1065
|
if np.any(finite):
|
|
1157
1066
|
norm[finite] = (a[finite] - vmin) / rng
|
|
@@ -1162,13 +1071,15 @@ class TiffViewer(QMainWindow):
|
|
|
1162
1071
|
else:
|
|
1163
1072
|
# True RGB mode (unchanged)
|
|
1164
1073
|
rgb = self._render_rgb()
|
|
1165
|
-
# ----------------------
|
|
1166
1074
|
|
|
1167
1075
|
h, w, _ = rgb.shape
|
|
1168
1076
|
self._last_rgb = rgb
|
|
1077
|
+
|
|
1169
1078
|
qimg = QImage(rgb.data, w, h, 3 * w, QImage.Format.Format_RGB888)
|
|
1170
1079
|
pix = QPixmap.fromImage(qimg)
|
|
1080
|
+
|
|
1171
1081
|
if self.pixmap_item is None:
|
|
1082
|
+
|
|
1172
1083
|
self.pixmap_item = QGraphicsPixmapItem(pix)
|
|
1173
1084
|
self.pixmap_item.setZValue(0.0)
|
|
1174
1085
|
self.scene.addItem(self.pixmap_item)
|
|
@@ -1193,6 +1104,7 @@ class TiffViewer(QMainWindow):
|
|
|
1193
1104
|
if nd is not None:
|
|
1194
1105
|
arr[arr == nd] = np.nan
|
|
1195
1106
|
self.data = arr
|
|
1107
|
+
|
|
1196
1108
|
self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
|
|
1197
1109
|
self.update_pixmap()
|
|
1198
1110
|
self.update_title()
|
|
@@ -1237,6 +1149,7 @@ class TiffViewer(QMainWindow):
|
|
|
1237
1149
|
# For other files, toggle between two colormaps
|
|
1238
1150
|
else:
|
|
1239
1151
|
self.cmap_name, self.alt_cmap_name = self.alt_cmap_name, self.cmap_name
|
|
1152
|
+
print(f"Colormap: {self.cmap_name}")
|
|
1240
1153
|
self.update_pixmap()
|
|
1241
1154
|
|
|
1242
1155
|
# Band switch
|
|
@@ -1297,12 +1210,14 @@ def run_viewer(
|
|
|
1297
1210
|
shapefile=None,
|
|
1298
1211
|
shp_color=None,
|
|
1299
1212
|
shp_width=None,
|
|
1300
|
-
subset=None
|
|
1213
|
+
subset=None,
|
|
1214
|
+
vmin=None,
|
|
1215
|
+
vmax=None,
|
|
1216
|
+
cartopy="on",
|
|
1217
|
+
timestep=None
|
|
1301
1218
|
):
|
|
1302
1219
|
|
|
1303
1220
|
"""Launch the TiffViewer app"""
|
|
1304
|
-
from PySide6.QtCore import Qt
|
|
1305
|
-
# QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
|
|
1306
1221
|
app = QApplication(sys.argv)
|
|
1307
1222
|
win = TiffViewer(
|
|
1308
1223
|
tif_path,
|
|
@@ -1313,7 +1228,11 @@ def run_viewer(
|
|
|
1313
1228
|
shapefiles=shapefile,
|
|
1314
1229
|
shp_color=shp_color,
|
|
1315
1230
|
shp_width=shp_width,
|
|
1316
|
-
subset=subset
|
|
1231
|
+
subset=subset,
|
|
1232
|
+
vmin=vmin,
|
|
1233
|
+
vmax=vmax,
|
|
1234
|
+
cartopy=cartopy,
|
|
1235
|
+
timestep=timestep,
|
|
1317
1236
|
)
|
|
1318
1237
|
win.show()
|
|
1319
1238
|
sys.exit(app.exec())
|
|
@@ -1321,17 +1240,34 @@ def run_viewer(
|
|
|
1321
1240
|
import click
|
|
1322
1241
|
|
|
1323
1242
|
@click.command()
|
|
1324
|
-
@click.version_option(
|
|
1243
|
+
@click.version_option(__version__, prog_name="viewtif")
|
|
1325
1244
|
@click.argument("tif_path", required=False)
|
|
1326
1245
|
@click.option("--band", default=1, show_default=True, type=int, help="Band number to display")
|
|
1327
1246
|
@click.option("--scale", default=1.0, show_default=True, type=int, help="Scale factor for display")
|
|
1328
1247
|
@click.option("--rgb", nargs=3, type=int, help="Three band numbers for RGB, e.g. --rgb 4 3 2")
|
|
1329
1248
|
@click.option("--rgbfiles", nargs=3, type=str, help="Three single-band TIFFs for RGB, e.g. --rgbfiles B4.tif B3.tif B2.tif")
|
|
1330
1249
|
@click.option("--shapefile", multiple=True, type=str, help="One or more shapefiles to overlay")
|
|
1331
|
-
@click.option("--shp-color", default="
|
|
1250
|
+
@click.option("--shp-color", default="cyan", show_default=True, help="Overlay color (name or #RRGGBB).")
|
|
1332
1251
|
@click.option("--shp-width", default=1.0, show_default=True, type=float, help="Overlay line width (screen pixels).")
|
|
1333
1252
|
@click.option("--subset", default=None, type=int, help="Open specific subdataset index in .hdf/.h5 file or variable in NetCDF file")
|
|
1334
|
-
|
|
1253
|
+
@click.option("--vmin", type=float, default=None, help="Manual minimum display value")
|
|
1254
|
+
@click.option("--vmax", type=float, default=None, help="Manual maximum display value")
|
|
1255
|
+
@click.option(
|
|
1256
|
+
"--timestep",
|
|
1257
|
+
type=int,
|
|
1258
|
+
default=None,
|
|
1259
|
+
help="For NetCDF files, jump directly to a specific time index (1-based)."
|
|
1260
|
+
)
|
|
1261
|
+
@click.option(
|
|
1262
|
+
"--cartopy",
|
|
1263
|
+
type=click.Choice(["on", "off"], case_sensitive=False),
|
|
1264
|
+
default="on",
|
|
1265
|
+
show_default=True,
|
|
1266
|
+
help="Use cartopy for NetCDF geospatial rendering."
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width, subset, vmin, vmax, cartopy, timestep):
|
|
1335
1271
|
"""Lightweight GeoTIFF, NetCDF, and HDF viewer."""
|
|
1336
1272
|
# --- Warn early if shapefile requested but geopandas missing ---
|
|
1337
1273
|
if shapefile and not HAVE_GEO:
|
|
@@ -1350,7 +1286,11 @@ def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width,
|
|
|
1350
1286
|
shapefile=shapefile,
|
|
1351
1287
|
shp_color=shp_color,
|
|
1352
1288
|
shp_width=shp_width,
|
|
1353
|
-
subset=subset
|
|
1289
|
+
subset=subset,
|
|
1290
|
+
vmin=vmin,
|
|
1291
|
+
vmax=vmax,
|
|
1292
|
+
cartopy=cartopy,
|
|
1293
|
+
timestep=timestep,
|
|
1354
1294
|
)
|
|
1355
1295
|
|
|
1356
1296
|
if __name__ == "__main__":
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viewtif
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Lightweight GeoTIFF, NetCDF, HDF/HDF5, and Esri File Geodatabase (.gdb) viewer with optional shapefile overlay. NetCDF and cartopy support available via pip install viewtif[netcdf].
|
|
5
5
|
Project-URL: Homepage, https://github.com/nkeikon/tifviewer
|
|
6
6
|
Project-URL: Source, https://github.com/nkeikon/tifviewer
|
|
7
7
|
Project-URL: Issues, https://github.com/nkeikon/tifviewer/issues
|
|
8
8
|
Author: Keiko Nomura
|
|
9
9
|
License: MIT
|
|
10
|
-
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.10
|
|
11
12
|
Requires-Dist: click>=8.1
|
|
12
13
|
Requires-Dist: matplotlib>=3.7
|
|
13
14
|
Requires-Dist: numpy>=1.23
|
|
@@ -26,6 +27,7 @@ Description-Content-Type: text/markdown
|
|
|
26
27
|
# viewtif
|
|
27
28
|
[](https://pepy.tech/project/viewtif)
|
|
28
29
|
[](https://pypi.org/project/viewtif/)
|
|
30
|
+
[](https://pypi.org/project/viewtif/)
|
|
29
31
|
|
|
30
32
|
A lightweight GeoTIFF viewer for quick visualization directly from the command line.
|
|
31
33
|
|
|
@@ -36,7 +38,7 @@ You can visualize single-band GeoTIFFs, RGB composites, HDF, NetCDF files and sh
|
|
|
36
38
|
```bash
|
|
37
39
|
pip install viewtif
|
|
38
40
|
```
|
|
39
|
-
> **Note:** On Linux, you may need python3-tk, libqt5gui5, or PySide6 dependencies.
|
|
41
|
+
> **Note:** Requires Python 3.10 or higher. On Linux, you may need python3-tk, libqt5gui5, or PySide6 dependencies.
|
|
40
42
|
>
|
|
41
43
|
>`viewtif` requires a graphical display environment.
|
|
42
44
|
> It may not run properly on headless systems (e.g., HPC compute nodes or remote servers without X11 forwarding).
|
|
@@ -49,19 +51,20 @@ pip install "viewtif[geo]"
|
|
|
49
51
|
> **Note:** For macOS(zsh) users:
|
|
50
52
|
> Make sure to include the quotes, or zsh will interpret it as a pattern.
|
|
51
53
|
|
|
52
|
-
####
|
|
54
|
+
#### NetCDF support
|
|
55
|
+
```bash
|
|
56
|
+
pip install "viewtif[netcdf]"
|
|
57
|
+
```
|
|
58
|
+
> **Note:** For enhanced geographic visualization with map projections, coastlines, and borders, install with cartopy: `pip install "viewtif[netcdf]"` (cartopy is included in the netcdf extra). If cartopy is not available, netCDF files will still display with standard rendering.
|
|
59
|
+
|
|
60
|
+
#### HDF/HDF5 & FileGDB support
|
|
61
|
+
GDAL is required to open `.hdf`, .`h5`, `.hdf5`, and `.gdb` files.
|
|
53
62
|
```bash
|
|
54
63
|
brew install gdal # macOS
|
|
55
64
|
sudo apt install gdal-bin python3-gdal # Linux
|
|
56
65
|
pip install GDAL
|
|
57
66
|
```
|
|
58
|
-
> **Note:** GDAL is required to open `.hdf`, .`h5`, and `.hdf5` files. If it’s missing, viewtif will display: `RuntimeError: HDF support requires GDAL.`
|
|
59
67
|
|
|
60
|
-
#### NetCDF support
|
|
61
|
-
```bash
|
|
62
|
-
brew install "viewtif[netcdf]"
|
|
63
|
-
```
|
|
64
|
-
> **Note:** For enhanced geographic visualization with map projections, coastlines, and borders, install with cartopy: `pip install "viewtif[netcdf]"` (cartopy is included in the netcdf extra). If cartopy is not available, netCDF files will still display with standard RGB rendering.
|
|
65
68
|
## Quick Start
|
|
66
69
|
```bash
|
|
67
70
|
# View a GeoTIFF
|
|
@@ -73,9 +76,16 @@ viewtif --rgbfiles \
|
|
|
73
76
|
HLS_B03.tif \
|
|
74
77
|
HLS_B02.tif
|
|
75
78
|
|
|
79
|
+
# View an RGB composite from a multi-band file
|
|
80
|
+
viewtif rgb.tif --rgb 4 3 2
|
|
81
|
+
|
|
76
82
|
# View with shapefile overlay
|
|
77
83
|
viewtif ECOSTRESS_LST.tif \
|
|
78
84
|
--shapefile Zip_Codes.shp
|
|
85
|
+
|
|
86
|
+
# Change the color and width
|
|
87
|
+
viewtif ECOSTRESS_LST.tif \
|
|
88
|
+
--shapefile Zip_Codes.shp --shp-color red --shp-width 2
|
|
79
89
|
```
|
|
80
90
|
### Update in v1.0.6: HDF/HDF5 support
|
|
81
91
|
`viewtif` can open `.hdf`, `.h5`, and `.hdf5` files that contain multiple subdatasets. When opened, it lists available subdatasets and lets you view one by index. You can also specify a band to display (default is the first band) or change bands interactively with '[' and ']'.
|
|
@@ -93,11 +103,7 @@ viewtif AG100.v003.33.-107.0001.h5 --subset 1 --band 3
|
|
|
93
103
|
`[WARN] raster lacks CRS/transform; cannot place overlays.`
|
|
94
104
|
|
|
95
105
|
### Update in v1.0.7: File Geodatabase (.gdb) support
|
|
96
|
-
`viewtif` can now open raster datasets stored inside Esri File Geodatabases (`.gdb`). When you open a .gdb directly, `viewtif
|
|
97
|
-
|
|
98
|
-
Most Rasterio installations already include the OpenFileGDB driver, so .gdb datasets often open without installing GDAL manually.
|
|
99
|
-
|
|
100
|
-
If you encounter: RuntimeError: GDB support requires GDAL, install GDAL as shown above to enable the driver.
|
|
106
|
+
`viewtif` can now open raster datasets stored inside Esri File Geodatabases (`.gdb`). When you open a .gdb directly, `viewtif` will list available raster datasets first, then you can choose one to view.
|
|
101
107
|
|
|
102
108
|
```bash
|
|
103
109
|
# List available raster datasets
|
|
@@ -114,16 +120,19 @@ If the dataset is very large (e.g., >20 million pixels), it will pause and warn
|
|
|
114
120
|
You can proceed manually or rerun with the `--scale` option for a smaller, faster preview.
|
|
115
121
|
|
|
116
122
|
### Update in v0.2.2: NetCDF support with optional cartopy visualization
|
|
117
|
-
`viewtif` now supports NetCDF (`.nc`) files
|
|
123
|
+
`viewtif` now supports NetCDF (`.nc`) files via xarray, with optional cartopy-based geographic visualization. Use `[` / `]` to move forward or backward through time steps, and press M to switch between the three built-in colormaps ("RdBu_r", "viridis", "magma").
|
|
118
124
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
### Update in v0.2.6: New user controls for NetCDF visualization
|
|
126
|
+
- `--vmin [value] --vmax [value]` let you set the display range (for all file types).
|
|
127
|
+
- `--timestep [int]` lets you jump directly to a chosen time slice.
|
|
128
|
+
- `cartopy off` let you see raw NetCDF images.
|
|
123
129
|
|
|
124
130
|
#### Examples
|
|
125
131
|
```bash
|
|
126
132
|
viewtif data.nc
|
|
133
|
+
viewtif data.nc --vmin 280 --vmax 320
|
|
134
|
+
viewtif data.nc --timestep 100
|
|
135
|
+
viewtif data.nc --cartopy off
|
|
127
136
|
```
|
|
128
137
|
|
|
129
138
|
## Controls
|
|
@@ -133,7 +142,7 @@ viewtif data.nc
|
|
|
133
142
|
| Arrow keys or `WASD` | Pan |
|
|
134
143
|
| `C` / `V` | Increase / decrease contrast |
|
|
135
144
|
| `G` / `H` | Increase / decrease gamma |
|
|
136
|
-
| `M` | Toggle colormap
|
|
145
|
+
| `M` | Toggle colormap. Single-band: viridis/magma. NetCDF: RdBu_r/viridis/magma. |
|
|
137
146
|
| `[` / `]` | Previous / next band (or time step) |
|
|
138
147
|
| `R` | Reset view |
|
|
139
148
|
|
|
@@ -149,6 +158,8 @@ viewtif data.nc
|
|
|
149
158
|
- Zip_Codes.shp and associated files
|
|
150
159
|
- HLS_B04.tif, HLS_B03.tif, HLS_B02.tif (RGB sample)
|
|
151
160
|
- AG100.v003.33.-107.0001.h5 (HDF5 file)
|
|
161
|
+
- [tasmax_mon_ACCESS-CM2_ssp370_r1i1p1f1_gn_2021.nc](https://data.nas.nasa.gov/nex-dcp30-cmip6/monthly/ACCESS-CM2/ssp370/r1i1p1f1/tasmax/tasmax_mon_ACCESS-CM2_ssp370_r1i1p1f1_gn_2021.nc)
|
|
162
|
+
- [tas_Amon_KIOST-ESM_ssp585_r1i1p1f1_gr1_201501-210012.nc](https://dap.ceda.ac.uk/badc/cmip6/data/CMIP6/ScenarioMIP/KIOST/KIOST-ESM/ssp585/r1i1p1f1/Amon/tas/gr1/v20191106/tas_Amon_KIOST-ESM_ssp585_r1i1p1f1_gr1_201501-210012.nc)
|
|
152
163
|
|
|
153
164
|
## Credit & License
|
|
154
165
|
`viewtif` was inspired by the NASA JPL Thermal Viewer — Semi-Automated Georeferencer (GeoViewer v1.12) developed by Jake Longenecker (University of Miami Rosenstiel School of Marine, Atmospheric & Earth Science) while at the NASA Jet Propulsion Laboratory, California Institute of Technology, with inspiration from JPL’s ECOSTRESS geolocation batch workflow by Andrew Alamillo. The original GeoViewer was released under the MIT License (2025) and may be freely adapted with citation.
|
|
@@ -156,9 +167,18 @@ viewtif data.nc
|
|
|
156
167
|
## Citation
|
|
157
168
|
Longenecker, Jake; Lee, Christine; Hulley, Glynn; Cawse-Nicholson, Kerry; Purkis, Sam; Gleason, Art; Otis, Dan; Galdamez, Ileana; Meiseles, Jacquelyn. GeoViewer v1.12: NASA JPL Thermal Viewer—Semi-Automated Georeferencer User Guide & Reference Manual. Jet Propulsion Laboratory, California Institute of Technology, 2025. PDF.
|
|
158
169
|
|
|
159
|
-
## License
|
|
160
|
-
This project is released under the MIT License.
|
|
161
|
-
|
|
162
170
|
## Contributors
|
|
163
171
|
- [@HarshShinde0](https://github.com/HarshShinde0) — added mouse-wheel and trackpad zoom support; added NetCDF support with [@nkeikon](https://github.com/nkeikon)
|
|
164
|
-
- [@p-vdp](https://github.com/p-vdp) — added File Geodatabase (.gdb) raster support
|
|
172
|
+
- [@p-vdp](https://github.com/p-vdp) — added File Geodatabase (.gdb) raster support
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
This project is released under the MIT License © 2025 Keiko Nomura.
|
|
176
|
+
|
|
177
|
+
If you find this tool useful, please consider supporting or acknowledging the author.
|
|
178
|
+
|
|
179
|
+
## Useful links
|
|
180
|
+
- [Demo by User](https://www.linkedin.com/posts/desmond-lartey_geospatial-analysts-heads-up-check-out-activity-7386336488050925568-G5bN?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
|
|
181
|
+
- [Demo at the initial release](https://www.linkedin.com/posts/keiko-nomura-0231891_dont-you-sometimes-just-want-to-see-a-activity-7383409138296528896-sbRM?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
|
|
182
|
+
- [Demo on HDF files](https://www.linkedin.com/posts/keiko-nomura-0231891_now-you-can-see-hdf-files-from-the-command-activity-7383963721561399296-F5i0?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
|
|
183
|
+
- [Demo on NetCDF files](https://www.linkedin.com/posts/keiko-nomura-0231891_you-can-now-view-netcdf-files-nc-from-activity-7386189562072670208-3A0V?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
|
|
184
|
+
- [Demo on Esri Geodatabases](https://www.linkedin.com/posts/keiko-nomura-0231891_did-you-know-viewtif-now-supports-esri-geodatabases-activity-7390165568688959488-pvpk?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
viewtif/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
viewtif/tif_viewer.py,sha256=-vO-62ApjcG7lVVTHB5ggc9qSeB7YmdR1gmfskp-Vzg,51628
|
|
3
|
+
viewtif-0.2.6.dist-info/METADATA,sha256=CJZC9X2CtZjzQTEVg_MKTKMGNS7Vq8hcTUFEQck0Er4,9541
|
|
4
|
+
viewtif-0.2.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
viewtif-0.2.6.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
|
|
6
|
+
viewtif-0.2.6.dist-info/licenses/LICENSE,sha256=-aRH3Wo6exTLQLNp9yj6HptkUXXtNjdUSzumkyRsK4s,1069
|
|
7
|
+
viewtif-0.2.6.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Keiko Nomura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
viewtif-0.2.4.dist-info/RECORD
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
viewtif/tif_viewer.py,sha256=a4fdzq8_r0YQ-LVoTiYv32WAxxuldCGhD9-pDJ1sEPA,57465
|
|
2
|
-
viewtif-0.2.4.dist-info/METADATA,sha256=Ow100gbhTP6qtJBRl5MQT14brtDPusML9aupaMwPJ0k,7280
|
|
3
|
-
viewtif-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
-
viewtif-0.2.4.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
|
|
5
|
-
viewtif-0.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|