diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index b6491063..d32bb7d2 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -40,60 +40,61 @@ class Debayer;
 class FrameBuffer;
 class PixelFormat;
 class Stream;
 struct StreamConfiguration;
 
 LOG_DECLARE_CATEGORY(SoftwareIsp)
 
 class SoftwareIsp : public Object
 {
 public:
 	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 		    ControlInfoMap *ipaControls);
 	~SoftwareIsp();
 
 	int loadConfiguration([[maybe_unused]] const std::string &filename) { return 0; }
 
 	bool isValid() const;
 
 	std::vector<PixelFormat> formats(PixelFormat input);
 
 	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize);
 
 	std::tuple<unsigned int, unsigned int>
 	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size);
 	uint32_t preferredInputStride(const PixelFormat &inputFormat, const Size &size);
 
 	int configure(const StreamConfiguration &inputCfg,
 		      const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 		      const ipa::softisp::IPAConfigInfo &configInfo,
 		      ControlInfoMap *ipaControls);
+	Rectangle inputCrop() const;
 
 	int exportBuffers(const Stream *stream, unsigned int count,
 			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
 
 	void processStats(const uint32_t frame, const uint32_t bufferId,
 			  const ControlList &sensorControls);
 
 	int start();
 	void stop();
 
 	void queueRequest(const uint32_t frame, const ControlList &controls);
 	int queueBuffers(uint32_t frame, FrameBuffer *input,
 			 const std::map<const Stream *, FrameBuffer *> &outputs);
 
 	void process(uint32_t frame, FrameBuffer *input, FrameBuffer *output);
 
 	Signal<FrameBuffer *> inputBufferReady;
 	Signal<FrameBuffer *> outputBufferReady;
 	Signal<uint32_t, uint32_t> ispStatsReady;
 	Signal<uint32_t, const ControlList &> metadataReady;
 	Signal<const ControlList &> setSensorControls;
 
 private:
 	void saveIspParams(uint32_t frame);
 	void setSensorCtrls(const ControlList &sensorControls);
 	void statsReady(uint32_t frame, uint32_t bufferId);
 	void inputReady(FrameBuffer *input);
 	void outputReady(FrameBuffer *output);
 	std::unique_ptr<Debayer> debayer_;
 	Thread ispWorkerThread_;
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 4f665900..c7b1b2b3 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -79,60 +79,71 @@ namespace libcamera {
  * not apply any real lens shading correction.
  */
 
 /**
  * \var DebayerParams::lscLutVersion
  * \brief Incremented on each \a lscLut change
  */
 
 /**
  * \class Debayer
  * \brief Base debayering class
  *
  * Base class that provides functions for setting up the debayering process.
  */
 
 LOG_DEFINE_CATEGORY(Debayer)
 
 /**
  * \brief Construct a Debayer object
  * \param[in] cm The camera manager
  */
 Debayer::Debayer(const CameraManager &cm)
 	: bench_(cm, "Debayer")
 {
 }
 
 Debayer::~Debayer()
 {
 }
 
+/**
+ * \fn Rectangle Debayer::inputCrop() const
+ * \brief Retrieve the input region visible in the configured output
+ *
+ * The rectangle is in input-buffer coordinates and excludes any additional
+ * alignment performed for statistics sampling. It is valid after a successful
+ * configure() call.
+ *
+ * \return The configured image crop
+ */
+
 /**
  * \fn int Debayer::configure()
  * \brief Configure the debayer object according to the passed in parameters
  * \param[in] inputCfg The input configuration
  * \param[in] outputCfgs The output configurations
  * \param[in] ccmEnabled Whether a color correction matrix is applied
  * \param[in] lscEnabled Whether lens shading correction grid is provided
  *
  * \return 0 on success, a negative errno on failure
  */
 
 /**
  * \fn Size Debayer::patternSize(PixelFormat inputFormat)
  * \brief Get the width and height at which the bayer pattern repeats
  * \param[in] inputFormat The input format
  *
  * Valid sizes are: 2x2, 4x2 or 4x4.
  *
  * \return Pattern size or an empty size for unsupported inputFormats
  */
 
 /**
  * \fn std::vector<PixelFormat> Debayer::formats(PixelFormat inputFormat)
  * \brief Get the supported output formats
  * \param[in] inputFormat The input format
  *
  * \return All supported output formats or an empty vector if there are none
  */
 
 /**
diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
index e30a0527..ef36f3c8 100644
--- a/src/libcamera/software_isp/debayer.h
+++ b/src/libcamera/software_isp/debayer.h
@@ -27,60 +27,61 @@
 
 namespace libcamera {
 
 class CameraManager;
 class FrameBuffer;
 
 LOG_DECLARE_CATEGORY(Debayer)
 
 class Debayer : public Object
 {
 public:
 	Debayer(const CameraManager &cm);
 	virtual ~Debayer() = 0;
 
 	virtual int configure(const StreamConfiguration &inputCfg,
 			      const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 			      bool ccmEnabled,
 			      bool lscEnabled) = 0;
 
 	virtual std::vector<PixelFormat> formats(PixelFormat inputFormat) = 0;
 
 	virtual std::tuple<unsigned int, unsigned int>
 	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) = 0;
 	virtual uint32_t preferredInputStride([[maybe_unused]] const PixelFormat &inputFormat, [[maybe_unused]] const Size &size) { return 0; }
 
 	virtual void process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, const DebayerParams &params) = 0;
 	virtual int start() { return 0; }
 	virtual void stop() {}
 
 	virtual SizeRange sizes(PixelFormat inputFormat, const Size &inputSize) = 0;
+	virtual Rectangle inputCrop() const = 0;
 
 	virtual const SharedFD &getStatsFD() = 0;
 
 	unsigned int frameSize() { return outputConfig_.frameSize; }
 
 	Signal<FrameBuffer *> inputBufferReady;
 	Signal<FrameBuffer *> outputBufferReady;
 
 	struct DebayerInputConfig {
 		Size patternSize;
 		unsigned int bpp;
 		unsigned int stride;
 		std::vector<PixelFormat> outputFormats;
 	};
 
 	struct DebayerOutputConfig {
 		unsigned int stride;
 		unsigned int frameSize;
 	};
 
 	DebayerInputConfig inputConfig_;
 	DebayerOutputConfig outputConfig_;
 	Size outputSize_;
 	PixelFormat inputPixelFormat_;
 	PixelFormat outputPixelFormat_;
 	bool swapRedBlueGains_;
 	Benchmark bench_;
 
 private:
 	virtual Size patternSize(PixelFormat inputFormat) = 0;
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index ae570893..0c025b75 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -20,60 +20,64 @@
 
 #include <libcamera/camera_manager.h>
 
 #include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/software_isp/debayer_params.h"
 #include "libcamera/internal/software_isp/swstats_cpu.h"
 
 #include "debayer.h"
 
 namespace libcamera {
 
 class DebayerCpuThread;
 class DebayerCpu : public Debayer
 {
 public:
 	DebayerCpu(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm);
 	~DebayerCpu();
 
 	int configure(const StreamConfiguration &inputCfg,
 		      const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 		      bool ccmEnabled,
 		      bool lscEnabled) override;
 	Size patternSize(PixelFormat inputFormat) override;
 	std::vector<PixelFormat> formats(PixelFormat input) override;
 	std::tuple<unsigned int, unsigned int>
 	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) override;
 	void process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, const DebayerParams &params) override;
 	int start() override;
 	void stop() override;
 	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize) override;
+	Rectangle inputCrop() const override
+	{
+		return window_.translatedBy({ static_cast<int>(xShift_), 0 });
+	}
 	const SharedFD &getStatsFD() override { return stats_->getStatsFD(); }
 
 private:
 	friend class DebayerCpuThread;
 
 	/**
 	 * \brief Called to debayer 1 line of Bayer input data to output format
 	 * \param[out] dst Pointer to the start of the output line to write
 	 * \param[in] src The input data
 	 *
 	 * Input data is an array of (patternSize_.height + 1) src
 	 * pointers each pointing to a line in the Bayer source. The middle
 	 * element of the array will point to the actual line being processed.
 	 * Earlier element(s) will point to the previous line(s) and later
 	 * element(s) to the next line(s).
 	 *
 	 * These functions take an array of src pointers, rather than
 	 * a single src pointer + a stride for the source, so that when the src
 	 * is slow uncached memory it can be copied to faster memory before
 	 * debayering. Debayering a standard 2x2 Bayer pattern requires access
 	 * to the previous and next src lines for interpolating the missing
 	 * colors. To allow copying the src lines only once 3 temporary buffers
 	 * each holding a single line are used, re-using the oldest buffer for
 	 * the next line and the pointers are swizzled so that:
 	 * src[0] = previous-line, src[1] = currrent-line, src[2] = next-line.
 	 * This way the 3 pointers passed to the debayer functions form
 	 * a sliding window over the src avoiding the need to copy each
 	 * line more than once.
 	 *
 	 * Similarly for bayer patterns which repeat every 4 lines, 5 src
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 5a27a38d..d6636302 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -309,61 +309,62 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg,
 	height_ = inputCfg.size.height;
 
 	if (outputCfgs.size() != 1) {
 		LOG(Debayer, Error)
 			<< "Unsupported number of output streams: "
 			<< outputCfgs.size();
 		return -EINVAL;
 	}
 
 	const StreamConfiguration &outputCfg = outputCfgs[0];
 	SizeRange outSizeRange = sizes(inputCfg.pixelFormat, inputCfg.size);
 	std::tie(outputConfig_.stride, outputConfig_.frameSize) =
 		strideAndFrameSize(outputCfg.pixelFormat, outputCfg.size);
 
 	if (!outSizeRange.contains(outputCfg.size) || outputConfig_.stride != outputCfg.stride) {
 		LOG(Debayer, Error)
 			<< "Invalid output size/stride: "
 			<< "\n  " << outputCfg.size << " (" << outSizeRange << ")"
 			<< "\n  " << outputCfg.stride << " (" << outputConfig_.stride << ")";
 		return -EINVAL;
 	}
 
 	outputPixelFormat_ = outputCfg.pixelFormat;
 	outputSize_ = outputCfg.size;
 	nativeOutputSize_ = outSizeRange.max;
 
 	/*
 	 * The GPU scales the visible sensor crop to outputSize_. Statistics
 	 * consume the original input buffer, so use sensor coordinates here.
 	 */
-	stats_->setWindow(debayerCrop(inputCfg.size, nativeOutputSize_, outputSize_));
+	inputCrop_ = debayerCrop(inputCfg.size, nativeOutputSize_, outputSize_);
+	stats_->setWindow(inputCrop_);
 
 	inputBufferCount_ = inputCfg.bufferCount;
 	outputBufferCount_ = outputCfg.bufferCount;
 
 	return 0;
 }
 
 Size DebayerEGL::patternSize(PixelFormat inputFormat)
 {
 	DebayerEGL::DebayerInputConfig config;
 
 	if (getInputConfig(inputFormat, config) != 0)
 		return {};
 
 	return config.patternSize;
 }
 
 std::vector<PixelFormat> DebayerEGL::formats(PixelFormat inputFormat)
 {
 	DebayerEGL::DebayerInputConfig config;
 
 	if (getInputConfig(inputFormat, config) != 0)
 		return std::vector<PixelFormat>();
 
 	return config.outputFormats;
 }
 
 std::tuple<unsigned int, unsigned int>
 DebayerEGL::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)
 {
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index 6910fa49..90e361ce 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -34,99 +34,101 @@ namespace libcamera {
 
 #define DEBAYER_EGL_MIN_SIMPLE_RGB_GAIN_TEXTURE_UNITS 4
 #define DEBAYER_OPENGL_COORDS 4
 
 class CameraManager;
 
 class DebayerEGL : public Debayer
 {
 public:
 	DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display);
 	~DebayerEGL();
 
 	int configure(const StreamConfiguration &inputCfg,
 		      const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 		      bool ccmEnabled,
 		      bool lscEnabled) override;
 
 	Size patternSize(PixelFormat inputFormat) override;
 
 	std::vector<PixelFormat> formats(PixelFormat input) override;
 	std::tuple<unsigned int, unsigned int> strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) override;
 	uint32_t preferredInputStride(const PixelFormat &inputFormat, const Size &size) override;
 
 	void process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, const DebayerParams &params) override;
 	int start() override;
 	void stop() override;
 
 	const SharedFD &getStatsFD() override { return stats_->getStatsFD(); }
 
 	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize) override;
+	Rectangle inputCrop() const override { return inputCrop_; }
 
 private:
 	static int getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config);
 	int initBayerShaders(PixelFormat inputFormat, PixelFormat outputFormat);
 	int getShaderVariableLocations();
 	void setShaderVariableValues(eGLImage &eGLImageIn, const DebayerParams &params);
 	int debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *mappedInputBuffer, std::optional<DmaSyncer> *inputBufferDmaSyncer);
 
 	eGLImage *getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer);
 	eGLImage *getCachedOutputFrameBuffer(FrameBuffer *output);
 
 	/* Shader program identifiers */
 	GLuint vertexShaderId_ = 0;
 	GLuint fragmentShaderId_ = 0;
 	GLuint programId_ = 0;
 
 	/* Pointer to object representing input texture */
 	std::deque<std::pair<SharedFD, std::unique_ptr<eGLImage>>> eglImageInCache_;
 	std::deque<std::pair<SharedFD, std::unique_ptr<eGLImage>>> eglImageOutCache_;
 	unsigned int inputBufferCount_;
 	unsigned int outputBufferCount_;
 
 	/* LSC lookup table */
 	bool lscEnabled_;
 	std::optional<eGLImage> eglImageLscLookup_;
 	std::optional<uint64_t> lscLutVersion_;
 
 	/* Shader parameters */
 	float firstRed_x_;
 	float firstRed_y_;
 	GLint attributeVertex_;
 	GLint attributeTexture_;
 	GLint textureUniformStep_;
 	GLint textureUniformSize_;
 	GLint textureUniformStrideFactor_;
 	GLint textureUniformBayerFirstRed_;
 	GLint textureUniformProjMatrix_;
 
 	GLint textureUniformBayerDataIn_;
 
 	/* Per-frame AWB gains */
 	GLint awbUniformDataIn_;
 
 	/* Lens shading correction */
 	GLint textureUniformLsc_;
 
 	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
 	GLint ccmUniformDataIn_;
 
 	/* Black Level compensation */
 	GLint blackLevelUniformDataIn_;
 
 	/* Gamma */
 	GLint gammaUniformDataIn_;
 
 	/* Contrast */
 	GLint contrastExpUniformDataIn_;
 
 	Size nativeOutputSize_;
+	Rectangle inputCrop_;
 	std::unique_ptr<SwStatsCpu> stats_;
 	eGL egl_;
 	uint32_t width_;
 	uint32_t height_;
 	GLint glFormat_;
 	unsigned int bytesPerPixel_;
 	uint32_t shaderStridePixels_;
 };
 
 } /* namespace libcamera */
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index 627625b4..8a06e53d 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -279,60 +279,74 @@ uint32_t SoftwareIsp::preferredInputStride(const PixelFormat &inputFormat, const
  * \brief Configure the SoftwareIsp object according to the passed in parameters
  * \param[in] inputCfg The input configuration
  * \param[in] outputCfgs The output configurations
  * \param[in] configInfo The IPA configuration data, received from the pipeline handler
  * \param[out] ipaControls The IPA controls to update
  * \return 0 on success, a negative errno on failure
  */
 int SoftwareIsp::configure(const StreamConfiguration &inputCfg,
 			   const std::vector<std::reference_wrapper<const StreamConfiguration>> &outputCfgs,
 			   const ipa::softisp::IPAConfigInfo &configInfo,
 			   ControlInfoMap *ipaControls)
 {
 	ASSERT(ipa_ && debayer_);
 
 	int ret = ipa_->configure(configInfo, ipaControls);
 	if (ret < 0)
 		return ret;
 
 	ret = debayer_->configure(inputCfg, outputCfgs, ccmEnabled_, lscEnabled_);
 	if (ret < 0)
 		return ret;
 
 	LOG(SoftwareIsp, Info)
 		<< "Input " << inputCfg.size
 		<< "-" << BayerFormat::fromPixelFormat(inputCfg.pixelFormat)
 		<< " stride " << inputCfg.stride;
 
 	return 0;
 }
 
+/**
+ * \brief Retrieve the input region used to produce the configured output
+ *
+ * The rectangle is expressed in input-buffer pixels, before any Bayer
+ * alignment applied by the statistics engine. It is valid after configure()
+ * succeeds and describes the selected CPU or EGL debayer backend.
+ *
+ * \return The configured input crop
+ */
+Rectangle SoftwareIsp::inputCrop() const
+{
+	return debayer_->inputCrop();
+}
+
 /**
  * \brief Export the buffers from the Software ISP
  * \param[in] stream Output stream exporting the buffers
  * \param[in] count Number of buffers to allocate
  * \param[out] buffers Vector to store the allocated buffers
  * \return The number of allocated buffers on success or a negative error code
  * otherwise
  */
 int SoftwareIsp::exportBuffers(const Stream *stream, unsigned int count,
 			       std::vector<std::unique_ptr<FrameBuffer>> *buffers)
 {
 	ASSERT(debayer_ != nullptr);
 
 	/* single output for now */
 	if (stream == nullptr)
 		return -EINVAL;
 
 	return dmaHeap_.exportBuffers(count, { debayer_->frameSize() }, buffers);
 }
 
 /**
  * \brief Queue a request and process the control list from the application
  * \param[in] frame The number of the frame which will be processed next
  * \param[in] controls The controls for the \a frame
  */
 void SoftwareIsp::queueRequest(const uint32_t frame, const ControlList &controls)
 {
 	ipa_->queueRequest(frame, controls);
 }
 
