Skip to content

Commit

Permalink
Multicam Refinements + GigE Support (#7)
Browse files Browse the repository at this point in the history
* fixing speed issues with RTSP stream

* adding serial_number to config if user doesn't provide serial_number

* adjusting name of method to _find_webcam_serial_numbers

* various refinements

* some gige troubleshooting

* Update README.md

* Update README.md

* adding functionality to assign Basler properties by name

* removing explicit log level setting

* Update README.md

* Update README.md

* Update README.md

* Automatically reformatting code with black and isort

* cleaning up the poetry file and downgrading the opencv-python version to help Raspberry Pi users

* adding automated tests and better handling for optional dependency import failures

* Update README.md

* Automatically reformatting code with black and isort

---------

Co-authored-by: Tim Huff <[email protected]>
Co-authored-by: Leo Dirac <[email protected]>
Co-authored-by: Auto-format Bot <[email protected]>
  • Loading branch information
4 people authored Jul 21, 2023
1 parent fe70ad1 commit 844a5d3
Show file tree
Hide file tree
Showing 9 changed files with 645 additions and 175 deletions.
65 changes: 52 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# FrameGrab by Groundlight
## A user-friendly library for grabbing images from cameras or streams

FrameGrab is an open-source Python library designed to make it easy to grab frames (images) from cameras or streams. The library supports webcams, RTSP streams, Basler USB cameras and Intel RealSense depth cameras.
FrameGrab is an open-source Python library designed to make it easy to grab frames (images) from cameras or streams. The library supports generic USB cameras (such as webcams), RTSP streams, Basler USB cameras, Basler GigE cameras, and Intel RealSense depth cameras.

FrameGrab also provides basic motion detection functionality. FrameGrab requires Python 3.7 or higher.

Expand All @@ -20,29 +20,36 @@ To install the FrameGrab library, simply run:
pip install framegrab
```

## Optional Dependencies
To use a Basler USB or GigE camera, you must separately install the `pypylon` package.

Similarly, to use Intel RealSense cameras, you must install `pyrealsense2`.

If you don't intend to use these camera types, you don't need to install these extra packages.

## Usage

### Frame Grabbing

Simple usage with a single webcam would look something like the following:
Simple usage with a single USB camera would look something like the following:

```
from framegrab import FrameGrabber
config = {
'input_type': 'webcam',
'input_type': 'generic_usb',
}
grabber = FrameGrabber.create_grabber(config)
```
`config` can contain many details and settings about your camera, but only `input_type` is required. Available `input_type` options are: `webcam`, `rtsp`, `realsense` and `basler_usb`.
`config` can contain many details and settings about your camera, but only `input_type` is required. Available `input_type` options are: `generic_usb`, `rtsp`, `realsense` and `basler`.

Here's an example of a single webcam configured with several options:
Here's an example of a single USB camera configured with several options:
```
config = {
'name': 'front door camera',
'input_type': 'webcam',
'name': 'Front Door Camera',
'input_type': 'generic_usb',
'id': {
'serial_number': 23432570
},
Expand Down Expand Up @@ -92,7 +99,7 @@ grabber.release()

You might have several cameras that you want to use in the same application. In this case, you can load the configurations from a yaml file and use `FrameGrabber.create_grabbers`.

If you have multiple cameras of the same type plugged in, it's recommended to provide serial numbers in the configurations; this ensures that each configuration is paired with the correct camera. If you don't provide serial numbers in your configurations, configurations will be paired with cameras in a sequential manner.
If you have multiple cameras of the same type plugged in, it's recommended that you include serial numbers in the configurations; this ensures that each configuration is paired with the correct camera. If you don't provide serial numbers in your configurations, configurations will be paired with cameras in a sequential manner.

Below is a sample yaml file containing configurations for three different cameras.
```
Expand All @@ -107,17 +114,17 @@ GL_CAMERAS: |
right: .8
- name: conference room
input_type: rtsp
address:
id:
rtsp_url: rtsp://admin:[email protected]/cam/realmonitor?channel=1&subtype=0
options:
crop:
absolute:
pixels:
top: 350
bottom: 1100
left: 1100
right: 2000
- name: workshop
input_type: webcam
input_type: generic_usb
id:
serial_number: B77D3A8F
```
Expand All @@ -138,12 +145,44 @@ grabbers = FrameGrabber.create_grabbers(configs)
for grabber in grabbers.values():
print(grabber.config)
frame = grabber.grab()
display_image(frame)
display_image(frame) # substitute this line for your preferred method of displaying images, such as cv2.imshow
grabber.release()
```
It is also possible to 'autodiscover' cameras. This will automatically connect to all cameras that are plugged into your machine, such as `webcam`, `realsense` and `basler_usb` cameras. Default configurations will be loaded for each camera. Please note that RTSP streams cannot be discovered in this manner; RTSP URLs must be specified in the configurations.
### Configurations
The table below shows all available configurations and the cameras to which they apply.
| Configuration Name | Example | Webcam | RTSP | Basler | Realsense |
|----------------------------|-----------------|------------|-----------|-----------|-----------|
| name | On Robot Arm | optional | optional | optional | optional |
| input_type | generic_usb | required | required | required | required |
| id.serial_number | 23458234 | optional | - | optional | optional |
| id.rtsp_url | rtsp://… | - | required | - | - |
| options.resolution.height | 480 | optional | - | - | optional |
| options.resolution.width | 640 | optional | - | - | optional |
| options.zoom.digital | 1.3 | optional | optional | optional | optional |
| options.crop.pixels.top | 100 | optional | optional | optional | optional |
| options.crop.pixels.bottom | 400 | optional | optional | optional | optional |
| options.crop.pixels.left | 100 | optional | optional | optional | optional |
| options.crop.pixels.right | 400 | optional | optional | optional | optional |
| options.crop.relative.top | 0.1 | optional | optional | optional | optional |
| options.crop.relative.bottom | 0.9 | optional | optional | optional | optional |
| options.crop.relative.left | 0.1 | optional | optional | optional | optional |
| options.crop.relative.right | 0.9 | optional | optional | optional | optional |
| options.depth.side_by_side | 1 | - | - | - | optional |

In addition to the configurations in the table above, you can set any Basler camera property by including `options.basler.<BASLER PROPERTY NAME>`. For example, it's common to set `options.basler.PixelFormat` to `RGB8`.

### Autodiscovery
Autodiscovery automatically connects to all cameras that are plugged into your machine or discoverable on the network, including `generic_usb`, `realsense` and `basler` cameras. Default configurations will be loaded for each camera. Please note that RTSP streams cannot be discovered in this manner; RTSP URLs must be specified in the configurations.

Autodiscovery is great for simple applications where you don't need to set any special options on your cameras. It's also a convenient method for finding the serial numbers of your cameras (if the serial number isn't printed on the camera).
```
grabbers = FrameGrabber.autodiscover()
# Print some information about the discovered cameras
for grabber in grabbers.values():
print(grabber.config)
grabber.release()
```

### Motion Detection
Expand Down
10 changes: 4 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
[tool.poetry]
name = "framegrab"
version = "0.2.1"
version = "0.3"
description = "Easily grab frames from cameras or streams"
authors = ["Groundlight <[email protected]>"]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.7"
opencv-python = "^4.7.0.72"
youtube-dl = "^2021.12.17"
pillow = "^9.5.0"
pafy = "^0.5.5"
opencv-python = "^4.4.0.46"
pyyaml = "^6.0.1"

[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
pytest = "^7.0.1"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
build-backend = "poetry.core.masonry.api"
19 changes: 19 additions & 0 deletions sample_scripts/autodiscover_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from framegrab import FrameGrabber

print('Autodiscovering cameras...')

grabbers = FrameGrabber.autodiscover()

print('-' * 100)
print(f'Found {len(grabbers)} camera(s): {list(grabbers.keys())}')

# Get a frame from each camera
for camera_name, grabber in grabbers.items():
frame = grabber.grab()

print(f'Grabbed frame from {camera_name} with shape {frame.shape}')
print(grabber.config)

grabber.release()

print('Autodiscover demo complete.')
33 changes: 33 additions & 0 deletions sample_scripts/multicamera_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from framegrab import FrameGrabber
import yaml
import cv2

# load the configurations from yaml
config_path = 'sample_config.yaml'
with open(config_path, 'r') as f:
data = yaml.safe_load(f)
configs = yaml.safe_load(data['GL_CAMERAS'])

print('Loaded the following configurations from yaml:')
print(configs)

# Create the grabbers
grabbers = FrameGrabber.create_grabbers(configs)

while True:
# Get a frame from each camera
for camera_name, grabber in grabbers.items():
frame = grabber.grab()

cv2.imshow(camera_name, frame)

key = cv2.waitKey(30)

if key == ord('q'):
break

cv2.destroyAllWindows()

for grabber in grabbers.values():
grabber.release()

24 changes: 24 additions & 0 deletions sample_scripts/sample_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
GL_CAMERAS: |
- name: Front Door
input_type: generic_usb
options:
zoom:
digital: 1.5
- name: Conference Room
input_type: rtsp
id:
rtsp_url: rtsp://admin:[email protected]/cam/realmonitor?channel=1&subtype=0
options:
crop:
relative:
top: .1
bottom: .9
left: .1
right: .9
- name: Workshop
input_type: basler
id:
serial_number: 12345678
options:
basler:
ExposureTime: 60000
26 changes: 26 additions & 0 deletions sample_scripts/single_camera_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Finds a single USB camera (or built-in webcam) and displays its feed in a window.
Press 'q' to quit.
"""

import cv2
from framegrab import FrameGrabber

config = {
'name': 'My Camera',
'input_type': 'generic_usb',
}

grabber = FrameGrabber.create_grabber(config)

while True:
frame = grabber.grab()

cv2.imshow('FrameGrab Single-Camera Demo', frame)

key = cv2.waitKey(30)
if key == ord('q'):
break

cv2.destroyAllWindows()

grabber.release()
Loading

0 comments on commit 844a5d3

Please sign in to comment.