Texture Segmentation
When color gives up, texture takes over
Overview
Texture is a defining characteristic of many images: think of properties like smooth and rough. If you treat pixel intensity as a surface—higher intensities as taller, lower intensities as shorter—then a rough region has more hills and valleys than a smooth one. We can measure that variation directly, using statistics like standard deviation, range, or entropy, and a region's texture can be just as useful a segmentation cue as its color. In fact, texture segmentation shines in exactly the cases where color segmentation falls apart: camouflage, stripes, and anything where the object and background share the same colors but not the same roughness.
This module is broken down into the following sections:
Things you should know
- Visualize an image's intensities as a 3D surface to build intuition for "texture"
- Explain why enhancing an image (e.g. with
imadjust) can amplify noise along with signal - Use the peak signal-to-noise ratio (
psnr) to compare a filtered image to its original - Use a line profile and a moving standard deviation to quantify texture along a single row of pixels
- Explain the difference between a range filter, a standard deviation filter, and an entropy filter
- Choose an appropriate neighborhood size and shape for a texture filter
- Build a complete texture-based segmentation pipeline: filter, binarize, clean, and overlay
Key Terminology you should know
- Texture: the local variation in pixel intensity within a neighborhood
- Neighborhood: the block of pixels around a given pixel that a filter examines
- Range filter: replaces each pixel with the range (max − min) of its neighborhood
- Standard deviation filter: replaces each pixel with the standard deviation of its neighborhood
- Entropy filter: replaces each pixel with the entropy (a measure of randomness) of its neighborhood
- Signal-to-noise ratio (SNR): a measure of how much a processed image has drifted from its original signal
Key Functions
- stdfilt: Local standard deviation of an image
- rangefilt: Local range of an image
- entropyfilt: Local entropy of an image
- psnr: Peak signal-to-noise ratio between two images
- movstd: Moving standard deviation
Visualizing Texture
We can think of texture as the surface of an image. Consider this single fluorescent spot:

Using meshgrid and surf, we can plot intensity as height, turning the image into a 3D landscape:
| Plot intensity as a 3D surface | |
|---|---|
What happens to that landscape when we enhance the image with imadjust?

After
imadjust, the bright spot has grown into a much taller peak—but so has the noise floor surrounding it, which was previously flat. Stretching the histogram increased the intensity (and therefore the "height") of every pixel, including the noise, and some of those noise pixels have now saturated at 255. Useimadjustwisely: it enhances signal and noise indiscriminately.
Comparing Spatial Filters
How do different smoothing filters affect this surface? We compare an average filter, a median filter, and a Gaussian filter, and use psnr (peak signal-to-noise ratio) to measure how far each one has drifted from the original:

The Gaussian filter edges out the other two (SNR 9.94 vs. 9.28 and 9.04), though all three noticeably round off the peak.
The Gaussian filter's sigma controls how aggressively it smooths. A larger sigma means more smoothing—and, predictably, a bigger departure from the original image:

SNR drops steadily as sigma grows (16.60 → 9.94 → 8.74 → 6.80 → 3.67). Smoothing is always a trade: more blur removes more noise, but it also erases more real signal. There's no free lunch—only a choice of how much of each you're willing to give up.
Quantifying Texture
Consider these two patches of bark:

Plotting each as a 3D surface doesn't tell us much on its own—both look like a tangle of peaks and valleys at this scale:

This is a case where plotting every pixel's intensity as height is too much information to interpret at a glance. We need a simpler way to compare the two.
Line Profiles
A line profile—the intensity values along a single row—simplifies the comparison a lot:
| Extract and plot a line profile | |
|---|---|

The rough bark's profile (magenta) swings much more dramatically—plunging into the dark gaps between bark plates and spiking back up—while the smooth bark's profile (cyan) stays in a tighter band.
Moving Standard Deviation
movstd calculates a standard deviation over a sliding window, turning the line profile itself into a texture signal:
| Moving standard deviation of each profile | |
|---|---|

The rough bark's moving standard deviation is nearly double the smooth bark's, on average—and it spikes far higher at the deep bark fissures. This gives us a single number that captures "how textured" a region is.
Types of Texture Filters
Instead of computing this one row at a time, we can run the same idea over an entire 2D neighborhood around every pixel. There are three common texture filters:
- Range filter (
rangefilt): replaces each pixel with the range of its neighborhood - Standard deviation filter (
stdfilt): replaces each pixel with the standard deviation of its neighborhood - Entropy filter (
entropyfilt): replaces each pixel with the entropy (a measure of randomness) of its neighborhood
Each one looks at a neighborhood around a pixel and reduces it to a single number describing the texture there.
Example: Flatfish
Consider this photo of a flatfish resting on sand:

This would be difficult to segment by color—the fish is camouflaged to match its surroundings. But the fish's speckled pattern looks a little coarser than the sand's. Is there a difference in texture? Let's try all three filters on the grayscale image and compare:
| Compare texture filters on the flatfish | |
|---|---|

All three filters silhouette the fish clearly against the sand—its scattered texture pattern is coarser than the sand's fine, uniform grain. The standard deviation and range filters produce almost identical results here; entropy is a bit lower-contrast but still shows the outline.
Binarizing and Cleaning the Mask
Since the fish reads as darker than the background in the std-filtered image, we complement it first so the fish becomes the foreground:
| Binarize the standard deviation filter | |
|---|---|

A little messy—the background sand still has enough fine-grained texture variation to generate a lot of salt-and-pepper noise.
Let's try smoothing the std-filtered image before binarizing, with two different filters:
| Smooth before binarizing: median vs. Gaussian | |
|---|---|

At first glance, the median-filtered mask (left) looks like the clear winner—the background is nearly spotless. But that's deceptive: a 3×3 median filter treats any small cluster of bright pixels as noise to be erased, and that includes the fine texture speckles that make up the fish's own pattern, not just the real background noise. The Gaussian-filtered mask (right) looks noisier at this stage, but a Gaussian blur doesn't selectively delete small bright regions—it smooths them together with their neighbors, which preserves the overall "this area is textured" signal.
The difference becomes obvious once we run both through identical cleanup steps:

The median path collapses to a few tiny fragments (barely 12,000 pixels total)—nearly the entire fish was erased along with the noise. The Gaussian path survives cleanup as a single, solid, accurate silhouette of the fish (over 600,000 pixels). The lesson: a filter that looks cleaner isn't necessarily preserving the right signal. Always check what survives the full pipeline, not just the next step.
Continuing with the Gaussian-filtered mask:
| Final cleanup | |
|---|---|
We can visualize the result either by burning a solid color into the mask region, or by drawing just its outline:

| Outline instead of a solid burn | |
|---|---|

Pretty, pretty good—the outline tracks the fish's body closely, including the thin tail fin.
Example: Zebras
An area with stripes has pixels with both very small and very large intensities nearby—a kind of texture that a range filter is well-suited to detect:

The size of the filter's neighborhood matters a great deal. The default is 3-by-3:
| Range filter with three neighborhood sizes | |
|---|---|

The default 3×3 neighborhood is too small: it fits entirely within a single stripe in places, so those pixels register no variation at all—notice the dark gaps inside the zebras. A 15×15 neighborhood is too large: entire zebras blow out into solid, clunky blobs that would oversegment the image. The 7×7 neighborhood is the sweet spot, cleanly tracing each animal's body.
Binarizing the 7×7 result gives a solid, if imperfect, zebra mask:
| Binarize the 7x7 range filter | |
|---|---|

The zebras are captured well, but notice the bright line of false positives running along the very bottom edge of the image, plus a few scattered specks in the grass.
imclearborderwould clean up the edge artifact;bwareaopenwould handle the stray specks.
Custom Neighborhood Shapes
A neighborhood doesn't have to be a square. You can build any shape with true and logical indexing:
| Rectangular and X-shaped neighborhoods | |
|---|---|

The wide rectangular neighborhood (matched to the zebras' horizontal stripe orientation) produces fuller, blockier bodies. The X-shaped neighborhood—which only samples the four corners and center of a 3×3 block—traces the individual stripes in finer detail instead of filling in solid blobs. Neighborhood shape, not just size, changes what a texture filter actually responds to.
Example: Snow Ferret
This is about as hard a camouflage case as it gets—white fur against white snow:

Applying all three filters with a larger, 19×19 neighborhood:
| Compare texture filters with a 19x19 neighborhood | |
|---|---|

This time, standard deviation and range only pick out the ferret's high-contrast facial features (eyes, nose, ear edges)—the body itself is too uniformly white to register on either filter. The entropy filter, however, captures the ferret's entire body, not just its face: even subtle texture differences between fur and snow register as a difference in local randomness.
Following the entropy filter with a median filter, binarizing, and cleaning up:
| Segment the ferret from its entropy map | |
|---|---|

A clean, accurate silhouette of the ferret, recovered entirely from texture—no color information was involved at any step.
A Course Function to Review Textures
Since generating and comparing all three texture filters is such a common first step, the course function mmGetTextureFilters automates it:
| Generate all three texture filters at once | |
|---|---|

With the smaller 7×7 neighborhood (instead of the 19×19 used above), the entropy map takes on a blockier, more quilted appearance—the same "entropy reveals the ferret's whole body" pattern is still there, just coarser.
Challenge
Would the entropy filter work better than the range filter on the zebras?
The range filter did a nice job finding the zebras earlier. Entropy found the whole snow ferret when range and standard deviation could only find its face. Would entropy do an even better job on the zebra image? Try an entropy filter on the zebras with a 7×7 neighborhood, binarize it, and compare to the range filter result.
| Try the entropy filter on the zebras | |
|---|---|

Entropy actually does worse here—the zebras barely stand out from the grass in the entropy map, and binarizing produces a mess with no clean separation. The grass is full of fine, irregular blades pointing every which way: visually busy, but genuinely random, which is exactly what high entropy measures. The zebra stripes, by contrast, are highly ordered—regularly alternating black and white—which drives up the range at each pixel without necessarily driving up randomness. There's no universally "best" texture filter: range looks for big swings between light and dark, standard deviation looks for overall spread, and entropy looks for disorder. Picking the right one means understanding what kind of texture distinguishes your object from its background in the first place.
Congratulations, you have made it to the end! 🦓