Repository: kornia/kornia
Pull request: [Feat] Stereo camera class
### Description
Added a StereoCamera class that allows users to reproject a disparity tensor to a point cloud as discussed in #836 .

I have many ideas for a stereo module and more functionality in the future, but wanted to get the setup and interface created first. 

Let me know where to add these ideas to the roadmap.

### Status
**Ready**

### Types of changes
<!--- Put an `x` in all the boxes that apply, and remove the not applicable items -->

## PR Checklist
### PR Implementer
- [x] Did you discuss the functionality or any breaking changes before ?
- [x] **Pass all tests**: did you test in local ? `make test`
- [x] Unittests: did you add tests for your new functionality ?
- [x] Documentations: did you build documentation ? `make build-docs`
- [x] Implementation: is your code well commented and follow conventions ? `make lint`
- [x] Docstrings & Typing: has your code documentation and typing ? `make mypy`
- [ ] Update notebooks & documentation if necessary

### KorniaTeam
<details>
  <summary>KorniaTeam workflow</summary>

  - [ ] Assign correct label
  - [ ] Assign PR to a reviewer
  - [ ] Does this PR close an Issue? (add `closes #IssueNumber` at the bottom if
        not already in description)

</details>

### Reviewer
<details>
  <summary>Reviewer workflow</summary>

  - [ ] Do all tests pass? (Unittests, Typing, Linting, Documentation, Environment)
  - [ ] Does the implementation follow `kornia` design conventions?
  - [ ] Is the documentation complete enough ?
  - [ ] Are the tests covering simple and corner cases ?

</details>


Problem statement:
[Feature request] StereoCamera model and OpenCV initializations
## 🚀 Feature
For Stereo Vision tasks it would be handy to have a `StereoCamera` model which can be initialized with two `kornia.geometry.camera.PinholeCamera` and the stereo calibration. The `StereoCamera` can have some handy methods such as `StereoCamera.get_disparity_from_depth` and `StereoCamera.get_depth_from_disparity` and other methods for working with disparity maps.

In addition to this, I propose a feature for both the `PinholeCamera` and proposed `StereoCamera` that would add a factory method to both, called something like `from_OpenCV_calibration`, such that we can easily initialize a `PinholeCamera` and `StereoCamera` from OpenCVs `calibrateCamera` and `stereoCalibrate`. OpenCVs calibration methods are the de facto standard in many stereo vision tasks.

## Motivation

This will help researchers and developers who work on Stereo Vision tasks and want to use the nice features of Kornia otherwise. A lot of research is being put into Stereo Vision and Disparity Map estimation and I think Kornia will be a valuable tool here.

## Additional context

I would be happy to contribute these proposed features.

Hints from issue discussion:
@copaah this sounds like a really good contribution idea. Could you sketch a bit the shape of this API before implementing ? If I'm not wrong `calibrateCamera` and `stereoCalibrate` just return the estimated  intrinsics/extrinsics. Why then a method called `from_OpenCV_calibration` ? the API could just consume those tensor matrices I guess.

/cc @jatentaki was doing some work on that
@edgarriba Thank you for your response.

Upon more consideration I think you are  right that the API should just consume the `P1` and `P2` camera matrices from `stereoRectify`, which is documented in OpenCV [here](https://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html#stereorectify):

![image](https://user-images.githubusercontent.com/6583837/104387620-5fba6c00-54ec-11eb-9dd7-fb3b41d4d6a0.png)

Initially this would support horizonal stereo.

This is akin to the ROS CameraMsg documented [here](http://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/CameraInfo.html).

The proposed API would then look something like

```
class StereoCamera:
    def __init__(
            self,
            left_camera: torch.Tensor,
            right_camera: torch.Tensor
    ):
        """
        TODO: Consider `left_camera` and `right_camera` to be of type `kornia.geometry.camera.PinholeCamera`
        :param left_camera: 
        :param right_camera: 
        """
        self._check_stereo_camera(left_camera, right_camera)
        self.left_camera = left_camera
        self.right_camera = right_camera

    def _check_stereo_camera(self, cam1, cam2):
        pass

    def depth_from_disparity(self, disparity_map: torch.Tensor):
        """
        :param disparity_map: Disparity tensors of shape (B, H, W)
        :return: Depth map of shape (B, H, W)
        """
        pass

    def disparity_from_depth(self, depth_map: torch.Tensor):
        """
        :param depth_map: Depth map of shape (B, H, W)
        :return: Disparity map of shape (B, H, W)
        """
        pass

    def reproject_disparity_to_point_cloud(self, disparity_map):
        pass

    def reproject_depth_to_3D(self, depth_map):
        pass

    def reproject_point_cloud_to_pixels(self, point_cloud):
        pass
```

Any thoughts?
@copaah loks like a good plan. Just one thing to consider when you implement - make sure that the underlying routines for the class methods are proper functionals that can be placed somewhere in kornia geometry core, either to `depth`, `epipolar` or in case we need a standalone `stereo` submodule.
@edgarriba I will! Looking into it this weekend and hopefully have a first draft.
@edgarriba I am writing a tutorial for the `StereoCamera` class. Does it go in `examples` or `tutorials`? Also, is there a better place to ask these questions?
@copaah join our slack https://join.slack.com/t/kornia/shared_invite/zt-csobk21g-CnydWe5fmvkcktIeRFGCEQ

Commit message: [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Patch to review:
diff --git a/docs/source/geometry.camera.rst b/docs/source/geometry.camera.rst
index 0e5423cf11..d57e51440a 100644
--- a/docs/source/geometry.camera.rst
+++ b/docs/source/geometry.camera.rst
@@ -8,3 +8,4 @@ kornia.geometry.camera
 
    geometry.camera.pinhole
    geometry.camera.perspective
+   geometry.camera.stereo
diff --git a/docs/source/geometry.camera.stereo.rst b/docs/source/geometry.camera.stereo.rst
new file mode 100644
index 0000000000..ea89fc80dc
--- /dev/null
+++ b/docs/source/geometry.camera.stereo.rst
@@ -0,0 +1,73 @@
+Stereo Camera
+-------------
+
+.. currentmodule:: kornia.geometry.camera.stereo
+
+In this module we provide the :class:`StereoCamera` that contains functionality for working with a horizontal stereo camera setup.
+
+The horizontal stereo camera setup is assumed to be calibrated and rectified such that the setup can be described by two camera matrices:
+
+The *left rectified camera matrix*:
+
+.. math::
+    \begin{bmatrix}
+    fx & 0  & cx & 0 \\
+    0  & fy & cy & 0 \\
+    0  & 0  & 1  & 0
+    \end{bmatrix}
+
+The *right rectified camera matrix*:
+
+.. math::
+    \begin{bmatrix}
+    fx & 0  & cx & tx * fx \\
+    0  & fy & cy & 0       \\
+    0  & 0  & 1  & 0
+    \end{bmatrix}
+
+where:
+    * :math:`fx` is the focal length in the x-direction in pixels.
+    * :math:`fy` is the focal length in the y-direction in pixels.
+    * :math:`cx` is the x-coordinate of the principal point in pixels.
+    * :math:`cy` is the y-coordinate of the principal point in pixels.
+    * :math:`tx` is the horizontal baseline in metric units.
+
+These camera matrices are obtained by calibrating your stereo camera setup which can be done `in OpenCV <https://docs.opencv.org/master/d9/d0c/group__calib3d.html#ga91018d80e2a93ade37539f01e6f07de5>`_.
+
+The :class:`StereoCamera` allows you to convert disparity maps to the real world 3D geometry represented by a point cloud.
+
+This is done by forming the :math:`Q` matrix:
+
+.. math::
+
+    Q = \begin{bmatrix}
+    fy * tx & 0       & 0   & -fy * cx * tx \\
+    0       & fx * tx & 0   & -fx * cy * tx \\
+    0       & 0       & 0   & fx * fy * tx  \\
+    0       & 0       & -fy & fy * (cx_{left} -cx_{right})
+    \end{bmatrix}
+
+Notice here that the x-coordinate for the principal point in the left and right camera :math:`cx` might differ, which is being taken into account here.
+
+Using the :math:`Q` matrix we can obtain the 3D points by:
+
+.. math::
+    \begin{bmatrix}
+    X \\
+    Y \\
+    Z \\
+    W
+    \end{bmatrix} = Q *
+    \begin{bmatrix}
+    u \\
+    v \\
+    disparity(y, v) \\
+    z
+    \end{bmatrix}
+
+.. autoclass:: StereoCamera
+    :members:
+
+    .. automethod:: __init__
+
+.. autofunction:: reproject_disparity_to_3D
diff --git a/kornia/geometry/camera/__init__.py b/kornia/geometry/camera/__init__.py
index c43a56ed02..0630c94d35 100644
--- a/kornia/geometry/camera/__init__.py
+++ b/kornia/geometry/camera/__init__.py
@@ -1,4 +1,5 @@
 from .perspective import project_points, unproject_points
 from .pinhole import cam2pixel, PinholeCamera, pixel2cam
+from .stereo import StereoCamera
 
-__all__ = ["PinholeCamera", "pixel2cam", "cam2pixel", "unproject_points", "project_points"]
+__all__ = ["PinholeCamera", "StereoCamera", "pixel2cam", "cam2pixel", "unproject_points", "project_points"]
diff --git a/kornia/geometry/camera/stereo.py b/kornia/geometry/camera/stereo.py
new file mode 100644
index 0000000000..9fc88af650
--- /dev/null
+++ b/kornia/geometry/camera/stereo.py
@@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
+        raise StereoException(f"Expected 'Q_matrix' to be of type torch.float32." f"Got {Q_matrix.dtype}")
+
+
+def reproject_disparity_to_3D(disparity_tensor: torch.Tensor, Q_matrix: torch.Tensor) -> torch.Tensor:
+    """
+    Reproject the disparity tensor to a 3D point cloud.
+
+    Args:
+        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+        Q_matrix (torch.Tensor): Tensor of Q matrices of shapes :math:`(B, 4, 4)`.
+
+    Returns:
+        torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+    """
+    _check_Q_matrix(Q_matrix)
+    _check_disparity_tensor(disparity_tensor)
+
+    batch_size, rows, cols = disparity_tensor.shape
+    dtype = disparity_tensor.dtype
+    device = disparity_tensor.device
+    homogenous_observation_ndim = 4
+    euclidian_observation_ndim = homogenous_observation_ndim - 1
+
+    # Construct a mesh grid of uv values, i.e. the tensors will contain 1:rows and 1:cols.
+    u, v = torch.meshgrid(
+        torch.arange(rows, dtype=dtype, device=device), torch.arange(cols, dtype=dtype, device=device)
+    )
+    u, v = u.expand(batch_size, -1, -1), v.expand(batch_size, -1, -1)
+
+    # The z dimension in homogenous coordinates are just 1.
+    z = torch.ones((batch_size, rows, cols), dtype=dtype, device=device)
+
+    # Stack the observations into a tensor of shape (batch_size, 4, -1) that contains all
+    # 4 dimensional vectors [u v disparity 1].
+    uvdz = torch.stack((u, v, disparity_tensor, z), 1).reshape(batch_size, homogenous_observation_ndim, -1)
+
+    # Matrix multiply all vectors with the Q matrix
+    hom_points = torch.bmm(Q_matrix, uvdz)
+
+    # Convert from homogenous to euclidian space.
+    z_points = torch.unsqueeze(hom_points[:, euclidian_observation_ndim], 1)
+    points = (hom_points / z_points)[:, :euclidian_observation_ndim]
+    points = points.permute(0, 2, 1)
+
+    # Final check that everything went well.
+    if not points.shape == (batch_size, rows * cols, euclidian_observation_ndim):
+        raise StereoException(
+            f"Something went wrong in `reproject_disparity_to_3D`. Expected the final output"
+            f"to be of shape {(batch_size, rows * cols, euclidian_observation_ndim)}."
+            f"But the computed point cloud had shape {points.shape}. "
+            f"Please ensure input are correct. If this is an error, please submit an issue."
+        )
+    return points
diff --git a/test/geometry/test_stereo.py b/test/geometry/test_stereo.py
new file mode 100644
index 0000000000..c9296d715b
--- /dev/null
+++ b/test/geometry/test_stereo.py
@@ -0,0 +1,198 @@
+from typing import Type
+
+import pytest
+import torch
+from torch.testing import assert_allclose
+
+import kornia
+
+
+@pytest.fixture(params=[1, 2, 4])
+def batch_size(request):
+    return request.param
+
+
+@pytest.fixture
+def dtype(scope="module"):
+    return torch.float32
+
+
+class _TestParams:
+    """Collection of test parameters for smoke test."""
+
+    height = 4
+    width = 6
+    fx = 1
+    fy = 2
+    cx = width / 2
+    cy = height / 2
+
+
+class _RealTestData:
+    """Collection of data from a real stereo setup."""
+
+    @property
+    def height(self):
+        return 375
+
+    @property
+    def width(self):
+        return 1242
+
+    @staticmethod
+    def _get_real_left_camera(batch_size, device, dtype):
+        cam = torch.tensor(
+            [
+                9.9640068207290187e02,
+                0.0,
+                3.7502582168579102e02,
+                0.0,
+                0.0,
+                9.9640068207290187e02,
+                2.4026374816894531e02,
+                0.0,
+                0.0,
+                0.0,
+                1.0,
+                0.0,
+            ],
+            device=device,
+            dtype=dtype,
+        ).reshape(3, 4)
+        return cam.expand(batch_size, -1, -1)
+
+    @staticmethod
+    def _get_real_right_camera(batch_size, device, dtype):
+        cam = torch.tensor(
+            [
+                9.9640068207290187e02,
+                0.0,
+                3.7502582168579102e02,
+                -5.4301732344712009e03,
+                0.0,
+                9.9640068207290187e02,
+                2.4026374816894531e02,
+                0.0,
+                0.0,
+                0.0,
+                1.0,
+                0.0,
+            ],
+            device=device,
+            dtype=dtype,
+        ).reshape(3, 4)
+        return cam.expand(batch_size, -1, -1)
+
+    @staticmethod
+    def _get_real_stereo_camera(batch_size, device, dtype):
+        return (
+            _RealTestData._get_real_left_camera(batch_size, device, dtype),
+            _RealTestData._get_real_right_camera(batch_size, device, dtype),
+        )
+
+
+class _SmokeTestData:
+    """Collection of smoke test data."""
+
+    @staticmethod
+    def _create_rectified_camera(params: Type[_TestParams], batch_size, device, dtype, tx_fx=None):
+        intrinsics = torch.zeros((3, 4), device=device, dtype=dtype)
+        intrinsics[..., 0, 0] = params.fx
+        intrinsics[..., 1, 1] = params.fy
+        intrinsics[..., 0, 2] = params.cx
+        intrinsics[..., 1, 2] = params.cy
+
+        if tx_fx:
+            intrinsics[..., 0, 3] = tx_fx
+
+        return intrinsics.expand(batch_size, -1, -1)
+
+    @staticmethod
+    def _create_left_camera(batch_size, device, dtype):
+        return _SmokeTestData._create_rectified_camera(_TestParams, batch_size, device, dtype)
+
+    @staticmethod
+    def _create_right_camera(batch_size, device, dtype, tx_fx):
+        return _SmokeTestData._create_rectified_camera(_TestParams, batch_size, device, dtype, tx_fx=tx_fx)
+
+    @staticmethod
+    def _create_stereo_camera(batch_size, device, dtype, tx_fx):
+        left_rectified_camera = _SmokeTestData._create_left_camera(batch_size, device, dtype)
+        right_rectified_camera = _SmokeTestData._create_right_camera(batch_size, device, dtype, tx_fx)
+        return left_rectified_camera, right_rectified_camera
+
+
+class TestStereoCamera:
+    """Test class for :class:`~kornia.geometry.camera.stereo.StereoCamera`"""
+
+    @staticmethod
+    def _create_disparity_tensor(batch_size, height, width, max_disparity, device):
+        size = (batch_size, height, width)
+        return torch.randint(size=size, low=0, high=max_disparity, device=device, dtype=torch.float32)
+
+    @staticmethod
+    def test_stereo_camera_attributes_smoke(batch_size, device, dtype):
+        """Test proper setup of the class for smoke data."""
+        tx_fx = -10
+        left_rectified_camera, right_rectified_camera = _SmokeTestData._create_stereo_camera(
+            batch_size, device, dtype, tx_fx
+        )
+
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
+
+        def _assert_all(x, y):
+            assert torch.all(torch.eq(x, y))
+
+        _assert_all(stereo_camera.fx, _TestParams.fx)
+        _assert_all(stereo_camera.fy, _TestParams.fy)
+        _assert_all(stereo_camera.cx_left, _TestParams.cx)
+        _assert_all(stereo_camera.cy, _TestParams.cy)
+        _assert_all(stereo_camera.tx, -tx_fx / _TestParams.fx)
+
+        assert stereo_camera.Q.shape == (batch_size, 4, 4)
+
+    @staticmethod
+    def test_stereo_camera_attributes_real(batch_size, device, dtype):
+        """Test proper setup of the class for real data."""
+        left_rectified_camera, right_rectified_camera = _RealTestData._get_real_stereo_camera(batch_size, device, dtype)
+
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
+        assert_allclose(stereo_camera.fx, left_rectified_camera[..., 0, 0])
+        assert_allclose(stereo_camera.fy, left_rectified_camera[..., 1, 1])
+        assert_allclose(stereo_camera.cx_left, left_rectified_camera[..., 0, 2])
+        assert_allclose(stereo_camera.cy, left_rectified_camera[..., 1, 2])
+        assert_allclose(stereo_camera.tx, -right_rectified_camera[..., 0, 3] / right_rectified_camera[..., 0, 0])
+
+        assert stereo_camera.Q.shape == (batch_size, 4, 4)
+
+    def test_reproject_disparity_to_3D_smoke(self, batch_size, device, dtype):
+        """Test reprojecting of disparity to 3D for smoke data."""
+        tx_fx = -10
+        left_rectified_camera, right_rectified_camera = _SmokeTestData._create_stereo_camera(
+            batch_size, device, dtype, tx_fx
+        )
+        disparity_tensor = self._create_disparity_tensor(
+            batch_size, _TestParams.height, _TestParams.width, max_disparity=2, device=device
+        )
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
+        xyz = stereo_camera.reproject_disparity_to_3D(disparity_tensor)
+
+        assert xyz.shape == (batch_size, _TestParams.height * _TestParams.width, 3)
+        assert xyz.dtype == torch.float32
+        assert xyz.device == device
+
+    def test_reproject_disparity_to_3D_real(self, batch_size, device, dtype):
+        """Test reprojecting of disparity to 3D for real data."""
+        height, width = _RealTestData().height, _RealTestData().width
+        max_disparity = 80
+        disparity_tensor = self._create_disparity_tensor(
+            batch_size, height, width, max_disparity=max_disparity, device=device
+        )
+        left_rectified_camera, right_rectified_camera = _RealTestData._get_real_stereo_camera(batch_size, device, dtype)
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
+
+        xyz = stereo_camera.reproject_disparity_to_3D(disparity_tensor)
+
+        assert xyz.shape == (batch_size, height * width, 3)
+        assert xyz.device == device
+        assert xyz.dtype == dtype


Code review comments:
diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
path: kornia/geometry/camera/stereo.py
text: @user1:
```suggestion
    def __init__(self, msg: str, *args, **kwargs):
```

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
path: kornia/geometry/camera/stereo.py
text: @user1:
```suggestion
        super().__init__(final_msg, *args, **kwargs)
```
why type ignore?

@author:
I think my editor complained, but it can be removed.

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
path: kornia/geometry/camera/stereo.py
text: @user1:
```suggestion
    if disparity_tensor.dtype in (torch.float16, torch.float32, torch.float64,):
```

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
+        raise StereoException(f"Expected 'Q_matrix' to be of type torch.float32." f"Got {Q_matrix.dtype}")
+
+
+def reproject_disparity_to_3D(disparity_tensor: torch.Tensor, Q_matrix: torch.Tensor) -> torch.Tensor:
+    """
+    Reproject the disparity tensor to a 3D point cloud.
+
+    Args:
+        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
path: kornia/geometry/camera/stereo.py
text: @user1:
```suggestion
        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, 1, H, W)`.
```
we try to represent like this single channel images, but open to discussion.

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
path: kornia/geometry/camera/stereo.py
text: @user1:
fp16/32/64

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
path: kornia/geometry/camera/stereo.py
text: @user1:
let's target directly to instantiate from `PinholeCamera` unless there another use case I miss.

@author:
I like this idea, but it is still unclear how to initialize the extrinsic of the `PinholeCamera` class for left and right respectively.

The most straight forward is keeping the intrinsic of each camera the same and then add the horizontal stereo offset to the extrensic camera of the right camera matrix. Pseudo code below:

```
left_int = [
[fx, 0, cx, 0],
[0, fy, cy, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]

right_int = 
[fx, 0, cx, 0],
[0, fy, cy, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]

left_ext = torch.eye(4)

right_ext = [[1, 0, 0, Tx * fx]
[0, 1, 0, 0]
[0, 0, 1, 0]
[0, 0, 0, 1]]
``` 

I think this is a bit more complicated and also breaks a little with ROS/OpenCV convention, but we could go this way.

What do you think?

@author:
@user1 any thoughts on this idea?

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
+        raise StereoException(f"Expected 'Q_matrix' to be of type torch.float32." f"Got {Q_matrix.dtype}")
+
+
+def reproject_disparity_to_3D(disparity_tensor: torch.Tensor, Q_matrix: torch.Tensor) -> torch.Tensor:
+    """
+    Reproject the disparity tensor to a 3D point cloud.
+
+    Args:
+        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+        Q_matrix (torch.Tensor): Tensor of Q matrices of shapes :math:`(B, 4, 4)`.
+
+    Returns:
+        torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+    """
+    _check_Q_matrix(Q_matrix)
+    _check_disparity_tensor(disparity_tensor)
+
+    batch_size, rows, cols = disparity_tensor.shape
+    dtype = disparity_tensor.dtype
+    device = disparity_tensor.device
+    homogenous_observation_ndim = 4
+    euclidian_observation_ndim = homogenous_observation_ndim - 1
+
+    # Construct a mesh grid of uv values, i.e. the tensors will contain 1:rows and 1:cols.
+    u, v = torch.meshgrid(
+        torch.arange(rows, dtype=dtype, device=device), torch.arange(cols, dtype=dtype, device=device)
+    )
+    u, v = u.expand(batch_size, -1, -1), v.expand(batch_size, -1, -1)
+
+    # The z dimension in homogenous coordinates are just 1.
+    z = torch.ones((batch_size, rows, cols), dtype=dtype, device=device)
+
+    # Stack the observations into a tensor of shape (batch_size, 4, -1) that contains all
+    # 4 dimensional vectors [u v disparity 1].
+    uvdz = torch.stack((u, v, disparity_tensor, z), 1).reshape(batch_size, homogenous_observation_ndim, -1)
+
+    # Matrix multiply all vectors with the Q matrix
+    hom_points = torch.bmm(Q_matrix, uvdz)
+
+    # Convert from homogenous to euclidian space.
+    z_points = torch.unsqueeze(hom_points[:, euclidian_observation_ndim], 1)
+    points = (hom_points / z_points)[:, :euclidian_observation_ndim]
path: kornia/geometry/camera/stereo.py
text: @user1:
we have `kornia.geometry.convert_points_from_homogeneous` that takes care of zero division

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
+        raise StereoException(f"Expected 'Q_matrix' to be of type torch.float32." f"Got {Q_matrix.dtype}")
+
+
+def reproject_disparity_to_3D(disparity_tensor: torch.Tensor, Q_matrix: torch.Tensor) -> torch.Tensor:
+    """
+    Reproject the disparity tensor to a 3D point cloud.
+
+    Args:
+        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+        Q_matrix (torch.Tensor): Tensor of Q matrices of shapes :math:`(B, 4, 4)`.
+
+    Returns:
+        torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+    """
+    _check_Q_matrix(Q_matrix)
+    _check_disparity_tensor(disparity_tensor)
+
+    batch_size, rows, cols = disparity_tensor.shape
+    dtype = disparity_tensor.dtype
+    device = disparity_tensor.device
+    homogenous_observation_ndim = 4
+    euclidian_observation_ndim = homogenous_observation_ndim - 1
+
+    # Construct a mesh grid of uv values, i.e. the tensors will contain 1:rows and 1:cols.
+    u, v = torch.meshgrid(
+        torch.arange(rows, dtype=dtype, device=device), torch.arange(cols, dtype=dtype, device=device)
+    )
+    u, v = u.expand(batch_size, -1, -1), v.expand(batch_size, -1, -1)
+
+    # The z dimension in homogenous coordinates are just 1.
+    z = torch.ones((batch_size, rows, cols), dtype=dtype, device=device)
+
+    # Stack the observations into a tensor of shape (batch_size, 4, -1) that contains all
+    # 4 dimensional vectors [u v disparity 1].
+    uvdz = torch.stack((u, v, disparity_tensor, z), 1).reshape(batch_size, homogenous_observation_ndim, -1)
path: kornia/geometry/camera/stereo.py
text: @user1:
we have `kornia.geometry.convert_points_to_homogeneous` that just pads in the correct dimension with ones

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
+        raise StereoException(f"Expected 'Q_matrix' to be of type torch.float32." f"Got {Q_matrix.dtype}")
+
+
+def reproject_disparity_to_3D(disparity_tensor: torch.Tensor, Q_matrix: torch.Tensor) -> torch.Tensor:
+    """
+    Reproject the disparity tensor to a 3D point cloud.
+
+    Args:
+        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+        Q_matrix (torch.Tensor): Tensor of Q matrices of shapes :math:`(B, 4, 4)`.
+
+    Returns:
+        torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+    """
+    _check_Q_matrix(Q_matrix)
+    _check_disparity_tensor(disparity_tensor)
+
+    batch_size, rows, cols = disparity_tensor.shape
+    dtype = disparity_tensor.dtype
+    device = disparity_tensor.device
+    homogenous_observation_ndim = 4
+    euclidian_observation_ndim = homogenous_observation_ndim - 1
+
+    # Construct a mesh grid of uv values, i.e. the tensors will contain 1:rows and 1:cols.
+    u, v = torch.meshgrid(
path: kornia/geometry/camera/stereo.py
text: @user1:
use `kornia.utils.create_meshgrid`

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
+            *args:
+            **kwargs:
+        """
+        doc_help = (
+            "\n Please check documents here: "
+            "https://kornia.readthedocs.io/en/latest/geometry.camera.stereo.html"
+            "for further information and examples."
+        )
+        final_msg = msg + doc_help
+        super().__init__(final_msg, *args, **kwargs)  # type: ignore
+
+
+class StereoCamera:
+    def __init__(self, rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Class representing a horizontal stereo camera setup.
+
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        self._check_stereo_camera(rectified_left_camera, rectified_right_camera)
+        self.rectified_left_camera: torch.Tensor = rectified_left_camera
+        self.rectified_right_camera: torch.Tensor = rectified_right_camera
+
+        self.device: torch.device = self.rectified_left_camera.device
+        self.dtype: torch.dtype = self.rectified_left_camera.dtype
+
+    @staticmethod
+    def _check_stereo_camera(rectified_left_camera: torch.Tensor, rectified_right_camera: torch.Tensor):
+        """
+        Utility function to ensure user specified correct camera matrices.
+        Args:
+            rectified_left_camera (torch.Tensor): The rectified left camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+            rectified_right_camera (torch.Tensor): The rectified right camera projection matrix
+                                                    of shape :math:`(B, 3, 4)`
+        """
+        # Ensure correct shapes
+        if rectified_left_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' to have 3 dimensions. " f"Got {rectified_left_camera.ndim}."
+            )
+
+        if rectified_right_camera.ndim != 3:
+            raise StereoException(
+                f"Expected 'rectified_right_camera' to have 3 dimension. " f"Got {rectified_right_camera.ndim}."
+            )
+
+        if rectified_left_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_left_camera' to be of shape (3, 4)."
+                f"Got {rectified_left_camera.shape[:1]}."
+            )
+
+        if rectified_right_camera.shape[:1] == (3, 4):
+            raise StereoException(
+                f"Expected each 'rectified_right_camera' to be of shape (3, 4)."
+                f"Got {rectified_right_camera.shape[:1]}."
+            )
+
+        # Ensure same devices for cameras.
+        if rectified_left_camera.device != rectified_right_camera.device:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' "
+                f"to be on the same devices."
+                f"Got {rectified_left_camera.device} and {rectified_right_camera.device}."
+            )
+
+        # Ensure same dtypes for cameras.
+        if rectified_left_camera.dtype != rectified_right_camera.dtype:
+            raise StereoException(
+                f"Expected 'rectified_left_camera' and 'rectified_right_camera' to"
+                f"have same dtype."
+                f"Got {rectified_left_camera.dtype} and {rectified_right_camera.dtype}."
+            )
+
+        # Ensure all intrinsics parameters (fx, fy, cx, cy) are the same in both cameras.
+        if not torch.all(torch.eq(rectified_left_camera[..., :, :3], rectified_right_camera[..., :, :3])):
+            raise StereoException(
+                f"Expected 'left_rectified_camera' and 'rectified_right_camera' to have"
+                f"same parameters except for the last column."
+                f"Got {rectified_left_camera[..., :, :3]} and {rectified_right_camera[..., :, :3]}."
+            )
+
+        # Ensure that tx * fx is negative and exists.
+        tx_fx = rectified_right_camera[..., 0, 3]
+        if torch.all(torch.gt(tx_fx, 0)):
+            raise StereoException(f"Expected :math:`T_x * f_x` to be negative." f"Got {tx_fx}.")
+
+    @property
+    def batch_size(self) -> int:
+        r"""Returns the batch size of the storage.
+
+        Returns:
+            int: scalar with the batch size
+        """
+        return self.rectified_left_camera.shape[0]
+
+    @property
+    def fx(self) -> torch.Tensor:
+        r"""Returns the focal length in the x-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 0]
+
+    @property
+    def fy(self) -> torch.Tensor:
+        r"""Returns the focal length in the y-direction. Note that the focal lengths of the rectified left and right
+        camera are assumed to be equal.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 1]
+
+    @property
+    def cx_left(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the left camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 0, 2]
+
+    @property
+    def cx_right(self) -> torch.Tensor:
+        r"""Returns the x-coordinate of the principal point for the right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_right_camera[..., 0, 2]
+
+    @property
+    def cy(self) -> torch.Tensor:
+        r"""Returns the y-coordinate of the principal point. Note that the y-coordinate of the principal points
+        is assumed to be equal for the left and right camera.
+
+        Returns:
+            torch.Tensor: tensor of shape :math:`(B)`
+        """
+        return self.rectified_left_camera[..., 1, 2]
+
+    @property
+    def tx(self) -> torch.Tensor:
+        """
+        The horizontal baseline between the two cameras.
+
+        Returns:
+            torch.tensor: Tensor of shape :math:`(B)`
+
+        """
+        return -self.rectified_right_camera[..., 0, 3] / self.fx
+
+    @property
+    def Q(self) -> torch.Tensor:
+        """
+        The Q matrix of the horizontal stereo setup. This matrix is used for reprojecting a disparity tensor to
+        the corresponding point cloud. Note that this is in a general form that allows different focal
+        lengths in the x and y direction.
+
+        Return:
+            torch.Tensor: The Q matrix of shape :math:`(B, 4, 4)`.
+        """
+        Q = torch.zeros((self.batch_size, 4, 4), device=self.device, dtype=self.dtype)
+        baseline = -self.tx
+        Q[:, 0, 0] = self.fy * baseline
+        Q[:, 0, 3] = -self.fy * self.cx_left * baseline
+        Q[:, 1, 1] = self.fx * baseline
+        Q[:, 1, 3] = -self.fx * self.cy * baseline
+        Q[:, 2, 3] = self.fx * self.fy * baseline
+        Q[:, 3, 2] = -self.fy
+        Q[:, 3, 3] = self.fy * (self.cx_left - self.cx_right)  # NOTE: This is usually zero.
+        return Q
+
+    def reproject_disparity_to_3D(self, disparity_tensor: torch.Tensor) -> torch.Tensor:
+        """
+        Reproject the disparity tensor to a 3D point cloud.
+
+        Args:
+            disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+
+        Returns:
+            torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
+        """
+        return reproject_disparity_to_3D(disparity_tensor, self.Q)
+
+
+def _check_disparity_tensor(disparity_tensor: torch.Tensor):
+    """
+    Utility function to ensure correct user provided correct disparity tensor.
+    Args:
+        disparity_tensor (torch.Tensor): The disparity tensor of shape :math:`(B, H, W)`.
+    """
+    if disparity_tensor.ndim != 3:
+        raise StereoException(f"Expected 'disparity_tensor' to have 3 dimensions." f"Got {disparity_tensor.ndim}.")
+
+    if disparity_tensor.dtype != torch.float32:
+        raise StereoException(
+            f"Expected 'disparity_tensor' to have dtype torch.float32." f"Got {disparity_tensor.dtype}"
+        )
+
+
+def _check_Q_matrix(Q_matrix: torch.Tensor):
+    """
+    Utility function to ensure Q matrix is of correct form.
+    Args:
+        Q_matrix (torch.Tensor): The Q matrix for reprojecting disparity to a point cloud of shape :math:`(B, 4, 4)`
+    """
+    if not Q_matrix.ndim == 3:
+        raise StereoException(f"Expected 'Q_matrix to have 3 dimenstions." f"Got {Q_matrix.ndim}")
+
+    if not Q_matrix.shape[1:] == (4, 4):
+        raise StereoException(
+            f"Expected last two dimensions of 'Q_matrix' to be of shape (4, 4)." f"Got {Q_matrix.shape}"
+        )
+
+    if not Q_matrix.dtype == torch.float32:
+        raise StereoException(f"Expected 'Q_matrix' to be of type torch.float32." f"Got {Q_matrix.dtype}")
+
+
+def reproject_disparity_to_3D(disparity_tensor: torch.Tensor, Q_matrix: torch.Tensor) -> torch.Tensor:
+    """
+    Reproject the disparity tensor to a 3D point cloud.
+
+    Args:
+        disparity_tensor (torch.Tensor): Disparity tensor of shape :math:`(B, H, W)`.
+        Q_matrix (torch.Tensor): Tensor of Q matrices of shapes :math:`(B, 4, 4)`.
+
+    Returns:
+        torch.Tensor: The 3D point cloud of shape :math:`(B, H * W, 3)`
path: kornia/geometry/camera/stereo.py
text: @user1:
shouldn't we return as `B,H,W,3` to help with visualization?

@author:
Yeah, I'll fix this.

@author:
Doen.

diff_hunk: @@ -0,0 +1,198 @@
+from typing import Type
+
+import pytest
+import torch
+from torch.testing import assert_allclose
+
+import kornia
+
+
+@pytest.fixture(params=[1, 2, 4])
+def batch_size(request):
+    return request.param
+
+
+@pytest.fixture
+def dtype(scope="module"):
+    return torch.float32
+
+
+class _TestParams:
+    """Collection of test parameters for smoke test."""
+
+    height = 4
+    width = 6
+    fx = 1
+    fy = 2
+    cx = width / 2
+    cy = height / 2
+
+
+class _RealTestData:
+    """Collection of data from a real stereo setup."""
+
+    @property
+    def height(self):
+        return 375
+
+    @property
+    def width(self):
+        return 1242
+
+    @staticmethod
+    def _get_real_left_camera(batch_size, device, dtype):
+        cam = torch.tensor(
+            [
+                9.9640068207290187e02,
+                0.0,
+                3.7502582168579102e02,
+                0.0,
+                0.0,
+                9.9640068207290187e02,
+                2.4026374816894531e02,
+                0.0,
+                0.0,
+                0.0,
+                1.0,
+                0.0,
+            ],
+            device=device,
+            dtype=dtype,
+        ).reshape(3, 4)
+        return cam.expand(batch_size, -1, -1)
+
+    @staticmethod
+    def _get_real_right_camera(batch_size, device, dtype):
+        cam = torch.tensor(
+            [
+                9.9640068207290187e02,
+                0.0,
+                3.7502582168579102e02,
+                -5.4301732344712009e03,
+                0.0,
+                9.9640068207290187e02,
+                2.4026374816894531e02,
+                0.0,
+                0.0,
+                0.0,
+                1.0,
+                0.0,
+            ],
+            device=device,
+            dtype=dtype,
+        ).reshape(3, 4)
+        return cam.expand(batch_size, -1, -1)
+
+    @staticmethod
+    def _get_real_stereo_camera(batch_size, device, dtype):
+        return (
+            _RealTestData._get_real_left_camera(batch_size, device, dtype),
+            _RealTestData._get_real_right_camera(batch_size, device, dtype),
+        )
+
+
+class _SmokeTestData:
+    """Collection of smoke test data."""
+
+    @staticmethod
+    def _create_rectified_camera(params: Type[_TestParams], batch_size, device, dtype, tx_fx=None):
+        intrinsics = torch.zeros((3, 4), device=device, dtype=dtype)
+        intrinsics[..., 0, 0] = params.fx
+        intrinsics[..., 1, 1] = params.fy
+        intrinsics[..., 0, 2] = params.cx
+        intrinsics[..., 1, 2] = params.cy
+
+        if tx_fx:
+            intrinsics[..., 0, 3] = tx_fx
+
+        return intrinsics.expand(batch_size, -1, -1)
+
+    @staticmethod
+    def _create_left_camera(batch_size, device, dtype):
+        return _SmokeTestData._create_rectified_camera(_TestParams, batch_size, device, dtype)
+
+    @staticmethod
+    def _create_right_camera(batch_size, device, dtype, tx_fx):
+        return _SmokeTestData._create_rectified_camera(_TestParams, batch_size, device, dtype, tx_fx=tx_fx)
+
+    @staticmethod
+    def _create_stereo_camera(batch_size, device, dtype, tx_fx):
+        left_rectified_camera = _SmokeTestData._create_left_camera(batch_size, device, dtype)
+        right_rectified_camera = _SmokeTestData._create_right_camera(batch_size, device, dtype, tx_fx)
+        return left_rectified_camera, right_rectified_camera
+
+
+class TestStereoCamera:
+    """Test class for :class:`~kornia.geometry.camera.stereo.StereoCamera`"""
+
+    @staticmethod
+    def _create_disparity_tensor(batch_size, height, width, max_disparity, device):
+        size = (batch_size, height, width)
+        return torch.randint(size=size, low=0, high=max_disparity, device=device, dtype=torch.float32)
+
+    @staticmethod
+    def test_stereo_camera_attributes_smoke(batch_size, device, dtype):
+        """Test proper setup of the class for smoke data."""
+        tx_fx = -10
+        left_rectified_camera, right_rectified_camera = _SmokeTestData._create_stereo_camera(
+            batch_size, device, dtype, tx_fx
+        )
+
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
+
+        def _assert_all(x, y):
+            assert torch.all(torch.eq(x, y))
+
+        _assert_all(stereo_camera.fx, _TestParams.fx)
+        _assert_all(stereo_camera.fy, _TestParams.fy)
+        _assert_all(stereo_camera.cx_left, _TestParams.cx)
+        _assert_all(stereo_camera.cy, _TestParams.cy)
+        _assert_all(stereo_camera.tx, -tx_fx / _TestParams.fx)
+
+        assert stereo_camera.Q.shape == (batch_size, 4, 4)
+
+    @staticmethod
+    def test_stereo_camera_attributes_real(batch_size, device, dtype):
+        """Test proper setup of the class for real data."""
+        left_rectified_camera, right_rectified_camera = _RealTestData._get_real_stereo_camera(batch_size, device, dtype)
+
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
+        assert_allclose(stereo_camera.fx, left_rectified_camera[..., 0, 0])
+        assert_allclose(stereo_camera.fy, left_rectified_camera[..., 1, 1])
+        assert_allclose(stereo_camera.cx_left, left_rectified_camera[..., 0, 2])
+        assert_allclose(stereo_camera.cy, left_rectified_camera[..., 1, 2])
+        assert_allclose(stereo_camera.tx, -right_rectified_camera[..., 0, 3] / right_rectified_camera[..., 0, 0])
+
+        assert stereo_camera.Q.shape == (batch_size, 4, 4)
+
+    def test_reproject_disparity_to_3D_smoke(self, batch_size, device, dtype):
+        """Test reprojecting of disparity to 3D for smoke data."""
+        tx_fx = -10
+        left_rectified_camera, right_rectified_camera = _SmokeTestData._create_stereo_camera(
+            batch_size, device, dtype, tx_fx
+        )
+        disparity_tensor = self._create_disparity_tensor(
+            batch_size, _TestParams.height, _TestParams.width, max_disparity=2, device=device
+        )
+        stereo_camera = kornia.StereoCamera(left_rectified_camera, right_rectified_camera)
path: test/geometry/test_stereo.py
text: @user1:
I don't think this should be accessible from the top level api

@author:
Agree.

diff_hunk: @@ -0,0 +1,287 @@
+import torch
+
+
+class StereoException(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        """
+        Custom exception for the :module:`~kornia.geometry.camera.stereo` module. Adds a general helper module
+        redirecting the user to the proper documentation site.
+        Args:
+            msg: Custom message to add to the general message.
path: kornia/geometry/camera/stereo.py
text: @user1:
```suggestion
            msg (str): Custom message to add to the general message.
```