A photo shot in dim light uses only a narrow band of the available brightness range — everything crowds between 40 and 90, and the detail is there but invisible. Histogram equalization stretches whatever range the image actually uses across the full 0–255, spreading the crowded values apart.
The trick is to use the cumulative histogram as the mapping function. A value's new brightness is determined by what fraction of the image is darker than it.
Task: write equalize(pixels) returning a new grid of integers.
The procedure:
0 to 255.cdf[v] is how many pixels are v or darker.cdf_min be the smallest non-zero entry in that cumulative list — the count of the darkest value actually present.pixels is a 2D grid of integers 0–255; the output has the same shape.cdf_min is what pins the darkest present value to exactly 0. Without it the darkest pixel comes out grey and the image never reaches full black.total - cdf_min is zero. Return a grid of 0 rather than dividing by zero — there is no range to stretch.round().Notice what equalization can't do: it only ever reassigns brightness values, never invents detail. Two pixels that were equal stay equal, so the ordering of the image is untouched. It simply redistributes the values so that each brightness band holds roughly the same number of pixels — which is also why it tends to exaggerate noise in flat regions.