diff --git a/include/libcamera/internal/software_isp/debayer_geometry.h b/include/libcamera/internal/software_isp/debayer_geometry.h
index 8812a3cf..bf6c6771 100644
--- a/include/libcamera/internal/software_isp/debayer_geometry.h
+++ b/include/libcamera/internal/software_isp/debayer_geometry.h
@@ -1,23 +1,48 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2026, Red Hat Inc.
  *
  * Geometry for scaling and cropping debayered images
  */
 
 #pragma once
 
+#include <utility>
 
 #include <libcamera/geometry.h>
+#include <libcamera/transform.h>
 
 namespace libcamera {
 
 /* Return the visible sensor rectangle, before statistics Bayer alignment. */
 inline Rectangle debayerCrop(const Size &inputSize, const Size &nativeSize,
 			     const Size &outputSize)
 {
 	const Size visibleSize = nativeSize.boundedToAspectRatio(outputSize);
 	return visibleSize.centeredTo(Rectangle(inputSize).center());
 }
 
+/*
+ * Map an image crop to physical pixel-array coordinates in native sensor pixels.
+ * The input must cover the whole analog crop. Undo the sensor transform
+ * before accounting for binning and the analog crop offset. Integer
+ * coordinates and dimensions are truncated as in transformedBetween().
+ */
+inline Rectangle debayerCropToSensor(Rectangle crop, Size inputSize,
+				     const Rectangle &analogCrop,
+				     Transform transform)
+{
+	if (!!(transform & Transform::Transpose)) {
+		std::swap(crop.x, crop.y);
+		std::swap(crop.width, crop.height);
+		std::swap(inputSize.width, inputSize.height);
+	}
+	if (!!(transform & Transform::HFlip))
+		crop.x = inputSize.width - crop.x - crop.width;
+	if (!!(transform & Transform::VFlip))
+		crop.y = inputSize.height - crop.y - crop.height;
+
+	return crop.transformedBetween(Rectangle(inputSize), analogCrop);
+}
+
 } /* namespace libcamera */
