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:
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:
- 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.
- Keep black modules, tint the surroundings. Brand colour in the background and quiet zone, PCS 1.0, polarity intact.
- 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:
- Interface
- GigE
- Resolution
- Up to 3 MP
- Lens mount
- M10
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:
- Interface
- Fast Ethernet
- Resolution
- Up to 3 MP
- Shutter
- Global
- Lens mount
- M5.8
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.
If you want headroom for several codes in one frame, a 5 MP global-shutter reader carries it:
- Interface
- Fast Ethernet
- Resolution
- 5 MP
- Sensor format
- 1/1.7"
- Shutter
- Global
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:
- Print or render at final size, quiet zone in its final colour.
- 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.
- Require 100 consecutive decodes with zero misses at commissioning, then alarm in production if the read rate drops below 99 percent.
- 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:
- Interface
- Fast Ethernet
- Resolution
- Up to 3 MP
- Shutter
- Global
- Lens mount
- M12
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.



