Skip to content

Commit 197d601

Browse files
committed
Add JPEG 2000 compression user guide notebook (#1048)
Covers write/read roundtrip, file size comparison with deflate, multi-band RGB example, and GPU acceleration notes.
1 parent 4dd9bfd commit 197d601

File tree

1 file changed

+101
-0
lines changed

1 file changed

+101
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "yox7s6qx13e",
6+
"source": "# JPEG 2000 compression for GeoTIFFs\n\nThe geotiff package supports JPEG 2000 (J2K) as a compression codec for both reading and writing. This is useful for satellite imagery workflows where J2K is common (Sentinel-2, Landsat, etc.).\n\nTwo acceleration tiers are available:\n- **CPU** via `glymur` (pip install glymur) -- works anywhere OpenJPEG is installed\n- **GPU** via NVIDIA's nvJPEG2000 library -- same optional pattern as nvCOMP for deflate/ZSTD\n\nThis notebook demonstrates write/read roundtrips with JPEG 2000 compression.",
7+
"metadata": {}
8+
},
9+
{
10+
"cell_type": "code",
11+
"id": "kamu534xsm",
12+
"source": "import numpy as np\nimport xarray as xr\nimport matplotlib.pyplot as plt\nimport tempfile\nimport os\n\nfrom xrspatial.geotiff import read_geotiff, write_geotiff",
13+
"metadata": {},
14+
"execution_count": null,
15+
"outputs": []
16+
},
17+
{
18+
"cell_type": "markdown",
19+
"id": "w7tlml1cyqj",
20+
"source": "## Generate synthetic elevation data\n\nWe'll create a small terrain-like raster to use as test data.",
21+
"metadata": {}
22+
},
23+
{
24+
"cell_type": "code",
25+
"id": "9fzhnpcn4xq",
26+
"source": "# Create a 256x256 synthetic terrain (uint16, typical for satellite imagery)\nrng = np.random.RandomState(42)\nyy, xx = np.meshgrid(np.linspace(-2, 2, 256), np.linspace(-2, 2, 256), indexing='ij')\nterrain = np.exp(-(xx**2 + yy**2)) * 10000 + rng.normal(0, 100, (256, 256))\nterrain = np.clip(terrain, 0, 65535).astype(np.uint16)\n\nda = xr.DataArray(\n terrain,\n dims=['y', 'x'],\n coords={\n 'y': np.linspace(45.0, 44.0, 256),\n 'x': np.linspace(-120.0, -119.0, 256),\n },\n attrs={'crs': 4326},\n name='elevation',\n)\n\nfig, ax = plt.subplots(figsize=(6, 5))\nda.plot(ax=ax, cmap='terrain')\nax.set_title('Synthetic elevation (uint16)')\nplt.tight_layout()\nplt.show()",
27+
"metadata": {},
28+
"execution_count": null,
29+
"outputs": []
30+
},
31+
{
32+
"cell_type": "markdown",
33+
"id": "8tsuyr3jbay",
34+
"source": "## Write with JPEG 2000 (lossless)\n\nPass `compression='jpeg2000'` to `write_geotiff`. The default is lossless encoding.",
35+
"metadata": {}
36+
},
37+
{
38+
"cell_type": "code",
39+
"id": "ystjp6v30d",
40+
"source": "tmpdir = tempfile.mkdtemp(prefix='j2k_demo_')\n\n# Write with JPEG 2000 compression\nj2k_path = os.path.join(tmpdir, 'elevation_j2k.tif')\nwrite_geotiff(da, j2k_path, compression='jpeg2000')\n\n# Compare file sizes with deflate\ndeflate_path = os.path.join(tmpdir, 'elevation_deflate.tif')\nwrite_geotiff(da, deflate_path, compression='deflate')\n\nnone_path = os.path.join(tmpdir, 'elevation_none.tif')\nwrite_geotiff(da, none_path, compression='none')\n\nj2k_size = os.path.getsize(j2k_path)\ndeflate_size = os.path.getsize(deflate_path)\nnone_size = os.path.getsize(none_path)\n\nprint(f\"Uncompressed: {none_size:>8,} bytes\")\nprint(f\"Deflate: {deflate_size:>8,} bytes ({deflate_size/none_size:.1%} of original)\")\nprint(f\"JPEG 2000: {j2k_size:>8,} bytes ({j2k_size/none_size:.1%} of original)\")",
41+
"metadata": {},
42+
"execution_count": null,
43+
"outputs": []
44+
},
45+
{
46+
"cell_type": "markdown",
47+
"id": "89y9zun97nb",
48+
"source": "## Read it back and verify lossless roundtrip\n\n`read_geotiff` auto-detects the compression from the TIFF header. No special arguments needed.",
49+
"metadata": {}
50+
},
51+
{
52+
"cell_type": "code",
53+
"id": "8vf9ljxkx03",
54+
"source": "# Read back and check lossless roundtrip\nda_read = read_geotiff(j2k_path)\n\nprint(f\"Shape: {da_read.shape}\")\nprint(f\"Dtype: {da_read.dtype}\")\nprint(f\"CRS: {da_read.attrs.get('crs')}\")\nprint(f\"Exact match: {np.array_equal(da_read.values, terrain)}\")\n\nfig, axes = plt.subplots(1, 2, figsize=(12, 5))\nda.plot(ax=axes[0], cmap='terrain')\naxes[0].set_title('Original')\nda_read.plot(ax=axes[1], cmap='terrain')\naxes[1].set_title('After JPEG 2000 roundtrip')\nplt.tight_layout()\nplt.show()",
55+
"metadata": {},
56+
"execution_count": null,
57+
"outputs": []
58+
},
59+
{
60+
"cell_type": "markdown",
61+
"id": "gcj96utnd3u",
62+
"source": "## Multi-band example (RGB)\n\nJPEG 2000 also handles multi-band imagery, which is the common case for satellite data.",
63+
"metadata": {}
64+
},
65+
{
66+
"cell_type": "code",
67+
"id": "mgv9xhsrcen",
68+
"source": "# Create a 3-band uint8 image\nrgb = np.zeros((128, 128, 3), dtype=np.uint8)\nrgb[:, :, 0] = np.linspace(0, 255, 128).astype(np.uint8)[None, :] # red gradient\nrgb[:, :, 1] = np.linspace(0, 255, 128).astype(np.uint8)[:, None] # green gradient\nrgb[:, :, 2] = 128 # constant blue\n\nda_rgb = xr.DataArray(\n rgb, dims=['y', 'x', 'band'],\n coords={'y': np.arange(128), 'x': np.arange(128), 'band': [0, 1, 2]},\n)\n\nrgb_path = os.path.join(tmpdir, 'rgb_j2k.tif')\nwrite_geotiff(da_rgb, rgb_path, compression='jpeg2000')\n\nda_rgb_read = read_geotiff(rgb_path)\nprint(f\"RGB shape: {da_rgb_read.shape}, dtype: {da_rgb_read.dtype}\")\nprint(f\"Exact match: {np.array_equal(da_rgb_read.values, rgb)}\")\n\nfig, axes = plt.subplots(1, 2, figsize=(10, 4))\naxes[0].imshow(rgb)\naxes[0].set_title('Original RGB')\naxes[1].imshow(da_rgb_read.values)\naxes[1].set_title('After J2K roundtrip')\nplt.tight_layout()\nplt.show()",
69+
"metadata": {},
70+
"execution_count": null,
71+
"outputs": []
72+
},
73+
{
74+
"cell_type": "markdown",
75+
"id": "zzga5hc3a99",
76+
"source": "## GPU acceleration\n\nOn systems with nvJPEG2000 installed (CUDA toolkit, RAPIDS environments), pass `gpu=True` to use GPU-accelerated J2K encode/decode. The API is the same -- it falls back to CPU automatically if the library isn't found.\n\n```python\n# GPU write (nvJPEG2000 if available, else CPU fallback)\nwrite_geotiff(cupy_data, \"output.tif\", compression=\"jpeg2000\", gpu=True)\n\n# GPU read (nvJPEG2000 decode if available)\nda = read_geotiff(\"satellite.tif\", gpu=True)\n```",
77+
"metadata": {}
78+
},
79+
{
80+
"cell_type": "code",
81+
"id": "x74nrht8kx",
82+
"source": "# Cleanup temp files\nimport shutil\nshutil.rmtree(tmpdir, ignore_errors=True)",
83+
"metadata": {},
84+
"execution_count": null,
85+
"outputs": []
86+
}
87+
],
88+
"metadata": {
89+
"kernelspec": {
90+
"display_name": "Python 3",
91+
"language": "python",
92+
"name": "python3"
93+
},
94+
"language_info": {
95+
"name": "python",
96+
"version": "3.11.0"
97+
}
98+
},
99+
"nbformat": 4,
100+
"nbformat_minor": 5
101+
}

0 commit comments

Comments
 (0)