-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Aart Stuurman <aartstuurman@hotmail.com>
- Loading branch information
1 parent
b345df5
commit 9aeb6cb
Showing
79 changed files
with
1,331 additions
and
202 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
--- | ||
name: Bug report | ||
about: Create a report to help us improve Revolve2 | ||
title: "[BUG]" | ||
labels: bug | ||
assignees: '' | ||
|
||
--- | ||
|
||
**Describe the bug** | ||
A clear and concise description of what the bug is. | ||
|
||
**To Reproduce** | ||
Steps to reproduce the behavior: | ||
1. Go to '...' | ||
2. Click on '....' | ||
3. Scroll down to '....' | ||
4. See error | ||
|
||
**Expected behavior** | ||
A clear and concise description of what you expected to happen. | ||
|
||
**Screenshots** | ||
If applicable, add screenshots to help explain your problem. | ||
|
||
**Platform:** | ||
- OS: [e.g. Windows, Linux, MacOS] | ||
- Python Version | ||
|
||
**Additional context** | ||
Add any other context about the problem here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
--- | ||
name: Feature request | ||
about: Suggest an idea for Revolve2 | ||
title: "[FEATURE]" | ||
labels: enhancement | ||
assignees: '' | ||
|
||
--- | ||
|
||
**Is your feature request related to a problem? Please describe.** | ||
A clear and concise description of what the problem is. | ||
|
||
**Describe the solution you'd like** | ||
A clear and concise description of what you want to happen, and in which package this solution should be. | ||
|
||
**Describe alternatives you've considered** | ||
A clear and concise description of any alternative solutions or features you've considered. | ||
Also mention your current workarounds if applicable. | ||
|
||
**Additional context** | ||
Add any other context or screenshots about the feature request here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<img align="right" width="150" height="150" src="./docs/source/logo.png"> | ||
|
||
### Contributing guide | ||
|
||
If you intend to contribute you may find this guide helpful: [Revolve2 Contributing Guide](https://ci-group.github.io/revolve2/contributing_guide/index.html).</br> | ||
Contributions are highly appreciated. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
"""Utility functions for the CI-group lab.""" | ||
from ._calibrate_camera import calibrate_camera | ||
from ._ip_camera import IPCamera | ||
|
||
__all__ = ["IPCamera", "calibrate_camera"] |
83 changes: 83 additions & 0 deletions
83
ci_group/revolve2/ci_group/ci_lab_utilities/_calibrate_camera.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import cv2 | ||
import numpy as np | ||
from numpy.typing import NDArray | ||
|
||
|
||
def calibrate_camera( | ||
calibration_images_paths: list[str], checkerboard_size: tuple[int, int] = (9, 9) | ||
) -> tuple[tuple[int, ...], NDArray[np.float_], NDArray[np.float_]]: | ||
""" | ||
Calibrate cameras for distortion and fisheye effects. | ||
In order to use this function effectively please use at least 5 valid calibration images, with differently places checkerboards. | ||
The checkerboard has to be fully visible with no occlusion, but it does mot have to lie flat on the ground. | ||
:param calibration_images_paths: The calibration images. | ||
:param checkerboard_size: The checkerboard size. Note if you have a 10 x 10 checkerboard the size should be (9, 9). | ||
:return: The dimension of the calibration images, the camera matrix and the distortion coefficient. | ||
""" | ||
subpix_criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.1) | ||
calibration_flags = ( | ||
cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC | ||
+ cv2.fisheye.CALIB_CHECK_COND | ||
+ cv2.fisheye.CALIB_FIX_SKEW | ||
) | ||
|
||
object_point = np.zeros( | ||
(1, checkerboard_size[0] * checkerboard_size[1], 3), np.float32 | ||
) | ||
object_point[0, :, :2] = np.mgrid[ | ||
0 : checkerboard_size[0], 0 : checkerboard_size[1] | ||
].T.reshape(-1, 2) | ||
|
||
_image_shape = None | ||
|
||
object_points = [] # 3d point in real world space | ||
image_points = [] # 2d points in image plane | ||
|
||
for image_path in calibration_images_paths: | ||
image = cv2.imread(image_path) | ||
if _image_shape is None: | ||
_image_shape = image.shape[:2] | ||
else: | ||
assert ( | ||
_image_shape == image.shape[:2] | ||
), "All images must share the same size." | ||
|
||
# Detect checkerboard | ||
returned, corners = cv2.findChessboardCorners( | ||
image, | ||
checkerboard_size, | ||
None, | ||
flags=cv2.CALIB_CB_ADAPTIVE_THRESH | ||
+ cv2.CALIB_CB_FAST_CHECK | ||
+ cv2.CALIB_CB_NORMALIZE_IMAGE, | ||
) | ||
if returned: | ||
object_points.append(object_point) | ||
cv2.cornerSubPix(image, corners, (3, 3), (-1, -1), subpix_criteria) | ||
image_points.append(corners) | ||
|
||
camera_matrix = np.zeros((3, 3)) | ||
distortion_coefficients = np.zeros((4, 1)) | ||
rotation_vectors = [ | ||
np.zeros((1, 1, 3), dtype=np.float64) for i in range(len(object_points)) | ||
] | ||
translation_vectors = [ | ||
np.zeros((1, 1, 3), dtype=np.float64) for i in range(len(object_points)) | ||
] | ||
|
||
# cv2 operations on numpy objects are in-place -> therefore we do not need to extract the return output. | ||
_ = cv2.fisheye.calibrate( | ||
object_points, | ||
image_points, | ||
image_size=image.shape[::-1], | ||
K=camera_matrix, | ||
D=distortion_coefficients, | ||
rvecs=rotation_vectors, | ||
tvecs=translation_vectors, | ||
flags=calibration_flags, | ||
criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6), | ||
) | ||
print(f"Found {len(object_points)} valid images for calibration") | ||
return image.shape[:2][::-1], camera_matrix, distortion_coefficients |
189 changes: 189 additions & 0 deletions
189
ci_group/revolve2/ci_group/ci_lab_utilities/_ip_camera.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import queue | ||
import threading | ||
import time | ||
|
||
import cv2 | ||
import numpy as np | ||
from numpy.typing import NDArray | ||
|
||
|
||
class IPCamera: | ||
""" | ||
A general class to steam and record from IP cameras via opencv. | ||
How to use: | ||
>>> address = "rtsp://<user>:<password>@<ip>:<port (554)>/..." | ||
>>> camera = IPCamera(camera_location=address, recording_path="<example_path>") | ||
>>> camera.start(display=True, record=True) | ||
If you are experiencing XDG_SESSION_TYPE error messages because of wayland use the following code before you start the recording: | ||
>>> import os | ||
>>> os.environ["XDG_SESSION_TYPE"] = "xcb" | ||
>>> os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;udp" | ||
""" | ||
|
||
_d_q: queue.Queue[cv2.typing.MatLike] # Display queue | ||
_r_q: queue.Queue[cv2.typing.MatLike] # Record queue | ||
|
||
"""Threads for camera functionality.""" | ||
_recieve_thread: threading.Thread | ||
_display_thread: threading.Thread | ||
_record_thread: threading.Thread | ||
|
||
_camera_location: str # Location (address) of the camera | ||
_is_running: bool # Allows to break threads | ||
_image_dimensions: tuple[int, int] | ||
_fps: int | ||
|
||
"""Maps for undistorting the camera.""" | ||
_map1: cv2.typing.MatLike | ||
_map2: cv2.typing.MatLike | ||
|
||
def __init__( | ||
self, | ||
camera_location: str, | ||
recording_path: str | None = None, | ||
image_dimensions: tuple[int, int] = (1920, 1080), | ||
distortion_coefficients: NDArray[np.float_] = np.array( | ||
[ | ||
[-0.2976428547328032], | ||
[3.2508343621538445], | ||
[-17.38410840159056], | ||
[30.01965021834286], | ||
] | ||
), | ||
camera_matrix: NDArray[np.float_] = np.array( | ||
[ | ||
[1490.4374643604199, 0.0, 990.6557248821284], | ||
[0.0, 1490.6535480621505, 544.6243597123726], | ||
[0.0, 0.0, 1.0], | ||
] | ||
), | ||
fps: int = 30, | ||
) -> None: | ||
""" | ||
Initialize the ip camera. | ||
:param camera_location: The location of the camera. | ||
:param recording_path: The path to store the recording. | ||
:param image_dimensions: The dimensions of the image produced by the camera and used for calibration. | ||
:param distortion_coefficients: The distortion coefficients for the camera. | ||
:param camera_matrix: The camera matrix for calibration. | ||
:param fps: The FPS of the camera. | ||
""" | ||
self._camera_location = camera_location | ||
self._recording_path = recording_path or f"{time.time()}_output.mp4" | ||
|
||
self._d_q = queue.Queue() | ||
self._r_q = queue.Queue() | ||
|
||
self._image_dimensions = image_dimensions | ||
self._fps = fps | ||
|
||
self._map1, self._map2 = cv2.fisheye.initUndistortRectifyMap( | ||
camera_matrix, | ||
distortion_coefficients, | ||
np.eye(3), | ||
camera_matrix, | ||
self._image_dimensions, | ||
cv2.CV_16SC2, | ||
) | ||
|
||
def _receive(self) -> None: | ||
"""Recieve data from the camera.""" | ||
capture = cv2.VideoCapture(self._camera_location, cv2.CAP_FFMPEG) | ||
capture.set(cv2.CAP_PROP_FRAME_WIDTH, self._image_dimensions[0]) | ||
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self._image_dimensions[1]) | ||
ret, frame = capture.read() | ||
|
||
frame = self._unfish(frame) | ||
self._d_q.put(frame) | ||
self._r_q.put(frame) | ||
while ret and self._is_running: | ||
ret, frame = capture.read() | ||
|
||
frame = self._unfish(frame) | ||
self._d_q.put(frame) | ||
self._r_q.put(frame) | ||
else: | ||
capture.release() | ||
|
||
def _display(self) -> None: | ||
"""Display the data from the camera.""" | ||
while self._is_running: | ||
if not self._d_q.empty(): | ||
frame = self._d_q.get() | ||
cv2.imshow("Camera View", frame) | ||
key = cv2.waitKey(1) | ||
if key == 27: # Exit the viewer using ESC-button | ||
self._is_running = False | ||
else: | ||
cv2.destroyAllWindows() | ||
|
||
def _record(self) -> None: | ||
"""Record the data from the camera.""" | ||
out = cv2.VideoWriter( | ||
self._recording_path, | ||
cv2.VideoWriter.fourcc(*"mp4v"), | ||
self._fps, | ||
self._image_dimensions, | ||
) | ||
print("Recording in progress.") | ||
while self._is_running: | ||
if not self._r_q.empty(): | ||
out.write(self._r_q.get()) | ||
else: | ||
print(f"Saving video to: {self._recording_path}") | ||
out.release() | ||
|
||
def _dump_record(self) -> None: | ||
"""Dump record queue if not used.""" | ||
while self._is_running: | ||
if not self._r_q.empty(): | ||
self._r_q.get() | ||
|
||
def _dump_display(self) -> None: | ||
"""Dump display queue if not used.""" | ||
while self._is_running: | ||
if not self._d_q.empty(): | ||
self._d_q.get() | ||
|
||
def _unfish(self, image: cv2.typing.MatLike) -> cv2.typing.MatLike: | ||
""" | ||
Remove fisheye effect from the camera. | ||
:param image: The image | ||
:return: The undistorted image. | ||
""" | ||
undistorted = cv2.remap( | ||
image, | ||
self._map1, | ||
self._map2, | ||
interpolation=cv2.INTER_LINEAR, | ||
borderMode=cv2.BORDER_CONSTANT, | ||
) | ||
return undistorted | ||
|
||
def start(self, record: bool = False, display: bool = True) -> None: | ||
""" | ||
Start the camera. | ||
:param record: Whether to record. | ||
:param display: Whether to display the video stream. | ||
""" | ||
assert ( | ||
record or display | ||
), "The camera is neither recording or displaying, are you sure you are using it?" | ||
self._recieve_thread = threading.Thread(target=self._receive) | ||
self._display_thread = threading.Thread( | ||
target=self._display if display else self._dump_display | ||
) | ||
self._record_thread = threading.Thread( | ||
target=self._record if record else self._dump_record | ||
) | ||
|
||
self._is_running = True | ||
|
||
self._recieve_thread.start() | ||
self._record_thread.start() | ||
self._display_thread.start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.