The image's 2D autocorrelation is a good generic way to find repeating structures, as others have commented. There are some details to doing this effectively:
- For this analysis, it is often fine to just convert the image to grayscale; that's what the code snippet below does. To extend this to a color-aware analysis, you could compute the autocorrelation for each color channel and aggregate the results.
- It helps to apply a window function first, to avoid boundary artifacts.
- For efficient computation, it helps to compute the autocorrelation through FFTs.
- Rather than use the whole spectrum in computing the autocorrelation, it helps to zero out very low frequencies, since these are irrelevant for texture.
- Rather than the plain autocorrelation, it helps to partially "equalize" or "whiten" the spectrum, as suggested by the Generalized Cross Correlation with Phase Transform (GCC-PHAT) technique.
Python code:
# Copyright 2022 Google LLC.
# SPDX-License-Identifier: Apache-2.0
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal
image = np.array(Image.open('texture.jpg').convert('L'), float)
# Window the image.
window_x = np.hanning(image.shape[1])
window_y = np.hanning(image.shape[0])
image *= np.outer(window_y, window_x)
# Transform to frequency domain.
spectrum = np.fft.rfft2(image)
# Partially whiten the spectrum. This tends to make the autocorrelation sharper,
# but it also amplifies noise. The -0.6 exponent is the strength of the
# whitening normalization, where -1.0 would be full normalization and 0.0 would
# be the usual unnormalized autocorrelation.
spectrum *= (1e-12 + np.abs(spectrum))**-0.6
# Exclude some very low frequencies, since these are irrelevant to the texture.
fx = np.arange(spectrum.shape[1])
fy = np.fft.fftshift(np.arange(spectrum.shape[0]) - spectrum.shape[0] // 2)
fx, fy = np.meshgrid(fx, fy)
spectrum[np.sqrt(fx**2 + fy**2) < 10] = 0
# Compute the autocorrelation and inverse transform.
acorr = np.real(np.fft.irfft2(np.abs(spectrum)**2))
plt.figure(figsize=(10, 10))
plt.imshow(acorr, cmap='Blues', vmin=0, vmax=np.percentile(acorr, 99.5))
plt.xlim(0, image.shape[1] / 2)
plt.ylim(0, image.shape[0] / 2)
plt.title('2D autocorrelation', fontsize=18)
plt.xlabel('Horizontal lag (px)', fontsize=15)
plt.ylabel('Vertical lag (px)', fontsize=15)
plt.show()
Output:

The period of the flannel texture is visible at the circled point at 282 px horizontally and 290 px vertically.