LLM_log #016: RGB is for Screens. Lab is for Humans — Color Scoring for Living Room Images

LLM_log #016: RGB is for Screens. Lab is for Humans — Color Scoring for Living Room Images

Highlights:

Every computer vision pipeline that touches color starts with the same mistake: using RGB. RGB is built for screens, not for human perception. In this post we build a complete color scoring system for living room images — from the right color space (Lab), through palette extraction (K-means), to a two-color harmony scorer tested on 10 global brand palettes. We discover why luxury brands deliberately score low, and what that means for your model. This is Post 01 of 7 in the Color in Living Rooms 101 series.

Color scoring pipeline: Image to Lab to K-means to Top-2 Colors to Score Pair with three scoring axes

The complete color scoring pipeline — from raw image through Lab conversion, K-means palette extraction, to the three-axis scoring system.

Tutorial Overview:

  1. The fundamental lie RGB tells you
  2. The Lab color space and ΔE
  3. Why green dominates — the biology
  4. The three dimensions of every color
  5. How K-means works — the magnet intuition
  6. From one color to two
  7. The two-color scoring algorithm
  8. What the scorer reveals — 8 pairs
  9. Color hierarchy — what the eye reads first
  10. Color and psychology — the two layers
  11. 10 global brands — scored
  12. The luxury cluster — why it exists
  13. Code appendix

1. The fundamental lie RGB tells you

Open any photo editing tool and you have RGB — three numbers, 0–255, for red, green, blue. Seems logical. The problem is that RGB is built for screens, not for humans.

Equal numeric steps in RGB are not equal visual changes.

Step Blue channel (+50 each) Green channel (+50 each)
1 (0, 0, 80) (0, 80, 0)
2 (0, 0, 130) (0, 130, 0)
3 (0, 0, 180) (0, 180, 0)
4 (0, 0, 230) (0, 230, 0)

Same math. Completely different visual experience. The green row feels dramatic. The blue row barely moves.

RGB inequality — blue steps barely change while green steps show huge jumps

Fig 1. Equal +50 steps across the blue channel (top) vs the green channel (bottom). Same arithmetic — completely different visual experience.

Why? Human vision is not linear. We have three types of cone cells and they don’t respond equally across the spectrum. We’re roughly 3× more sensitive to green than blue. RGB ignores this completely.


2. The Lab color space and ΔE

Lab was designed by the CIE (the international lighting authority) to match human perception. Three channels:

Channel Name Range What it means
L* Lightness 0 → 100 0 = absolute black, 100 = absolute white
a* Green ↔ Red −128 → +127 negative = green, positive = red
b* Blue ↔ Yellow −128 → +127 negative = blue, positive = yellow

Built around opponent color theory — how your actual cone cells encode color signals. Your brain doesn’t measure R, G, B separately. It measures light-vs-dark, red-vs-green, and blue-vs-yellow. That’s exactly what \(L^*, a^*, b^*\) are. The b* axis is your warm/cool temperature signal: negative = cool (blue), positive = warm (yellow/amber).

Lab color space — L* lightness, a* green-red, b* blue-yellow axes with delta-E distance

Fig 2. The three Lab axes visualized. L* goes black to white. a* goes green to red. b* goes blue to yellow. The ΔE formula gives perceptual distance in this space.

The payoff: ΔE (Delta E)

Delta E equation: square root of delta L squared plus delta a squared plus delta b squared

ΔE — the perceptual distance formula. One number that tells you how different two colors look to a human.

$$\Delta E = \sqrt{\Delta L^{*2} + \Delta a^{*2} + \Delta b^{*2}}$$

A single number: the perceptual distance between two colors.

ΔE Meaning
1 Just noticeable difference (JND) — smallest a human can see
< 10 Similar colors
> 50 High contrast, very different

In RGB you have no equivalent. A numeric distance of 50 in RGB could be a tiny visual change or an enormous one depending on which channel and which range.

Key idea: RGB is a coordinate system for pixels. Lab is a coordinate system for human vision. Use the wrong one and every downstream calculation — palette extraction, harmony scoring, contrast measurement — is built on a lie.


3. Why green dominates — the biology

Human cone cells: S cones 6% blue, L cones 32% red, M cones 62% green

Cone cell distribution — circle sizes proportional to population. Green dominates with 62% of all cones.

Your eye has three types of cone cells: S (short), M (medium), L (long wavelength). The critical numbers:

Cone Color % of all cones Peaks at
S Blue 6% ~450nm
L Red 32% ~560nm
M Green 62% ~530nm

62% of your cones are M-type and they peak at green-yellow. And L cones (red-sensitive) also have heavy overlap in that same zone. So green gets double coverage — both M and L cones fire together for it. Blue only gets the 6% S cones. This is why equal steps in green feel huge and equal steps in blue feel tiny.

The practical consequence: Green is the cheapest way to create visual interest in a room. A single plant pulls attention disproportionately. Green also reads as safe and natural at the biological level — it signals food and water nearby. No other color combines that physical dominance with psychological safety.


4. The three dimensions of every color

Every color has exactly three independent properties simultaneously. Confuse them and everything downstream breaks.

Hue wheel showing 0-360 degrees with warm and cool labels

Fig 3. Hue is just an angle on the color wheel — 0° to 360°. Warm hues (red–yellow) occupy 0°–180°. Cool hues (blue–green) occupy 180°–360°.

Hue is the angle on the color wheel. Just a number from 0° to 360°. Red = 0°, yellow = 90°, green = 150°, blue = 240°. When a designer says two colors “clash” they usually mean they’re at an awkward angle — not 0° (same family), not 60° (cohesive analogous), not 180° (intentional complementary).

Saturation is how much color is in the color. Same hue angle, just drained toward grey. A fully saturated red is a traffic light. 20% saturation red is a dusty rose. The critical insight: saturation 0% is always grey — regardless of hue.

Lightness is the brightness dial. Same red, same saturation — just how much light. A high-lightness red is blush pink. A low-lightness red is burgundy. Still 0° hue.

Saturation strips from vivid to grey, lightness strips from light to dark

Fig 4. Saturation drains color toward grey (left). Lightness adjusts brightness (right). Both are completely independent of hue angle.

Hue distance — how designers measure relationships

Angle apart Relationship Feeling
0°–30° Monochromatic Same family, very cohesive
30°–60° Analogous Related, harmonious
150°–210° Complementary Maximum contrast, maximum energy
Everything else Unclassified Tension, clash, or just awkward

Real room names are mostly just saturation levels

Linen, terracotta, clay, burnt orange, sage, forest green — most of these are not different hue families. Several are the same orange-red hue at different saturation levels. When interior designers say “muted palette” they almost always mean low saturation, not different hues.

Room name Hue Saturation Lightness
Linen / plaster orange-red 8% 82%
Sand / terracotta orange-red 35% 72%
Clay / brick orange-red 55% 55%
Burnt orange orange-red 75% 45%
Sage green 25% 55%
Forest / emerald green 70% 30%

Three lines to lock it in:

  • Hue = which color family. The angle. Red, green, blue, orange.
  • Saturation = how much of that color. Full = vivid. Zero = grey.
  • Lightness = how much light. High = pale/bright. Low = deep/dark.

5. How K-means works — the magnet intuition

K-means answers this question: given thousands of pixels, what are the K most representative colors?

Imagine every pixel in the image as a physical dot floating in 3D space. Its coordinates are its color (\(L^*, a^*, b^*\)). Similar colors cluster close together. You want to find the natural cluster centers.

The algorithm — 4 steps, repeating

  1. Place K random magnets anywhere in the space (random starting centroids)
  2. Every pixel snaps to its nearest magnet — nearest by Euclidean distance \(\sqrt{\Delta L^{*2} + \Delta a^{*2} + \Delta b^{*2}}\)
  3. Each magnet slides to the average position of all pixels attached to it
  4. Repeat steps 2–3 until the magnets stop moving

Pixel cloud in color space showing three natural clusters

Fig 5. Start: every pixel as a dot in 3D color space. Three natural clusters visible — beige, dark brown, blue-grey. Goal: find the center of each cluster.

Three random centroids placed in color space

Fig 6. Step 1: place K=3 random centroids anywhere in the space. Starting position doesn’t matter.

Pixels assigned to nearest centroid by color

Fig 7. Step 2: every pixel snaps to its nearest centroid. Assignments are probably wrong — centroids aren’t in position yet.

Centroids moving toward the mean of their clusters

Fig 8. Step 3: each centroid slides to the mean position of all pixels attached to it. Repeat until convergence.

Final converged centroids forming the extracted palette

Fig 9. Converged: centroid color = palette color. Fraction of pixels per cluster = coverage weight. This weight feeds your 60-30-10 analysis.

Why the color space matters critically

The same algorithm in two different spaces gives completely different results. “Nearest” in RGB is numeric closeness — meaningless to human perception. “Nearest” in Lab is \(\Delta E\) — perceptual closeness. A bright green and a bright yellow-green have similar R+G values in RGB, so K-means groups them together. In Lab their \(\Delta E\) is large — they look clearly different to a human — so K-means correctly separates them.

RGB wrongly groups green and yellow together while Lab correctly separates them

Fig 10. RGB K-means groups green + yellow-green together (similar channel values). Lab K-means correctly separates them (\(\Delta E\) is large). Same code — one line changes.

RGB K-means: "nearest" = numerically close      → wrong clusters
Lab K-means: "nearest" = perceptually close (ΔE) → correct clusters
from skimage.color import rgb2lab
from sklearn.cluster import KMeans
import numpy as np

def extract_palette_lab(image_rgb, k=5):
    pixels = image_rgb.reshape(-1, 3).astype(np.float32) / 255.0
    lab = rgb2lab(pixels.reshape(1, -1, 3)).reshape(-1, 3)
    km = KMeans(n_clusters=k, n_init=10, random_state=42)
    labels = km.fit_predict(lab)
    coverage = np.bincount(labels, minlength=k) / len(labels)
    return km.cluster_centers_, coverage  # centers in Lab space

6. From one color to two — why color only exists in relationship

One color alone means nothing. Color only exists in relationship. The moment you put two colors next to each other, three things happen simultaneously that your eye evaluates instantly:

  1. Hue relationship — where are they on the color wheel, how far apart? Determines harmony type.
  2. Lightness contrast \(\Delta L^*\) — is one clearly darker? Without this, neither wins. No hierarchy.
  3. Saturation balance — are they competing or is one supporting the other?

All three have to work. Perfect hue harmony + zero lightness contrast = muddy. Perfect contrast + zero harmony = aggressive. Great matching means all three axes are intentional.


7. The two-color scoring algorithm

You have two hex colors. You want one number that says how well they match. You need to measure three independent axes and combine them. A pair can fail in three completely different ways:

  • Same lightness → no hierarchy (axis 1 fails)
  • Wrong hue angle → unresolved clash (axis 2 fails)
  • Both saturated → competing for dominance (axis 3 fails)

Axis 1 — Lightness contrast (\(\Delta L^*\)) — weight: 45%

Extract \(L^*\) from each color. Subtract. That’s it.

\(\Delta L^*\) What the eye sees Score
0–15 Muddy — no winner, eye bounces 0.10
15–30 Soft contrast — readable but gentle 0.50
30–60 Clear hierarchy — sweet spot 0.90
>60 Dramatic — works but can feel harsh 0.75

Three color pairs showing low, optimal, and extreme lightness contrast

Fig 11. Left: \(\Delta L^*\)=4 — muddy, no winner. Centre: \(\Delta L^*\)=70 — clear hierarchy, sweet spot. Right: \(\Delta L^*\)=100 — dramatic, can overpower.

This gets 45% of the weight because it’s the foundation. Even terrible hue combinations work if one color is clearly darker than the other. Without \(\Delta L^*\), no pair can succeed.

Axis 2 — Hue harmony — weight: 35%

Convert each color to HSV, extract the hue angle (0°–360°), measure the shortest arc between them.

Distance Relationship Score
One color sat < 0.12 Neutral — always works 0.80
< 30° Monochromatic 0.75
30°–60° Analogous — cohesive 0.85
150°–210° Complementary — maximum energy 0.95
Anything else Unclassified — tension or clash 0.40

Three pairs showing analogous, complementary, and unclassified hue relationships

Fig 12. 40° analogous (0.85), 180° complementary (0.95), 100° unclassified (0.40). The unclassified zone is where most bad rooms live.

The neutral rule is critical: if one color has near-zero saturation, hue angle is irrelevant. Cream doesn’t have a hue fight with navy because cream has no hue to fight with.

Axis 3 — Saturation balance — weight: 20%

Measure the saturation of each color (0–1 scale). Take the absolute difference.

Sat difference Meaning Score
> 0.35 Clear role: one dominant, one supporting 0.95
0.15–0.35 Mild distinction 0.75
Both < 0.20 Both muted — safe but flat 0.65
Both high, diff < 0.15 Competing — neither becomes background 0.30

Three pairs showing clear saturation roles, competing saturations, and both muted

Fig 13. Clear role (diff=0.64, score=0.95) vs competing (diff=0.02, score=0.30) vs both muted (score=0.65).

This gets only 20% because a bad saturation balance can be rescued by strong contrast and good hue. Hermès proves this.

The final score

Two-color scoring formula: 45% contrast, 35% harmony, 20% saturation with all score ranges

The complete scoring formula with all three axes and their score ranges.

$$\text{score} = 0.45 \times \text{contrast} + 0.35 \times \text{harmony} + 0.20 \times \text{saturation}$$

Hermès traced through the algorithm

Hermes orange and dark brown traced through the scorer: contrast 0.90, harmony 0.75, saturation 0.30, final 0.73

Hermès walked through the scorer step by step. The algorithm says “decent.” Your eye says “expensive.”

  • \(\Delta L^*\) = 44 → contrast score = 0.90
  • Hue = 0° apart (both ~25° on wheel) → monochromatic → harmony score = 0.75
  • Sat diff = 0.10 → competing → sat score = 0.30
  • Final = (0.45×0.90) + (0.35×0.75) + (0.20×0.30) = 0.405 + 0.263 + 0.06 = 0.73

The algorithm sees competing saturation and penalizes it. A designer sees competing saturation and calls it expensive tension. That gap is Post 05.


8. What the scorer reveals — 8 example pairs

Pair \(\Delta L^*\) Hue dist Score Verdict
Hermès orange + dark brown 43.2 0.73 decent
Scandi warm white + navy 56.2 170° 0.88 great
Muddy — same tone, same hue 1.4 0.37 poor
Both neutrals — flat 8.2 0.45 poor
Complementary red + green 15.8 146° 0.42 poor
Analogous warm browns 24.3 0.64 decent
Monochromatic two oranges 4.7 0.37 poor
White + pure red 46.8 0.88 great

The Hermès observation: scores only 0.73 — the algorithm sees same hue zone and competing saturation. It doesn’t know this is intentional tension. That’s the gap Post 05 fills — tension as a positive signal, not a flaw.


9. Color hierarchy — what the eye reads first

The eye doesn’t have a preference for light or dark. It has one rule: go to the highest contrast point relative to surroundings. The element with the greatest \(\Delta L^*\) against its background gets read first. Always.

But spatial physics matters in rooms:

  • Dark colors absorb light → appear to advance toward you → feel heavier, closer, grounded
  • Light colors reflect light → appear to recede → feel airier, farther, larger

Five hierarchy cases: dark on light, light on dark, competing, three-level, accent cheat

The five hierarchy cases. Only Cases 1, 2, 4, and 5 work. Case 3 (competing) always fails.

The five hierarchy cases

Case 1 — Clear hierarchy, dark on light. Dark sofa on light wall. \(\Delta L^* = 58\). Eye reads sofa first, wall recedes. Classic, effortless.

Case 2 — Reversed hierarchy, light on dark. Light sofa on dark wall. \(\Delta L^* = 62\). Still works — dramatic and enclosing. The room feels smaller and more intimate.

Case 3 — Competing colors. Same lightness sofa and wall. \(\Delta L^* = 4\). Eye bounces between them looking for a winner. Finds none. Exhausting. This is the muddy middle.

Case 4 — Three-color hierarchy. Dark sofa (\(L^* = 18\)) + mid floor (\(L^* = 52\)) + light wall (\(L^* = 89\)). Every transition \(\Delta L^* > 30\). The room has a clear reading order: sofa → floor → wall. This is the goal.

Case 5 — The accent cheat. Small area of high contrast breaks the neutral hierarchy entirely. A green pillow (\(\Delta L^* = 44\), hue break=160°) in a beige room. 5% of the area. 100% of the first glance. Designers use this deliberately to create focal points.

When do colors compete?

Three conditions — any one is enough:

  1. Same lightness + same saturation → neither can be background, eye oscillates
  2. Same hue family, different saturations → reads as a mistake, not a decision
  3. Both high saturation, different hues → neither recedes, room feels aggressive

The fix in all three cases: one color leads on every axis, or one color is explicitly neutral. You cannot have two winners.


10. Color and psychology — the two layers

Color psychology has two distinct layers that most people confuse.

Layer 1 — Hardwired (universal across cultures)

Built into primate biology. Consistent globally.

Signal Effect Why
High lightness Safe, open, spacious Bright = open savanna, no predators
Low lightness Enclosed, intimate Darkness = threat, survival alert
Warm hues (red, orange) Mild stress arousal, appetite, alertness Blood, fire — attention signals
Cool hues (blue, green) Calm, lower heart rate, time feels faster Water, sky, safety

Layer 2 — Cultural (learned, varies by region)

  • White = mourning (Asia) vs celebration (West)
  • Red = danger (universal biology) vs luck (China) vs revolution (Soviet)
  • Green = envy (English) vs fertility (Middle East)

Your scoring system encodes whichever associations are in its training data. A model trained on Western living rooms learns Western emotional associations. Know this limitation.

Two-color emergent psychology

Combinations produce emotional effects not predictable from individual colors alone.

Five color pairs showing luxury, calm, grounded, stress, and sophisticated tension effects

Fig 14. Five archetypal two-color emotional effects — none of these are predictable from either color alone.

Pair Emotional effect Why
Warm saturated + very dark warm Luxury, confidence Energy held in tension with depth
Light cool neutral + deep navy Calm authority Safe + trustworthy, no biological threat signal
Warm cream + deep forest green Grounded warmth Most-liked living room combo globally — natural, organic
Two fully saturated hues, similar lightness Stress, urgency Eye bounces, heart rate rises
Warm light + cool dark Sophisticated tension Temperature contrast = quiet drama

The last one is the most interesting: warm light + cool dark is not predictable from either color alone. Warm cream is unremarkable. Deep indigo is cold. Together they create what designers call “quiet drama” — the same asymmetry principle as Hermès but across temperature instead of saturation.

The cultural caveat: If you score 5000 living room images scraped from Pinterest (mostly Western, mostly affluent), your scorer will learn that cream + navy = high quality. Show it a Moroccan riad interior with saturated terracotta + deep cobalt and it will score it poorly — not because it’s wrong, but because it’s outside the training distribution. Always know what your data is encoding.


11. 10 famous brands — scored

Running 10 global brands through the two-color scorer. Sorted by score.

Ten global brands scored by two-color harmony from Ferrari 0.85 to Rolex 0.62

Fig 15. Brand palette scores from Ferrari (0.85) down to Rolex (0.62). The luxury cluster at 0.71–0.73 is not random.

Brand Color 1 Color 2 \(\Delta L^*\) Hue dist Harmony type Score
Ferrari #CC0000 #D4AF37 30.3 46° analogous 0.85
Tiffany #82D4C8 #1A1A1A 70.4 171° neutral+color 0.81
Porsche #1A1A1A #C4A84A 60.4 46° neutral+color 0.81
IKEA #0058A3 #FFDA1A 50.6 157° complementary 0.80
McDonald’s #DA291C #FFC72C 35.3 40° analogous 0.76
Hermès #E8863A #3A1F0D 50.4 monochromatic 0.73
LV #8B6914 #1A1200 40.6 monochromatic 0.73
Chanel #000000 #F5F5F0 96.4 neutral pair 0.71
Nike #111111 #F5F5F5 91.5 neutral pair 0.71
Rolex #006039 #AA8B3A 23.9 112° split-comp 0.62

Ferrari scores highest (0.85) — and it’s not obvious why. Red and gold are both warm, both saturated — they should fight. But \(\Delta L^*\)=30 lands exactly in the sweet spot, the 46° analogous angle is cohesive, and saturation difference of 0.26 gives gold a clear supporting role. The algorithm and the eye agree on this one.

Chanel and Nike score identically (0.71) — for completely different reasons. Both score lower than expected because \(\Delta L^*\)>60 gets penalized as “too dramatic.” What the scorer misses: these brands have transcended color. Black + white is a concept, not a palette. Scoring it like a living room is the wrong frame entirely.

Rolex scores lowest (0.62) — the most interesting scorer failure. \(\Delta L^*\)=24 is soft contrast, 112° is an awkward split-complementary angle. By every technical rule, green + gold shouldn’t work. But Rolex has used this pairing for 70 years and it reads as the most expensive combination on the list. \(\Delta L^*\) doesn’t capture cultural association. Green + gold = forest + treasure. The association is so deep the technical weakness disappears.

The pattern that shouldn’t exist but does: luxury brands cluster at 0.71–0.73, mass market at 0.76–0.85. Luxury brands deliberately choose pairings that create tension or transcend the scoring axes. They don’t want “harmonious.” They want “unmistakable.” Mass market brands optimize for immediate legibility — high contrast, clear harmony, unambiguous roles.

The scorer measures color harmony. Brands use color as identity. Those are different problems. A living room palette should score 0.80+. A brand identity can afford to score 0.62 if the tension is deliberate and consistently applied over decades. The Rolex combination only works because every Rolex ad, box, and dial has used it since 1953. The cultural weight overrides the technical weakness. That gap is Post 05.


12. The luxury cluster — why it exists

Luxury brands score 0.62–0.73. Mass market scores 0.76–0.85. This is not random. Four formulas emerge from analyzing 15 luxury brands in Lab space:

Formula 1 — Dark neutral + warm gold.
Color 1: \(L^*\)<15, saturation ≈ 0 (near black). Color 2: \(L^*\)=65–75, \(b^*\)>15 (warm gold/amber). Maximum authority. Black = weight, silence. Gold = warmth, value.
Brands: Rolls Royce, Porsche, Lamborghini

Formula 2 — Dark warm anchor + gold/tan.
Both colors in the same warm hue family (monochromatic, <10° apart). Huge \(\Delta L^*\) gap (35–55). No cool tones anywhere — tension is entirely in lightness.
Brands: Hermès, LV, Cartier, Bottega

Formula 3 — Pure neutral pair.
Both colors near-zero saturation, extreme \(\Delta L^*\). The palette IS the concept — absence of color signals transcendence above color. Only works with decades of brand equity.
Brands: Chanel, Nike, Apple

Formula 4 — Dark cool anchor + warm gold.
Deep green or navy (\(L^*\)=15–35) + warm gold (\(L^*\)=65–75). Temperature contrast: nature + treasure, forest + wealth. Cultural meaning overrides technical weakness.
Brands: Rolex, Gucci, Bentley

Four luxury color formulas: dark neutral plus gold, warm monochromatic, pure neutral, and cool anchor plus gold

Fig 16. Four luxury color formulas visualized. All four avoid complementary angles. Tension comes from the \(L^*\) axis, not the hue axis.

Luxury brands cluster at 0.62-0.73, mass market at 0.76-0.85 on the score bar

Luxury vs mass market score zones. Luxury brands deliberately score lower — tension over harmony.

The luxury Lab fingerprint

Scatter plot showing luxury brands cluster in monochromatic zone with moderate contrast

Fig 17. \(\Delta L^*\) vs hue distance: luxury cluster occupies the monochromatic zone (low hue distance) with moderate-to-high contrast. Mass market clusters toward complementary angles.

The luxury cluster occupies the monochromatic zone (low hue distance, <30°) with moderate-to-high \(\Delta L^*\) (35–65). They use the same hue family with extreme lightness variation. Tension is in the \(L^*\) axis, not the hue axis.

Property Luxury anchor color Luxury secondary
\(L^*\) 10–30 60–80
\(a^*\) 2–10 2–8
\(b^*\) 5–18 10–30
Saturation 0–0.85 0.35–0.75
Temperature warm or neutral warm

Both colors tend warm (positive \(b^*\)). Never blue as the dominant. Never pure vivid primaries. The combination reads as: weight + warmth + restraint.

The luxury formula in Lab space: one dark anchor (\(L^*\)<30), one warm secondary (\(L^*\)=60–75, \(b^*\)>10), both in the same warm hue family, \(\Delta L^*\)=35–55. The scorer sees “monochromatic with competing saturation” and gives it 0.73. Your eye sees it as expensive. That gap is the entire argument of Post 05.


13. Code appendix

Two-color pair scorer

def score_pair(hex1, hex2):
    rgb1, rgb2 = hex_to_rgb(hex1), hex_to_rgb(hex2)
    lab1, lab2 = rgb_to_lab(rgb1), rgb_to_lab(rgb2)
    hsv1, hsv2 = rgb_to_hsv(rgb1), rgb_to_hsv(rgb2)

    # 1. Lightness contrast
    delta_L = abs(lab1[0] - lab2[0])
    if delta_L < 15:   contrast_score = 0.1   # muddy
    elif delta_L < 30: contrast_score = 0.5   # soft
    elif delta_L < 60: contrast_score = 0.9   # clear hierarchy
    else:              contrast_score = 0.75  # dramatic

    # 2. Hue harmony
    ang_dist = angular_distance(hue1, hue2)
    if both_neutral:             harmony_score = 0.70
    elif one_neutral:            harmony_score = 0.80
    elif ang_dist < 30:          harmony_score = 0.75  # monochromatic
    elif ang_dist < 60:          harmony_score = 0.85  # analogous
    elif 150 <= ang_dist <= 210: harmony_score = 0.95  # complementary
    else:                        harmony_score = 0.40  # tension/clash

    # 3. Saturation balance
    sat_diff = abs(s1 - s2)
    if sat_diff > 0.35:   sat_score = 0.95  # clear role
    elif sat_diff > 0.15: sat_score = 0.75  # mild distinction
    else:                 sat_score = 0.30  # competing

    return 0.45 * contrast_score + 0.35 * harmony_score + 0.20 * sat_score

Batch scoring 5000 images

def score_image(image_path):
    hex1, hex2, cov1, cov2 = extract_top2_palette(image_path)
    result = score_pair(hex1, hex2, verbose=False)
    result["coverage_1"] = cov1
    result["coverage_2"] = cov2
    return result

# ~4 img/sec on CPU → 5000 images ≈ 20 min
# output: one row per image with final_score, harmony_type, delta_L, hue_distance

dataHacker.rs — LLM_log #016: Color in Living Rooms 101 — Post 01 of 7

Vladimir Matic | datahacker.rs

Next: Post 02 — The 60-30-10 rule. Why equal coverage always looks chaotic.