Skip to content

Commit

Permalink
Merge my adafruithat changes with the pscopehat version
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanjli committed Mar 14, 2024
1 parent 1e9eac5 commit af3bdc1
Show file tree
Hide file tree
Showing 7 changed files with 572 additions and 83 deletions.
420 changes: 397 additions & 23 deletions control/adafruithat/planktoscope/imagernew/__init__.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def do_GET(self):
time.sleep(self.delay)

except Exception as e:
logger.info(f"Removed streaming client {self.client_address}")
logger.info(f"Removed streaming client {self.client_address}") #FIXME client_address is not defined, remove it?
else:
self.send_error(404)
self.end_headers()
Expand Down
66 changes: 66 additions & 0 deletions control/adafruithat/planktoscope/imagernew/picam_threading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
################################################################################
# Practical Libraries
################################################################################

# Library to execute picamera in a separate thread within the imager process
import threading

# Logger library compatible with multiprocessing
from loguru import logger

# Library to create a queue for commands coming to the camera
#import queue

# Library to manage time commands and delay execution for a given time
import time

################################################################################
# Class for the implementation of picamera2 thread
################################################################################

class PicamThread(threading.Thread):
"""This class contains the main definitions of picamera thread"""

def __init__(self, camera, command_queue, stop_event):
"""Initialize the picamera thread class
Args:
camera: picamera instance
command_queue (queue.Queue): queue for commands, when info must be exchanged safely between several threads
stop_event (multiprocessing.Event or threading.Event): shutdown event
"""
super().__init__()
self.__picam = camera
self.command_queue = command_queue #FIXME remove the queue for now if not used
self.stop_event = stop_event

@logger.catch
def run(self):
try:
self.__picam.start()
except Exception as e:
logger.exception(
f"An exception has occured when starting up picamera2: {e}"
)
try:
self.__picam.start(True)
except Exception as e:
logger.exception(
f"A second exception has occured when starting up picamera2: {e}"
)
logger.error("This error can't be recovered from, terminating now")
raise e
try:
while not self.stop_event.is_set():
"""if not self.command_queue.empty():
try:
# Retrieve a command from the queue with a timeout to avoid indefinite blocking
command = self.command_queue.get(timeout=0.1)
except Exception as e:
logger.exception(f"An error has occurred while handling a command: {e}")"""
pass
time.sleep(0.01)
finally:
self.__picam.stop()
self.__picam.close()

41 changes: 40 additions & 1 deletion control/adafruithat/planktoscope/imagernew/picamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@
# Class for the implementation of Picamera 2
################################################################################
class picamera:
"""This class contains the main definitions of picamera2 monitoring the PlanktoScope's camera"""

def __init__(self, output, *args, **kwargs):
"""Initialize the picamera class
Args:
output (picam_streamer.StreamingOutput): receive encoded video frames directly from the encoder and forward them to network sockets
"""
# Note(ethanjli): if we instantiate Picamera2 here in one process and then call the start
# method from a child process, then the start method's call of self.__picam.configure
# will block forever because we've basically duplicated the Picamera2 object when we forked
Expand Down Expand Up @@ -71,6 +78,36 @@ def start(self, force=False):
#self.__picam.start_recording(JpegEncoder(), FileOutput(self.__output), Quality.HIGH)
self.__picam.start_recording(MJPEGEncoder(), FileOutput(self.__output), Quality.HIGH)

#NOTE function drafted as a target of the camera thread (simple version)
"""def preview_picam(self):
try:
self.__picam.start()
except Exception as e:
logger.exception(
f"An exception has occured when starting up picamera2: {e}"
)
try:
self.__picam.start(True)
except Exception as e:
logger.exception(
f"A second exception has occured when starting up picamera2: {e}"
)
logger.error("This error can't be recovered from, terminating now")
raise e
try:
while not self.stop_event.is_set():
if not self.command_queue.empty():
try:
# Retrieve a command from the queue with a timeout to avoid indefinite blocking
command = self.command_queue.get(timeout=0.1)
except Exception as e:
logger.exception(f"An error has occurred while handling a command: {e}")
pass
time.sleep(0.01)
finally:
self.__picam.stop()
self.__picam.close()"""

@property
def sensor_name(self):
"""Sensor name of the connected camera
Expand Down Expand Up @@ -257,12 +294,14 @@ def capture(self, path=""):
#metadata = self.__picam.capture_file(path) #use_video_port
request = self.__picam.capture_request()
request.save("main", path)
request.release()

time.sleep(0.1)
request.release()

def stop(self):
"""Release the camera"""
logger.debug("Releasing the camera now")
self.__picam.stop_preview()
self.__picam.stop_recording()

def close(self):
Expand Down
82 changes: 48 additions & 34 deletions control/pscopehat/planktoscope/imagernew/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ def __init__(self, stop_event, exposure_time=10000, iso=100):
)
configuration = {}

self.__camera_type = "v2.1"
self.__camera_type = configuration.get("camera_type", "v2.1")

# parse the config data. If the key is absent, we are using the default value
self.__camera_type = configuration.get("camera_type", self.__camera_type)

#self.command_queue = queue.Queue()
self.command_queue = queue.Queue()
#self.shutdown_event = threading.Event()
self.stop_event = stop_event
self.__imager = planktoscope.imagernew.state_machine.Imager()
Expand All @@ -66,21 +64,12 @@ def __init__(self, stop_event, exposure_time=10000, iso=100):
self.__pump_direction = "FORWARD"
self.__img_goal = None
self.imager_client = None
self.streaming_output = planktoscope.imagernew.picam_streamer.StreamingOutput()
self.__error = 0

# Initialize the camera
self.streaming_output = planktoscope.imagernew.picam_streamer.StreamingOutput()
self.__camera = planktoscope.imagernew.picamera.picamera(self.streaming_output)

"""if self.__camera.sensor_name == "IMX219": # Camera v2.1
self.__resolution = (3280, 2464)
elif self.__camera.sensor_name == "IMX477": # Camera HQ
self.__resolution = (4056, 3040)
else:
self.__resolution = (1280, 1024)
logger.error(
f"The connected camera {self.__camera.sensor_name} is not recognized, please check your camera"
)"""
self.__resolution = None # this is set by the start method

#self.__iso = iso
self.__exposure_time = exposure_time
Expand All @@ -100,22 +89,6 @@ def __init__(self, stop_event, exposure_time=10000, iso=100):
self.__export_path = ""
self.__global_metadata = None

logger.info("Initialising the camera with the default settings")
# TODO identify the camera parameters that can be accessed and initialize them
self.__camera.exposure_time = self.__exposure_time
time.sleep(0.1)

self.__camera.exposure_mode = self.__exposure_mode
time.sleep(0.1)

self.__camera.white_balance = self.__white_balance
time.sleep(0.1)

self.__camera.white_balance_gain = self.__white_balance_gain
time.sleep(0.1)

self.__camera.image_gain = self.__image_gain

logger.success("planktoscope.imager is initialised and ready to go!")

# copied #
Expand Down Expand Up @@ -591,11 +564,52 @@ def run(self):
logger.info("Starting the camera and streaming server threads")
try:
# Initialize the camera thread
self.camera_thread = planktoscope.imagernew.picam_threading.PicamThread(self.__camera, self.stop_event)

#TODO Start the video recording
self.camera_thread = planktoscope.imagernew.picam_threading.PicamThread(self.__camera, self.command_queue, self.stop_event)

# Note(ethanjli): the camera must be started in the same process as anything which uses
# self.streaming_output, such as our StreamingHandler. This is because
# self.streaming_output does not synchronize state across independent processes!
# TODO(ethanjli): it would be cleaner if we can start the camera and the StreamingServer
# separately from the MQTT client; if it's possible, we can figure that out later.
# TODO(W7CH): Start the video recording
self.camera_thread.start()

except Exception as e:
logger.exception(
f"An exception has occured when starting up picamera2: {e}"
)
try:
self.__camera.start(True)
except Exception as e:
logger.exception(
f"A second exception has occured when starting up picamera2: {e}"
)
logger.error("This error can't be recovered from, terminating now")
raise e

logger.info("Initialising the camera with the default settings...")
# TODO identify the camera parameters that can be accessed and initialize them
self.__camera.exposure_time = self.__exposure_time
time.sleep(0.1)
self.__camera.exposure_mode = self.__exposure_mode
time.sleep(0.1)
self.__camera.white_balance = self.__white_balance
time.sleep(0.1)
self.__camera.white_balance_gain = self.__white_balance_gain
time.sleep(0.1)
self.__camera.image_gain = self.__image_gain

"""if self.__camera.sensor_name == "IMX219": # Camera v2.1
self.__resolution = (3280, 2464)
elif self.__camera.sensor_name == "IMX477": # Camera HQ
self.__resolution = (4056, 3040)
else:
self.__resolution = (1280, 1024)
logger.error(
f"The connected camera {self.__camera.sensor_name} is not recognized, please check your camera"
)"""

try:
address = ("", 8000)
fps = 15
refresh_delay = 1 / fps
Expand Down
21 changes: 1 addition & 20 deletions control/pscopehat/planktoscope/imagernew/picam_streamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,6 @@
import http.server
from threading import Condition

PAGE = """\
<html>
<head>
<title>picamera2 MJPEG streaming demo</title>
</head>
<body>
<h1>Picamera2 MJPEG Streaming Demo</h1>
<img src="stream.mjpg" width="800" height="600" />
</body>
</html>
"""

################################################################################
# Classes for the PiCamera Streaming
################################################################################
Expand All @@ -42,15 +30,8 @@ def __init__(self, delay, output, *args, **kwargs):
def do_GET(self):
if self.path == "/":
self.send_response(301)
self.send_header("Location", "/index.html") #stream.mjpg
self.end_headers()
elif self.path == '/index.html':
content = PAGE.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.send_header("Location", "/stream.mjpg")
self.end_headers()
self.wfile.write(content)
elif self.path == "/stream.mjpg":
self.send_response(200)
self.send_header("Age", 0)
Expand Down
23 changes: 19 additions & 4 deletions control/pscopehat/planktoscope/imagernew/picamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,20 @@ def __init__(self, output, *args, **kwargs):
Args:
output (picam_streamer.StreamingOutput): receive encoded video frames directly from the encoder and forward them to network sockets
"""
self.__picam = Picamera2()
# Note(ethanjli): if we instantiate Picamera2 here in one process and then call the start
# method from a child process, then the start method's call of self.__picam.configure
# will block forever because we've basically duplicated the Picamera2 object when we forked
# into a child process. So instead we initialize self.__picam to None here, and we'll
# properly initialize self.__picam in the start method, which is called by a different
# process than the process which calls __init__.
self.__picam = None
self.__controls = {}
self.__output = output
self.__sensor_name = ""

# TODO decide which stream to display (main, lores or raw)
def start(self, force=False):
self.__picam = Picamera2()
logger.debug("Starting up picamera2")
if force:
# let's close the camera first
Expand All @@ -51,6 +58,12 @@ def start(self, force=False):
half_resolution = [dim // 2 for dim in self.__picam.sensor_resolution]
main_stream = {"size": half_resolution}
lores_stream = {"size": (640, 480)}
# Note(ethanjli): if we use "lores" as our encode argument, we must use the MJPEGEncoder
# instead of JpegEncoder. This is because on the RPi4 the lores stream only outputs as
# YUV420, but JpegEncoder only accepts RGB. By contrast, MJPEGEncoder can handle YUV420.
# If we do need RGB output for something, we'll have to use the "main" stream instead of the
# "lores" stream for that. For details, refer to Table 1 on page 59 of the picamera2 manual
# at https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf.
config = self.__picam.create_video_configuration(main_stream, lores_stream, encode="lores")
self.__picam.configure(config)

Expand All @@ -60,8 +73,10 @@ def start(self, force=False):
self.__height = self.__picam.sensor_resolution[1]

# Start recording with video encoding and writing video frames
self.__picam.start_preview(Preview.QT) #FIXME it's recommended when the image needs to be shown on another networked device
self.__picam.start_recording(JpegEncoder(), FileOutput(self.__output), Quality.HIGH)
# Note(ethanjli): see note above about JpegEncoder vs. MJPEGEncoder compatibility with
# "lores" streams!
#self.__picam.start_recording(JpegEncoder(), FileOutput(self.__output), Quality.HIGH)
self.__picam.start_recording(MJPEGEncoder(), FileOutput(self.__output), Quality.HIGH)

#NOTE function drafted as a target of the camera thread (simple version)
"""def preview_picam(self):
Expand Down Expand Up @@ -273,7 +288,7 @@ def capture(self, path=""):
"""Capture an image (in full resolution)
Args:
path (str, optional): Path to image file. Default to "".
path (str, optional): Path to image file. Defaults to "".
"""
logger.debug(f"Capturing an image to {path}")
#metadata = self.__picam.capture_file(path) #use_video_port
Expand Down

0 comments on commit af3bdc1

Please sign in to comment.