From 81bf57f0b8710f0c55e08d399da100c18802208a Mon Sep 17 00:00:00 2001
From: andersct <andersct@umich.edu>
Date: Sat, 8 Aug 2020 15:31:21 -0400
Subject: [PATCH] Add pinhole camera simulation

- add scene elements for visualization
- add example that displays top down and projection to camera image
---
 examples/display_scene_camera.py | 52 ++++++++++++++++++++++++++++++++
 misc/matrix_building.py          | 13 ++++++++
 rigid_body_models/boat.py        |  3 +-
 scene_elements/__init__.py       |  0
 scene_elements/camera.py         | 30 ++++++++++++++++++
 scene_elements/scene.py          | 27 +++++++++++++++++
 scene_elements/sphere.py         | 41 +++++++++++++++++++++++++
 scene_elements/water.py          | 52 ++++++++++++++++++++++++++++++++
 8 files changed, 217 insertions(+), 1 deletion(-)
 create mode 100644 examples/display_scene_camera.py
 create mode 100644 scene_elements/__init__.py
 create mode 100644 scene_elements/camera.py
 create mode 100644 scene_elements/scene.py
 create mode 100644 scene_elements/sphere.py
 create mode 100644 scene_elements/water.py

diff --git a/examples/display_scene_camera.py b/examples/display_scene_camera.py
new file mode 100644
index 0000000..3b11813
--- /dev/null
+++ b/examples/display_scene_camera.py
@@ -0,0 +1,52 @@
+import numpy as np
+from scene_elements.scene import Scene
+from scene_elements.sphere import Sphere
+from scene_elements.water import Water
+from scene_elements.camera import Camera
+from misc.matrix_building import rot_yaw_xyz1
+import matplotlib.pyplot as plt
+
+
+def main():
+    scene = Scene([
+        Sphere([1, 2], color='green'),
+        Sphere([-1, 2], color='red'),
+        Sphere([2, 6], color='yellow'),
+        Sphere([0, 7], color='black'),
+        Water(),
+    ])
+
+    cam_f = 420.  # f * px/m scaling based on sensor size
+    cam_rows = 480
+    cam_cols = 640
+    cam_K = np.array([
+        [cam_f, 0, cam_rows/2, 0],
+        [0, cam_f, cam_cols/2, 0],
+        [0, 0, 1., 0],
+    ])
+    cam_Rt = np.array([
+        [0, 0, 1, -.5],
+        [0, -1, 0, 0],
+        [1, 0, 0, 0],
+        [0, 0, 0, 1],
+    ])
+    camera = Camera(K=cam_K, Rt=cam_Rt, cam_cols=cam_cols, cam_rows=cam_rows)
+    body2world_Rt = rot_yaw_xyz1(np.pi/(2 + .2))
+    camera.set_body2world_Rt(body2world_Rt)
+
+    # make fig for top view and image view
+    fig_scene, ax_scene = plt.subplots()
+    scene.draw_top_view(ax_scene)
+    ax_scene.set_xlim([-6, 6])
+    ax_scene.set_ylim([-2, 10])
+    ax_scene.set_aspect('equal')
+    plt.show()
+
+    fig_scene, ax_image = plt.subplots()
+    scene.draw_image_view(ax_image, camera)
+    ax_image.set_aspect('equal')
+    plt.show()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/misc/matrix_building.py b/misc/matrix_building.py
index 1a34e3f..d1e32be 100644
--- a/misc/matrix_building.py
+++ b/misc/matrix_building.py
@@ -11,3 +11,16 @@ def rot_2d(theta):
     mat = np.array([[a, -b], [b, a]])
     return mat
 
+
+def rot_yaw_xyz1(theta):
+    """
+    :param theta: rad
+    :return: 4, 4 | homogeneous ccw rotation matrix
+    """
+    mat = np.eye(4)
+    mat[:2, :2] = rot_2d(theta)
+    return mat
+
+
+
+
diff --git a/rigid_body_models/boat.py b/rigid_body_models/boat.py
index ab525f5..36ecf4c 100644
--- a/rigid_body_models/boat.py
+++ b/rigid_body_models/boat.py
@@ -21,9 +21,10 @@ class MountedThrusters(object):
 
 class DiamondMountedThrusters(object):
     """
+    Body coordinate frame:
     ^ y
     |
-    --> x
+    --> x (z-up, so right-handed)
 
       3       0
      ---------
diff --git a/scene_elements/__init__.py b/scene_elements/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scene_elements/camera.py b/scene_elements/camera.py
new file mode 100644
index 0000000..ffbec9f
--- /dev/null
+++ b/scene_elements/camera.py
@@ -0,0 +1,30 @@
+import numpy as np
+
+
+class Camera(object):
+
+    def __init__(self, K, Rt, cam_cols=None, cam_rows=None):
+        """
+        Coordinates:
+        [cam] <--Rt-- [body] <--body2world_Rt-- [world]
+        :param K: 3, 4 | intrinsic matrix
+        :param Rt: 4, 4 | extrinsic matrix - body2cam
+        :param cam_cols:
+        :param cam_rows:
+        """
+        self.cam_cols = cam_cols
+        self.cam_rows = cam_rows
+        self.K = K
+        self.Rt = Rt
+        self.P = K.dot(Rt)
+        self.cam_center = Rt[:3, :3].T.dot(-Rt[:-1, -1])
+        self.body2world_Rt = np.eye(4)
+
+    def set_body2world_Rt(self, Rt):
+        self.body2world_Rt = Rt
+
+    def project(self, x):
+        return self.P.dot(self.body2world_Rt.T.dot(x))
+
+    def get_camera_center_world(self):
+        return self.body2world_Rt[:3, :3].dot(self.cam_center)
diff --git a/scene_elements/scene.py b/scene_elements/scene.py
new file mode 100644
index 0000000..3fd3f57
--- /dev/null
+++ b/scene_elements/scene.py
@@ -0,0 +1,27 @@
+import numpy as np
+
+
+class Scene(object):
+
+    def __init__(self, element_list):
+        self.element_list = element_list
+
+    def draw_top_view(self, ax):
+        ax.grid(True)
+        for element in self.element_list:
+            element.draw_top_view(ax)
+
+    def draw_image_view(self, ax, camera):
+        ax.set_xlim([0, camera.cam_cols])
+        ax.set_ylim([0, camera.cam_rows])
+        # ax.grid(True)
+
+        # apply world to cam body Rt
+        cam_center_world = camera.get_camera_center_world()[:2]
+        dist_from_cam = [np.linalg.norm(element.xy - cam_center_world)
+                         if len(element.xy) > 0 else np.inf
+                         for element in self.element_list]
+        zorder_list = len(self.element_list) - np.argsort(np.asarray(dist_from_cam))
+        for i in range(len(self.element_list)):
+            self.element_list[i].draw_image_view(
+                ax, camera, zorder=zorder_list[i])
diff --git a/scene_elements/sphere.py b/scene_elements/sphere.py
new file mode 100644
index 0000000..f549f2a
--- /dev/null
+++ b/scene_elements/sphere.py
@@ -0,0 +1,41 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as pa
+
+
+class Sphere(object):
+
+    def __init__(self, xy, z=0., radius=0.1, color=None):
+        self.xy = np.asarray(xy)
+        self.z = z
+        self.radius = radius
+        self.color = color
+
+    def draw_top_view(self, ax):
+        circle = pa.Circle(tuple(self.xy), radius=self.radius, facecolor=self.color)
+        ax.add_artist(circle)
+
+    def draw_image_view(self, ax, camera, zorder=0):
+        """
+        Do not draw if behind camera.
+        (Out of frame objects can be drawn, they will just be cut off)
+        :param ax:
+        :param camera:
+        :param zorder: ordering for fake depth
+        :return:
+        """
+        xyz1 = np.hstack([self.xy, self.z, 1.])
+        top_bot_xyz1 = np.array([xyz1, xyz1]).T
+        top_bot_xyz1[2, :] += [self.radius, -self.radius]
+        top_bot_xy1 = camera.project(top_bot_xyz1)
+        if np.any(top_bot_xy1[-1] <= 0):
+            return
+        top_bot_xy1 /= top_bot_xy1[-1]
+        xy = top_bot_xy1[:-1].mean(axis=1)
+        r = np.linalg.norm(top_bot_xy1[:-1, 0] - xy)/2
+        # reverse xy to place into plot coordinates, since cam x is vertical
+        circle = pa.Circle(
+            tuple(xy)[::-1], radius=r,
+            facecolor=self.color, zorder=zorder
+        )
+        ax.add_artist(circle)
diff --git a/scene_elements/water.py b/scene_elements/water.py
new file mode 100644
index 0000000..91a35b6
--- /dev/null
+++ b/scene_elements/water.py
@@ -0,0 +1,52 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as pa
+
+
+class Water(object):
+
+    def __init__(self, z=0., bound_xy=()):
+        self.xy = ()
+        self.z = z
+        self.color = '#80ebff'
+        self.bound_xy = bound_xy
+        if len(bound_xy) == 0:
+            theta = np.linspace(0, 2*np.pi, num=720, endpoint=False)
+            self.bound_xy = np.array([
+                np.cos(theta), np.sin(theta)
+            ]) * 1e5
+
+    def draw_top_view(self, ax):
+        m = 200
+        rect = pa.Rectangle((-m/2, -m/2), width=m, height=m, facecolor=self.color, zorder=-1)
+        ax.add_artist(rect)
+
+    def draw_image_view(self, ax, camera, zorder=-1):
+        """
+        Do not draw if behind camera.
+        (Out of frame objects can be drawn, they will just be cut off)
+        :param ax:
+        :param camera:
+        :param zorder: ordering for fake depth
+        :return:
+        """
+        bound_xyz1 = np.vstack([self.bound_xy, 0*self.bound_xy])
+        bound_xyz1[2, :] = self.z
+        bound_xyz1[3, :] = 1
+        bound_xy1 = camera.project(bound_xyz1)
+        bound_xy1 = bound_xy1[:, bound_xy1[-1] > 1e-2]
+        if bound_xy1.size == 0:
+            return
+        bound_xy1 /= bound_xy1[-1]
+        # use furthest point on each side of camera fov
+        # lower two points given by assumption we are surrounded by water
+        ind_min = bound_xy1[1].argmin()
+        ind_max = bound_xy1[1].argmax()
+        verts = np.asarray([
+            [0, 0],
+            bound_xy1[:2, ind_min],
+            bound_xy1[:2, ind_max],
+            [0, camera.cam_cols],
+        ])
+        poly = pa.Polygon(verts[:, ::-1], facecolor=self.color, zorder=zorder)
+        ax.add_artist(poly)
-- 
GitLab