Code readingExplainer

Coloured QR codes a scanner can read: contrast rules for readability

Pick coloured QR codes a scanner still reads: the PCS formula, grayscale maths for common colour pairs, why inverted codes fail and how to verify readability.

LumaSpec Vision11 min read Share on LinkedIn

On this page 10 sections
  1. 01The number that decides: print contrast signal
  2. 02Worked example: grade a brand palette in grayscale
  3. 03Why light-on-dark codes fail even at perfect contrast
  4. 04On screen: the CSS colour never reaches the canvas pixels
  5. 05Under machine-vision lights, the colour rules move
  6. 06How many pixels a coloured QR actually needs
  7. 07Verify the recoloured code before it ships
  8. 08When the usual answers go wrong
  9. 09The rules in one place
  10. 10Questions

Brand guidelines arrive, the QR code on the label or the web page turns teal, and the reads stop. The fix is not taste but one number: the contrast between the two module colours as the scanner sees it in grayscale. Label designers, HMI developers and line engineers all hit the same wall, and the same formula clears it: print contrast signal (PCS). The sections below give the PCS maths with worked colour pairs, the polarity trap behind light-on-dark codes, what CSS can and cannot do to a canvas-drawn code, and how the reader's light colour moves the rules. You finish able to sign off a pair with numbers instead of a phone in hand.

The number that decides: print contrast signal

A code reader does not see red or teal. It binarizes the image: around a local threshold, every module becomes a dark pixel or a light pixel, and the decode survives only if that split survives. The quantity the whole chain rests on is print contrast signal, the relative gap between light and dark reflectance - or, on a screen, luminance:

PCS = (L_light - L_dark) / L_light Y = 0.299R + 0.587G + 0.114B

For displays, compute Y from the sRGB values with the ITU-R BT.601 weights above; for print, L is measured reflectance and the same formula applies. Most symbology specs and reader datasheets set the floor at PCS 0.5, and some accept 0.45. Design to 0.65 or better, because blur, tilt and ambient light all eat margin.

Hue is not contrast. A red-and-green pair can look vivid and still convert to two nearly equal gray values, which is a PCS near zero. Two rules follow: judge every pair in grayscale, and put the lower-luma colour in the modules, never in the background.

Worked example: grade a brand palette in grayscale

Take pairs styled like real brand palettes. The hex values are illustrative; the arithmetic is the point. Convert each colour with the weights above, then apply the PCS formula.

Green #4CAF50 modules on white: Y_dark = 0.299 × 76 + 0.587 × 175 + 0.114 × 80 = 22.7 + 102.7 + 9.1, about 135. PCS = (255 - 135) / 255 = 0.47. Bold against white on screen, below the 0.5 floor on any scanner.

Teal #00838F on cream #FFF3E0: Y_dark = 0.587 × 131 + 0.114 × 143 = 76.9 + 16.3, about 93. Y_light = 0.299 × 255 + 0.587 × 243 + 0.114 × 224 = 76.2 + 142.6 + 25.5, about 244. PCS = (244 - 93) / 244 = 0.62. Passes, with modest margin.

Darken the same green to #1B5E20 and the hue survives: Y_dark = 0.299 × 27 + 0.587 × 94 + 0.114 × 32 = 8.1 + 55.2 + 3.6, about 67. PCS = (255 - 67) / 255 = 0.74. That pair ships.

Modules Background Y dark Y light PCS Verdict at a 0.5 floor
Black #000000 White #FFFFFF 0 255 1.00 pass
Navy #000080 White 15 255 0.94 pass
Dark red #C00000 White 57 255 0.78 pass
Teal #00838F Cream #FFF3E0 93 244 0.62 pass
Green #4CAF50 White 135 255 0.47 fail
Cyan #00BCD4 White 135 255 0.47 fail
Yellow #FFD500 White 201 255 0.21 fail

One footnote keeps the method honest: decoders do not all weight grayscale the same way. Under the BT.709 weights (0.2126, 0.7152, 0.0722), the green pair gives Y about 147 and PCS = (255 - 147) / 255 = 0.42, still a fail. The verdicts hold across weightings; only the margins move.

Decision: ship pairs at PCS 0.65 or more. Treat 0.5 to 0.65 as conditional on a verifier grade check. Recolour anything below 0.5.

Why light-on-dark codes fail even at perfect contrast

Invert a black-on-white code and the PCS arithmetic still returns 1.0, yet reads vanish. The reason is polarity. A QR decoder locates the three finder patterns as dark squares inside light fields and samples modules on that assumption, and plenty of decoder stacks never try the inverse. In the field this shows up as dark-themed apps that render codes light-on-dark and return nothing, while the identical code on a white plate reads instantly.

The quiet zone turns a borderline case into a hard failure. ISO/IEC 18004 requires a quiet zone four modules wide, in the light colour. A dark interface drops the code straight onto the dark background, so the code loses its zone and its polarity together. The sizing side lives in the QR code quiet zone and integer module scaling guide; the polarity fix is the same plate. Render on a light panel at least four modules wide, coloured like the light modules, everywhere the code appears.

On screen: the CSS colour never reaches the canvas pixels

Barcode components in web HMIs and SCADA pages draw the code themselves, module by module, onto a canvas element. The style props you can reach - value text, error text, the component frame - style the markup around the drawing. The black modules are not a DOM colour at all. That is why the browser devtools show no rule carrying the black: the drawing call painted the pixels directly, and CSS cannot restyle pixels inside a canvas.

When the component exposes no colour options, three routes work:

  1. Generate the image yourself. Render an SVG or PNG in your chosen pair - whole pixels per module, quiet zone included - and hand that image to the component. The DataMatrix generation walkthrough shows the write-the-pixels pattern in .NET; the same pattern produces a coloured QR.
  2. Keep black modules, tint the surroundings. Brand colour in the background and quiet zone, PCS 1.0, polarity intact.
  3. CSS filters, for debugging only. The CSS invert() filter function inverts the colour samples in the input image, so on a black-on-white code invert(1) returns white-on-black - exactly the polarity-broken code class that many decoders refuse. Drop the invert and the chain changes: sepia() and hue-rotate() leave pure black modules black, because the sepia transform has no offsets, and tint only the light background. No filter chain recolours just the modules while keeping dark-on-light.

Under machine-vision lights, the colour rules move

Everything above assumed white light. A fixed code reader with a monochrome sensor and a narrowband LED grades the pair by reflectance at the LED's wavelength, not by human colour. The monochrome camera contrast explainer works the bright-versus-dark logic through, and the LED colour guide matches light colour to surface.

Around 630 nm red light, red, orange, yellow and white surfaces reflect strongly and read light; black, blue and green absorb and read dark. A red-on-white label that passes every grayscale check on screen therefore collapses under a red LED: the modules brighten to nearly the background and PCS falls toward 0. The cure is not to abandon the red but to change the wavelength. Under blue light near 460 nm, the red modules absorb, read dark, and the same label reads cleanly again.

Decide in this order: fix the palette, then pick the illumination wavelength that maximizes the gap, or take a white-light reader so the pair grades exactly as it does on paper.

When the palette must stay untouched, white illumination is the neutral choice:

Fixed code readersHikrobot MV-ID813M-05S-WBN-SR 1.3 MP resolution, 4.9 mm focal length, White light, SR, GigE, Static cableMV-ID813M-05S-WBN-SR
Interface
GigE
Resolution
Up to 3 MP
Lens mount
M10
$719.00View part

Glossy labels add a second enemy: specular glare paints bright patches over modules and quiet zone and flips dark modules light. A polarized front end rejects that glare before it reaches the sensor:

Fixed code readersHikrobot MV-IDB005EXI-05NRP Fully isolated version, 1.3 MP resolution, 4.63 mm focal length, 70 mm focus distance, Polarized industrial code readerMV-IDB005EXI-05NRP
Interface
Fast Ethernet
Resolution
Up to 3 MP
Shutter
Global
Lens mount
M5.8
$719.00 · quotedView and quote

How many pixels a coloured QR actually needs

Contrast kills coloured codes far more often than resolution does, and the arithmetic shows why. A version 1 QR spans 21 x 21 modules and each version adds 4 modules per side, so version 2 spans 25 and version 10 spans 57. Add the four-module quiet zone to each side and the totals are 33, 65 and, for version 40, 185 modules. At 3 pixels per module - the working minimum for machine reading, 2 on crisp matte labels - those need 99, 195 and 555 pixels across the code.

A 1.3 MP sensor delivers roughly 1,100 pixels across the frame even in a square format, so every reader in the fixed code readers category covers version 40 with room to spare. Recoloured module edges bleed a little more than black ones, which is the reason to budget 3 pixels per module rather than 2 - not a reason to buy a bigger camera.

Camera resolution for the smallest feature
Units
Pixels needed
–
Sensor size
– MP
Smallest sensor that fits
–
Pixel on the part
– mm
Pixels across the feature, that sensor
–

Pixels needed = field / feature × pixels per feature on each axis (field incl. margin). The lens must resolve the pixels too: Nyquist = 1000 / (2 × pixel µm) lp/mm.

Find cameras with enough pixels Open the full calculator

If you want headroom for several codes in one frame, a 5 MP global-shutter reader carries it:

Fixed code readers2560 x 2048 fixed code reader, 30 fps, 60 codes/s, 8 mm lens (LS-CRD-0028)LS-CRD-0028
Interface
Fast Ethernet
Resolution
5 MP
Sensor format
1/1.7"
Shutter
Global
$920.00View part

The LS-CRD-0028 resolves 2560 x 2048 at 30 fps and 60 codes/s through an 8 mm lens. That span fits version 40 more than four times over.

Verify the recoloured code before it ships

A coloured code that passes the spreadsheet still has to pass the reader. Four checks, in order:

  1. Print or render at final size, quiet zone in its final colour.
  2. Read it with the reader class that runs on the line at the working distance, and with a handheld code reader for spot checks, at the steepest tilt the fixture allows.
  3. Require 100 consecutive decodes with zero misses at commissioning, then alarm in production if the read rate drops below 99 percent.
  4. On a verifier, ISO/IEC 15415 grades run from 4 (A) down to 0 (F), and label standards such as GS1's set the floor at 1.5 (C). Clear grade 3 (B) with a recoloured code to keep margin.

Check the decoded string, not just any decode: the pass/fail match verification station wires the expected value into the decision, and the print quality grading guide extends the same discipline to graded output. For the bench, a compact reader with a 12 mm focal length and 1.3 MP resolution covers close-range label reads:

Fixed code readersHikrobot MV-ID3013PM-12M-WBN 12mm focal length 1.3 MP Compact Industrial Code ReaderMV-ID3013PM-12M-WBN
Interface
Fast Ethernet
Resolution
Up to 3 MP
Shutter
Global
Lens mount
M12
$1,979.00View part

When the usual answers go wrong

  • Mid-tone brand modules (saturated green, cyan, orange): darken the modules rather than shrinking them. The move from #4CAF50 to #1B5E20 lifts PCS from 0.47 to 0.74 with the hue intact.
  • Dark-themed page or HMI: draw the plate instead of hunting for a decoder that reads inverted codes - light panel, four-module quiet zone, black or near-black modules.
  • Red modules mandated by brand: switch illumination to blue near 460 nm or read under white light. Never grade a red pair under a red LED, where it measures at PCS near 0.
  • High PCS on paper, failing on the line: contrast is solved, so move to geometry - an infringed quiet zone, modules off whole pixels, focus blur. The quiet-zone failure walkthrough covers the zone case end to end.

The rules in one place

  • Convert the pair to grayscale luma (0.299, 0.587, 0.114) and compute PCS.
  • Floor 0.5, design target 0.65, ship at 0.74 or better when the palette allows.
  • Lower-luma colour as modules, higher as background, never inverted.
  • Quiet zone: four modules wide, light colour, in every placement.
  • Brand colour goes in the background and quiet zone; black modules need no approval.
  • Choose illumination after the palette: blue near 460 nm for red modules, white for neutrality, red only when the modules are blue or green.
  • Budget 3 pixels per module: version 10 fits in 195 pixels, version 40 in 555.
  • Verify with 100 consecutive decodes at commissioning, alarm below 99 percent in production, and clear grade 3 (B) on a verifier.

To pick the reader you will verify on, start at the camera finder, or go straight into the system configurator with the working distance and label size in hand. The code-reading topic page collects the rest of the decode rules, from quiet zones to trigger timing.

Questions

What PCS should a coloured QR code reach before it goes on a label?

Target a print contrast signal of 0.65 or more, with 0.5 as the floor most symbology specs and reader datasheets quote. Convert both colours to grayscale luma with Y = 0.299R + 0.587G + 0.114B, compute (L_light - L_dark) / L_light, and read the result against those two numbers. Between 0.35 and 0.5 expect intermittent decodes that change from reader to reader. Below 0.35 the code is decoration, not data.

Do any decoders read inverted QR codes, with light modules on a dark background?

Some modern phone and PC libraries try both polarities, but a deployment cannot rely on it: mobile decoder stacks have shipped that return nothing at all on inverted codes, and dark-themed interfaces lose the quiet zone at the same time. Render on a light plate at least four modules wide, coloured identically to the light modules, and the polarity question disappears for every reader you will meet.

Which colour pairs work under red LED light on a fixed reader?

Pair dark absorbers with strong reflectors: black, dark blue or dark green modules on a white, yellow or orange background survive red illumination. The classic casualty is red-on-white - it passes every on-screen grayscale check, yet under a red LED the modules brighten nearly to background level and PCS collapses. Switch that label to blue light near 460 nm and the red modules read dark again.

How do I recolour a QR code drawn by a web component that has no colour option?

Generate the code yourself and hand the component a finished image: an SVG or PNG drawn in your chosen pair, every module a whole number of device pixels, quiet zone included in the light colour. CSS cannot do the job. invert() flips the whole code to light-on-dark, and without invert the black modules stay black through sepia() and hue-rotate(), so only the background takes the tint.

How many pixels do I need across a large QR code?

Budget 3 pixels per module for machine reading, 2 on crisp matte labels. A version 2 code spans 25 modules, or 33 with quiet zones, so 99 pixels; version 10 needs 195; version 40 fits in 555. A 1.3 MP sensor delivers roughly 1,100 pixels across even in a square format, so resolution is almost never the reason a coloured QR fails. Contrast, quiet zone and polarity fail it first.