9.1 Developing, testing and debugging⧉
PS0 sets up the toolchain, a minimal image class, and image I/O. → Chapter 1.5, Problem sets.
A blur can return a valid image with a dark border. A resampler can produce a sharp picture full of aliasing. Neither needs to crash. Getting image code to work requires both checking its numbers and looking at what those numbers produce. Start with one stage and an input small enough to check by hand.
9.1.1 The workflow: build, run, look at the picture⧉
Save intermediate images and open them. This is the image-processing equivalent of a print statement. When the final image has black specks, inspect the inputs and outputs of the suspected stage, then check the values at those pixels. The first place a defect becomes visible helps localize the problem; an earlier numerical error may still be its cause.
A PNG is convenient for display, but a display copy is not the underlying array. Negative values, values above one, and small floating-point differences can disappear when you clip and quantize for export. Keep the original buffer or save a format that preserves its range and precision. Record its shape, type, color encoding, minimum, maximum, and counts of NaN and infinity. Chapter 8.1, Image representation explains why a buffer's interpretation matters as much as its values.
Use the same display transform and scale for comparisons. Independently normalizing two images can hide an incorrect gain. For signed differences, map zero to a neutral gray and positive and negative errors to opposite colors; state the amplification. Mark nonfinite values separately. A black difference image may mean agreement, or merely differences too small to survive the display conversion.
Write a helper that displays an array and reports its range. Make the scale explicit and easy to reuse. If inspecting an intermediate takes one line, you will do it while developing instead of only after the final output fails.
9.1.2 Principles⧉
Test incrementally. Implement one stage, check its output, and retain that check before adding the next. When a long pipeline fails, inspect an intermediate near the middle to narrow the search. This is a useful shortcut when the error remains visible downstream; clipping, normalization, and later filters can conceal it, so a good-looking intermediate does not clear everything upstream.
Change one thing between runs. Keep the input, parameters, random seed, and build fixed while testing a proposed correction. Otherwise a better picture does not tell you which change helped. Once the correction works, repeat the test on other inputs and, for stochastic methods, other seeds.
Check the assumptions between stages: channel order, dimensions, coordinate origin, units, transfer function, and boundary policy. A function may compute exactly what it was asked to compute on data in the wrong domain. Inspect actual values as well as reading the code.
Temporarily zero a contribution or change a parameter to an extreme whose effect you can predict. A matching change confirms that this path affects this input as expected. No visible change warrants investigation: perhaps the result is unused, the input does not exercise the path, or the display hides the difference. Neither outcome proves the whole function correct.
For a ten-stage pipeline, inspect the middle output first. If a visible defect is already present there, inspect an earlier intermediate; if it first appears later, move downstream. Repeating this can narrow the suspect region in a few checks. Then test the suspected stage on an input with a known answer. The shortcut assumes the defect remains observable: a later clamp can hide an earlier overflow, and an erroneous normalization can make a bad intermediate look plausible. Keep both the numerical array and a documented display mapping when using intermediate images to localize the fault.
Check the part you are most inclined to exempt from checking: an old helper, a familiar conversion, or the expression that looks too simple to be wrong. Familiarity is not evidence about the values in this run. When inspection of the source stops producing new information, print the actual indices and samples or display the intermediate. Your prediction of what the code does may be the mistaken assumption you are trying to find.
9.1.3 Test on inputs you can verify by hand⧉
A tiny input lets you compare every output value against a calculation independent of the implementation. Use several sizes, including a non-square image, odd dimensions, and a single row or column. These expose transposed axes, vector tails, and boundary assumptions that a convenient square image misses.
- A constant tests the expected gain. A normalized blur with constant-preserving boundaries should return that constant; a derivative should return zero. Zero padding deliberately darkens the border of a nonzero constant, so decide whether that is the specified behavior before calling it a bug. For color code, use different constants in the three channels to expose channel swaps.
- An impulse tests how a local input spreads. For a linear, shift-invariant filter away from boundaries, the output gives the convolution kernel. For a general linear operator it gives one column of the operator, which may differ at another location. Test center and boundary impulses separately.
- A step edge exposes ringing, halos, shifts, and color fringes. Try both orientations and a diagonal.
- A rectangle adds corners and unequal extents. It is useful for checking coordinate conventions and differences between horizontal and vertical processing.
Apply these tests to individual stages. Compare integer results exactly when the specification requires it. For floating-point results, choose tolerances in the working units, accounting for rounding and changes in reduction order. One common comparison is
The absolute tolerance handles values near zero; the relative tolerance allows error proportional to magnitude. Check nonfinite values before this comparison. A large tolerance can conceal an algorithmic error, so report the maximum difference and its location as well as pass/fail.
A constant removes spatial variation. If a normalized blur changes its interior value, check the sum of weights and the arithmetic type. If only the border changes, check the extension rule. These are distinct failures with distinct fixes.
Apply the same discipline inside the pipeline: feed these inputs to your intermediate stages, not only to the final program. A failed whole-pipeline impulse test tells you that something is broken; running each stage on an impulse tells you which stage. The synthetic inputs and the binary-search habit are the same idea from two directions: small inputs make each stage checkable, and checking each stage localizes the bug.
9.1.4 Finding good real test inputs⧉
Synthetic tests cover particular properties, not all possible inputs. Follow them with a fixed set of photographs spanning the conditions the method will encounter: fine foliage and smooth skies, clipped highlights and noisy shadows, saturated colors and neutrals, faces under varied illumination, and a range of skin tones and cameras.
Keep a few standard test images so results can be compared with the literature, such as the Kodak set, a zone plate or resolution chart, and a color checker, beside your own photographs. Keep every useful failure as a regression case, with the parameters and expected behavior recorded. Also reserve photographs that you do not use for tuning. A regression suite checks that an old problem stays fixed; an untouched evaluation set checks whether improvements extend beyond the images you have repeatedly adjusted against. Use the noise models in Chapter 2.8, Noise, signal-to-noise ratio and dynamic range when constructing synthetic corruptions, and include real captures because a simulated noise model leaves out some camera behavior.
Organize that set around reasons for failure. Texture and smooth fields pull denoising in opposite directions; a noisy shadow and a clipped highlight exercise different ends of the measurement range. A saturated patch stresses gamut handling while a neutral patch exposes a small cast. Standard photographs and charts help comparisons with published results, but a known failure from your own pipeline deserves a permanent place beside them.
9.1.5 Per-algorithm debug recipes⧉
The algorithm chapters give more specific checks:
- Chapter 6.2, Convolution: an interior impulse, a constant, and an asymmetric kernel to distinguish convolution from correlation.
- Part 15, Demosaicking and joint methods: constant color patches with unequal channel values, at each mosaic phase.
- Chapter 10.1, Bilateral filtering: a constant and the limiting behavior as the range tolerance changes.
- Chapter 13.2, Denoising by averaging: a known image translation before combining frames, followed by tests with occlusion and motion.
State the expected answer before running the code. Use an independent calculation or a simple reference implementation; copying the same indexing expression into the test can reproduce the same mistake.
9.1.6 Crashes and bounds⧉
Out-of-bounds accesses are common in neighborhood operations: a neighbor at the left edge has a negative column, or the last row asks for a sample at height. In C++, an invalid read or write is undefined behavior. A write may corrupt adjacent memory and trigger a crash much later. Invalid reads, dangling pointers, and data races can also fail without a useful message.
Check indices at the point of access, use a debugger, and enable the compiler's memory-error diagnostics during development. Assert properties that the algorithm actually requires, such as valid extents or a nonzero normalization denominator. Do not assert that every intermediate lies in $[0,1]$: many linear filters and color transforms legitimately produce values outside that range.
Debug assertions help catch internal mistakes, but they do not replace input validation in a release build. Reject unsupported dimensions and formats at the interface. For intended accesses outside an image, apply the specified extension rule (repeat, mirror, wrap, or zero) before reading memory. An assertion catches an unhandled access; it does not choose the correct boundary condition.
Start with the first relevant diagnostic and reduce the code to a small example. Later errors may be consequences of the first. Documentation, a search, or an LLM can help interpret a message, but check that the proposed fix preserves the operation and types you intended.
Put a check where the assumption is first used. For example, a resampler can produce an invalid source coordinate long before an eventual crash in an unrelated allocation. Checking the coordinate immediately before the load identifies the operation that broke the contract. Similarly, after computing a normalized filter, check that its weights sum to the expected value; after a transform, check finiteness before sending the result to the next stage. The first failed invariant is usually more informative than the final wrong image.
Keep internal development assertions separate from input validation. A release build may disable assertions, but it still needs to reject unsupported external sizes, formats, and indices safely. Removing a debug check must not make malformed input legal.
9.1.7 Vibe coding: writing image code with an LLM⧉
An LLM can draft image I/O, interfaces, and algorithm implementations. Review all of them. A successful file read does not establish the channel order or color encoding, and a plausible resampler may still omit the low-pass filter needed for downsampling.
In a reproduction of Kemelmacher-Shlizerman and colleagues' Photobios Kemelmacher-Shlizerman et al. 2011, I watched a model handle face registration and a dynamic-programming path through a photo collection, then repeatedly get a simple cross-fade wrong. Competence on one part gave no assurance about another. The paper describes the underlying method; this was my experience reproducing it with an LLM.
Give generated code the same tests as your own: small known answers, boundary cases, independent reference comparisons, and real photographs. Ask for the assumptions about ranges, strides, and color space explicitly. An image that looks reasonable is a starting point for review.
Ask for a minimal reproduction or several hypotheses that a test can distinguish. Run the proposed correction on the failing case and the regression suite. Treat the explanation as a hypothesis until the code and observations support it.
Make inspection cheap, test one stage at a time, and keep the failures you fix. Hand-checkable inputs catch mistakes that photographs hide; photographs expose behavior that small synthetic tests do not cover.
Recap: big lessons of this chapter
Make inspection cheap, test one stage at a time, and keep the failures you fix. Hand-checkable inputs catch mistakes that photographs hide; photographs expose behavior that small synthetic tests do not cover.