diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 0a2516ac7d61..009ee341f18c 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -27,6 +27,37 @@ namespace libcamera {
 
 LOG_DEFINE_CATEGORY(IPU3)
 
+class ImgUDevice {
+public:
+	ImgUDevice()
+		: imgu(nullptr), input(nullptr), output(nullptr),
+		  viewfinder(nullptr), stat(nullptr)
+	{
+	}
+
+	~ImgUDevice()
+	{
+		delete imgu;
+		delete input;
+		delete output;
+		delete viewfinder;
+		delete stat;
+	}
+
+	void init(MediaDevice *media, unsigned int index);
+
+	unsigned int index_;
+	std::string imguName_;
+	MediaDevice *mediaDevice_;
+
+	V4L2Subdevice *imgu;
+	V4L2Device *input;
+	V4L2Device *output;
+	V4L2Device *viewfinder;
+	V4L2Device *stat;
+	/* \todo Add param video device for 3A tuning */
+};
+
 struct CIO2Device {
 	CIO2Device()
 		: output(nullptr), csi2(nullptr), sensor(nullptr)
@@ -56,6 +87,7 @@ public:
 	void bufferReady(Buffer *buffer);
 
 	CIO2Device cio2;
+	ImgUDevice *imgu;
 
 	Stream stream_;
 };
@@ -83,6 +115,7 @@ public:
 	bool match(DeviceEnumerator *enumerator);
 
 private:
+	static constexpr unsigned int IPU3_IMGU_COUNT = 2;
 	static constexpr unsigned int IPU3_BUFFER_COUNT = 4;
 
 	IPU3CameraData *cameraData(const Camera *camera)
@@ -92,10 +125,18 @@ private:
 	}
 
 	int mediaBusToCIO2Format(unsigned int code);
+	V4L2Device *openDevice(MediaDevice *media, const std::string &name);
+	V4L2Subdevice *openSubdevice(MediaDevice *media,
+				     const std::string &name);
 
 	int initCIO2(unsigned int index, CIO2Device *cio2);
+	void deleteCIO2(CIO2Device *cio2);
+
+	int initImgU(ImgUDevice *imgu);
+
 	void registerCameras();
 
+	ImgUDevice imgus_[IPU3_IMGU_COUNT];
 	std::shared_ptr<MediaDevice> cio2MediaDev_;
 	std::shared_ptr<MediaDevice> imguMediaDev_;
 };
@@ -360,11 +401,45 @@ bool PipelineHandlerIPU3::match(DeviceEnumerator *enumerator)
 		return false;
 	}
 
+	if (imguMediaDev_->open()) {
+		cio2MediaDev_->close();
+		return false;
+	}
+
+	if (imguMediaDev_->disableLinks())
+		goto error_close_mdev;
+
+	for (unsigned int i = 0; i < IPU3_IMGU_COUNT; ++i)
+		imgus_[i].init(imguMediaDev_.get(), i);
+
 	registerCameras();
 
 	cio2MediaDev_->close();
+	imguMediaDev_->close();
 
 	return true;
+
+error_close_mdev:
+	cio2MediaDev_->close();
+	imguMediaDev_->close();
+
+	return false;
+}
+
+/* ----------------------------------------------------------------------------
+ * Helpers
+ */
+
+/**
+ * \brief Initialize fields of the ImgU instance
+ * \param mediaDevice The ImgU instance media device
+ * \param index The ImgU instance index
+ */
+void ImgUDevice::init(MediaDevice *mediaDevice, unsigned int index)
+{
+	index_ = index;
+	imguName_ = "ipu3-imgu " + std::to_string(index_);
+	mediaDevice_ = mediaDevice;
 }
 
 int PipelineHandlerIPU3::mediaBusToCIO2Format(unsigned int code)
@@ -411,6 +486,114 @@ int PipelineHandlerIPU3::mediaBusToCIO2Format(unsigned int code)
 	}
 }
 
+/**
+ * \brief Create and open the video device with \a name in media device \a media
+ *
+ * \todo Make a generic helper out of this method.
+ *
+ * \return Pointer to the video device on success, nullptr otherwise
+ */
+V4L2Device *PipelineHandlerIPU3::openDevice(MediaDevice *media,
+					    const std::string &name)
+{
+	MediaEntity *entity = media->getEntityByName(name);
+	if (!entity) {
+		LOG(IPU3, Error)
+			<< "Failed to get entity '" << name << "'";
+		return nullptr;
+	}
+
+	V4L2Device *dev = new V4L2Device(entity);
+	if (dev->open()) {
+		delete dev;
+		return nullptr;
+	}
+
+	return dev;
+}
+
+/**
+ * \brief Create and open the subdevice with \a name in media device \a media
+ *
+ * \todo Make a generic helper out of this method.
+ *
+ * \return Pointer to the subdevice on success, nullptr otherwise
+ */
+V4L2Subdevice *PipelineHandlerIPU3::openSubdevice(MediaDevice *media,
+						  const std::string &name)
+{
+	MediaEntity *entity = media->getEntityByName(name);
+	if (!entity) {
+		LOG(IPU3, Error)
+			<< "Failed to get entity '" << name << "'";
+		return nullptr;
+	}
+
+	V4L2Subdevice *dev = new V4L2Subdevice(entity);
+	if (dev->open()) {
+		delete dev;
+		return nullptr;
+	}
+
+	return dev;
+}
+
+/* ----------------------------------------------------------------------------
+ * IPU3 pipeline configuration
+ */
+
+/**
+ * \brief Initialize and configure components of the ImgU instance
+ *
+ * Create and open the devices and subdevices in the ImgU instance.
+ * This methods configures the ImgU instance for capture operations, and
+ * should be called at stream configuration time.
+ *
+ * \todo Expand the ImgU configuration with controls setting
+ *
+ * \return 0 on success or a negative error code otherwise
+ * \retval -ENODEV Failed to open one of the video devices or subdevices
+ */
+int PipelineHandlerIPU3::initImgU(ImgUDevice *imgu)
+{
+	imgu->imgu = openSubdevice(imgu->mediaDevice_, imgu->imguName_);
+	if (!imgu->imgu)
+		return -ENODEV;
+
+	imgu->input = openDevice(imgu->mediaDevice_,
+				 imgu->imguName_ + " input");
+	if (!imgu->input)
+		goto error_delete_imgu;
+
+	imgu->output = openDevice(imgu->mediaDevice_,
+				  imgu->imguName_ + " output");
+	if (!imgu->output)
+		goto error_delete_input;
+
+	imgu->viewfinder = openDevice(imgu->mediaDevice_,
+				      imgu->imguName_ + " viewfinder");
+	if (!imgu->viewfinder)
+		goto error_delete_output;
+
+	imgu->stat = openDevice(imgu->mediaDevice_,
+				imgu->imguName_ + " 3a stat");
+	if (!imgu->stat)
+		goto error_delete_vf;
+
+	return 0;
+
+error_delete_vf:
+	delete imgu->viewfinder;
+error_delete_output:
+	delete imgu->output;
+error_delete_input:
+	delete imgu->input;
+error_delete_imgu:
+	delete imgu->imgu;
+
+	return -ENODEV;
+}
+
 /**
  * \brief Initialize components of the CIO2 device \a index used by a camera
  * \param index The CIO2 device index
@@ -475,23 +658,12 @@ int PipelineHandlerIPU3::initCIO2(unsigned int index, CIO2Device *cio2)
 	if (ret)
 		goto error_delete_csi2;
 
-	entity = cio2MediaDev_->getEntityByName(cio2Name);
-	if (!entity) {
-		LOG(IPU3, Error)
-			<< "Failed to get entity '" << cio2Name << "'";
-		ret = -EINVAL;
+	cio2->output = openDevice(cio2MediaDev_.get(), cio2Name);
+	if (!cio2->output)
 		goto error_delete_csi2;
-	}
-
-	cio2->output = new V4L2Device(entity);
-	ret = cio2->output->open();
-	if (ret)
-		goto error_delete_output;
 
 	return 0;
 
-error_delete_output:
-	delete cio2->output;
 error_delete_csi2:
 	delete cio2->csi2;
 error_delete_sensor:
@@ -500,6 +672,16 @@ error_delete_sensor:
 	return ret;
 }
 
+/**
+ * \brief Delete all devices associated with a CIO2 unit
+ */
+void PipelineHandlerIPU3::deleteCIO2(CIO2Device *cio2)
+{
+	delete cio2->output;
+	delete cio2->csi2;
+	delete cio2->sensor;
+}
+
 /*
  * Cameras are created associating an image sensor (represented by a
  * media entity with function MEDIA_ENT_F_CAM_SENSOR) to one of the four
@@ -512,7 +694,7 @@ void PipelineHandlerIPU3::registerCameras()
 	 * image sensor is connected to it.
 	 */
 	unsigned int numCameras = 0;
-	for (unsigned int id = 0; id < 4; ++id) {
+	for (unsigned int id = 0; id < 4 && numCameras < 2; ++id) {
 		std::unique_ptr<IPU3CameraData> data =
 			utils::make_unique<IPU3CameraData>(this);
 		std::set<Stream *> streams{ &data->stream_ };
@@ -523,6 +705,18 @@ void PipelineHandlerIPU3::registerCameras()
 		if (ret)
 			continue;
 
+		/**
+		 * \todo Dynamically assign ImgU devices; as of now, limit
+		 * support to two cameras only, and assign imgu0 to the first
+		 * one and imgu1 to the second.
+		 */
+		data->imgu = &imgus_[numCameras];
+		ret = initImgU(data->imgu);
+		if (ret) {
+			deleteCIO2(cio2);
+			continue;
+		}
+
 		std::string cameraName = cio2->sensor->deviceName() + " "
 				       + std::to_string(id);
 		std::shared_ptr<Camera> camera =
