Skip to content

How texture is turned into numbers

Tonight · ~30 min · read + a worked matrix · energy: medium · setup: none

“GLCM” and “GLRLM” are acronyms that intimidate until you see one built by hand. This lesson does that: discretise a tiny image, build a co-occurrence matrix from it, and read two features off the matrix. After this, the texture families are not a wall of jargon — they are all variations of “build a matrix that counts a kind of spatial pattern, then summarise it”.

The anchor: you cannot count texture on raw HU

A tumour ROI might contain HU values ranging from −900 (necrosis/air) to +60 (soft tissue) to +400 (calcium). If you tried to count “how often grey level a sits next to grey level b” on the raw HU integers, you would get a gigantic, mostly-empty matrix (thousands of possible levels) where almost no pair repeats. Texture matrices need a small set of grey levels to be meaningful — and that requires discretisation first.

Step 1 — discretise

Discretisation quantises the continuous HU range into a small number of grey levels. PyRadiomics’ default is fixed bin width (in HU): the intensity axis is divided into bins of equal width binWidth, equally spaced from 0, and each voxel lands in the bin

bin_index = floor(HU / binWidth)

PyRadiomics then shifts every bin by a constant so that the smallest grey level inside the ROI becomes 1 — the full rule is X_b = floor(HU / binWidth) - floor(min(HU_ROI) / binWidth) + 1. That shift is just a relabeling: it does not change how many grey levels exist or which voxels share a bin, so for understanding texture it is the floor(HU / binWidth) part that matters.

PyRadiomics vs IBSI. This differs slightly from the IBSI fixed-bin-size (FBS) method, which uses a ceiling division and spaces bins from the ROI’s minimum intensity. PyRadiomics documents this deviation (VERIFIED against the official PyRadiomics source). The consequence is the same: the bin width is a feature-determining choice.

With binWidth = 25 (PyRadiomics’ default), a few HU values land as follows:

HU floor(HU / 25)
−40 −2
−30 −2
−20 −1
+30 +1

So −40 and −30 collapse into the same bin (−2), while −20 lands one bin up (−1) and +30 another (+1). The result is a small-alphabet “image” whose values are bin indices. Discretisation is not a detail; it is the texture feature, as the next lesson shows dramatically.

Step 2 — a worked GLCM

Take a tiny 4×4 ROI, already discretised to three grey levels {1, 2, 3}:

1 1 2 3
1 2 2 3
2 2 3 3
1 2 3 3

A GLCM (Grey-Level Co-occurrence Matrix) counts, for a given offset, how often grey level i appears next to grey level j. Use offset “one voxel to the right” (horizontal neighbour, distance 1). Walking each row left-to-right and recording each (left, right) pair:

Row pairs
row 1 (1,1) (1,2) (2,3)
row 2 (1,2) (2,2) (2,3)
row 3 (2,2) (2,3) (3,3)
row 4 (1,2) (2,3) (3,3)

Tallying into a matrix (rows = i, columns = j):

j=1 j=2 j=3
i=1 [ 1 3 0 ]
i=2 [ 0 2 4 ]
i=3 [ 0 0 2 ]

Twelve pairs total (4 rows × 3 pairs). Normalising by 12 turns counts into probabilities — that is the matrix features are computed from.

Step 3 — read features off the matrix

Now the matrix is the texture, and features are summaries of it:

  • Joint Entropy measures how spread-out the probability mass is. A matrix concentrated on a few cells (a region with few, predictable grey-level transitions) has low entropy; a matrix spread across many cells has high entropy. In this example five cells are occupied with fairly balanced mass, so entropy is moderate. Note what entropy does not measure: it does not care how far from the diagonal a cell is — only how the mass is spread. A speckled image would fill the far off-diagonal cells (1,3),(3,1) and raise entropy.
  • Contrast weights pairs by (i − j)², so it does care about distance from the diagonal — far-off-diagonal pairs (big grey-level jumps) dominate it. Here every occupied cell is on or next to the diagonal (1,2),(2,3),(2,2),(3,3), so contrast is low; if (1,3) were common, contrast would spike.

The two features measure different things: a smoothly-graded region (1→2→3) can have low contrast (small grey-level jumps) yet moderate entropy (mass spread over several cells). Treat them as complementary summaries of the same matrix, not as synonyms.

The other four families are the same idea with a different notion of “pattern”: GLRLM counts runs of equal levels along a direction; GLSZM counts 3D zones of connected equal levels; GLDM counts dependence on neighbours; NGTDM counts the difference between a voxel and its local average. Build a matrix that counts the pattern, summarise it, name the summary. That is all a texture feature is.

flowchart LR
    IMG["discretised ROI"] -->|M["count co-occurring pairs<br/>at an offset"]| GLCM["GLCM matrix"]
    GLCM -->|summarise| F1["Joint Entropy<br/>Contrast<br/>…"]
    IMG -->|"count runs of equal levels"| GLRLM["GLRLM matrix"]
    GLRLM -->|summarise| F2["Long/Short Run<br/>Emphasis …"]

Why this matters before the vocabulary

Once you have built one GLCM by hand, three things that looked like jargon become obvious:

  • The matrix depends entirely on the discretised image, so changing binWidth rebuilds a different matrix and different features (next lesson).
  • The matrix depends on the offset/direction, so 2D-vs-3D and distance choices change it.
  • The features are summaries of a counting matrix, not measurements of biology. Two textures can produce similar Joint Entropy for different reasons; the feature is a compressed statistic, not a biomarker.

This is exactly the lesson TRACE-CT’s extract_joint_entropy() makes concrete: it runs the whole chain (CT load → SEG decode → 1 mm B-spline → NN mask → HU clip → PyRadiomics original_glcm_JointEntropy at binWidth 25) so you can see one feature, end to end, reproducibly.

Stop and think — then reveal

Take the worked GLCM above. If the ROI had been noisier — say level 3 appeared next to level 1 several times — which of Joint Entropy and Contrast would change, and in which direction?

Both would tend to rise, for related but distinct reasons. Contrast weights pairs by (i−j)², so (1,3)/(3,1) pairs (jump of 2, weight 4) contribute heavily — they push contrast up sharply. Joint Entropy rises because the probability mass spreads into more cells, making the distribution less concentrated. So adding far-off-diagonal transitions tends to raise contrast and entropy together. The caveat from above still holds: a smoothly graded region (1→2→3) can have low contrast yet moderate entropy, so the two are correlated but not identical summaries of disorder.

What to retain

  1. Texture features are computed on the discretised image, never raw HU. Discretisation (fixed bin width) reduces thousands of HU values to a small grey level alphabet.
  2. A GLCM counts co-occurring grey-level pairs at an offset; features (Joint Entropy, Contrast, …) are summaries of that matrix. The other families are the same pattern: count a spatial relationship into a matrix, summarise it.
  3. Two GLCM summaries to keep distinct: Contrast = how far co-occurring pairs sit from the diagonal (magnitude of local grey-level jumps); Joint Entropy = how broadly probability mass is spread across co-occurrence states. They often rise together in noisy textures, but one does not determine the other — a smoothly graded texture can have low contrast yet moderate entropy. Either way, the features are compressed statistics of a counting matrix, not biology.
  4. The matrix depends on discretisation, offset, direction, and 2D/3D — all feature-determining choices to pin and report.

Next: the step that has the largest single effect on texture features — why binWidth changes the answer.