Barcode auf der Flasche — realistische Testfotos aus einem sauberen Code

Ein sauberer DataMatrix (ein Arzneimittel-Packungscode) wird um eine Flasche gelegt und wie ein Handyfoto beleuchtet: Die Module werden zu den Rändern schmaler, die Zeilen biegen sich zu Bögen, wenn die Kamera von oben schaut, das Licht fällt zu den Seiten ab, dazu Glanz, Sensorrauschen und Unschärfe. Der Zylinder ist ein im Skript berechnetes stampGrid-Gitter — kein gezeichnetes Gitter. Genau so erzeugt MLC Barcode den Testsatz für seinen Leser: Jedes Bild stammt aus einem Code mit bekanntem Inhalt, ein Test kann also prüfen, was gelesen wurde. Dieselbe Idee liefert Trainingsdaten für Bildverarbeitungs-Modelle — beliebig viele Varianten, die Labels gibt es gratis dazu.

INPUT
INPUT — Barcode auf der Flasche — realistische Testfotos aus einem sauberen Code
1 clean · 2 bottle 60° · 3 90° from above · 4 turned 30°, from above
1 clean · 2 bottle 60° · 3 90° from above · 4 turned 30°, from above — Barcode auf der Flasche — realistische Testfotos aus einem sauberen Code
JavaScript
// Barcode on a bottle — realistic test photos from one clean code.
// demo_barcode_bottle.js
//
// A clean DataMatrix (a medicine pack code) is wrapped around a bottle and
// lit like a phone photo: the code narrows towards the edges, its rows
// bend into arcs when the camera looks down, the light falls off to the
// sides, a glare stripe, sensor noise and blur. Four
// variants in a 2×2 grid — from flat to "hard to read".
//
// This is how MLC Barcode builds the test set for its reader
// (github.com/hmsoft0815/mlc_barcode, tests/testdata/cylinder.js):
// every image is generated from a code with known content, so a test can
// check what was read. The same idea gives training data for vision
// models — any number of variants, labels for free.
//
// The cylinder is a stampGrid mesh computed in the script (no hand-drawn
// grid): x = cx + R·sin θ, and seen from above a point drops by
// R·(cos θ − cos θ₀)·sin(tilt).
//!INPUT: INPUT
//!OUTPUT: OUTPUT
//!PARAM: NOISE:number=0.03,min=0,max=0.3
//!PARAM: GLARE:number=0.25,min=0,max=1
//!PARAM: SEED:integer=1,min=0,max=1000000
// SEED: gleiche Zahl → gleiches Rauschen (reproduzierbar); 0 → jedes Mal neu.

// 2×2 tiles of 640×400 (photo + caption band): 1280×800, the 16:10 of
// the gallery cards, so the preview shows all four steps.
const TW = 640, TH = 356, LABEL = 44;
const rad = (d) => (d * Math.PI) / 180;

// One photo: SPAN degrees of the circumference covered by the code, the
// bottle turned by TURN, the camera looking down by TILT. SPAN 0 = flat.
function bottlePhoto(span, turn, tilt) {
  const img = Engine.createColoredImage(TW, TH, "#8a8478");
  const code = Engine.loadImage(INPUT);
  const codeW = 180;
  if (span === 0) {
    img.fillRectangle(0, 0, TW, TH, "#f6f5f0");
    const x0 = (TW - codeW) / 2, y0 = (TH - codeW) / 2;
    img.stampAt(code, [px(x0, y0), px(x0 + codeW, y0), px(x0 + codeW, y0 + codeW), px(x0, y0 + codeW)]);
    code.free();
    return img;
  }
  const R = codeW / rad(span);
  const cx = TW / 2 - R * Math.sin(rad(turn)); // the code stays centred
  const left = Math.max(0, Math.round(cx - R)), right = Math.min(TW, Math.round(cx + R));
  img.fillRectangle(left, 0, right - left, TH, "#f6f5f0");

  const cols = 33, rows = 3;
  const height = codeW * Math.cos(rad(tilt));
  const top = (TH - height) / 2;
  const drop = (t) => R * (Math.cos(t) - Math.cos(rad(turn))) * Math.sin(rad(tilt));
  const nodes = [];
  for (let r = 0; r < rows; r++) {
    const y = top + (height * r) / (rows - 1);
    const row = [];
    for (let c = 0; c < cols; c++) {
      const t = rad(turn) + (c / (cols - 1) - 0.5) * rad(span);
      row.push((cx + R * Math.sin(t)) / TW, (y + drop(t)) / TH);
    }
    nodes.push(row);
  }
  img.stampGrid(code, { rows, cols, nodes }, Interp.Bicubic);
  code.free();

  // Light falls off with the surface angle: multiply a gradient over the body.
  const bodyW = right - left;
  const cv = Engine.createCanvas(bodyW, TH);
  const stops = [], colors = [];
  for (let i = 0; i <= 16; i++) {
    const xr = (left + (i / 16) * bodyW - cx) / R;
    const v = Math.round(255 * (1 - 0.55 * (1 - Math.cos(Math.asin(Math.max(-1, Math.min(1, xr)))))));
    const h = v.toString(16).padStart(2, "0");
    stops.push(i / 16);
    colors.push(`#${h}${h}${h}`);
  }
  cv.linearGradient(px(0, 0), px(bodyW, 0), colors, stops);
  cv.drawPathStr(`M 0,0 L ${bodyW},0 L ${bodyW},${TH} L 0,${TH} Z`, false);
  const shade = cv.toImage();
  img.blendAt(shade, px(left, 0), 1.0, Blend.Multiply);
  shade.free();
  cv.free();

  // A soft glare stripe just beside the code.
  if (GLARE > 0) {
    const gx = cx + R * Math.sin(rad(turn - span / 2 - 12));
    const gw = Math.max(8, R * 0.18);
    const gcv = Engine.createCanvas(Math.round(2 * gw), TH);
    gcv.linearGradient(px(0, 0), px(2 * gw, 0), ["#00000000", "#ffffffff", "#00000000"], [0, 0.5, 1]);
    gcv.drawPathStr(`M 0,0 L ${2 * gw},0 L ${2 * gw},${TH} L 0,${TH} Z`, false);
    const glare = gcv.toImage();
    img.blendAt(glare, px(Math.round(gx - gw), 0), GLARE, Blend.Screen);
    glare.free();
    gcv.free();
  }
  return img;
}

const variants = [
  { span: 0, turn: 0, tilt: 0, label: "clean code" },
  { span: 60, turn: 0, tilt: 0, label: "on a bottle, 60°" },
  { span: 90, turn: 0, tilt: 20, label: "90°, camera from above" },
  { span: 90, turn: 30, tilt: 20, label: "90°, turned 30°, from above" },
];

const grid = Engine.createColoredImage(TW * 2, (TH + LABEL) * 2, "#1b1b1d");
variants.forEach((v, i) => {
  const x = (i % 2) * TW, y = Math.floor(i / 2) * (TH + LABEL);
  const photo = bottlePhoto(v.span, v.turn, v.tilt);
  if (v.span > 0) {
    photo.gaussianBlur(0.8);
    if (NOISE > 0) photo.addNoise({ type: "gaussian", sigma: NOISE, color: false, seed: SEED || undefined });
  }
  grid.blendAt(photo, px(x, y), 1.0, Blend.Over);
  photo.free();
  grid.drawText(`${i + 1}  ${v.label}`, x + TW / 2, y + TH + 30, { font: "Inter", size: 22, color: "#e8e6e1", anchor: "middle" });
});
// thin separators between the tiles
grid.fillRectangle(TW - 1, 0, 2, grid.height, "#1b1b1d");
grid.save(OUTPUT);

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0