{"record":{"id":"46d67839ec761ade","repo":"keras-team/keras","slug":"input-arrays-must-be-multi-channel-2d-images","errorCode":null,"errorMessage":"Input arrays must be multi-channel 2D images.","messagePattern":"Input arrays must be multi-channel 2D images\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"keras/src/legacy/preprocessing/image.py","lineNumber":1808,"sourceCode":"    \"\"\"\n    # Input sanity checks:\n    # 1. x must 2D image with one or more channels (i.e., a 3D tensor)\n    # 2. channels must be either first or last dimension\n    if np.unique([row_axis, col_axis, channel_axis]).size != 3:\n        raise ValueError(\n            \"'row_axis', 'col_axis', and 'channel_axis' must be distinct\"\n        )\n\n    # shall we support negative indices?\n    valid_indices = set([0, 1, 2])\n    actual_indices = set([row_axis, col_axis, channel_axis])\n    if actual_indices != valid_indices:\n        raise ValueError(\n            f\"Invalid axis' indices: {actual_indices - valid_indices}\"\n        )\n\n    if x.ndim != 3:\n        raise ValueError(\"Input arrays must be multi-channel 2D images.\")\n    if channel_axis not in [0, 2]:\n        raise ValueError(\n            \"Channels are allowed and the first and last dimensions.\"\n        )\n\n    transform_matrix = None\n    if theta != 0:\n        theta = np.deg2rad(theta)\n        rotation_matrix = np.array(\n            [\n                [np.cos(theta), -np.sin(theta), 0],\n                [np.sin(theta), np.cos(theta), 0],\n                [0, 0, 1],\n            ]\n        )\n        transform_matrix = rotation_matrix\n\n    if tx != 0 or ty != 0:","sourceCodeStart":1790,"sourceCodeEnd":1826,"githubUrl":"https://github.com/keras-team/keras/blob/7a34a03db60bf60042242d6a556fc3be119046a5/keras/src/legacy/preprocessing/image.py#L1790-L1826","documentation":"apply_affine_transform works only on 3D arrays (a 2D image plus a channel dimension). If x.ndim != 3 it raises this ValueError right after the axis checks. Grayscale images stored as 2D arrays and batched 4D tensors are both rejected.","triggerScenarios":"Passing a 2D grayscale image (H, W) with no channel axis; passing a 4D batch (N, H, W, C) directly to apply_affine_transform or random_rotation/random_shift/random_shear/random_zoom.","commonSituations":"Loading grayscale images with PIL/imageio that yield shape (H, W); forgetting to slice a batch tensor before augmenting one image at a time.","solutions":["Expand grayscale images to (H, W, 1) with np.expand_dims(img, -1)","For batches, loop: out[i] = apply_affine_transform(x[i], ...)","Prefer tf.keras.layers.RandomRotation and other preprocessing layers for batched data"],"exampleFix":"# before\nrandom_rotation(gray_img, 20)  # shape (H, W)\n# after\nrandom_rotation(np.expand_dims(gray_img, -1), 20)  # (H, W, 1)","handlingStrategy":"validation","validationCode":"assert img.ndim == 3, f'expected 3D image, got {img.ndim}D'\nif img.ndim == 2:\n    img = img[..., None]","typeGuard":"def is_single_image(x):\n    return getattr(x, 'ndim', None) == 3","tryCatchPattern":null,"preventionTips":["Normalize grayscale images to (H, W, 1) at load time","Loop over the batch dimension instead of passing batches"],"tags":["keras","preprocessing","image","tensor-shape"],"backgroundTag":"tensor-dimension-mismatch","analyzedSha":"7a34a03db60bf60042242d6a556fc3be119046a5","analyzedAt":"2026-08-25T21:25:25.994Z","schemaVersion":2},"datasetVersion":"2026-08-26T02:17:13.382Z"}