omnicam 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnicam-0.1.0/LICENSE +21 -0
- omnicam-0.1.0/PKG-INFO +143 -0
- omnicam-0.1.0/README.md +86 -0
- omnicam-0.1.0/pyproject.toml +49 -0
- omnicam-0.1.0/setup.cfg +4 -0
- omnicam-0.1.0/src/camerapy/__init__.py +31 -0
- omnicam-0.1.0/src/camerapy/base_camera.py +161 -0
- omnicam-0.1.0/src/camerapy/cv_camera_base.py +44 -0
- omnicam-0.1.0/src/camerapy/file_capture.py +12 -0
- omnicam-0.1.0/src/camerapy/gazebo_camera.py +39 -0
- omnicam-0.1.0/src/camerapy/gstreamer_capture.py +42 -0
- omnicam-0.1.0/src/camerapy/internet_capture.py +20 -0
- omnicam-0.1.0/src/camerapy/pi_camera.py +138 -0
- omnicam-0.1.0/src/camerapy/readonly_camera.py +25 -0
- omnicam-0.1.0/src/camerapy/screen_capture.py +45 -0
- omnicam-0.1.0/src/camerapy/simple_camera.py +20 -0
- omnicam-0.1.0/src/omnicam.egg-info/PKG-INFO +143 -0
- omnicam-0.1.0/src/omnicam.egg-info/SOURCES.txt +19 -0
- omnicam-0.1.0/src/omnicam.egg-info/dependency_links.txt +1 -0
- omnicam-0.1.0/src/omnicam.egg-info/requires.txt +13 -0
- omnicam-0.1.0/src/omnicam.egg-info/top_level.txt +1 -0
omnicam-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Oğuzhan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
omnicam-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omnicam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package for camera utilities
|
|
5
|
+
Author-email: OguzhanUmutlu <contact@oguzhanumutlu.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Oğuzhan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/OguzhanUmutlu/omnicam
|
|
29
|
+
Project-URL: Issues, https://github.com/OguzhanUmutlu/omnicam/issues
|
|
30
|
+
Project-URL: Documentation, https://github.com/OguzhanUmutlu/omnicam#readme
|
|
31
|
+
Keywords: camera,opencv,vision,stream,capture
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Topic :: Multimedia :: Video
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
44
|
+
Requires-Python: >=3.8
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Requires-Dist: numpy>=1.21
|
|
48
|
+
Provides-Extra: opencv
|
|
49
|
+
Requires-Dist: opencv-python>=4.7; extra == "opencv"
|
|
50
|
+
Provides-Extra: screen
|
|
51
|
+
Requires-Dist: mss>=9.0; extra == "screen"
|
|
52
|
+
Provides-Extra: pi
|
|
53
|
+
Requires-Dist: picamera2>=0.3; extra == "pi"
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# omnicam
|
|
59
|
+
|
|
60
|
+
A small, unified API for reading frames from USB cameras, IP streams, video files, screen capture, and Raspberry Pi
|
|
61
|
+
cameras.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- Unified `BaseCamera` interface across backends
|
|
66
|
+
- OpenCV-based capture for USB webcams, video files, RTSP/HTTP streams, and GStreamer pipelines
|
|
67
|
+
- Optional Raspberry Pi Camera support (Picamera2)
|
|
68
|
+
- Optional screen capture via `mss`
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install omnicam
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Optional extras:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install omnicam[opencv]
|
|
80
|
+
pip install omnicam[screen]
|
|
81
|
+
pip install omnicam[pi]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quickstart
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from omnicam import SimpleCamera
|
|
88
|
+
|
|
89
|
+
with SimpleCamera(index=0) as cam:
|
|
90
|
+
cam.open()
|
|
91
|
+
frame = cam.read()
|
|
92
|
+
if frame is not None:
|
|
93
|
+
print(frame.shape)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Camera types
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from omnicam import FileCapture, InternetCapture, ScreenCapture
|
|
100
|
+
|
|
101
|
+
# Video file
|
|
102
|
+
cam = FileCapture("/path/to/video.mp4")
|
|
103
|
+
|
|
104
|
+
# IP camera or MJPEG stream
|
|
105
|
+
cam = InternetCapture("http://192.168.1.10:8080/video")
|
|
106
|
+
|
|
107
|
+
# Screen capture (requires omnicam[screen])
|
|
108
|
+
cam = ScreenCapture(index=1)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Raspberry Pi camera
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from omnicam import PiCamera
|
|
115
|
+
|
|
116
|
+
cam = PiCamera(model="IMX219", resolution="720p")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## GStreamer and Gazebo
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from omnicam import GStreamerCapture, GazeboCamera
|
|
123
|
+
|
|
124
|
+
pipeline = (
|
|
125
|
+
GStreamerCapture.GstPipeline()
|
|
126
|
+
.add("v4l2src", device="/dev/video0")
|
|
127
|
+
.add("videoconvert")
|
|
128
|
+
.add("appsink")
|
|
129
|
+
)
|
|
130
|
+
cam = GStreamerCapture(pipeline)
|
|
131
|
+
|
|
132
|
+
# Gazebo (requires gz tools available on PATH)
|
|
133
|
+
cam = GazeboCamera(topic_name="my_camera")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- `numpy` is required. OpenCV-based backends need `omnicam[opencv]`.
|
|
139
|
+
- GStreamer and Gazebo features rely on system-level tooling.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT. See `LICENSE`.
|
omnicam-0.1.0/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# omnicam
|
|
2
|
+
|
|
3
|
+
A small, unified API for reading frames from USB cameras, IP streams, video files, screen capture, and Raspberry Pi
|
|
4
|
+
cameras.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Unified `BaseCamera` interface across backends
|
|
9
|
+
- OpenCV-based capture for USB webcams, video files, RTSP/HTTP streams, and GStreamer pipelines
|
|
10
|
+
- Optional Raspberry Pi Camera support (Picamera2)
|
|
11
|
+
- Optional screen capture via `mss`
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install omnicam
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Optional extras:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install omnicam[opencv]
|
|
23
|
+
pip install omnicam[screen]
|
|
24
|
+
pip install omnicam[pi]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from omnicam import SimpleCamera
|
|
31
|
+
|
|
32
|
+
with SimpleCamera(index=0) as cam:
|
|
33
|
+
cam.open()
|
|
34
|
+
frame = cam.read()
|
|
35
|
+
if frame is not None:
|
|
36
|
+
print(frame.shape)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Camera types
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from omnicam import FileCapture, InternetCapture, ScreenCapture
|
|
43
|
+
|
|
44
|
+
# Video file
|
|
45
|
+
cam = FileCapture("/path/to/video.mp4")
|
|
46
|
+
|
|
47
|
+
# IP camera or MJPEG stream
|
|
48
|
+
cam = InternetCapture("http://192.168.1.10:8080/video")
|
|
49
|
+
|
|
50
|
+
# Screen capture (requires omnicam[screen])
|
|
51
|
+
cam = ScreenCapture(index=1)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Raspberry Pi camera
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from omnicam import PiCamera
|
|
58
|
+
|
|
59
|
+
cam = PiCamera(model="IMX219", resolution="720p")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## GStreamer and Gazebo
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from omnicam import GStreamerCapture, GazeboCamera
|
|
66
|
+
|
|
67
|
+
pipeline = (
|
|
68
|
+
GStreamerCapture.GstPipeline()
|
|
69
|
+
.add("v4l2src", device="/dev/video0")
|
|
70
|
+
.add("videoconvert")
|
|
71
|
+
.add("appsink")
|
|
72
|
+
)
|
|
73
|
+
cam = GStreamerCapture(pipeline)
|
|
74
|
+
|
|
75
|
+
# Gazebo (requires gz tools available on PATH)
|
|
76
|
+
cam = GazeboCamera(topic_name="my_camera")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Notes
|
|
80
|
+
|
|
81
|
+
- `numpy` is required. OpenCV-based backends need `omnicam[opencv]`.
|
|
82
|
+
- GStreamer and Gazebo features rely on system-level tooling.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "omnicam"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Python package for camera utilities"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "OguzhanUmutlu", email = "contact@oguzhanumutlu.com" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.21"
|
|
17
|
+
]
|
|
18
|
+
keywords = ["camera", "opencv", "vision", "stream", "capture"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Topic :: Multimedia :: Video",
|
|
31
|
+
"Topic :: Software Development :: Libraries",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
opencv = ["opencv-python>=4.7"]
|
|
36
|
+
screen = ["mss>=9.0"]
|
|
37
|
+
pi = ["picamera2>=0.3"]
|
|
38
|
+
dev = ["pytest>=7.0"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/OguzhanUmutlu/omnicam"
|
|
42
|
+
Issues = "https://github.com/OguzhanUmutlu/omnicam/issues"
|
|
43
|
+
Documentation = "https://github.com/OguzhanUmutlu/omnicam#readme"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
package-dir = {"" = "src"}
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
omnicam-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from .base_camera import BaseCamera
|
|
4
|
+
from .cv_camera_base import CVCameraBase
|
|
5
|
+
from .file_capture import FileCapture
|
|
6
|
+
from .gazebo_camera import GazeboCamera
|
|
7
|
+
from .gstreamer_capture import GStreamerCapture
|
|
8
|
+
from .internet_capture import InternetCapture
|
|
9
|
+
from .pi_camera import PiCamera
|
|
10
|
+
from .readonly_camera import ReadonlyCamera
|
|
11
|
+
from .screen_capture import ScreenCapture
|
|
12
|
+
from .simple_camera import SimpleCamera
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = version("omnicam")
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BaseCamera",
|
|
21
|
+
"CVCameraBase",
|
|
22
|
+
"FileCapture",
|
|
23
|
+
"GazeboCamera",
|
|
24
|
+
"GStreamerCapture",
|
|
25
|
+
"InternetCapture",
|
|
26
|
+
"PiCamera",
|
|
27
|
+
"ReadonlyCamera",
|
|
28
|
+
"ScreenCapture",
|
|
29
|
+
"SimpleCamera",
|
|
30
|
+
"__version__",
|
|
31
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from abc import abstractmethod, ABC
|
|
2
|
+
from typing import Optional, Literal
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
resolutions = {
|
|
7
|
+
"240p": (426, 240),
|
|
8
|
+
"360p": (640, 360),
|
|
9
|
+
"480p": (854, 480),
|
|
10
|
+
"720p": (1280, 720),
|
|
11
|
+
"HD": (1280, 720),
|
|
12
|
+
"1080p": (1920, 1080),
|
|
13
|
+
"HD+": (1920, 1080),
|
|
14
|
+
"1440p": (2560, 1440),
|
|
15
|
+
"2160p": (3840, 2160)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CameraInfo:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
short_name: str,
|
|
24
|
+
focal_length_mm: tuple[float, float],
|
|
25
|
+
pixel_size_um: float,
|
|
26
|
+
max_resolution: tuple[int, int],
|
|
27
|
+
aperture_f: float,
|
|
28
|
+
hfov_deg: float,
|
|
29
|
+
focus_type: Literal["Fixed", "Autofocus", "Manual", "Unknown"],
|
|
30
|
+
has_ir_filter: bool
|
|
31
|
+
):
|
|
32
|
+
self.name = name
|
|
33
|
+
self.short_name = short_name
|
|
34
|
+
self.focal_length_mm = focal_length_mm
|
|
35
|
+
self.pixel_size_um = pixel_size_um
|
|
36
|
+
self.max_resolution = max_resolution
|
|
37
|
+
self.aperture_f = aperture_f
|
|
38
|
+
self.hfov_deg = hfov_deg
|
|
39
|
+
self.focus_type = focus_type
|
|
40
|
+
self.has_ir_filter = has_ir_filter
|
|
41
|
+
|
|
42
|
+
pixel_size_mm = self.pixel_size_um / 1000.0
|
|
43
|
+
self.base_focal_length = (self.focal_length_mm[0] / pixel_size_mm, self.focal_length_mm[1] / pixel_size_mm)
|
|
44
|
+
|
|
45
|
+
def focal_length(self, resolution: tuple[int, int]):
|
|
46
|
+
return (
|
|
47
|
+
self.base_focal_length[0] * resolution[0] / self.max_resolution[0],
|
|
48
|
+
self.base_focal_length[1] * resolution[1] / self.max_resolution[1]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def from_focal_length(name: str, short_name: str, focal_length: tuple[float, float],
|
|
53
|
+
resolution: tuple[int, int] = (9999, 9999)):
|
|
54
|
+
pixel_size_mm = 0.001
|
|
55
|
+
focal_length_mm = (focal_length[0] * pixel_size_mm, focal_length[1] * pixel_size_mm)
|
|
56
|
+
return CameraInfo(
|
|
57
|
+
name=name,
|
|
58
|
+
short_name=short_name,
|
|
59
|
+
focal_length_mm=focal_length_mm,
|
|
60
|
+
pixel_size_um=pixel_size_mm * 1000,
|
|
61
|
+
max_resolution=resolution,
|
|
62
|
+
aperture_f=0.0,
|
|
63
|
+
hfov_deg=0.0,
|
|
64
|
+
focus_type="Unknown",
|
|
65
|
+
has_ir_filter=False
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BaseCamera(ABC):
|
|
70
|
+
def __init__(self, info: Optional[CameraInfo] = None):
|
|
71
|
+
self.closed = False
|
|
72
|
+
self.roll_deg = 0.0
|
|
73
|
+
self.pitch_deg = 0.0
|
|
74
|
+
self.yaw_deg = 0.0
|
|
75
|
+
self.offset = np.array([0.0, 0.0, -0.1], dtype=np.float64)
|
|
76
|
+
self.info = info
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def _open(self):
|
|
80
|
+
raise NotImplementedError("open method must be implemented by subclass")
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def _read(self) -> "np.ndarray | None":
|
|
84
|
+
raise NotImplementedError("read method must be implemented by subclass")
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def _release(self):
|
|
88
|
+
raise NotImplementedError("release method must be implemented by subclass")
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def _size(self) -> tuple[int, int]:
|
|
92
|
+
raise NotImplementedError("size method must be implemented by subclass")
|
|
93
|
+
|
|
94
|
+
def _focus(self, rectangle: tuple[int, int, int, int]):
|
|
95
|
+
raise NotImplementedError("focus method must be implemented by subclass")
|
|
96
|
+
|
|
97
|
+
def open(self):
|
|
98
|
+
if self.closed:
|
|
99
|
+
raise PermissionError("Attempted to open a closed camera.")
|
|
100
|
+
self._open()
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def size(self):
|
|
104
|
+
return self._size()
|
|
105
|
+
|
|
106
|
+
def focus(self, rectangle: tuple[int, int, int, int]):
|
|
107
|
+
if self.closed:
|
|
108
|
+
raise PermissionError("Attempted to focus a closed camera.")
|
|
109
|
+
if self.info is not None and self.info.focus_type in ("Fixed", "Unknown"):
|
|
110
|
+
raise ValueError(f"This camera has a {self.info.focus_type} focus and cannot be adjusted.")
|
|
111
|
+
self._focus(rectangle)
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def focal_length(self) -> tuple[float, float]:
|
|
115
|
+
if self.info is None:
|
|
116
|
+
raise ValueError("CameraInfo must be provided to calculate focal length")
|
|
117
|
+
return self.info.focal_length(self.size())
|
|
118
|
+
|
|
119
|
+
def width(self):
|
|
120
|
+
return self.size()[0]
|
|
121
|
+
|
|
122
|
+
def height(self):
|
|
123
|
+
return self.size()[1]
|
|
124
|
+
|
|
125
|
+
def fx(self):
|
|
126
|
+
return self.focal_length()[0]
|
|
127
|
+
|
|
128
|
+
def fy(self):
|
|
129
|
+
return self.focal_length()[1]
|
|
130
|
+
|
|
131
|
+
def read(self):
|
|
132
|
+
if self.closed:
|
|
133
|
+
raise PermissionError("Attempted to read from a closed camera.")
|
|
134
|
+
frame = self._read()
|
|
135
|
+
if frame is None:
|
|
136
|
+
self.closed = True
|
|
137
|
+
return frame
|
|
138
|
+
|
|
139
|
+
def release(self):
|
|
140
|
+
if not self.closed:
|
|
141
|
+
self.closed = True
|
|
142
|
+
self._release()
|
|
143
|
+
|
|
144
|
+
def frames(self):
|
|
145
|
+
while True:
|
|
146
|
+
frame = self.read()
|
|
147
|
+
if frame is None:
|
|
148
|
+
break
|
|
149
|
+
yield frame
|
|
150
|
+
|
|
151
|
+
def __enter__(self):
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
155
|
+
self.release()
|
|
156
|
+
|
|
157
|
+
def readonly(self):
|
|
158
|
+
from .readonly_camera import ReadonlyCamera
|
|
159
|
+
if isinstance(self, ReadonlyCamera):
|
|
160
|
+
return self
|
|
161
|
+
return ReadonlyCamera(self)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .base_camera import BaseCamera, CameraInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CVCameraBase(BaseCamera):
|
|
5
|
+
def __init__(self, args: list, timeout=None, info: CameraInfo = None, open_error=None, timeout_error=None,
|
|
6
|
+
**kwargs):
|
|
7
|
+
super().__init__(info=info)
|
|
8
|
+
try:
|
|
9
|
+
import cv2
|
|
10
|
+
except Exception as exc:
|
|
11
|
+
raise ImportError(
|
|
12
|
+
"OpenCV is required for this feature. Install with 'pip install omnicam[opencv]'.") from exc
|
|
13
|
+
self._cv2 = cv2
|
|
14
|
+
self.open_error = open_error or RuntimeError(f"Camera could not be opened with args {args}")
|
|
15
|
+
self.timeout_error = timeout_error or TimeoutError(
|
|
16
|
+
f"Failed to open camera with args {args} within {timeout} seconds")
|
|
17
|
+
self.cv_args = args
|
|
18
|
+
self.cv_kwargs = kwargs
|
|
19
|
+
self.cam = None
|
|
20
|
+
self._frame_size = None
|
|
21
|
+
self.timeout = timeout
|
|
22
|
+
|
|
23
|
+
def _open(self):
|
|
24
|
+
self.cam = self._cv2.VideoCapture(*self.cv_args, **self.cv_kwargs)
|
|
25
|
+
if self.timeout is not None:
|
|
26
|
+
start_time = self._cv2.getTickCount()
|
|
27
|
+
while not self.cam.isOpened():
|
|
28
|
+
elapsed_time = (self._cv2.getTickCount() - start_time) / self._cv2.getTickFrequency()
|
|
29
|
+
if elapsed_time > self.timeout:
|
|
30
|
+
raise self.timeout_error
|
|
31
|
+
elif not self.cam.isOpened():
|
|
32
|
+
raise self.open_error
|
|
33
|
+
self._frame_size = int(self.cam.get(self._cv2.CAP_PROP_FRAME_WIDTH)), int(
|
|
34
|
+
self.cam.get(self._cv2.CAP_PROP_FRAME_HEIGHT))
|
|
35
|
+
|
|
36
|
+
def _read(self):
|
|
37
|
+
ret, frame = self.cam.read()
|
|
38
|
+
return frame if ret else None
|
|
39
|
+
|
|
40
|
+
def _release(self):
|
|
41
|
+
self.cam.release()
|
|
42
|
+
|
|
43
|
+
def _size(self):
|
|
44
|
+
return self._frame_size
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .base_camera import CameraInfo
|
|
4
|
+
from .cv_camera_base import CVCameraBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FileCapture(CVCameraBase):
|
|
8
|
+
def __init__(self, path: "os.PathLike | str", info: CameraInfo = None, open_error=None):
|
|
9
|
+
if not os.path.exists(path):
|
|
10
|
+
raise ValueError(f"File {path} does not exist")
|
|
11
|
+
super().__init__([str(path)], info=info,
|
|
12
|
+
open_error=open_error or ValueError(f"Could not open video file {path}"))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from .base_camera import CameraInfo
|
|
4
|
+
from .gstreamer_capture import GStreamerCapture
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GazeboCamera(GStreamerCapture):
|
|
8
|
+
def __init__(self, port=4000, topic_name: str = None, timeout=5, info: CameraInfo = None, open_error=None,
|
|
9
|
+
timeout_error=None):
|
|
10
|
+
self.topic_name = topic_name
|
|
11
|
+
self.port = port
|
|
12
|
+
super().__init__(
|
|
13
|
+
GStreamerCapture.GstPipeline()
|
|
14
|
+
.add("udpsrc", port=self.port)
|
|
15
|
+
.add_caps("application/x-rtp")
|
|
16
|
+
.add("rtph264depay")
|
|
17
|
+
.add("h264parse")
|
|
18
|
+
.add("avdec_h264")
|
|
19
|
+
.add("videoconvert")
|
|
20
|
+
.add("appsink", sync="false"),
|
|
21
|
+
timeout=timeout, info=info,
|
|
22
|
+
open_error=open_error or ValueError(f"Could not open Gazebo camera stream on port {self.port}"),
|
|
23
|
+
timeout_error=timeout_error or TimeoutError(
|
|
24
|
+
f"Could not open Gazebo camera stream on port {self.port} within {timeout} seconds")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def _open(self):
|
|
28
|
+
if self.topic_name is not None:
|
|
29
|
+
GazeboCamera.start_gz_stream(self.topic_name)
|
|
30
|
+
super()._open()
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def start_gz_stream(topic_name):
|
|
34
|
+
subprocess.run([
|
|
35
|
+
"gz", "topic",
|
|
36
|
+
"-t", f"/{topic_name}/image/enable_streaming",
|
|
37
|
+
"-m", "gz.msgs.Boolean",
|
|
38
|
+
"-p", "data: true"
|
|
39
|
+
])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .base_camera import CameraInfo
|
|
2
|
+
from .cv_camera_base import CVCameraBase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GStreamerCapture(CVCameraBase):
|
|
6
|
+
class GstPipeline:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.elements = []
|
|
9
|
+
|
|
10
|
+
def add(self, element_name, **properties):
|
|
11
|
+
if not properties:
|
|
12
|
+
self.elements.append(element_name)
|
|
13
|
+
return self
|
|
14
|
+
props_str = " ".join(f"{k}={v}" for k, v in properties.items())
|
|
15
|
+
element = f"{element_name} {props_str}".strip()
|
|
16
|
+
self.elements.append(element)
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def add_caps(self, caps: str):
|
|
20
|
+
self.elements.append(caps)
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return " ! ".join(self.elements)
|
|
25
|
+
|
|
26
|
+
def __init__(self, gstreamer_string: "GstPipeline | str", timeout=5, info: CameraInfo = None, open_error=None,
|
|
27
|
+
timeout_error=None):
|
|
28
|
+
try:
|
|
29
|
+
import cv2
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"OpenCV is required for this feature. Install with 'pip install omnicam[opencv]'."
|
|
33
|
+
) from exc
|
|
34
|
+
gstreamer_string = str(gstreamer_string)
|
|
35
|
+
self.gstreamer_string = gstreamer_string
|
|
36
|
+
super().__init__(
|
|
37
|
+
[gstreamer_string, cv2.CAP_GSTREAMER],
|
|
38
|
+
timeout=timeout, info=info,
|
|
39
|
+
open_error=open_error or RuntimeError(f"Could not open GStreamer stream with \"{gstreamer_string}\""),
|
|
40
|
+
timeout_error=timeout_error or TimeoutError(
|
|
41
|
+
f"Could not open GStreamer stream with \"{gstreamer_string}\" within {timeout} seconds")
|
|
42
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .base_camera import CameraInfo
|
|
2
|
+
from .cv_camera_base import CVCameraBase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InternetCapture(CVCameraBase):
|
|
6
|
+
def __init__(self, url: str, timeout=5, info: CameraInfo = None, args: list = None,
|
|
7
|
+
open_error=None, timeout_error=None):
|
|
8
|
+
if not url.startswith("http://") and not url.startswith("https://") and not url.startswith("rtsp://"):
|
|
9
|
+
url = "http://" + url
|
|
10
|
+
domain = url.split("//")[1].split("/")[0]
|
|
11
|
+
if ":" not in domain:
|
|
12
|
+
url = url.replace(domain, domain + ":8080")
|
|
13
|
+
if "/" not in url.split("//")[1]:
|
|
14
|
+
url += "/video"
|
|
15
|
+
super().__init__(
|
|
16
|
+
[url, *(args or [])], timeout=timeout, info=info,
|
|
17
|
+
open_error=open_error or ValueError(f"Could not open video stream from URL {url}"),
|
|
18
|
+
timeout_error=timeout_error or TimeoutError(
|
|
19
|
+
f"Could not open video stream from URL {url} within {timeout} seconds")
|
|
20
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from .base_camera import BaseCamera, resolutions, CameraInfo
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PiCamera(BaseCamera):
|
|
7
|
+
rpi_camera_definitions = [
|
|
8
|
+
CameraInfo(
|
|
9
|
+
name="Camera Module 1", short_name="OV5647",
|
|
10
|
+
focal_length_mm=(3.60, 3.60), pixel_size_um=1.40, max_resolution=(2592, 1944),
|
|
11
|
+
aperture_f=2.9, hfov_deg=53.5, focus_type="Fixed", has_ir_filter=True
|
|
12
|
+
),
|
|
13
|
+
CameraInfo(
|
|
14
|
+
name="Camera Module 1 NoIR", short_name="OV5647 NoIR",
|
|
15
|
+
focal_length_mm=(3.60, 3.60), pixel_size_um=1.40, max_resolution=(2592, 1944),
|
|
16
|
+
aperture_f=2.9, hfov_deg=53.5, focus_type="Fixed", has_ir_filter=False
|
|
17
|
+
),
|
|
18
|
+
CameraInfo(
|
|
19
|
+
name="Camera Module 2", short_name="IMX219",
|
|
20
|
+
focal_length_mm=(3.04, 3.04), pixel_size_um=1.12, max_resolution=(3280, 2464),
|
|
21
|
+
aperture_f=2.0, hfov_deg=62.2, focus_type="Fixed", has_ir_filter=True
|
|
22
|
+
),
|
|
23
|
+
CameraInfo(
|
|
24
|
+
name="Camera Module 2 NoIR", short_name="IMX219 NoIR",
|
|
25
|
+
focal_length_mm=(3.04, 3.04), pixel_size_um=1.12, max_resolution=(3280, 2464),
|
|
26
|
+
aperture_f=2.0, hfov_deg=62.2, focus_type="Fixed", has_ir_filter=False
|
|
27
|
+
),
|
|
28
|
+
CameraInfo(
|
|
29
|
+
name="Camera Module 3 - Standard", short_name="IMX708 Standard",
|
|
30
|
+
focal_length_mm=(4.74, 4.74), pixel_size_um=1.40, max_resolution=(4608, 2592),
|
|
31
|
+
aperture_f=1.8, hfov_deg=66.0, focus_type="Autofocus", has_ir_filter=True
|
|
32
|
+
),
|
|
33
|
+
CameraInfo(
|
|
34
|
+
name="Camera Module 3 - Standard NoIR", short_name="IMX708 NoIR",
|
|
35
|
+
focal_length_mm=(4.74, 4.74), pixel_size_um=1.40, max_resolution=(4608, 2592),
|
|
36
|
+
aperture_f=1.8, hfov_deg=66.0, focus_type="Autofocus", has_ir_filter=False
|
|
37
|
+
),
|
|
38
|
+
CameraInfo(
|
|
39
|
+
name="Camera Module 3 - Wide", short_name="IMX708 Wide",
|
|
40
|
+
focal_length_mm=(2.75, 2.75), pixel_size_um=1.40, max_resolution=(4608, 2592),
|
|
41
|
+
aperture_f=2.2, hfov_deg=102.0, focus_type="Autofocus", has_ir_filter=True
|
|
42
|
+
),
|
|
43
|
+
CameraInfo(
|
|
44
|
+
name="Camera Module 3 - Wide NoIR", short_name="IMX708 Wide NoIR",
|
|
45
|
+
focal_length_mm=(2.75, 2.75), pixel_size_um=1.40, max_resolution=(4608, 2592),
|
|
46
|
+
aperture_f=2.2, hfov_deg=102.0, focus_type="Autofocus", has_ir_filter=False
|
|
47
|
+
),
|
|
48
|
+
CameraInfo(
|
|
49
|
+
name="High Quality Camera w/ 6mm Lens", short_name="IMX477 6mm",
|
|
50
|
+
focal_length_mm=(6.00, 6.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
51
|
+
aperture_f=1.2, hfov_deg=55.0, focus_type="Manual", has_ir_filter=True
|
|
52
|
+
),
|
|
53
|
+
CameraInfo(
|
|
54
|
+
name="High Quality Camera w/ 16mm Lens", short_name="IMX477 16mm",
|
|
55
|
+
focal_length_mm=(16.00, 16.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
56
|
+
aperture_f=1.4, hfov_deg=22.2, focus_type="Manual", has_ir_filter=True
|
|
57
|
+
),
|
|
58
|
+
CameraInfo(
|
|
59
|
+
name="High Quality Camera w/ 35mm Lens", short_name="IMX477 35mm",
|
|
60
|
+
focal_length_mm=(35.00, 35.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
61
|
+
aperture_f=1.7, hfov_deg=10.1, focus_type="Manual", has_ir_filter=True
|
|
62
|
+
),
|
|
63
|
+
CameraInfo(
|
|
64
|
+
name="High Quality Camera w/ 8mm M12 Lens", short_name="IMX477 M12-8mm",
|
|
65
|
+
focal_length_mm=(8.00, 8.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
66
|
+
aperture_f=1.8, hfov_deg=49.0, focus_type="Manual", has_ir_filter=True
|
|
67
|
+
),
|
|
68
|
+
CameraInfo(
|
|
69
|
+
name="High Quality Camera w/ 25mm M12 Lens", short_name="IMX477 M12-25mm",
|
|
70
|
+
focal_length_mm=(25.00, 25.00), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
71
|
+
aperture_f=2.4, hfov_deg=14.4, focus_type="Manual", has_ir_filter=True
|
|
72
|
+
),
|
|
73
|
+
CameraInfo(
|
|
74
|
+
name="High Quality Camera w/ 2.7mm M12 Fisheye", short_name="IMX477 M12-Fish",
|
|
75
|
+
focal_length_mm=(2.70, 2.70), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
76
|
+
aperture_f=2.5, hfov_deg=140.0, focus_type="Manual", has_ir_filter=True
|
|
77
|
+
),
|
|
78
|
+
CameraInfo(
|
|
79
|
+
name="Global Shutter Camera w/ 6mm Lens", short_name="IMX296 6mm",
|
|
80
|
+
focal_length_mm=(6.00, 6.00), pixel_size_um=3.45, max_resolution=(1456, 1088),
|
|
81
|
+
aperture_f=1.2, hfov_deg=45.0, focus_type="Manual", has_ir_filter=True
|
|
82
|
+
),
|
|
83
|
+
CameraInfo(
|
|
84
|
+
name="Global Shutter Camera w/ 16mm Lens", short_name="IMX296 16mm",
|
|
85
|
+
focal_length_mm=(16.00, 16.00), pixel_size_um=3.45, max_resolution=(1456, 1088),
|
|
86
|
+
aperture_f=1.4, hfov_deg=17.8, focus_type="Manual", has_ir_filter=True
|
|
87
|
+
),
|
|
88
|
+
CameraInfo(
|
|
89
|
+
name="Raspberry Pi AI Camera", short_name="IMX500",
|
|
90
|
+
focal_length_mm=(4.74, 4.74), pixel_size_um=1.55, max_resolution=(4056, 3040),
|
|
91
|
+
aperture_f=1.79, hfov_deg=66.3, focus_type="Manual", has_ir_filter=True
|
|
92
|
+
)
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
rpi_cameras = {}
|
|
96
|
+
for cam_info in rpi_camera_definitions:
|
|
97
|
+
rpi_cameras[cam_info.name] = cam_info
|
|
98
|
+
rpi_cameras[cam_info.short_name] = cam_info
|
|
99
|
+
|
|
100
|
+
def __init__(self, model: Union[str, CameraInfo], resolution: "tuple[int, int] | str" = "720p"):
|
|
101
|
+
super().__init__()
|
|
102
|
+
if not isinstance(model, CameraInfo):
|
|
103
|
+
if model not in PiCamera.rpi_cameras:
|
|
104
|
+
raise ValueError(f"Unsupported: {model}. Supported models: {', '.join(PiCamera.rpi_cameras.keys())}")
|
|
105
|
+
if isinstance(resolution, str) and resolution not in resolutions and resolution not in resolutions.values():
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Unsupported resolution: {resolution}. Supported resolutions: {', '.join(str(r) for r in resolutions.values())}")
|
|
108
|
+
model = PiCamera.rpi_cameras[model]
|
|
109
|
+
if isinstance(resolution, str):
|
|
110
|
+
resolution = resolutions[resolution]
|
|
111
|
+
self.resolution = resolution
|
|
112
|
+
self.model = model
|
|
113
|
+
|
|
114
|
+
def _open(self):
|
|
115
|
+
try:
|
|
116
|
+
from picamera2 import Picamera2
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise ImportError("PiCamera could not be initialized. Is picamera2 installed?") from e
|
|
119
|
+
self.cam = Picamera2()
|
|
120
|
+
self.cam.preview_configuration.main.format = "RGB888"
|
|
121
|
+
self.cam.preview_configuration.main.size = self.resolution
|
|
122
|
+
self.cam.configure("preview")
|
|
123
|
+
self.cam.start()
|
|
124
|
+
|
|
125
|
+
def _read(self):
|
|
126
|
+
return self.cam.capture_array()
|
|
127
|
+
|
|
128
|
+
def _release(self):
|
|
129
|
+
if not self.closed:
|
|
130
|
+
self.closed = True
|
|
131
|
+
self.cam.stop()
|
|
132
|
+
self.cam.close()
|
|
133
|
+
|
|
134
|
+
def _size(self):
|
|
135
|
+
return self.resolution
|
|
136
|
+
|
|
137
|
+
def focal_length(self):
|
|
138
|
+
return self.model.focal_length(self.size())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .base_camera import BaseCamera
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ReadonlyCamera(BaseCamera):
|
|
5
|
+
def __init__(self, cam: BaseCamera):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.cam = cam
|
|
8
|
+
|
|
9
|
+
def _open(self):
|
|
10
|
+
self.cam._open()
|
|
11
|
+
|
|
12
|
+
def _read(self):
|
|
13
|
+
return self.cam._read()
|
|
14
|
+
|
|
15
|
+
def _release(self):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def _size(self):
|
|
19
|
+
return self.cam._size()
|
|
20
|
+
|
|
21
|
+
def _focus(self, rectangle: tuple[int, int, int, int]):
|
|
22
|
+
self.cam._focus(rectangle)
|
|
23
|
+
|
|
24
|
+
def focal_length(self):
|
|
25
|
+
return self.cam.focal_length()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from .base_camera import BaseCamera
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ScreenCapture(BaseCamera):
|
|
7
|
+
# region: {"top": int, "left": int, "width": int, "height": int}
|
|
8
|
+
def __init__(self, index=1, region: "dict[str, int] | None" = None,
|
|
9
|
+
focal_length: "tuple[float, float] | None" = None):
|
|
10
|
+
try:
|
|
11
|
+
import mss
|
|
12
|
+
self.mss = mss
|
|
13
|
+
except Exception as e:
|
|
14
|
+
raise ImportError("mss is required for this feature. Install with 'pip install omnicam[screen]'.") from e
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.index = index
|
|
17
|
+
self.region = region
|
|
18
|
+
self._focal_length = focal_length
|
|
19
|
+
|
|
20
|
+
def _open(self):
|
|
21
|
+
self.cap = self.mss.mss()
|
|
22
|
+
self.monitor = self.cap.monitors[self.index]
|
|
23
|
+
self.region = self.region if self.region is not None else self.monitor
|
|
24
|
+
|
|
25
|
+
def _read(self):
|
|
26
|
+
return np.array(self.cap.grab(
|
|
27
|
+
{
|
|
28
|
+
"top": self.monitor["top"] + self.region["top"],
|
|
29
|
+
"left": self.monitor["left"] + self.region["left"],
|
|
30
|
+
"width": self.region["width"],
|
|
31
|
+
"height": self.region["height"]
|
|
32
|
+
} if self.region is not None else self.monitor))
|
|
33
|
+
|
|
34
|
+
def _release(self):
|
|
35
|
+
self.cap.close()
|
|
36
|
+
|
|
37
|
+
def _size(self):
|
|
38
|
+
if self.region is not None:
|
|
39
|
+
return self.region["width"], self.region["height"]
|
|
40
|
+
return self.monitor["width"], self.monitor["height"]
|
|
41
|
+
|
|
42
|
+
def focal_length(self):
|
|
43
|
+
if self._focal_length is None:
|
|
44
|
+
raise ValueError("Focal length not set for ScreenCapture")
|
|
45
|
+
return self._focal_length
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
|
|
3
|
+
from .base_camera import CameraInfo
|
|
4
|
+
from .cv_camera_base import CVCameraBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimpleCamera(CVCameraBase):
|
|
8
|
+
def __init__(self, index=0, info: CameraInfo = None):
|
|
9
|
+
try:
|
|
10
|
+
import cv2
|
|
11
|
+
except Exception as exc:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"OpenCV is required for this feature. Install with 'pip install omnicam[opencv]'."
|
|
14
|
+
) from exc
|
|
15
|
+
args = [index, cv2.CAP_DSHOW] if platform.system() == "Windows" else [index, cv2.CAP_V4L2]
|
|
16
|
+
try:
|
|
17
|
+
super().__init__(args, info=info)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
raise RuntimeError(f"Camera with index {index} could not be opened: {e}") from e
|
|
20
|
+
self.index = index
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omnicam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package for camera utilities
|
|
5
|
+
Author-email: OguzhanUmutlu <contact@oguzhanumutlu.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Oğuzhan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/OguzhanUmutlu/omnicam
|
|
29
|
+
Project-URL: Issues, https://github.com/OguzhanUmutlu/omnicam/issues
|
|
30
|
+
Project-URL: Documentation, https://github.com/OguzhanUmutlu/omnicam#readme
|
|
31
|
+
Keywords: camera,opencv,vision,stream,capture
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Topic :: Multimedia :: Video
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
44
|
+
Requires-Python: >=3.8
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Requires-Dist: numpy>=1.21
|
|
48
|
+
Provides-Extra: opencv
|
|
49
|
+
Requires-Dist: opencv-python>=4.7; extra == "opencv"
|
|
50
|
+
Provides-Extra: screen
|
|
51
|
+
Requires-Dist: mss>=9.0; extra == "screen"
|
|
52
|
+
Provides-Extra: pi
|
|
53
|
+
Requires-Dist: picamera2>=0.3; extra == "pi"
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# omnicam
|
|
59
|
+
|
|
60
|
+
A small, unified API for reading frames from USB cameras, IP streams, video files, screen capture, and Raspberry Pi
|
|
61
|
+
cameras.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- Unified `BaseCamera` interface across backends
|
|
66
|
+
- OpenCV-based capture for USB webcams, video files, RTSP/HTTP streams, and GStreamer pipelines
|
|
67
|
+
- Optional Raspberry Pi Camera support (Picamera2)
|
|
68
|
+
- Optional screen capture via `mss`
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install omnicam
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Optional extras:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install omnicam[opencv]
|
|
80
|
+
pip install omnicam[screen]
|
|
81
|
+
pip install omnicam[pi]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quickstart
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from omnicam import SimpleCamera
|
|
88
|
+
|
|
89
|
+
with SimpleCamera(index=0) as cam:
|
|
90
|
+
cam.open()
|
|
91
|
+
frame = cam.read()
|
|
92
|
+
if frame is not None:
|
|
93
|
+
print(frame.shape)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Camera types
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from omnicam import FileCapture, InternetCapture, ScreenCapture
|
|
100
|
+
|
|
101
|
+
# Video file
|
|
102
|
+
cam = FileCapture("/path/to/video.mp4")
|
|
103
|
+
|
|
104
|
+
# IP camera or MJPEG stream
|
|
105
|
+
cam = InternetCapture("http://192.168.1.10:8080/video")
|
|
106
|
+
|
|
107
|
+
# Screen capture (requires omnicam[screen])
|
|
108
|
+
cam = ScreenCapture(index=1)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Raspberry Pi camera
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from omnicam import PiCamera
|
|
115
|
+
|
|
116
|
+
cam = PiCamera(model="IMX219", resolution="720p")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## GStreamer and Gazebo
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from omnicam import GStreamerCapture, GazeboCamera
|
|
123
|
+
|
|
124
|
+
pipeline = (
|
|
125
|
+
GStreamerCapture.GstPipeline()
|
|
126
|
+
.add("v4l2src", device="/dev/video0")
|
|
127
|
+
.add("videoconvert")
|
|
128
|
+
.add("appsink")
|
|
129
|
+
)
|
|
130
|
+
cam = GStreamerCapture(pipeline)
|
|
131
|
+
|
|
132
|
+
# Gazebo (requires gz tools available on PATH)
|
|
133
|
+
cam = GazeboCamera(topic_name="my_camera")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Notes
|
|
137
|
+
|
|
138
|
+
- `numpy` is required. OpenCV-based backends need `omnicam[opencv]`.
|
|
139
|
+
- GStreamer and Gazebo features rely on system-level tooling.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/camerapy/__init__.py
|
|
5
|
+
src/camerapy/base_camera.py
|
|
6
|
+
src/camerapy/cv_camera_base.py
|
|
7
|
+
src/camerapy/file_capture.py
|
|
8
|
+
src/camerapy/gazebo_camera.py
|
|
9
|
+
src/camerapy/gstreamer_capture.py
|
|
10
|
+
src/camerapy/internet_capture.py
|
|
11
|
+
src/camerapy/pi_camera.py
|
|
12
|
+
src/camerapy/readonly_camera.py
|
|
13
|
+
src/camerapy/screen_capture.py
|
|
14
|
+
src/camerapy/simple_camera.py
|
|
15
|
+
src/omnicam.egg-info/PKG-INFO
|
|
16
|
+
src/omnicam.egg-info/SOURCES.txt
|
|
17
|
+
src/omnicam.egg-info/dependency_links.txt
|
|
18
|
+
src/omnicam.egg-info/requires.txt
|
|
19
|
+
src/omnicam.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
camerapy
|