{
  "name": "ideal_observer_ct_resolution",
  "version": "1.1.0",
  "description": "Minimum detectable disc diameter and spatial-frequency limit for CT imaging under the projection-domain SKE/BKE ideal observer (Rose criterion). Reconstruction-independent upper bound on detectability. Provides both the at-isocentre closed form and a fan-beam off-axis extension that maps d_min(P) across the phantom.",
  "authors": ["Falk L. Wiegmann", "Nancy L. Ford"],
  "affiliation": "University of British Columbia",
  "year": 2026,
  "primary_reference": "Barrett HH, Myers KJ. Foundations of Image Science. Wiley, 2004.",
  "inputs": {
    "N0": {
      "description": "Photons per pixel in the unattenuated beam (noise-equivalent quanta).",
      "units": "photons/pixel",
      "type": "number",
      "minimum": 1,
      "example": 4203
    },
    "mu_bg": {
      "description": "Linear attenuation coefficient of the background (e.g. water at the scan kVp).",
      "units": "mm^-1",
      "type": "number",
      "minimum": 0,
      "example": 0.0219
    },
    "L": {
      "description": "Background path length through phantom diameter (mm). For a uniform circular phantom, L = 2R.",
      "units": "mm",
      "type": "number",
      "minimum": 0,
      "example": 80
    },
    "N_theta": {
      "description": "Total number of projection angles in the scan.",
      "units": "count",
      "type": "integer",
      "minimum": 1,
      "example": 220
    },
    "delta_a_obj": {
      "description": "Detector pixel pitch projected to the object plane at the isocentre.",
      "units": "mm",
      "type": "number",
      "minimum": 0,
      "example": 0.0249
    },
    "delta_mu": {
      "description": "Linear-attenuation contrast of the feature vs background. For HU contrast: delta_mu = (delta_HU / 1000) * mu_water.",
      "units": "mm^-1",
      "type": "number",
      "example": 0.01095
    },
    "d_prime_threshold": {
      "description": "Detectability threshold for the Rose criterion. Classical value is 3; Hsieh et al. 2022 argue the minimum viable is 5.",
      "units": "dimensionless",
      "type": "number",
      "minimum": 1,
      "default": 3,
      "example": 3
    }
  },
  "extra_inputs_off_axis": {
    "R": {
      "description": "Phantom radius (mm). Typically L/2.",
      "units": "mm",
      "type": "number",
      "minimum": 0,
      "example": 40
    },
    "D": {
      "description": "Source-to-isocentre distance (SOD, mm). Must be greater than R.",
      "units": "mm",
      "type": "number",
      "example": 396.40
    },
    "arc_start_deg": {
      "description": "Start of projection arc in degrees (lab frame).",
      "units": "degrees",
      "type": "number",
      "example": -96
    },
    "arc_end_deg": {
      "description": "End of projection arc in degrees (lab frame). For a full scan, arc_end - arc_start >= 360.",
      "units": "degrees",
      "type": "number",
      "example": 96
    }
  },
  "outputs": {
    "N_bg": {
      "description": "Mean photons per pixel after passing through the background (at isocentre).",
      "units": "photons/pixel",
      "formula_python": "N0 * math.exp(-mu_bg * L)",
      "formula_latex": "N_{bg} = N_0\\,e^{-\\mu_{bg} L}"
    },
    "d_min": {
      "description": "Minimum detectable disc diameter at the given contrast, at the isocentre.",
      "units": "mm",
      "formula_python": "(3 * d_prime_threshold**2 * delta_a_obj / (2 * N_theta * N_bg * delta_mu**2)) ** (1/3)",
      "formula_latex": "d_{\\min} = \\left(\\dfrac{3\\,(d'_{th})^2\\,\\Delta a_{obj}}{2\\,N_\\theta\\,N_{bg}\\,\\Delta\\mu^2}\\right)^{1/3}"
    },
    "resolution_lp_per_mm": {
      "description": "Spatial-frequency limit corresponding to d_min.",
      "units": "lp/mm",
      "formula_python": "1.0 / (2 * d_min)",
      "formula_latex": "f = \\dfrac{1}{2\\,d_{\\min}}"
    }
  },
  "outputs_off_axis": {
    "chord_fan": {
      "description": "Chord length through phantom for ray from point source S(theta) through voxel P=(x,y). Source S(theta) = (D*cos theta, D*sin theta).",
      "units": "mm",
      "formula_python": "sx, sy = D*math.cos(theta), D*math.sin(theta)\ndx, dy = x - sx, y - sy\nlen2 = dx*dx + dy*dy\nSdot = sx*dx + sy*dy\ndisc = (Sdot*Sdot)/len2 - (D*D - R*R)\nchord = 2 * math.sqrt(disc) if disc > 0 else 0",
      "formula_latex": "\\text{chord}(\\theta, P) = 2\\sqrt{(S\\cdot\\hat d)^2 - (D^2 - R^2)},\\quad \\hat d = (P-S)/|P-S|"
    },
    "delta_a_obj_local": {
      "description": "Local object-plane pixel pitch at voxel P for projection angle theta.",
      "units": "mm",
      "formula_python": "delta_a_obj * math.hypot(x - D*math.cos(theta), y - D*math.sin(theta)) / D",
      "formula_latex": "\\Delta a_{obj}(P, \\theta) = \\Delta a_{obj}\\cdot |P-S(\\theta)|/D"
    },
    "d_min_at_P": {
      "description": "Minimum detectable disc diameter at voxel P=(x,y), summing over all N_theta projections in the scan arc.",
      "units": "mm",
      "formula_python": "# theta_j = N_theta values evenly spaced in [arc_start_rad, arc_end_rad]\nS = sum(\n    N0 * math.exp(-mu_bg * chord_fan(theta_j, x, y, R, D))\n    / math.hypot(x - D*math.cos(theta_j), y - D*math.sin(theta_j))\n    for theta_j in projection_angles\n)\nd_min_at_P = (3 * d_prime_threshold**2 * delta_a_obj / (2 * delta_mu**2 * D * S)) ** (1/3)",
      "formula_latex": "d_{\\min}(P) = \\left(\\dfrac{3\\,(d'_{th})^2\\,\\Delta a_{obj}}{2\\,\\Delta\\mu^2\\,D\\,\\sum_\\theta \\dfrac{N_0\\,e^{-\\mu_{bg}\\,\\text{chord}(\\theta,P)}}{|P-S(\\theta)|}}\\right)^{1/3}",
      "reduces_to": "At P = (0,0) and D -> infinity, this collapses to the at-isocentre d_min above."
    }
  },
  "example": {
    "context": "GE eXplore CT 120 micro-CT at 80 kVp, 500 HU object contrast, at isocentre.",
    "inputs": {
      "N0": 4203,
      "mu_bg": 0.0219,
      "L": 80,
      "N_theta": 220,
      "delta_a_obj": 0.0249,
      "delta_mu": 0.01095,
      "d_prime_threshold": 3
    },
    "expected_outputs": {
      "N_bg": 729.0,
      "d_min_mm": 0.260,
      "resolution_lp_per_mm": 1.93
    }
  },
  "n0_measurement": {
    "description": "Procedure to measure N0 from raw flat-field projections.",
    "inputs_required": ["dark frame", "bright frame", "two adjacent bright projections"],
    "steps": [
      "Flat-field correct each projection: T = (I - dark) / (bright - dark)",
      "Select air pixels: those with T in [0.8, 1.2] in both adjacent frames",
      "Compute differenced variance: Var(T) = Var[(T1 - T2) / sqrt(2)]",
      "Invert for noise-equivalent photon count: N0 = 1 / Var(T)"
    ],
    "notes": "The sqrt(2) factor removes correlated fixed-pattern noise between the two frames. The resulting N0 is the noise-equivalent quanta per pixel and is the correct input to the formulas, independent of detector gain."
  },
  "assumptions": [
    "Poisson photon statistics",
    "Gaussian approximation (valid when N_bg >> 1)",
    "Signal location known exactly (SKE)",
    "Background known exactly (BKE)",
    "Closed-form at-isocentre uses parallel-beam approximation",
    "Off-axis extension uses 2D fan-beam point source at distance D; cone-beam (off-central-slice) effects are not modelled"
  ],
  "limitations": "SKE and BKE make this an upper bound. Real reconstructions in structured anatomy will be worse, typically 3–7x in d_min."
}
