C++: grayscale conversion and thresholding

Convert color pixels to brightness with luminance weights and split them into foreground and background.

C++
#include <cmath>
#include <iostream>
#include <vector>

struct Rgb {
  int r, g, b;
};

int main() {
  std::vector<Rgb> pixels = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
  std::vector<int> gray;
  double total = 0;
  for (const Rgb& p : pixels) {
    int value = static_cast<int>(std::lround(0.299 * p.r + 0.587 * p.g + 0.114 * p.b));
    gray.push_back(value);
    total += value;
  }
  double threshold = total / gray.size();

  std::cout << "gray:";
  for (int v : gray) {
    std::cout << " " << v;
  }
  std::cout << "\nthreshold: " << threshold << "\nbinary:";
  for (int v : gray) {
    std::cout << " " << (v > threshold ? 1 : 0);
  }
  std::cout << "\n";
}
Output
gray: 76 150 29 255
threshold: 127.5
binary: 0 1 0 1

Explanation

A color pixel has red, green, and blue channels. A grayscale image keeps only brightness. Because human vision is most sensitive to green and least to blue, the standard luma formula weights the channels unequally: 0.299·R + 0.587·G + 0.114·B.

Thresholding turns a grayscale image into a binary one. Pixels brighter than the threshold become 1 (foreground) and the rest become 0. A simple choice of threshold is the mean brightness. Methods such as Otsu's choose it automatically from the histogram. Binary images are the input to many later steps, such as finding connected regions.

Explore the dry run
Guided walkthroughReady
1#include <cmath>
2#include <iostream>
3#include <vector>
4
5struct Rgb {
6 int r, g, b;
7};
8
9int main() {
10 std::vector<Rgb> pixels = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
11 std::vector<int> gray;
12 double total = 0;
13 for (const Rgb& p : pixels) {
14 int value = static_cast<int>(std::lround(0.299 * p.r + 0.587 * p.g + 0.114 * p.b));
15 gray.push_back(value);
16 total += value;
17 }
18 double threshold = total / gray.size();
19
20 std::cout << "gray:";
21 for (int v : gray) {
22 std::cout << " " << v;
23 }
24 std::cout << "\nthreshold: " << threshold << "\nbinary:";
25 for (int v : gray) {
26 std::cout << " " << (v > threshold ? 1 : 0);
27 }
28 std::cout << "\n";
29}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Weighted sum of the channels, rounded: pure red is 76, pure green 150, pure blue 29, white 255.gray = 76, 150, 29, 255
—
The mean brightness (76 + 150 + 29 + 255) / 4 = 127.5 is used as the threshold.threshold = 127.5
—
Print the threshold; the binary values follow on the same line.threshold = 127.5
—
Only green and white are brighter than 127.5.threshold = 127.5
gray: 76 150 29 255
threshold: 127.5
binary: 0 1 0 1

Cost and common mistakes

O(p) time for p pixels.

  • Averaging the three channels (R+G+B)/3 makes pure blue look as bright as pure green, which does not match perception.
  • A single global threshold fails when lighting is uneven. Adaptive thresholding computes a different threshold for each region.

Try it yourself

Which weights are used by the older NTSC formula and by modern HDTV (Rec. 709)?

Reveal answer and reasoning

NTSC (Rec. 601) uses 0.299, 0.587, 0.114. Rec. 709 uses 0.2126, 0.7152, 0.0722.

Further reading