Image Processing & Computational Photography
Point Operations
Operations that compute each output pixel from the input value at the same pixel alone, such as negatives, log and power-law maps, contrast stretches, windows and thresholds; why every one is a look-up table, and what an 8-bit table merges, skips and cannot invert.
beginner
A point operation computes each output pixel from the value of the same pixel in the input, and from nothing else: not its neighbours, not its position, not the rest of the image. Negatives, logarithmic and power-law curves, contrast stretches, display windows, clipping and thresholds are all point operations. They are also called intensity transformations, grey-level transformations or tone curves. Because an 8-bit image has only 256 possible values, every point operation on it can be computed once for each value and stored as a look-up table (LUT) of 256 entries. Applying the operation then means one table lookup per pixel.
The image processing overview places point operations next to neighbourhood, global and geometric operations and summarizes the common ones. This article covers them in depth: the families of mappings, how they compose, what they do to the histogram, and what is lost when the input and the output are both quantized to 8 bits. Data-driven tables such as histogram equalization, and display windows for CT data in Hounsfield units, have their own articles.
Definition
A point operation applies one function to every pixel value:
for every position . The same is used everywhere. An operation whose function changes with position, such as a threshold that depends on a local mean or the tile-wise mappings of CLAHE, is a neighbourhood operation even though each pixel still passes through a table. The name is also used, more loosely, for pixel-wise operations on several registered images, such as averaging exposures or subtracting a background, where each output depends only on the values at the same position in each input.
Intuition
Picture the operation as a curve that takes an input brightness on the horizontal axis to an output brightness on the vertical axis. The identity is the diagonal. A negative is the other diagonal. A curve that rises steeply over some input range spreads those values apart and increases their contrast. Where the curve is flat, it squeezes values together. Clipping is a curve that is flat at the ends, and a threshold is a single step.
Two facts follow from the picture. First, the curve sees only brightness, so it treats a pixel on an edge exactly like a pixel of the same value in a flat region: it cannot tell signal from noise, or an object from the background. Second, with 8-bit data the curve is really a staircase of 256 steps on each axis. Where the curve is steep, adjacent inputs land several output levels apart and leave unused levels in between. Where it is flat, several inputs land on the same output level, and the difference between them is gone for good.
Formal Definition
For an image with levels ( for 8 bits), a point operation is a map
usually obtained from a continuous curve on by rounding and clipping, . The table is the vector .
The histogram of the output. Every pixel at level goes to level , so the output histogram is obtained by moving counts. Writing and for the input and output histograms,
The set is the preimage of . An output level with an empty preimage stays empty, whatever the image. An output level whose preimage has several elements merges those input levels into one. For continuous intensities and a differentiable, strictly monotonic , the change-of-variables formula of probability gives the output density
A slope above 1 thins the density, and a slope below 1 piles it up. The discrete version of the same thing is a histogram with empty bins (combs) where the slope exceeds 1, and merged levels where it falls below 1.
Families of Point Operations
All the formulas below are written for an 8-bit range . Implementations often first normalize to .
Identity and negative
The negative reverses the scale. It converts between the two polarities of a binary mask or a scanned document (dark text on light paper, or light on dark). scikit-image’s invert subtracts the image from the maximum of its unsigned integer type. For signed integers it subtracts from , because the range of a signed type is asymmetric: negating in int8 would give again. Floats are subtracted from 1, or negated when they are declared signed (scikit-image, _invert.py).
Logarithmic and exponential maps
A logarithmic curve, for example
which maps 0 to 0 and 255 to 255, has a slope of . The slope is about 46 at and falls below 1 at , so the curve expands dark values strongly and compresses the rest. It suits data whose useful range spans orders of magnitude, such as a Fourier amplitude spectrum, which the Fourier transform article displays as . The exponential is its inverse. “Log transform” does not name a single curve. scikit-image’s adjust_log computes times a gain (its docstring writes the formula with ; the code uses ), a far milder curve whose slope runs from at black to about at white (scikit-image, exposure.py).
Power laws
A power law maps
With it expands dark values and compresses bright ones, and so brightens the image. With it does the opposite. scikit-image’s adjust_gamma describes itself as gamma correction, “also known as Power Law Transform”, and computes it in this normalized form, with an optional gain. Its docstring notes that shifts the histogram left and darkens the image (scikit-image, exposure.py). The power laws with exponents and are inverses of each other, so and undo each other exactly in continuous arithmetic.
Here the power law is a mapping chosen to change the appearance or the statistics of an image. The same curves also serve as encodings: standard color encodings such as sRGB store gamma-encoded values that are not proportional to light, as the color spaces article explains. That use, called gamma correction, comes with its own questions of standards and of how codes are spent across the range. A power-law point operation applied to encoded values is a different curve in linear light, so the two uses should not be mixed up.
Linear stretches, clipping and windows
A linear contrast stretch maps an input range onto the full output range and clips outside it:
The limits are often taken from the image itself, as its minimum and maximum or as low and high percentiles, which makes the table depend on the image while it is still applied pixel by pixel. scikit-image’s rescale_intensity stretches the image’s own minimum and maximum by default, or an in_range given by the caller. With the default out_range it then converts the result to the input’s data type with astype, which truncates instead of rounding: its docstring example maps 51, 102, 153 to 0, 127, 255, where rounding would give 128 for the middle value (scikit-image, exposure.py). A medical display window, given by a centre and a width, is the same clipped linear map on a different input scale. The Hounsfield units article gives the DICOM definition and shows what windowing does to contrast and noise.
Piecewise-linear curves generalize the stretch with several segments: a gentle slope in a range that should be compressed and a steep one where detail matters. Braun and Fairchild (1999) describe three classical ways of fitting the lightness range of an image into the smaller range of an output device such as a printer. Linear rescaling lowers perceived contrast and makes shadows look hazy. Hard clipping approximately keeps the mean lightness but maps many inputs to one value and flattens shadow detail. Soft clipping and “knee” functions compress the shadows instead of clipping them. Their manuscript, read here, does not say which of their two 1999 versions (the journal article or the SPIE proceedings paper) it is.
Sigmoids
An S-shaped curve compresses both ends and steepens the middle, raising mid-tone contrast inside a fixed output range. Braun and Fairchild built their lightness remapping functions from a discrete cumulative normal function stored as a look-up table, with one parameter, , setting the centre of the S and another, , setting its slope; a smaller gives more contrast. They chose the parameters for six test images at four output dynamic ranges through psychophysical experiments, so that an image compressed into a device’s limited lightness range kept its perceived contrast. scikit-image’s adjust_sigmoid, which cites their paper, uses the logistic function of the normalized value instead, with a cutoff and gain by default (scikit-image, exposure.py).
Thresholds and quantizers
A threshold maps every value at or above to one level and the rest to another. It is the most extreme flat-and-steep curve: all 256 levels collapse to two. Choosing well, globally or locally, is the subject of thresholding. Several thresholds give a staircase that quantizes the image to a few levels. Posterization keeps the top bits of each value, , a staircase of equal steps. Bit-plane slicing extracts bit of each value, , so that each bit can be viewed as a binary image.
Data-driven tables
The table can be computed from the image. A percentile stretch is one example. Histogram equalization is another: it uses the image’s cumulative histogram as , so that the output levels are used about equally often. Histogram matching maps through one cumulative histogram and the inverse of another. Once the table is computed, all of these are applied as ordinary point operations, and everything below applies to them.
Properties
Every 8-bit point operation is a table
A function of the pixel value alone can take at most 256 different inputs on 8-bit data, so evaluating it 256 times and indexing the table gives the same image as evaluating it at every pixel. The table is the efficient form for large images (256 evaluations instead of millions), and it accepts any , whether or not it has a formula. scikit-image uses the same idea: its adjust_gamma builds a 256-entry table when the input is uint8 and computes the formula directly for other types (scikit-image, exposure.py). For 16-bit data the table has 65,536 entries. For floating-point data a table would need an entry for every representable value, so the formula is evaluated at every pixel instead.
Composition
Applying , then , then is the single point operation , and its table is obtained by passing the table of through the other two: T3[T2[T1]] in NumPy. A chain of curve adjustments therefore costs one lookup per pixel, however long it is. The composed table is exact for the quantized operations. It is not the same as composing the continuous curves and rounding once, because every intermediate rounding can move a value by up to half a level, and the following curve can then amplify that shift. When a pipeline can work in floating point, rounding once at the end avoids this.
Merges, gaps and invertibility
Since a table maps 256 levels into 256 levels, every level that two inputs share leaves one output level unused elsewhere: if the outputs cover the whole range, the number of merged input levels equals the number of empty output levels. A table can be inverted only if it is a bijection. In our reasoning, a strictly increasing map of onto itself must be the identity, and a strictly decreasing one must be the negative, so every other monotonic 8-bit-to-8-bit point operation merges some levels and cannot be undone exactly. This includes “invertible” curves such as power laws and logarithms. Their continuous inverses exist, but once two inputs share an output, no inverse can tell them apart.
The best that any inverse table can do is to send each output level back to the middle of its preimage. If the preimages are runs of consecutive levels, as they are for monotonic maps, a run of levels from to has a midpoint within of every member. So the smallest possible worst-case round-trip error is , where is the largest preimage (our derivation).
Where merges and gaps occur follows from the slope (our derivation):
- Power law, . The slope exceeds 1 for , below about level 55. So dark levels are spread apart, leaving gaps, and bright levels are merged. Level 1 maps to , which leaves levels 1 to 27 of the output empty.
- Power law, . The pattern is reversed. Every input with , which means levels 0 to 21 (level 21 gives 0.4963), rounds to 0, so 22 dark levels become black.
- Logarithm. With the form above, level 1 maps to , which leaves levels 1 to 31 of the output empty.
- Stretch. A stretch with slope cannot fill all 256 levels from 128 inputs. Clipping merges everything at or below into 0 and everything at or above into 255.
What a point operation cannot do
A point operation cannot use spatial context. It changes how values are displayed or quantized, but it cannot separate a pixel’s signal from its noise, and it can only lose information, through merges and clipping, never add it. The image processing and Hounsfield units articles show the consequence for contrast and noise: a smooth curve multiplies a small contrast and the noise around it by the same local slope, so their ratio does not change. Reducing noise needs a neighbourhood operation, such as one of the filters in image filtering.
Invariance to monotonic maps
What a point operation preserves can be used to build measures that ignore it. A strictly increasing map keeps the order of pixel values. So descriptors built from comparisons between single pixels do not change under any such map: the bits of local binary patterns, and the census transform, as the brightness constancy article describes. Comparisons between smoothed or interpolated values are unchanged only by gain and offset, and by other monotonic maps only approximately, as binary descriptors and local binary patterns both discuss. A non-monotonic map destroys order but, if it is one-to-one, not the information shared between two images. It follows from the definition of mutual information, which depends only on the joint probabilities of values and not on the values themselves, that relabelling the levels of one image one-to-one leaves the mutual information unchanged. The authors of the mutual information registration paper passed one image through a non-monotonic (quadratic) intensity map and, as that article reports, found alignment not significantly affected.
Color point operations
A color image can be passed through one table per channel. Applying the same nonlinear curve to red, green and blue separately changes the ratios between the channels, and with them the hue and saturation. The ratios are preserved only by scaling all three channels by the same factor (our reasoning). Contrast and tone curves can therefore be applied to a lightness or luma channel after converting to a suitable color space, as the histogram equalization article describes for equalization. A table indexed by all three channels at once can express any color-to-color mapping, but for 8-bit channels a full table would need million entries.
Examples
The example applies a negative, the logarithm above, power laws with and , a threshold at 128 and a stretch of 64–191 onto 0–255. Each is a continuous curve followed by rounding to 8 bits (np.rint, which rounds halves to even, then clipping). The test images are:
- a synthetic ramp, pixels, in which every level from 0 to 255 occurs exactly 256 times, so its counts are counts over the 256 table entries;
- scikit-image’s bundled
cameraphotograph.
The script checks four things:
- A1: that each table gives the same image as evaluating its formula at every pixel.
- A2: that three tables applied in turn (power law 0.4, then stretch, then negative) equal one composed table, and how the composed table differs from the continuous composition rounded once.
- A3: that tables written from the scikit-image 0.25.2 source reproduce
adjust_gamma,invert,rescale_intensityandadjust_log. - B: what each mapping loses on the ramp. The measures are:
- distinct output levels and merged levels;
- the largest preimage;
- empty levels inside the output range, and the longest run of them;
- the round trip through the analytic inverse (power laws 0.4 and 2.5 invert each other), giving the worst error, the number of levels not recovered and the mean error over pixels;
- the worst error of the best possible inverse table, which sends each output to the middle of its preimage.
The script runs in under a second with NumPy 2.3.5 and scikit-image 0.25.2. Two runs printed identical output.
import os
os.environ["OPENBLAS_NUM_THREADS"] = "1" # one BLAS thread, set before NumPy loads
import numpy as np
from skimage import data, exposure, util
def q(x): # back to 8 bits: round half to even, clip
return np.clip(np.rint(x), 0, 255).astype(np.uint8)
ops = { # continuous maps on [0, 255]
"negative": lambda x: 255 - x,
"log": lambda x: 255 * np.log(1 + x) / np.log(256),
"gamma 0.4": lambda x: 255 * (x / 255) ** 0.4,
"gamma 2.5": lambda x: 255 * (x / 255) ** 2.5,
"threshold": lambda x: np.where(x >= 128, 255.0, 0.0),
"stretch": lambda x: 255 * (np.clip(x, 64, 191) - 64) / 127,
}
inverse = {
"negative": lambda s: 255 - s,
"log": lambda s: np.exp(s * np.log(256) / 255) - 1,
"gamma 0.4": ops["gamma 2.5"],
"gamma 2.5": ops["gamma 0.4"],
"threshold": None, # no inverse exists
"stretch": lambda s: 64 + 127 * s / 255,
}
levels = np.arange(256, dtype=np.float64)
lut = {k: q(f(levels)) for k, f in ops.items()} # one 256-entry table per operation
ramp = np.tile(np.arange(256, dtype=np.uint8), (256, 1)) # every level, 256 pixels each
camera = data.camera()
counts = np.bincount(ramp.ravel(), minlength=256)
print("ramp: levels", np.count_nonzero(counts), "pixels per level", counts.min(), "-", counts.max(),
"| camera:", camera.shape, camera.dtype)
# A1. Table lookup against evaluating the formula at every pixel
print("\nA1 pixels where table != direct camera ramp")
for k, f in ops.items():
d = [int((lut[k][im] != q(f(im.astype(np.float64)))).sum()) for im in (camera, ramp)]
print(f" {k:<29} {d[0]:7d} {d[1]:6d}")
# A2. Three operations in sequence are one table
l1, l2, l3 = lut["gamma 0.4"], lut["stretch"], lut["negative"]
composed = l3[l2[l1]]
print("\nA2 sequence vs composed table, camera pixels differing:",
int((l3[l2[l1[camera]]] != composed[camera]).sum()))
once = q(ops["negative"](ops["stretch"](ops["gamma 0.4"](levels)))) # rounded only at the end
diff = np.abs(composed.astype(int) - once.astype(int))
print(" composed table vs continuous composition rounded once:",
int((diff > 0).sum()), "levels differ, max", int(diff.max()))
# A3. Hand-written tables against scikit-image 0.25.2
trunc = lambda x: x.astype(np.uint8) # astype truncates toward zero
hand = {
"adjust_gamma(0.4)": (lut["gamma 0.4"], lambda im: exposure.adjust_gamma(im, 0.4)),
"adjust_gamma(2.5)": (lut["gamma 2.5"], lambda im: exposure.adjust_gamma(im, 2.5)),
"invert": (lut["negative"], util.invert),
"rescale_intensity(64, 191)": (trunc((np.clip(levels, 64.0, 191.0) - 64.0) / (191.0 - 64.0) * 255.0),
lambda im: exposure.rescale_intensity(im, in_range=(64, 191))),
"adjust_log": (trunc(np.log2(1 + levels / 255.0) * 255.0), exposure.adjust_log),
}
print("\nA3 pixels where hand != library camera ramp")
for k, (t, fn) in hand.items():
d = [int((t[im] != fn(im)).sum()) for im in (camera, ramp)]
print(f" {k:<29} {d[0]:7d} {d[1]:6d}")
print(" rescale_intensity (truncating) vs rounded stretch:",
int((hand["rescale_intensity(64, 191)"][0] != lut["stretch"]).sum()), "levels differ")
# B. What each 8-bit mapping loses, measured on the ramp
print("\nB op distinct merged largest empty longest | round trip: max levels mean | best")
print(" preimage levels gap | err wrong err | inverse")
for k in ops:
out = lut[k][ramp]
hist = np.bincount(out.ravel(), minlength=256)
used = np.flatnonzero(hist)
distinct = used.size
preimage = np.bincount(lut[k], minlength=256) # input levels per output level
empty = int(used[-1] - used[0] + 1 - distinct)
longest = int((np.diff(used) - 1).max()) if distinct > 1 else 0
# best possible table inverse: each output goes back to the middle of its preimage
lo = np.full(256, 255); hi = np.zeros(256, dtype=int)
np.minimum.at(lo, lut[k], np.arange(256)); np.maximum.at(hi, lut[k], np.arange(256))
best = int(np.abs((lo + hi)[out] // 2 - ramp.astype(int)).max())
if inverse[k] is None:
rt = f"{'-':>5} {'-':>6} {'-':>7}"
else:
err = np.abs(q(inverse[k](out.astype(np.float64))).astype(int) - ramp)
rt = f"{err.max():5d} {np.count_nonzero(err[0]):6d} {err.mean():7.4f}"
print(f" {k:<10} {distinct:8d} {256 - distinct:6d} {preimage.max():8d} {empty:6d} {longest:7d}"
f" | {rt} | {best:5d}")
mid = ramp[:, 64:192]
err = np.abs(q(inverse["stretch"](lut["stretch"][mid].astype(np.float64))).astype(int) - mid)
print(" stretch round trip on inputs 64-191: max err", err.max(), "| levels wrong", np.count_nonzero(err[0]))
Output:
ramp: levels 256 pixels per level 256 - 256 | camera: (512, 512) uint8
A1 pixels where table != direct camera ramp
negative 0 0
log 0 0
gamma 0.4 0 0
gamma 2.5 0 0
threshold 0 0
stretch 0 0
A2 sequence vs composed table, camera pixels differing: 0
composed table vs continuous composition rounded once: 57 levels differ, max 1
A3 pixels where hand != library camera ramp
adjust_gamma(0.4) 0 0
adjust_gamma(2.5) 0 0
invert 0 0
rescale_intensity(64, 191) 0 0
adjust_log 0 0
rescale_intensity (truncating) vs rounded stretch: 63 levels differ
B op distinct merged largest empty longest | round trip: max levels mean | best
preimage levels gap | err wrong err | inverse
negative 256 0 1 0 0 | 0 0 0.0000 | 0
log 125 131 6 131 31 | 3 131 0.6445 | 3
gamma 0.4 173 83 3 83 27 | 1 83 0.3242 | 1
gamma 2.5 173 83 22 83 2 | 21 83 1.2891 | 11
threshold 2 254 128 254 254 | - - - | 64
stretch 128 128 65 128 2 | 64 128 16.2500 | 32
stretch round trip on inputs 64-191: max err 0 | levels wrong 0
The first line confirms the data. The ramp has all 256 levels with 256 pixels each, and camera is a uint8 image.
All three checks held.
- A1: every table reproduced direct evaluation, with 0 differing pixels for all six operations on both images.
- A2: the three tables in sequence gave the composed table’s image exactly. The composed table differs from the continuous composition rounded once at 57 of the 256 levels, by one level each time: the intermediate roundings, not the curves, account for the difference.
- A3: the hand tables reproduced all four scikit-image functions,
adjust_gammaat both exponents included, with 0 differing pixels on both images. Therescale_intensityandadjust_logtables truncate, as the library’sastypedoes. For the stretch, truncating instead of rounding changes 63 levels.
What the mappings lose (B).
- The negative is the only bijection. It keeps 256 levels and round-trips with error 0. Every other operation merges levels (131 for the logarithm, 83 for each power law, 128 for the stretch and 254 for the threshold), and none comes back exactly: the worst round-trip errors are 3, 1 and 21 levels for the logarithm and the power laws at 0.4 and 2.5, and 64 for the stretch, while the threshold has no inverse at all.
- Every merge leaves a gap. In every row the number of empty levels equals the number merged, which by the counting argument in Properties means that each output reaches both 0 and 255.
- Gaps sit where the slope exceeds 1. The longest gaps are where the derivations put them. The logarithm skips 31 levels just above black, and the power law with skips 27 there.
- Merges sit where the slope is below 1. The power law with merges 22 dark levels into black, its largest preimage.
- The two power laws are not inverses in 8 bits. Each keeps 173 levels. The round trip 0.4 then 2.5 is off by at most 1 level, but 2.5 then 0.4 is off by up to 21, because the 22 levels sent to black all come back as black.
- The best inverse matches the derivation. Its worst error is in every row, and it is never worse than the analytic inverse. It equals the analytic inverse for the logarithm and and roughly halves the worst error for and the stretch. For the threshold, which has no analytic inverse, the best table still errs by up to 64 levels.
- An inverse can recover at most one level per output. So at least the merged levels must come back wrong. Here the number of levels not recovered equals the number merged in every row, so each analytic inverse recovers one level per output.
- The stretch loses only through clipping. It leaves 128 empty levels in a comb with runs of at most 2. On the unclipped inputs 64–191 its round trip is exact (error 0, 0 levels wrong), so its whole round-trip error of up to 64 comes from the two runs of 65 levels that clipping merges into 0 and 255. A stretch that fills the range on screen still holds only the 128 levels it started with.
Every expected outcome held, for every operation and on both images where both were measured. The curves and their parameters were fixed before the script was first run, and the results illustrate the derivations in the Properties section.
Common Misconceptions
- “Contrast enhancement adds information.” A point operation can only keep or merge levels. A stretch that fills the output range leaves a comb of empty levels in the histogram, and its steeper slope amplifies noise just as much as contrast.
- “A curve followed by its inverse returns the original.” That holds only for the identity and the negative on 8-bit data. In the example, a power law of 2.5 followed by 0.4 moves dark pixels by up to 21 levels.
- “The order of rounding does not matter.” Composing quantized tables and rounding a continuous composition once gave different results at 57 levels in the example. Keeping intermediate values in floating point and rounding once avoids this.
- “Library functions round.” scikit-image’s
rescale_intensityandadjust_logconvert withastype, which truncates, whileadjust_gammarounds itsuint8table withrint(scikit-image,exposure.py). Hand-written replacements that round will differ from them, as the 63 levels of the stretch in the example show. - “ always brightens.” It brightens under the convention used by scikit-image and in this article. An encoding curve and its decoding curve use reciprocal exponents (sRGB decodes with an exponent of 2.4 in its power segment and encodes with , as the color spaces article gives), so the direction has to be read from the formula, not from the word “gamma”.
- “A point operation acts like a filter.” It uses no neighbours, so it cannot blur, sharpen or denoise. A threshold computed from a local window is a neighbourhood operation, even though each pixel is still compared with a value.
- “Applying a curve to R, G and B keeps the colours.” Any per-channel curve that is not a common scaling changes channel ratios, and with them hue and saturation.
Where It Is Used
- Display and inspection. Stretches, windows and log curves map data with a large or unusual range, such as CT data in Hounsfield units, Fourier spectra and high-bit-depth sensor data, to the levels a display can show.
- Camera pipelines and tone reproduction. In the image signal processor that the digital camera article describes, the last colour steps are gamma compression and an S-shaped tone curve. HDR imaging uses global curves, which are point operations, to compress radiance into a displayable range.
- Segmentation. A threshold is the simplest segmentation, and multi-level thresholds quantize an image into classes.
- Contrast normalization. Histogram equalization and percentile stretches compute their table from the image and apply it pixel by pixel.
- Learning pipelines. Brightness and contrast jitter in data augmentation are random point operations. Scaling 8-bit values to and subtracting a channel mean are affine point operations.
- Robust matching. Descriptors and costs designed to ignore monotonic intensity changes, such as local binary patterns and census costs for stereo and optical flow, are built on the order of values that such maps preserve.
Related
- Image Processing
Computing new images, or simple measurements, from images by point, neighborhood, global and geometric operations; how enhancement, restoration and learned methods build on them, and where image processing meets computer vision.
- Histogram Equalization
A contrast enhancement method that maps each intensity level through the image's cumulative histogram so that output levels are used approximately uniformly, with adaptive variants such as CLAHE that equalize local regions.
- Thresholding
Segmenting an image by comparing each pixel's intensity with a threshold, chosen by hand, from the histogram as in Otsu's method, or from each pixel's neighborhood as in Sauvola's method.
- Color Spaces
A color space assigns numbers to colors; choosing and converting between RGB, CIE XYZ, CIELAB, YCbCr, and HSV correctly matters for both image processing and learning.
- Hounsfield Units
The scale on which CT scanners report attenuation, fixed at 0 for water and −1000 for air, how scanners are calibrated to it, why tissue values still depend on the X-ray energy and the scanner, and how windowing turns it into a displayed image.
- Image Filtering
Computing each output pixel from a neighborhood of input pixels, the family of operations behind smoothing, sharpening, edge detection, and denoising.
References
- scikit-image developers (2025). skimage.exposure: rescale_intensity, adjust_gamma, adjust_log and adjust_sigmoid, skimage/exposure/exposure.py, scikit-image v0.25.2 source code.
- scikit-image developers (2025). skimage.util.invert, skimage/util/_invert.py, scikit-image v0.25.2 source code.
- Braun, G. J. & Fairchild, M. D. (1999). Image Lightness Rescaling Using Sigmoidal Contrast Enhancement Functions. Journal of Electronic Imaging, 8, 380–393 (authors' manuscript read).