Skip to content

Commit affd6c6

Browse files
committed
Add GLCM texture user guide notebook (#963)
Demonstrates single/multi metric usage, angle effects, Dask support, and parameter reference on a synthetic four-texture raster.
1 parent b28d71c commit affd6c6

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "zvy4yyjkifj",
6+
"source": "# GLCM Texture Metrics\n\nGray-Level Co-occurrence Matrix (GLCM) texture features capture spatial patterns in raster data that spectral bands alone cannot distinguish. They were introduced by Haralick et al. (1973) and remain a standard tool in remote sensing classification, geological mapping, and medical imaging.\n\n`xrspatial.glcm_texture` computes six Haralick features over a sliding window:\n\n| Metric | What it measures |\n|---|---|\n| **contrast** | Intensity difference between neighboring pixels |\n| **dissimilarity** | Mean absolute gray-level difference |\n| **homogeneity** | Inverse difference moment (smooth vs. rough) |\n| **energy** | Sum of squared GLCM entries (uniformity) |\n| **correlation** | Linear dependency of gray levels |\n| **entropy** | Randomness of the co-occurrence distribution |",
7+
"metadata": {}
8+
},
9+
{
10+
"cell_type": "code",
11+
"id": "3a06tk9sxuh",
12+
"source": "import numpy as np\nimport xarray as xr\nimport matplotlib.pyplot as plt\nfrom xrspatial import glcm_texture",
13+
"metadata": {},
14+
"execution_count": null,
15+
"outputs": []
16+
},
17+
{
18+
"cell_type": "markdown",
19+
"id": "cs9lld5daa",
20+
"source": "## Synthetic test raster\n\nWe'll build a 100x100 raster with four quadrants that have distinct textures: smooth, noisy, striped, and checkerboard. GLCM metrics should clearly separate these regions.",
21+
"metadata": {}
22+
},
23+
{
24+
"cell_type": "code",
25+
"id": "mqtgqmsnvlp",
26+
"source": "# Build a 100x100 raster with four texture quadrants\nrng = np.random.default_rng(42)\ndata = np.zeros((100, 100), dtype=np.float64)\n\n# Top-left: smooth gradient\ndata[:50, :50] = np.linspace(0, 1, 50)[np.newaxis, :]\n\n# Top-right: random noise\ndata[:50, 50:] = rng.random((50, 50))\n\n# Bottom-left: vertical stripes\ndata[50:, :50] = np.tile([0.0, 1.0], 25)[np.newaxis, :]\n\n# Bottom-right: checkerboard\ncb = np.zeros((50, 50))\ncb[::2, 1::2] = 1.0\ncb[1::2, ::2] = 1.0\ndata[50:, 50:] = cb\n\nagg = xr.DataArray(data, dims=['y', 'x'])\n\nfig, ax = plt.subplots(figsize=(5, 5))\nax.imshow(data, cmap='gray')\nax.set_title('Input raster (4 texture regions)')\nplt.tight_layout()\nplt.show()",
27+
"metadata": {},
28+
"execution_count": null,
29+
"outputs": []
30+
},
31+
{
32+
"cell_type": "markdown",
33+
"id": "5dh3jr9qbba",
34+
"source": "## Computing a single metric\n\nThe simplest usage: pass a single metric name and get a 2-D result back.",
35+
"metadata": {}
36+
},
37+
{
38+
"cell_type": "code",
39+
"id": "2fgm4xz3uwj",
40+
"source": "contrast = glcm_texture(agg, metric='contrast', window_size=7, levels=64)\n\nfig, axes = plt.subplots(1, 2, figsize=(10, 4))\naxes[0].imshow(data, cmap='gray')\naxes[0].set_title('Input')\nim = axes[1].imshow(contrast.values, cmap='inferno')\naxes[1].set_title('GLCM Contrast')\nplt.colorbar(im, ax=axes[1], shrink=0.8)\nplt.tight_layout()\nplt.show()",
41+
"metadata": {},
42+
"execution_count": null,
43+
"outputs": []
44+
},
45+
{
46+
"cell_type": "markdown",
47+
"id": "d6oq5gt2i6",
48+
"source": "## Computing multiple metrics at once\n\nPass a list of metric names to get a 3-D result with a leading `metric` dimension. This is more efficient than calling `glcm_texture` separately for each metric when using the numpy backend, since the GLCM is built once per window position.",
49+
"metadata": {}
50+
},
51+
{
52+
"cell_type": "code",
53+
"id": "7ah4bm6v5ut",
54+
"source": "metrics = ['contrast', 'dissimilarity', 'homogeneity', 'energy', 'correlation', 'entropy']\ntextures = glcm_texture(agg, metric=metrics, window_size=7, levels=64)\n\nfig, axes = plt.subplots(2, 3, figsize=(14, 9))\nfor ax, name in zip(axes.flat, metrics):\n vals = textures.sel(metric=name).values\n im = ax.imshow(vals, cmap='viridis')\n ax.set_title(name.capitalize())\n plt.colorbar(im, ax=ax, shrink=0.7)\nplt.suptitle('All six GLCM texture metrics', fontsize=14, y=1.01)\nplt.tight_layout()\nplt.show()",
55+
"metadata": {},
56+
"execution_count": null,
57+
"outputs": []
58+
},
59+
{
60+
"cell_type": "markdown",
61+
"id": "hats5p5wfk5",
62+
"source": "## Effect of angle\n\nThe `angle` parameter controls the direction of pixel pairing:\n- `0` -- horizontal (right)\n- `45` -- upper-right diagonal\n- `90` -- vertical (up)\n- `135` -- upper-left diagonal\n- `None` (default) -- average over all four\n\nDirectional textures like stripes respond differently depending on the angle.",
63+
"metadata": {}
64+
},
65+
{
66+
"cell_type": "code",
67+
"id": "nkzw9wmyxio",
68+
"source": "fig, axes = plt.subplots(1, 4, figsize=(16, 4))\nfor ax, ang in zip(axes, [0, 45, 90, 135]):\n result = glcm_texture(agg, metric='contrast', window_size=7,\n levels=64, angle=ang)\n im = ax.imshow(result.values, cmap='inferno')\n ax.set_title(f'Contrast (angle={ang})')\n plt.colorbar(im, ax=ax, shrink=0.7)\nplt.tight_layout()\nplt.show()",
69+
"metadata": {},
70+
"execution_count": null,
71+
"outputs": []
72+
},
73+
{
74+
"cell_type": "markdown",
75+
"id": "fbwobep1n9f",
76+
"source": "## Dask support\n\n`glcm_texture` works on chunked Dask arrays out of the box. The input is quantized globally (so gray-level mapping stays consistent across chunks), then each chunk computes GLCM features independently via `map_overlap`.",
77+
"metadata": {}
78+
},
79+
{
80+
"cell_type": "code",
81+
"id": "keurc3dmugs",
82+
"source": "import dask.array as da\n\ndask_agg = xr.DataArray(\n da.from_array(data, chunks=(50, 50)),\n dims=['y', 'x'],\n)\n\ndask_contrast = glcm_texture(dask_agg, metric='contrast',\n window_size=7, levels=64)\nprint(f'Result type: {type(dask_contrast.data)}')\nprint(f'Chunks: {dask_contrast.data.chunks}')\n\n# Verify it matches the numpy result\nnp.testing.assert_allclose(\n contrast.values, dask_contrast.values,\n rtol=1e-10, equal_nan=True,\n)\nprint('Dask result matches numpy result.')",
83+
"metadata": {},
84+
"execution_count": null,
85+
"outputs": []
86+
},
87+
{
88+
"cell_type": "markdown",
89+
"id": "vda99az5wo",
90+
"source": "## Parameters reference\n\n| Parameter | Default | Description |\n|---|---|---|\n| `metric` | `'contrast'` | One metric name (str) or a list of names |\n| `window_size` | `7` | Side length of the sliding window (must be odd, >= 3) |\n| `levels` | `64` | Number of gray levels for quantization (2-256) |\n| `distance` | `1` | Pixel pair distance |\n| `angle` | `None` | 0, 45, 90, 135, or None (average all four) |\n\nLower `levels` values run faster but lose gray-level resolution. For most remote sensing work, 32-64 levels is a good balance.",
91+
"metadata": {}
92+
}
93+
],
94+
"metadata": {
95+
"kernelspec": {
96+
"display_name": "Python 3",
97+
"language": "python",
98+
"name": "python3"
99+
},
100+
"language_info": {
101+
"name": "python",
102+
"version": "3.10.0"
103+
}
104+
},
105+
"nbformat": 4,
106+
"nbformat_minor": 5
107+
}

0 commit comments

Comments
 (0)