Skip to content

Commit

Permalink
version 1.0.2 (#461)
Browse files Browse the repository at this point in the history
Co-authored-by: Aart Stuurman <aartstuurman@hotmail.com>
  • Loading branch information
oliverweissl and surgura authored Feb 28, 2024
1 parent b345df5 commit 9aeb6cb
Show file tree
Hide file tree
Showing 79 changed files with 1,331 additions and 202 deletions.
31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
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.
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
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.
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
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.
8 changes: 5 additions & 3 deletions ci_group/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "revolve2-ci-group"
version = "1.0.1"
version = "1.0.2"
description = "Revolve2: Computational Intelligence Group experimentation tools and standards."
readme = "../README.md"
authors = [
"Aart Stuurman <aartstuurman@hotmail.com>",
"Oliver Weissl <oliver.weissl@outlook.com>",
"Oliver Weissl <o.weissl@vu.nl>",
]
repository = "https://github.com/ci-group/revolve2"
classifiers = [
Expand All @@ -38,13 +38,15 @@ script = "revolve2/ci_group/morphological_novelty_metric/_build_cmodule.py"

[tool.poetry.dependencies]
python = "^3.10,<3.12"
revolve2-modular-robot-simulation = "1.0.1"
revolve2-modular-robot-simulation = "1.0.2"
noise = "^1.2.2"
multineat = "^0.12"
sqlalchemy = "^2.0.0"
numpy = "^1.21.2"
Cython = "^3.0.4"
setuptools = "^68.2.2"
opencv-contrib-python = "^4.9.0.80"
opencv-python = "^4.9.0.80"

[tool.poetry.extras]
dev = []
5 changes: 5 additions & 0 deletions ci_group/revolve2/ci_group/ci_lab_utilities/__init__.py
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 ci_group/revolve2/ci_group/ci_lab_utilities/_calibrate_camera.py
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 ci_group/revolve2/ci_group/ci_lab_utilities/_ip_camera.py
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()
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def develop(
:param genotype: The genotype to create the body from.
:returns: The create body.
"""
max_parts = 10
max_parts = 20

body_net = multineat.NeuralNetwork()
genotype.BuildPhenotype(body_net)
Expand Down Expand Up @@ -127,9 +127,8 @@ def __add_child(
new_pos = np.array(np.round(position + attachment_point.offset), dtype=np.int64)
child_type, child_rotation = __evaluate_cppn(body_net, new_pos, chain_length)
angle = child_rotation * (np.pi / 2.0)
child = child_type(angle)
if child_type is None or not module.module_reference.can_set_child(
child, attachment_index
child := child_type(angle), attachment_index
):
return None

Expand Down
Loading

0 comments on commit 9aeb6cb

Please sign in to comment.