Promotional Display Alignment Checks: Automating Compliance for Dynamic Retail Fixtures

Within the Planogram Sync & SKU Mapping Strategies section, promotional display alignment checks are the stage that validates everything standard bay logic deliberately refuses to touch: endcaps, freestanding floor displays, clip strips, pallet drops, and secondary placement zones. These fixtures operate under compressed campaign windows, aggressive SKU rotation, contractual vendor commitments, and physical geometry that bears no resemblance to a linear gondola shelf — so feeding them through the standard facing-count pipeline produces noise, not compliance. This page defines the data contract a promotional check enforces, the spatial-reasoning architecture that scores a non-linear fixture, the campaign-scoped tolerance configuration that tunes it, the failure modes you will actually hit on glossy promotional packaging under fluorescent glare, and the throughput you need to design against when an entire chain resets its endcaps on the same Monday morning.

The promotional-display alignment stage, end to end A campaign manifest and edge-captured display imagery enter together; the manifest is the ground truth and carries an effective-date window. Frames are geometrically rectified with perspective correction and CLAHE, run through object detection to bounding boxes, mapped to fixture zones using a tiered, curved, or pyramid geometry chosen from the fixture type, then scored against campaign rules in the order identity, effective-date, then spatial drift, before a typed display-compliance record is emitted. The effective-date window is annotated under the inputs and the zone tolerance bands under the scoring gate. Promotional-alignment stage · manifest + display imagery → display-compliance record Campaign manifest + display imagery manifest = ground truth Rectification perspective + CLAHE Object detection bounding boxes YOLOv8 / RT‑DETR Fixture-zone mapping tiered · curved · pyramid Campaign-scoped scoring gate identity→dates→drift Display-compliance record graded per-zone state effective-date window valid_from → valid_to · capture inside zone tolerance bands IoU · centroid mm · orientation° authorization and effective-date are checked before spatial drift — an aligned but expired or unauthorized SKU is still a violation

Concept & Data Contract Jump to heading

A promotional check sits outside the standard fixture pipeline and has two hard boundaries of its own. At the inbound boundary it consumes two things: edge-captured display imagery (handheld audit photos or autonomous-robot frames) and a campaign manifest — the authoritative description of what the display should contain during a specific promotional window. The manifest is not a planogram in the gondola sense; it describes a fixture type (endcap, floor stack, pallet, clip strip), a set of authorized promotional SKUs with their effective-date range, store-tier eligibility, an approved substitution hierarchy for stockouts, and the contractual facing or share-of-display commitment the vendor paid for. At the outbound boundary it produces a display-compliance record: a per-zone reconciliation of expected against observed product, a graded compliance state, and the provenance needed to defend a vendor chargeback months after the campaign ends.

The reason this stage cannot reuse the standard facing contract is the effective-date window. A promotional SKU is legitimate only between its valid_from and valid_to timestamps; the same physical product photographed a week early or a week late is a violation. So the manifest, not the permanent catalog, is the ground truth, and every record references the manifest revision it was scored against.

A display-compliance record looks like this:

{
  "planogram_id": "PROMO-2026-SUMMER-ENDCAP-07",
  "fixture_id": "STORE-0421-ENDCAP-A3",
  "fixture_type": "endcap_tiered",
  "campaign_revision": 12,
  "capture_timestamp": "2026-06-28T08:11:04Z",
  "valid_from": "2026-06-22T00:00:00Z",
  "valid_to": "2026-07-05T23:59:59Z",
  "compliance_percentage": 79.5,
  "zones": [
    {
      "zone_id": "STORE-0421-ENDCAP-A3-TIER-2",
      "expected_sku": "00012345678905",
      "observed_sku": "00012345678905",
      "iou": 0.71,
      "centroid_offset_mm": 34.0,
      "orientation_offset_deg": 6.2,
      "compliance_state": "aligned"
    }
  ],
  "out_of_stock_flags": ["STORE-0421-ENDCAP-A3-TIER-3"],
  "misplaced_sku_list": ["00098765432107"],
  "price_tag_mismatch_count": 1,
  "unauthorized_sku_list": ["00055500011122"]
}

The compliance_state enum is graded rather than binary — aligned, misaligned, wrong_sku, out_of_stock, unauthorized, and expired_promo — because a category manager defending a vendor agreement needs to distinguish a hero product that drifted one tier from a competitor SKU squatting on paid display space. The unauthorized_sku_list is unique to this stage: on standard shelves an unexpected SKU is a misplacement, but on a paid endcap it is a contractual breach worth a chargeback, so it gets its own first-class field. As with the rest of the section, the graded states reconcile downstream against the velocity- and margin-weighted scoring owned by Automating Facings vs Actuals Validation, which merges promotional results back into reporting only after they are scored against campaign rules — never against baseline bay tolerances.

Implementation Architecture Jump to heading

The pipeline runs four stages over each captured frame: rectify, detect, map to fixture zones, then score against the manifest. Rectification matters more here than on flat gondola runs because promotional fixtures are photographed at awkward angles — a floor stack shot from above, an endcap caught at the end of an aisle with heavy perspective skew. The raw frame is corrected with the store’s calibration matrix and adaptive histogram equalization (CLAHE) before any inference runs, which is the same front-end discipline the detector relies on in Bounding Box Extraction & SKU Localization.

Detection produces bounding boxes from a retail-tuned model — a YOLOv8 or RT-DETR variant, configured as described in Optimizing YOLOv8 for Grocery Shelf Detection. The non-trivial step is mapping pixel detections to fixture zones on a non-linear display. A flat shelf can be sliced with horizontal shelf-line detection; an endcap pyramid or a gravity-fed bin cannot. The mapper therefore selects a geometry strategy from the manifest’s fixture_type — Cartesian zone grids for flat tiers, polar transforms for curved facings, and z-ordered tier bands for stacked pyramids — and assigns each detection to the zone whose intersection-over-union it maximizes:

from dataclasses import dataclass


@dataclass(frozen=True)
class FixtureZone:
    zone_id: str
    expected_sku: str
    polygon_mm: list[tuple[float, float]]  # zone boundary in fixture-plane mm
    tier: int


@dataclass(frozen=True)
class Detection:
    sku: str | None          # resolved barcode/OCR identity, None if unresolved
    polygon_mm: list[tuple[float, float]]
    centroid_mm: tuple[float, float]
    orientation_deg: float
    confidence: float


@dataclass(frozen=True)
class ZoneResult:
    zone_id: str
    expected_sku: str
    observed_sku: str | None
    iou: float
    centroid_offset_mm: float
    orientation_offset_deg: float
    compliance_state: str


def _iou(a: list[tuple[float, float]], b: list[tuple[float, float]]) -> float:
    from shapely.geometry import Polygon  # vetted geometry kernel; avoid hand-rolled clipping
    pa, pb = Polygon(a), Polygon(b)
    if not pa.is_valid or not pb.is_valid:
        return 0.0
    union = pa.union(pb).area
    return pa.intersection(pb).area / union if union else 0.0


def map_detections_to_zones(
    zones: list[FixtureZone],
    detections: list[Detection],
    min_iou: float = 0.4,
) -> list[ZoneResult]:
    """Assign each detection to the zone it best overlaps and score alignment.

    Greedy by descending IoU so the strongest overlap claims a zone first,
    which prevents a single sprawling detection from capturing every tier.
    """
    pairs = sorted(
        ((_iou(z.polygon_mm, d.polygon_mm), z, d) for z in zones for d in detections),
        key=lambda t: t[0],
        reverse=True,
    )
    claimed_zones: set[str] = set()
    claimed_dets: set[int] = set()
    results: list[ZoneResult] = []

    for iou, zone, det in pairs:
        if iou < min_iou or zone.zone_id in claimed_zones or id(det) in claimed_dets:
            continue
        claimed_zones.add(zone.zone_id)
        claimed_dets.add(id(det))
        cx, cy = _centroid(zone.polygon_mm)
        offset = ((det.centroid_mm[0] - cx) ** 2 + (det.centroid_mm[1] - cy) ** 2) ** 0.5
        results.append(
            ZoneResult(
                zone_id=zone.zone_id,
                expected_sku=zone.expected_sku,
                observed_sku=det.sku,
                iou=round(iou, 3),
                centroid_offset_mm=round(offset, 1),
                orientation_offset_deg=round(abs(det.orientation_deg), 1),
                compliance_state="pending",
            )
        )

    for zone in zones:                       # zones no detection claimed are empties
        if zone.zone_id not in claimed_zones:
            results.append(
                ZoneResult(zone.zone_id, zone.expected_sku, None, 0.0, 0.0, 0.0, "out_of_stock")
            )
    return results

Scoring then resolves each pending zone against the manifest. This is where the campaign rules diverge from bay logic: the engine checks identity, effective-date validity, and authorization before it ever looks at spatial drift, because a perfectly aligned expired or unauthorized SKU is still a violation:

def score_zone(
    result: ZoneResult,
    authorized_skus: set[str],
    captured_at: str,
    window: tuple[str, str],
    tolerance: dict[str, float],
) -> ZoneResult:
    valid_from, valid_to = window
    in_window = valid_from <= captured_at <= valid_to

    if result.observed_sku is None:
        state = "out_of_stock"
    elif result.observed_sku not in authorized_skus:
        state = "unauthorized"
    elif not in_window:
        state = "expired_promo"
    elif result.observed_sku != result.expected_sku:
        state = "wrong_sku"
    elif (
        result.iou >= tolerance["min_iou"]
        and result.centroid_offset_mm <= tolerance["max_centroid_mm"]
        and result.orientation_offset_deg <= tolerance["max_orientation_deg"]
    ):
        state = "aligned"
    else:
        state = "misaligned"

    return ZoneResult(**{**result.__dict__, "compliance_state": state})

The libraries are chosen deliberately: shapely provides a vetted polygon kernel so curved and tiered zones are handled by the same IoU call rather than a fragile hand-rolled clipper, and greedy descending-IoU assignment stops one sprawling pyramid detection from swallowing every tier. The fixture-level compliance_percentage is then a contractually weighted aggregation — paid hero zones count more than filler — rather than a flat ratio of aligned zones.

Production Configuration & Tuning Jump to heading

Campaign-scoped tolerances belong in versioned configuration, never in code, because a promotional buyer changes them per campaign without a deploy, and because the bands themselves are reconciled against ground-truth audits by Threshold Tuning for Compliance Accuracy. Promotional fixtures need looser spatial bands than gondola shelves but stricter identity and authorization rules. A typical configuration scopes tolerance by fixture type and overrides per campaign tier:

promotional_alignment:
  review_confidence_floor: 0.72      # below this, route frame to human review
  min_consecutive_scans_for_alert: 2 # suppress single-frame shopper occlusion
  tolerance_by_fixture:
    endcap_tiered:
      min_iou: 0.50
      max_centroid_mm: 60.0
      max_orientation_deg: 15.0
    floor_pyramid:
      min_iou: 0.40                   # stacked silhouettes overlap; loosen IoU
      max_centroid_mm: 90.0
      max_orientation_deg: 25.0
    clip_strip:
      min_iou: 0.55
      max_centroid_mm: 40.0
      max_orientation_deg: 10.0
  campaign_overrides:
    cpg_paid_hero: { min_iou: 0.65, max_centroid_mm: 30.0 }  # contractual, strict
    seasonal_decor: { min_iou: 0.30, max_centroid_mm: 120.0 } # cosmetic, wide
  weighting:
    contractual: 0.7                  # paid placement dominates the score
    cosmetic: 0.3

The review_confidence_floor of 0.72 is the single most consequential setting on promotional fixtures, because glossy seasonal packaging under fluorescent glare produces a wide band of low-confidence detections; sending anything below that floor to human review rather than auto-flagging is what keeps a campaign launch from drowning ops in false violations. The min_consecutive_scans_for_alert gate of 2 exists for the same reason it does on standard shelves — a shopper standing in front of a floor stack should not generate an out-of-stock — and the wide floor_pyramid IoU of 0.40 reflects that stacked silhouettes genuinely overlap. The contractual weighting of 0.7 ensures a competitor SKU squatting on a paid hero zone moves the score far more than a cosmetic décor element drifting a few centimeters.

Failure Modes & Debugging Workflow Jump to heading

When a display-compliance record looks wrong, the root cause is almost always one of five recurring problems. Work them in this order:

  1. Glare-induced detection dropout on glossy promo packaging. Symptom: high-gloss seasonal or beverage displays read empty zones or low confidence, worse near overhead fluorescents. Reproduce by replaying frames captured at the glare angle. Fix: confirm CLAHE rectification is running before inference, add exposure-bracketed captures, and let the review_confidence_floor route the borderline frames to human review rather than tuning the detector blind.
  2. Expired or early campaign flagged as the wrong violation. Symptom: a physically correct display scores unauthorized or wrong_sku when the real problem is the capture fell outside the valid_from/valid_to window. Reproduce by scoring a frame timestamped a day before campaign launch. Fix: verify the campaign_revision and effective-date window on the record match the campaign active at capture_timestamp — the date check must run before the identity check, as the scoring order enforces.
  3. Tier bleed on stacked pyramids. Symptom: products assigned to an upper tier are scored against a lower zone, so one tier over-faces while its neighbor under-faces. Reproduce on any multi-tier pyramid photographed from below. Fix: confirm the z-ordered tier bands are derived from the manifest fixture_type and that greedy descending-IoU assignment is preventing a single tall detection from claiming multiple tiers; for ambiguous cases add depth-aware segmentation.
  4. Unauthorized substitution misclassified as misplacement. Symptom: a vendor-approved out-of-stock substitution is flagged unauthorized, or a genuine competitor intrusion is excused as a swap. Root cause is the substitution hierarchy in the manifest, not spatial scoring. Fix: confirm the approved substitution list is loaded into authorized_skus, and resolve borderline identity through the multi-frame consensus and barcode fallback in Error Handling in Computer Vision Pipelines.
  5. Calibration drift on autonomous robots and handheld devices. Symptom: centroid offsets creep upward fleet-wide over weeks, inflating misaligned states with no real merchandising change. Reproduce by comparing reprojection error across a device’s capture history. Fix: schedule weekly fiducial-marker (AprilTag/ArUco) calibration, log reprojection error, and trigger automatic recalibration when it exceeds 1.5 pixels.

Keep a labeled error repository bucketed by these five root causes; the quarterly distribution tells you whether to retrain the detector, re-tune the bands, or service a device, and it is the only way to stop the same misclassification recurring at every campaign reset.

Scaling & Performance Benchmarks Jump to heading

Promotional scoring is cheap per record — polygon IoU and a handful of comparisons over already-resolved detections — so the scaling constraint is burst fan-out, not per-frame compute. The defining load characteristic of this stage is synchrony: an entire chain flips its endcaps on the same campaign-launch morning, so captures do not arrive smoothly, they arrive as a wall. A single CPU worker scores a typical 12-zone endcap in under 10 ms, but the queue depth on launch day can spike by two orders of magnitude over a steady-state weekday.

Partition the scoring queue by store_id to preserve per-fixture capture ordering, because the consecutive-scan alert logic corrupts if frames for the same fixture interleave out of order. Design against a soft latency SLA of one minute from capture to a category manager’s dashboard during the launch window, and autoscale on consumer lag rather than CPU — a backlog here almost always signals a coordinated reset, not a compute shortfall, and adding scoring workers clears it linearly. Follow the same edge discipline as the rest of the platform: run rectification and detection on the store-level server that already hosts inference, and sync only the typed display-compliance record upstream, never raw promotional frames. At chain scale that keeps the promotional tier a rounding error on the analytics bill while still emitting a per-zone, per-capture audit trail durable enough to settle a vendor chargeback dispute long after the campaign ends.

Frequently Asked Questions Jump to heading

Why does promotional alignment need its own pipeline instead of the standard facing check? Because the inputs and the rules are different. Promotional fixtures have no linear shelf line to slice, so zone mapping needs polar and tiered geometry rather than horizontal facing tallies. More importantly, a promotional SKU is valid only inside its effective-date window and only if it appears on the paid authorization list — concepts that do not exist in baseline bay logic. Scoring an endcap against standard tolerance bands produces noise, which is why these results are computed separately and merged into reporting only after campaign-rule scoring.

How are tiered and curved displays mapped when there is no shelf line? The mapper selects a geometry strategy from the manifest fixture_type. Flat tiers use a Cartesian zone grid, curved facings use a polar transform measuring radial displacement from the fixture centerline, and stacked pyramids use z-ordered tier bands. Each detection is then assigned to the zone whose intersection-over-union it maximizes, with greedy descending-IoU assignment so one sprawling detection cannot claim every tier.

Why score identity and dates before spatial alignment? Because a perfectly placed product can still be a violation. A SKU photographed outside its valid_from/valid_to window is an expired promotion, and a competitor product on a paid endcap is a contractual breach regardless of how neatly it sits in the zone. Checking authorization and the effective-date window before spatial drift ensures these higher-severity states are not masked by a clean IoU score.

What confidence floor keeps glossy promo packaging from generating false violations? Set review_confidence_floor to 0.72. High-gloss seasonal and beverage packaging under fluorescent glare produces a wide band of low-confidence detections; routing anything below the floor to human review rather than auto-flagging keeps a campaign launch from flooding ops with false positives. Pair it with CLAHE rectification before inference and exposure-bracketed capture to raise the raw confidence in the first place.

How does an unauthorized SKU on a paid display differ from a normal misplacement? On a standard shelf an unexpected SKU is a misplacement to be corrected. On a paid promotional fixture it is a breach of a vendor agreement, so it gets its own unauthorized_sku_list field and its own unauthorized compliance state. That distinction is what lets a category manager turn a detection into a documented chargeback, which is why the manifest’s authorization list — including approved out-of-stock substitutions — is load-bearing for this stage.

Back to top