Skip to content

Differential Box-Counting and the FracLac Protocol

Jupyter notebook

This page was rendered from the stored outputs of 4_dbc_fraclac.ipynb as of 2026-10-24; it is not executed at build time.

View on GitHub · Open in nbviewer · Open in Colab · Raw .ipynb

Kernel: Python 3 (ipykernel) · Environment: Python 3 with numpy and matplotlib; imports docs/notebooks/metabolic-scaling/review/fractal_review_utils.py · Companion page: metabolic-scaling/methods/

Part II: Metabolic Scaling Theory and Biological Fractals


Overview

This notebook supports the Methods sections "Differential Box-Counting Methodology" and "FracLac Analysis Procedures". It (1) implements differential box-counting (DBC) from scratch for a grayscale image (Sarkar–Chaudhuri), (2) validates the estimator against fractional Brownian motion (fBm) surfaces of known Hurst exponent \(H\), whose graph has box dimension \(D = 3 - H\), over \(H = 0.3, 0.5, 0.7\), (3) documents the FracLac protocol used in the paper (regular box-count + differential box-count, box-size power series with factor \(0.1\)), and (4) plots one fBm surface with its log–log DBC regression.

The from-scratch DBC here is cross-checked against fractal_review_utils.differential_box_count (the project's review apparatus). Computed results are tagged [COMPUTED]; theorem-based statements [ASSERTED].

1. Differential box-counting from scratch (Sarkar–Chaudhuri) [COMPUTED]

Classic box-counting works on binary sets. DBC extends it to a grayscale image \(g(x,y)\) by treating intensity as a third (height) dimension. Partition the image into \(\varepsilon\times\varepsilon\) boxes in the plane. Over each box, the intensity range is covered by a column of boxes of height \(h\); the number of intensity-boxes the surface spans in that column is

\[ n_r = \left\lceil \frac{g_{\max}}{h}\right\rceil - \left\lceil \frac{g_{\min}}{h}\right\rceil + 1, \]

with the Sarkar–Chaudhuri convention that the box height scales with the planar box size, \(h = G\,\varepsilon / L\) (where \(G\) is the number of gray levels and \(L\) the image side). Summing \(n_r\) over all boxes gives \(N(\varepsilon)\), and the dimension is the slope of \(\log N(\varepsilon)\) vs. \(\log(1/\varepsilon)\). For a grayscale surface this dimension lives in \([2,3]\).

import numpy as np
import matplotlib.pyplot as plt

def differential_box_count(gray, box_sizes=None, n_levels=256):
    """Sarkar-Chaudhuri DBC on a grayscale image. Returns dict with dimension, r2,
    box sizes (eps), and counts N(eps). Dimension range [2,3]."""
    g = np.asarray(gray, dtype=float)
    g -= g.min()
    if g.max() > 0:
        g = g / g.max() * (n_levels - 1)   # rescale intensity to [0, G]
    G = n_levels - 1
    L = min(g.shape)
    if box_sizes is None:
        box_sizes = [s for s in (2,3,4,6,8,12,16,24,32,48,64) if s <= L//2]
    box_sizes = np.array(sorted(set(box_sizes)), dtype=int)
    counts = []
    for s in box_sizes:
        h = max(G * s / L, 1e-9)                    # gray-box height
        ny, nx = (g.shape[0]//s)*s, (g.shape[1]//s)*s
        blocks = g[:ny,:nx].reshape(ny//s, s, nx//s, s)
        gmin = blocks.min(axis=(1,3))
        gmax = blocks.max(axis=(1,3))
        nr = np.ceil(gmax/h) - np.ceil(gmin/h) + 1.0
        counts.append(max(nr.sum(), 1.0))
    eps = box_sizes.astype(float)
    counts = np.array(counts, float)
    # slope of log N vs log(1/eps)
    x, y = np.log(1.0/eps), np.log(counts)
    A = np.vstack([x, np.ones_like(x)]).T
    coef, *_ = np.linalg.lstsq(A, y, rcond=None)
    slope = coef[0]
    yhat = A @ coef
    r2 = 1 - np.sum((y-yhat)**2)/np.sum((y-y.mean())**2)
    return {"dimension": float(slope), "r2": float(r2), "eps": eps, "counts": counts}

# sanity: a flat image has no intensity variation -> dimension ~ 2 (planar)
flat = np.full((256,256), 100.0)
print("flat image DBC dimension (expect ~2.0):", round(differential_box_count(flat)["dimension"], 4))
flat image DBC dimension (expect ~2.0): 2.0186

2. fBm surfaces of known dimension \(D = 3 - H\) [ASSERTED theorem + COMPUTED validation]

A 2-D fractional Brownian motion surface (graph embedded in 3-D) has box / Hausdorff dimension

\[ D = 3 - H \qquad (0 < H < 1 \Rightarrow 2 < D < 3), \]

an established result (Mandelbrot; Falconer). Smaller \(H\) = rougher surface = larger \(D\). This gives ground-truth targets to validate the DBC estimator. We generate fBm by spectral synthesis: a random field whose radial power spectrum is \(P(k)\propto k^{-(2H+2)}\), inverse-FFT'd to real space. We use the project's fractal_review_utils.fbm_surface, which implements exactly this and documents \(D=3-H\).

import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), "..", "review")))  # review/ helpers, relative to this notebook
import fractal_review_utils as fru

SIZE = 512
H_values = [0.3, 0.5, 0.7]
surfaces = {H: fru.fbm_surface(size=SIZE, H=H, seed=7) for H in H_values}

print(f"Generated {len(surfaces)} fBm surfaces ({SIZE}x{SIZE}) via spectral synthesis.")
for H, s in surfaces.items():
    print(f"  H={H}: intensity range [{s.min():.3f}, {s.max():.3f}], "
          f"std={s.std():.3f}, analytic D = 3 - H = {fru.fbm_surface_dimension(H):.2f}")
Generated 3 fBm surfaces (512x512) via spectral synthesis.
  H=0.3: intensity range [0.000, 1.000], std=0.123, analytic D = 3 - H = 2.70
  H=0.5: intensity range [0.000, 1.000], std=0.141, analytic D = 3 - H = 2.50
  H=0.7: intensity range [0.000, 1.000], std=0.160, analytic D = 3 - H = 2.30
# --- Validate: run DBC on each surface, compare estimated D to true D = 3 - H ---
box_sizes = [2,3,4,6,8,12,16,24,32,48,64]   # geometric-ish ladder

print(f"{'H':>4} | {'true D=3-H':>10} | {'DBC (scratch)':>13} | "
      f"{'DBC (utils)':>11} | {'r^2':>6} | {'err(scratch)':>12}")
print("-"*70)
rows = []
for H in H_values:
    surf = surfaces[H]
    r_scratch = differential_box_count(surf, box_sizes=box_sizes)
    r_utils   = fru.differential_box_count(surf, sizes=np.array(box_sizes))
    true_D = 3 - H
    err = r_scratch["dimension"] - true_D
    rows.append((H, true_D, r_scratch["dimension"], r_utils["dimension"], r_scratch["r2"], err))
    print(f"{H:>4} | {true_D:>10.3f} | {r_scratch['dimension']:>13.3f} | "
          f"{r_utils['dimension']:>11.3f} | {r_scratch['r2']:>6.3f} | {err:>+12.3f}")

print()
print("[COMPUTED] DBC recovers the ordering D(H=0.3) > D(H=0.5) > D(H=0.7)")
print("           i.e. rougher surfaces (smaller H) yield larger dimension,")
print("           tracking the analytic D = 3 - H. Absolute values carry a")
print("           finite-size / discretization bias (see caveat).")
   H | true D=3-H | DBC (scratch) | DBC (utils) |    r^2 | err(scratch)
----------------------------------------------------------------------
 0.3 |      2.700 |         2.330 |       2.330 |  0.998 |       -0.370
 0.5 |      2.500 |         2.218 |       2.219 |  0.998 |       -0.282
 0.7 |      2.300 |         2.106 |       2.107 |  0.998 |       -0.194

[COMPUTED] DBC recovers the ordering D(H=0.3) > D(H=0.5) > D(H=0.7)
           i.e. rougher surfaces (smaller H) yield larger dimension,
           tracking the analytic D = 3 - H. Absolute values carry a
           finite-size / discretization bias (see caveat).

Note on finite-size bias [COMPUTED observation]

DBC estimates on finite, pixelated fBm surfaces are known to be biased: the Sarkar–Chaudhuri box-height convention, the finite gray-level quantization, and the limited range of box sizes all compress the estimated dimension toward the middle of \([2,3]\). What the estimator reliably recovers is the monotone trend with \(H\) (rougher \(\to\) larger \(D\)) and dimensions in the correct \([2,3]\) band; the absolute value is an approximation, not an exact match to \(3-H\). This is exactly why the manuscript's use of DBC needs the ground-truth controls demonstrated here.

# Quantify the trend: correlation between H and estimated dimension (should be strongly negative)
Hs   = np.array([r[0] for r in rows])
Dest = np.array([r[2] for r in rows])
Dtru = np.array([r[1] for r in rows])
# monotone decreasing in H?
mono = np.all(np.diff(Dest) < 0)
corr = np.corrcoef(Hs, Dest)[0,1]
print(f"estimated D vs H: values = {np.round(Dest,3)} for H = {list(Hs)}")
print(f"  strictly decreasing in H? {mono}")
print(f"  Pearson corr(H, D_est)   = {corr:+.3f}  (expect strongly negative)")
print(f"  mean |D_est - (3-H)|     = {np.mean(np.abs(Dest-Dtru)):.3f}  (finite-size bias)")
assert mono, "DBC should give a monotone-decreasing D with H"
print("\n[COMPUTED] Estimator is monotone in H and sits in [2,3]; validated.")
estimated D vs H: values = [2.33  2.218 2.106] for H = [np.float64(0.3), np.float64(0.5), np.float64(0.7)]
  strictly decreasing in H? True
  Pearson corr(H, D_est)   = -1.000  (expect strongly negative)
  mean |D_est - (3-H)|     = 0.282  (finite-size bias)

[COMPUTED] Estimator is monotone in H and sits in [2,3]; validated.

3. The FracLac protocol used in the paper [documentation of cited method]

The manuscript states: "Images were analyzed using the open-source image analysis software FracLac for ImageJ. The fractal dimension was calculated using both regular box-counting and the differential box-count technique. For differential box-counting, a power series with an exponentially increasing box size factor of 0.1 was used."

What that means, made concrete:

  • Two estimators. Regular (binary) box-counting gives a dimension in \([1,2]\) for a plane-embedded silhouette; differential box-counting gives a dimension in \([2,3]\) for the grayscale intensity surface. They measure different objects (see the volume/mass-dimension notebook — this is why the \(3/2\) vs. \(3\) distinction matters).
  • "Power series with an exponentially increasing box size factor of 0.1." FracLac builds its ladder of box sizes as a geometric (power) series: successive box sizes grow by a fixed multiplicative factor. A factor expressed as \(0.1\) means each box size is \(1.1\times\) the previous (a \(10\%\), i.e. \(0.1\)-fraction, increase per step), producing box sizes \(s_i = s_0\,(1.1)^i\) rather than the doubling (\(\times 2\)) ladder used above. A denser (finer-factor) ladder gives more \((\,\varepsilon, N)\) points for the regression at the cost of correlated samples.

The cell below builds the FracLac-style \(1.1\times\) ladder and re-runs DBC with it, showing the protocol produces more regression points and a comparable estimate.

def fraclac_box_sizes(L, factor=0.1, s0=2):
    """FracLac-style geometric ladder: each size = previous * (1+factor)."""
    sizes, s = [], float(s0)
    while s <= L//2:
        sizes.append(int(round(s)))
        s *= (1.0 + factor)
    return np.array(sorted(set(sizes)), dtype=int)

L = SIZE
fl_sizes = fraclac_box_sizes(L, factor=0.1, s0=2)
print(f"FracLac 1.1x ladder ({len(fl_sizes)} box sizes): {[int(x) for x in fl_sizes]}")
print(f"(vs the {len(box_sizes)}-point doubling ladder used earlier: {box_sizes})")
print()

# re-run DBC with the FracLac ladder on each surface
print(f"{'H':>4} | {'true D=3-H':>10} | {'DBC 2x ladder':>13} | {'DBC FracLac 1.1x':>16}")
print("-"*54)
for H in H_values:
    d2  = differential_box_count(surfaces[H], box_sizes=box_sizes)["dimension"]
    dfl = differential_box_count(surfaces[H], box_sizes=list(fl_sizes))["dimension"]
    print(f"{H:>4} | {3-H:>10.3f} | {d2:>13.3f} | {dfl:>16.3f}")
print()
print("[COMPUTED] The denser FracLac ladder gives a comparable estimate with")
print("           many more (eps, N) regression points, as the paper's protocol intends.")
FracLac 1.1x ladder (42 box sizes): [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 18, 20, 22, 24, 26, 29, 32, 35, 38, 42, 46, 51, 56, 62, 68, 75, 82, 91, 100, 110, 120, 133, 146, 160, 176, 194, 213, 235]
(vs the 11-point doubling ladder used earlier: [2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64])

   H | true D=3-H | DBC 2x ladder | DBC FracLac 1.1x
------------------------------------------------------
 0.3 |      2.700 |         2.330 |            2.488
 0.5 |      2.500 |         2.218 |            2.368
 0.7 |      2.300 |         2.106 |            2.248

[COMPUTED] The denser FracLac ladder gives a comparable estimate with
           many more (eps, N) regression points, as the paper's protocol intends.

4. One fBm surface and its log–log DBC regression [COMPUTED figure]

H_show = 0.5
surf = surfaces[H_show]
r = differential_box_count(surf, box_sizes=list(fl_sizes))
eps, counts = r["eps"], r["counts"]

fig, (axL, axR) = plt.subplots(1, 2, figsize=(12, 5))

# left: the surface
im = axL.imshow(surf, cmap="terrain")
axL.set_title(f"fBm surface, H={H_show}  (analytic D = 3−H = {3-H_show:.2f})")
axL.set_xlabel("x (px)"); axL.set_ylabel("y (px)")
fig.colorbar(im, ax=axL, fraction=0.046, pad=0.04, label="normalized intensity")

# right: log-log DBC regression
x, y = np.log(1.0/eps), np.log(counts)
coef = np.polyfit(x, y, 1)
axR.scatter(x, y, s=28, color="#4C72B0", zorder=3, label="N(ε) (FracLac 1.1× ladder)")
xx = np.array([x.min(), x.max()])
axR.plot(xx, coef[0]*xx + coef[1], "-", color="#C44E52", lw=2,
         label=f"slope = {coef[0]:.3f}  (est. D)\n$r^2$ = {r['r2']:.3f}")
axR.set_xlabel(r"$\log(1/\varepsilon)$")
axR.set_ylabel(r"$\log N(\varepsilon)$")
axR.set_title("Differential box-count regression")
axR.legend(frameon=False, fontsize=10)
axR.grid(True, alpha=0.2)
fig.tight_layout()

print(f"fBm H={H_show}: estimated D = {r['dimension']:.3f}, "
      f"analytic D = {3-H_show:.3f}, r^2 = {r['r2']:.3f}")
print(f"regression used {len(eps)} box sizes from the FracLac 1.1x ladder.")
fBm H=0.5: estimated D = 2.368, analytic D = 2.500, r^2 = 0.997
regression used 42 box sizes from the FracLac 1.1x ladder.

Figure 1: output of code cell 11

Summary

Verified / computed [COMPUTED]:

  • A from-scratch Sarkar–Chaudhuri DBC estimator; returns \(\approx 2.0\) on a flat (planar) image as expected.
  • fBm surfaces generated by spectral synthesis for \(H=0.3, 0.5, 0.7\); DBC recovers a dimension that is strictly monotone-decreasing in \(H\), sits in \([2,3]\), and tracks the analytic \(D=3-H\) (strong negative correlation). The from-scratch estimator agrees with the project's fractal_review_utils.differential_box_count.
  • The FracLac protocol reproduced concretely: regular + differential box-count, with a geometric box-size ladder using a \(0.1\) (i.e. \(\times 1.1\)) growth factor; the denser ladder yields many more regression points and a comparable estimate.
  • One fBm surface plotted alongside its log–log DBC regression.

Asserted from cited theory [ASSERTED]:

  • The fBm surface dimension identity \(D=3-H\) (Mandelbrot; Falconer), used as ground truth, is asserted, not derived here.

Caveats:

  • DBC on finite, quantized images carries a finite-size / discretization bias: absolute dimension estimates compress toward the middle of \([2,3]\) and do not exactly equal \(3-H\). The estimator reliably recovers the trend and the correct dimension band, which is what makes it usable — and why ground-truth controls (as done here) are necessary before trusting DBC on real specimen images.
  • All surfaces here are synthetic fBm with known dimension, generated explicitly for validation; no empirical specimen data are presented.