PaddlePaddle/PaddleOCR · error

Unexpected recognition channels: ${String(channels)}

Error message

Unexpected recognition channels: ${String(channels)}

What it means

Thrown in rec preprocessing when the first entry of image_shape (channels) parsed from inference.yml is not 3. The rec pipeline only implements 3-channel BGR input construction; a config declaring 1-channel (grayscale) or other channel counts is rejected per-sample rather than at config parse, because imageShape[0] is only checked where pixels are packed.

Source

Thrown at paddleocr-js/packages/core/src/models/rec.ts:194

function preprocess(context: { cv: OpenCv; config: RecModelConfig }, mats: Mat[]): RecSample[] {
  const samples: RecSample[] = [];
  for (let i = 0; i < mats.length; i += 1) {
    samples.push(preprocessSample(context, mats[i], i));
  }
  return samples;
}

function preprocessSample(
  context: { cv: OpenCv; config: RecModelConfig },
  cropMat: Mat,
  inputIndex: number
): RecSample {
  const { cv, config } = context;
  const [channels, targetH, baseW] = config.imageShape;
  const srcW = cropMat.cols;
  const srcH = cropMat.rows;
  if (channels !== 3) {
    throw new Error(`Unexpected recognition channels: ${String(channels)}`);
  }
  const ratio = srcW / Math.max(1, srcH);
  const maxWhRatio = Math.max(baseW / Math.max(1, targetH), ratio);
  const recW = clamp(Math.trunc(targetH * maxWhRatio), 1, MAX_REC_WIDTH);
  const resizedW = Math.min(recW, Math.ceil(targetH * ratio));
  const resized = new cv.Mat();
  const bgr = new cv.Mat();
  cv.resize(cropMat, resized, new cv.Size(resizedW, targetH), 0, 0, cv.INTER_LINEAR);
  if (resized.channels() === 4) {
    cv.cvtColor(resized, bgr, cv.COLOR_RGBA2BGR);
  } else if (resized.channels() === 1) {
    cv.cvtColor(resized, bgr, cv.COLOR_GRAY2BGR);
  } else {
    resized.copyTo(bgr);
  }
  const resizedChw = toBgrFloatCHWFromBgr(bgr.data, resizedW, targetH, REC_NORMALIZE);
  const chw = new Float32Array(3 * targetH * recW);
  const dstPerChannel = targetH * recW;

View on GitHub (pinned to 2661c7c0ef)

Solutions

  1. Set RecResizeImg.image_shape in inference.yml to a 3-channel form, e.g. [3, 48, 320]
  2. If the axes were transposed, restore the canonical [C, H, W] order
  3. Ensure the values are plain YAML integers, not quoted strings
  4. Use the bundled rec model assets to avoid config drift

Example fix

# before (inference.yml)
RecResizeImg:
  image_shape: [1, 48, 320]

# after
RecResizeImg:
  image_shape: [3, 48, 320]
Defensive patterns

Strategy: validation

Validate before calling

const [channels] = recModel.config.imageShape;
if (channels !== 3) {
  throw new Error(`Rec model declares ${channels} channels; this runtime requires 3`);
}

Type guard

function isThreeChannelShape(shape: unknown): shape is [3, number, number] {
  return Array.isArray(shape) && shape.length >= 3 && shape[0] === 3;
}

Try / catch

try { await recModel.predict(cv, crops); } catch (e) {
  if (e instanceof Error && /Unexpected recognition channels/.test(e.message)) {
    // fix inference.yml image_shape to [3, H, W] and reload the model
  } else throw e;
}

Prevention

When it happens

Trigger: Loading a rec model whose inference.yml has RecResizeImg.image_shape: [1, 32, 320] (a grayscale export); a config where image_shape entries are strings (["3","48","320"]) so the !== 3 comparison fails; mis-ordered shape like [48, 320, 3].

Common situations: Re-exporting a rec model trained on grayscale without restoring 3-channel input in the config; hand-editing image_shape and transposing the axes; using an experimental model variant with different channel conventions.

Related errors


AI-assisted analysis of PaddlePaddle/PaddleOCR@2661c7c0ef (2026-08-14). Data as JSON: /api/errors/3bccd44f41c56464. Report an issue: GitHub.