Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 8 additions & 2 deletions science_officer/Iceberg/Iceberg_Max_Depth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import platform
import sys
from pathlib import Path
from dataclasses import replace

PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OFFICER_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(OFFICER_ROOT))

os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|max_delay;0"

Expand Down Expand Up @@ -47,6 +49,10 @@ def parse_args():
# Initialize image source
try:
config = frame_capture.FrameSourceConfig.from_args(args)
config = replace(
config,
no_signal_image_path=str(PROJECT_ROOT / "Assets" / "nosignal_dark.jpg"),
)
frame_source = frame_capture.create_frame_source(config)
frame_source.start()
except ValueError as exc:
Expand Down
10 changes: 8 additions & 2 deletions science_officer/coral_garden/garden_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import platform
import sys
from pathlib import Path
from dataclasses import replace

PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OFFICER_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(OFFICER_ROOT))

os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|max_delay;0"

Expand Down Expand Up @@ -46,6 +48,10 @@ def parse_args():
# Initialize image source
try:
config = frame_capture.FrameSourceConfig.from_args(args)
config = replace(
config,
no_signal_image_path=str(PROJECT_ROOT / "Assets" / "nosignal_dark.jpg"),
)
frame_source = frame_capture.create_frame_source(config)
frame_source.start()
except ValueError as exc:
Expand Down
11 changes: 9 additions & 2 deletions science_officer/invasive_craba/yolo_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@

from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from dataclasses import replace

PROJECT_ROOT = Path(__file__).resolve().parents[2]
OFFICER_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(OFFICER_ROOT))

from shared import frame_capture
from shared import common_args
Expand Down Expand Up @@ -77,6 +80,10 @@ def parse_args():
# Load or initialize image source
try:
config = frame_capture.FrameSourceConfig.from_args(args)
config = replace(
config,
no_signal_image_path=str(PROJECT_ROOT / "Assets" / "nosignal_dark.jpg"),
)
frame_source = frame_capture.create_frame_source(config)
frame_source.start()
except ValueError as exc:
Expand Down
106 changes: 68 additions & 38 deletions science_officer/shared/frame_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class FrameSourceConfig:
width: Optional[int] = None
height: Optional[int] = None
ffmpeg_loglevel: str = "warning"
no_signal_image_path: Optional[str] = None

@classmethod
def from_args(cls, args):
Expand All @@ -34,6 +35,7 @@ def from_args(cls, args):
width=width,
height=height,
ffmpeg_loglevel=getattr(args, "ffmpeg_loglevel", "warning"),
no_signal_image_path=getattr(args, "no_signal_image_path", None),
)


Expand Down Expand Up @@ -82,6 +84,7 @@ def _create_ffmpeg_frame_source(config: FrameSourceConfig):
width=config.width,
height=config.height,
loglevel=config.ffmpeg_loglevel,
no_signal_image_path=config.no_signal_image_path,
)


Expand All @@ -100,23 +103,38 @@ def __init__(
width,
height,
rtsp_transport="tcp",
loglevel="error"
loglevel="error",
no_signal_image_path=None,
retry_delay=3.0,
):
self.url = url
self.width = width
self.height = height
self.frame_size = width * height * 3
self.rtsp_transport = rtsp_transport
self.loglevel = loglevel
self.retry_delay = retry_delay
self.use_videotoolbox = platform.system() == "Darwin"

self.proc = None
self.thread = None
self.running = False
self.lock = threading.Lock()
self.frame = None

self.no_signal_frame = cv2.imread(no_signal_image_path)
self.no_signal_frame = cv2.resize(
self.no_signal_frame,
(self.width, self.height),
)

self.frame = self.no_signal_frame.copy()

def start(self):
self.running = True
self.thread = threading.Thread(target=self._connection_loop, daemon=True)
self.thread.start()

def _build_cmd(self):
cmd = [
"ffmpeg",
"-hide_banner",
Expand Down Expand Up @@ -153,16 +171,39 @@ def start(self):
"pipe:1",
]

self.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=self.frame_size * 4,
)
return cmd

self.running = True
self.thread = threading.Thread(target=self._reader_loop, daemon=True)
self.thread.start()
def _connection_loop(self):
while self.running:
with self.lock:
self.frame = self.no_signal_frame.copy()

cmd = self._build_cmd()

self.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=self.frame_size * 4,
)

while self.running:
raw = self._read_exact(self.frame_size)

if raw is None:
break

frame = np.frombuffer(raw, dtype=np.uint8)
frame = frame.reshape((self.height, self.width, 3))

with self.lock:
self.frame = frame.copy()

self._stop_ffmpeg()

if self.running:
print(f"FFmpeg stream disconnected. Retrying in {self.retry_delay} seconds...")
time.sleep(self.retry_delay)

def _read_exact(self, size):
chunks = []
Expand All @@ -179,41 +220,30 @@ def _read_exact(self, size):

return b"".join(chunks)

def _reader_loop(self):
while self.running:
raw = self._read_exact(self.frame_size)

if raw is None:
self.running = False
break
def read(self):
if not self.running:
return False, None

frame = np.frombuffer(raw, dtype=np.uint8)
frame = frame.reshape((self.height, self.width, 3))
with self.lock:
return True, self.frame.copy()

with self.lock:
self.frame = frame.copy()
def _stop_ffmpeg(self):
if self.proc is None:
return

def read(self):
while self.running:
with self.lock:
if self.frame is not None:
return True, self.frame.copy()
self.proc.terminate()

time.sleep(0.005)
try:
self.proc.wait(timeout=2)
except subprocess.TimeoutExpired:
self.proc.kill()
self.proc.wait()

return False, None
self.proc = None

def release(self):
self.running = False

if self.proc is not None:
self.proc.terminate()

try:
self.proc.wait(timeout=2)
except subprocess.TimeoutExpired:
self.proc.kill()
self.proc.wait()
self._stop_ffmpeg()

class OpenCVFrameSource:
def __init__(self, source, source_type, width=None, height=None):
Expand Down