Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add basic USB-Camera support #10

Merged
merged 21 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: refactor camera code
Signed-off-by: Patrick Gehrsitz <mryel00.github@gmail.com>
  • Loading branch information
mryel00 committed Apr 3, 2024
commit d7cedac5f800b93ec5de74a59bd25a99db37e513
28 changes: 0 additions & 28 deletions spyglass/camera.py

This file was deleted.

22 changes: 22 additions & 0 deletions spyglass/camera/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from .camera import Camera
from .csi import CSI
from .usb import USB

from picamera2 import Picamera2

def init_camera(
width: int,
height: int,
fps: int,
autofocus: str,
lens_position: float,
autofocus_speed: str,
camera_num: int) -> Camera:

picam2 = Picamera2(camera_num)
if picam2._is_rpi_camera():
cam = CSI(width, height, fps, autofocus, lens_position, autofocus_speed)
else:
cam = USB(width, height, fps, autofocus, lens_position, autofocus_speed)
cam.configure()
return cam
68 changes: 68 additions & 0 deletions spyglass/camera/camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from abc import ABC, abstractmethod
from picamera2 import Picamera2
from .. import logger
from .. server import StreamingServer, StreamingHandler

class Camera(ABC):
def __init__(self,
picam2: Picamera2,
width: int,
height: int,
fps: int,
autofocus: str,
lens_position: float,
autofocus_speed: str):

self.picam2 = picam2
self.width = width
self.height = height
self.fps = fps
self.autofocus = autofocus
self.lens_position = lens_position
self.autofocus_speed = autofocus_speed

def create_controls(self):
controls = {'FrameRate': self.fps}

if 'AfMode' in self.picam2.camera_controls:
controls['AfMode'] = self.autofocus
controls['AfSpeed'] = self.autofocus_speed
if self.autofocus == self.libcamera.controls.AfModeEnum.Manual:
controls['LensPosition'] = self.lens_position
else:
print('Attached camera does not support autofocus')

return controls

def _run_server(self,
bind_address,
port,
output,
streaming_handler: StreamingHandler,
stream_url='/stream',
snapshot_url='/snapshot'):
logger.info('Server listening on %s:%d', bind_address, port)
logger.info('Streaming endpoint: %s', stream_url)
logger.info('Snapshot endpoint: %s', snapshot_url)
address = (bind_address, port)
streaming_handler.output = output
streaming_handler.stream_url = stream_url
streaming_handler.snapshot_url = snapshot_url
current_server = StreamingServer(address, streaming_handler)
current_server.serve_forever()

@abstractmethod
def configure(self):
pass

@abstractmethod
def stop(self):
pass

@abstractmethod
def start_and_run_server(self,
bind_address,
port,
stream_url='/stream',
snapshot_url='/snapshot'):
pass
48 changes: 48 additions & 0 deletions spyglass/camera/csi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import io

from picamera2.encoders import MJPEGEncoder
from picamera2.outputs import FileOutput
from threading import Condition

from . import camera
from .. server import StreamingHandler

class CSI(camera.Camera):
def configure(self):
controls = self.create_controls()

self.picam2.configure(
self.picam2.create_video_configuration(
main={'size': (self.width, self.height)},
controls=controls
)
)

def start_and_run_server(self,
bind_address,
port,
stream_url='/stream',
snapshot_url='/snapshot'):

class StreamingOutput(io.BufferedIOBase):
def __init__(self):
self.frame = None
self.condition = Condition()

def write(self, buf):
with self.condition:
self.frame = buf
self.condition.notify_all()
output = StreamingOutput()
self.picam2.start_recording(MJPEGEncoder(), FileOutput(output))
self._run_server(
bind_address,
port,
output,
StreamingHandler(),
stream_url=stream_url,
snapshot_url=snapshot_url
)

def stop(self):
self.picam2.stop_recording()
37 changes: 37 additions & 0 deletions spyglass/camera/usb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from . import camera

from spyglass.server import StreamingHandler, StreamingServer
from .. import logger

class USB(camera.Camera):
def configure(self):
controls = self.create_controls()

self.picam2.configure(
self.picam2.create_preview_configuration(
main={'size': (self.width, self.height), 'format': 'MJPEG'},
controls=controls
)
)

def start_and_run_server(self,
bind_address,
port,
stream_url='/stream',
snapshot_url='/snapshot'):
class StreamingHandlerUSB(StreamingHandler):
def get_frame(self):
#TODO: Cuts framerate in 1/n with n streams open, add some kind of buffer
return self.output.capture_buffer()
self.picam2.start()
self._run_server(
bind_address,
port,
self.picam2,
StreamingHandlerUSB(),
stream_url=stream_url,
snapshot_url=snapshot_url
)

def stop(self):
self.picam2.stop()
52 changes: 12 additions & 40 deletions spyglass/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,7 @@
import sys
import libcamera
import re
from spyglass.camera import init_camera
from spyglass.usbCamera import init_usb_camera
from spyglass.server import StreamingOutput
from spyglass.server import run_server
from spyglass.usbServer import run_usb_server
from picamera2.encoders import MJPEGEncoder
from picamera2.outputs import FileOutput

from .camera import init_camera

def main(args=None):
"""Entry point for hello cli.
Expand All @@ -32,38 +25,18 @@ def main(args=None):
width, height = split_resolution(parsed_args.resolution)
stream_url = parsed_args.stream_url
snapshot_url = parsed_args.snapshot_url
is_usbcam = parsed_args.is_usbcam

if is_usbcam:
picam2 = init_usb_camera(
width,
height,
parsed_args.fps,
parse_autofocus(parsed_args.autofocus),
parsed_args.lensposition,
parse_autofocus_speed(parsed_args.autofocusspeed),
parsed_args.camera_num)
try:
picam2.start()
run_usb_server(bind_address, port, picam2, stream_url, snapshot_url)
finally:
picam2.stop()
else:
picam2 = init_camera(
width,
height,
parsed_args.fps,
parse_autofocus(parsed_args.autofocus),
parsed_args.lensposition,
parse_autofocus_speed(parsed_args.autofocusspeed),
parsed_args.camera_num)
try:
output = StreamingOutput()
picam2.start_recording(MJPEGEncoder(), FileOutput(output))
run_server(bind_address, port, output, stream_url, snapshot_url)
finally:
picam2.stop_recording()

cam = init_camera(width,
height,
parsed_args.fps,
parse_autofocus(parsed_args.autofocus),
parsed_args.lensposition,
parse_autofocus_speed(parsed_args.autofocusspeed),
parsed_args.camera_num)
try:
cam.start_and_run_server(bind_address, port, stream_url, snapshot_url)
finally:
cam.stop()

# region args parsers

Expand Down Expand Up @@ -133,7 +106,6 @@ def get_parser():
parser.add_argument('-s', '--autofocusspeed', type=str, default='normal', choices=['normal', 'fast'],
help='Autofocus speed. Only used with Autofocus continuous')
parser.add_argument('-n', '--camera_num', type=int, default=0, help='Camera number to be used')
parser.add_argument('-u', '--is_usbcam', action='store_true', help='Make MJPEG USB-Cams usable. Only use if normal method doesn\'t work')
return parser

# endregion cli args
14 changes: 0 additions & 14 deletions spyglass/server.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
#!/usr/bin/python3

import io
import logging
import socketserver
from http import server
from threading import Condition
from . import logger


class StreamingOutput(io.BufferedIOBase):
def __init__(self):
self.frame = None
self.condition = Condition()

def write(self, buf):
with self.condition:
self.frame = buf
self.condition.notify_all()


class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
allow_reuse_address = True
daemon_threads = True
Expand Down
28 changes: 0 additions & 28 deletions spyglass/usbCamera.py

This file was deleted.

22 changes: 0 additions & 22 deletions spyglass/usbServer.py

This file was deleted.