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
58 changes: 41 additions & 17 deletions examples/boson_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
python boson_record.py \
--serial_port path/to/serial/port \
--video_port path/to/video/port \
--raw_output_path path/to/output.npy \
--video_output_path path/to/output.mp4 \
--autonorm_path path/to/output.mp4 \
--globalnorm_path path/to/output.mp4 \
--metadata_path path/to/metadata.yaml \
--bosonsdk_path path/to/BosonSDK/SDK_USER_PERMISSIONS \
--recording_time 60

Expand All @@ -19,7 +20,7 @@
"""

import argparse
from time import sleep
from time import time, sleep
from heatseek.boson_capture import BosonCapture


Expand All @@ -34,13 +35,15 @@ def main():
Default is \dev\ttyACM0.
--video_port (str, opt): Path to video port.
Default is \dev\video0.
--raw_output_path (str, opt): Filepath to save
radiometric output (.npy)
--video_output_path (str, opt): Filepath to save
normalized video (.mp4)
--autonorm_path (str, opt): Filepath to save
normalized viewable video (.mp4)
--globalnorm_path (str, opt): Filepath to save
normalized radiometric (.mp4)
--metadata_path (str, opt): Filepath to save
radiometric metadata
--bosonsdk_path (str, opt) Filepath to boson sdk folder
--recording_time (int, opt): Duration of recording in seconds.
Default is 10s.
Default is 3hrs

Returns:
None
Expand All @@ -53,12 +56,15 @@ def main():
parser.add_argument('--video_port',
type=str,
help='path to video port. Default is /dev/video0')
parser.add_argument('--raw_output_path',
parser.add_argument('--autonorm_path',
type=str,
help='filepath to save raw radiometric output')
parser.add_argument('--video_output_path',
help='filepath to save noramlized viewable video')
parser.add_argument('--globalnorm_path',
type=str,
help='filepath to save normalized video output')
help='filepath to save normalized radiometric output')
parser.add_argument('--metadata_path',
type=str,
help='filepath to save radiometric metadata')
parser.add_argument('--bosonsdk_path',
type=str,
help='filepath to boson sdk')
Expand All @@ -67,14 +73,32 @@ def main():
help='time in s to record')
args = parser.parse_args()

# Camera Setup
camera = BosonCapture(serial_port=args.serial_port,
video_port=args.video_port,
sdkpath=args.bosonsdk_path)
camera.start_recording(raw=args.raw_output_path,
norm=args.video_output_path)
sleep(args.recording_time or 10)
camera.stop_recording()
camera.release_camera()
camera.start_recording(autonorm=args.autonorm_path,
globalnorm=args.globalnorm_path,
meta=args.metadata_path)
max_recording_time = args.recording_time or 10800 # 3hrs default
start_time = time()

# Recording Loop
try:
print('Recording ... Press CTRL+C to stop manually')
while True:
elapsed = time() - start_time
if elapsed >= max_recording_time:
print('Reached max recording time. Stopping recording')
break
sleep(.01)

except KeyboardInterrupt:
print('Stopping Recording')

finally:
camera.stop_recording()
camera.release_camera()


if __name__ == '__main__':
Expand Down
144 changes: 104 additions & 40 deletions heatseek/boson_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
import sys
import os
from time import sleep
import subprocess
from importlib import import_module
from datetime import datetime
import yaml
import numpy as np
import cv2
from heatseek.capture import Capture
Expand Down Expand Up @@ -46,7 +49,7 @@ def __init__(self, serial_port=None, video_port=None, sdkpath=None):
parameters

Args:
serial_port (str, opt): path to serial port.
serial_port (str, opt): path to serial port.
Defaults to \dev\ttyACM0.
video_port (str, opt): path to video port.
Defaults to \dev\video0.
Expand All @@ -60,8 +63,12 @@ def __init__(self, serial_port=None, video_port=None, sdkpath=None):
self.recording = False
self.height = 256
self.width = 320
self.raw_data_fpath = None
self.viewable_video_fpath = None
self.GLOBAL_MIN = 28000
self.GLOBAL_MAX = 32000
self.n_frames = 0
self.autonorm_fpath = None
self.globalnorm_fpath = None
self.metadata_fpath = None
self.recording_thread = None
self.raw_mm = None

Expand Down Expand Up @@ -135,22 +142,25 @@ def take_image(self):

print('Taking Image- PLACEHOLDER')

def start_recording(self, raw=None, norm=None):
def start_recording(self, autonorm=None, globalnorm=None, meta=None):
"""Begin thread for continuous recording

Args:
raw (str, opt): filepath to save raw radiometric data.
Defaults to output.npy
norm (str, opt): filepath to save normalized mp4 video.
Defaults to output.mp4
autonorm (str, opt): filepath to save frame by frame
noramlized video. Defaults to autonorm_{timestamp}.mp4
globalnorm (str, opt): filepath to save globally
normalized mp4 video. Defaults to globalnorm_{timestamp}.mp4
"""

if self.recording:
print('Recording in Progress!')
return

self.raw_data_fpath = raw or 'output.npy'
self.viewable_video_fpath = norm or 'output.mp4'
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

self.autonorm_fpath = autonorm or f'autonorm_{timestamp}.mp4'
self.globalnorm_fpath = globalnorm or f'globalnorm_{timestamp}.mkv'
self.metadata_fpath = meta or f'metadata_{timestamp}.yaml'

self.recording = True
self.recording_thread = threading.Thread(
Expand All @@ -159,6 +169,13 @@ def start_recording(self, raw=None, norm=None):
self.recording_thread.start()
print('Staring Recording...')

def _get_center_temp(self, frame):
"""Internal method: returns pixel value of frame center
"""

center = frame[int(self.height/2), int(self.width/2)]
return center

def _record_loop(self):
"""Internal method: recording loop to continuously capture frames

Expand All @@ -167,25 +184,38 @@ def _record_loop(self):
memory is full
"""

# video settings
# MP4 Setup
cap = cv2.VideoCapture(self.video_port, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
cap.set(cv2.CAP_PROP_CONVERT_RGB, 0)
cap.set(cv2.CAP_PROP_FOURCC,
cv2.VideoWriter_fourcc('Y', '1', '6', ' '))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(self.viewable_video_fpath,
fourcc,
60,
(self.width, self.height))
cv2.VideoWriter_fourcc('Y', '1', '6', ' '))
mpv4_fourcc = cv2.VideoWriter_fourcc(*'mp4v')
autonorm_writer = cv2.VideoWriter(self.autonorm_fpath,
mpv4_fourcc,
60,
(self.width, self.height))

# MKV Setup
proc = subprocess.Popen([
"ffmpeg", "-y",
"-f", "rawvideo",
"-pixel_format", "gray",
"-video_size", f"{self.width}x{self.height}",
"-framerate", "60",
"-i", "-",
"-c:v", "libx264",
self.globalnorm_fpath
], stdin=subprocess.PIPE)

# raw video setup
self.raw_mm = np.memmap(self.raw_data_fpath,
dtype=np.uint16,
mode='w+',
shape=(50000, self.height, self.width))
frame_index = 0
# self.raw_mm = np.memmap('raw_test.raw',
# dtype=np.uint16,
# mode='w+',
# shape=(600, self.height, self.width))

self.n_frames = 0

try:
while self.recording:
Expand All @@ -194,24 +224,39 @@ def _record_loop(self):
print('Frame grab failed. Stopping Recording')
break

# Viewable frame
frame_8bit = cv2.normalize(frame, None, 0, 255,
norm_type=cv2.NORM_MINMAX).astype(np.uint8)
frame_color = cv2.applyColorMap(frame_8bit,
cv2.COLORMAP_INFERNO).astype(np.uint8)
writer.write(frame_color)
# Autonorm Frame (MP4)
frame_8bit_autonorm = cv2.normalize(frame, None, 0, 255,
norm_type=cv2.NORM_MINMAX).astype(np.uint8)
frame_color = cv2.applyColorMap(frame_8bit_autonorm,
cv2.COLORMAP_INFERNO).astype(np.uint8)
autonorm_writer.write(frame_color)

# Globalnorm Frame (MKV)
frame_32bit = frame.astype(np.float32)
frame_8bit_globalnorm = (255.0 * (frame_32bit-self.GLOBAL_MIN)/(self.GLOBAL_MAX-self.GLOBAL_MIN)).astype(np.uint8)
proc.stdin.write(frame_8bit_globalnorm.tobytes())

# Raw Video
if frame_index > self.raw_mm.shape[0]:
print('Reached Preallocated Size, Stopping Recording')
break
self.raw_mm[frame_index] = frame
frame_index += 1
self.raw_mm.flush()
# if frame_index < self.raw_mm.shape[0]:
# if self.n_frames > self.raw_mm.shape[0]:
# print('Reached Preallocated Size, Stopping Recording')
# break
# self.raw_mm[self.n_frames] = frame
# frame_index += 1
# self.n_frames += 1
# self.raw_mm.flush()

self.n_frames += 1

finally:
cap.release()
autonorm_writer.release()

self._finalize_recording()

proc.stdin.close()
proc.wait()

def stop_recording(self):
"""Stop ongoing recording session.

Expand Down Expand Up @@ -239,14 +284,33 @@ def _finalize_recording(self):

self.recording = False

if self.raw_mm is not None:
self.raw_mm.flush()
del self.raw_mm
self.raw_mm = None
# if self.raw_mm is not None:
# self.raw_mm.flush()
# del self.raw_mm
# self.raw_mm = None

self._write_radiometric_metadata()

print('Recording Successfully Completed')
print(f'Radiometric data saved: {self.raw_data_fpath}')
print(f'Viewable Video: {self.viewable_video_fpath}')
print(f'Viewable Video: {self.autonorm_fpath}')
print(f'Globally normalized Video: {self.globalnorm_fpath}')
print(f'Radiometric Metadata: {self.metadata_fpath}')

def _write_radiometric_metadata(self):
"""Saves out all radiometric video metadata
"""

meta = {
'width': self.width,
'height': self.height,
'n_frames': self.n_frames,
'min': self.GLOBAL_MIN,
'max': self.GLOBAL_MAX,
'dtype': 'uint16',
}

with open(self.metadata_fpath, "w", encoding="utf-8") as f:
yaml.safe_dump(meta, f, sort_keys=False)

def release_camera(self):
"""Release camera resources.
Expand Down
16 changes: 15 additions & 1 deletion heatseek/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import requests
from .data_utils import download_dataset, write_data_yaml
from .train import train
from .preprocess import reduce_background
from .preprocess import reduce_background, reduce_background_radiometric
from .detect_track import detect_and_track
from .density_annotator import annotate_folder
def main():
Expand Down Expand Up @@ -58,6 +58,17 @@ def main():
help="YAML config for optical‐flow thresholds",
)

# preprocess
pre_r = subs.add_parser("preprocess_radiometric", help="Reduce background in a radiometric video")
pre_r.add_argument("--input", required=True, help="Input video path")
pre_r.add_argument("--output", required=True, help="Output video path")
pre_r.add_argument("--metadata", required=True, help="Radiometic video metadata path")
pre_r.add_argument(
"--config",
default="heatseek/config/preproc_config.yaml",
help="YAML config for optical‐flow thresholds",
)

# track
track = subs.add_parser("track", help="Detect + track objects in a video")
track.add_argument("--input", required=True, help="Input video path")
Expand Down Expand Up @@ -112,6 +123,9 @@ def main():
elif args.cmd == "preprocess":
reduce_background(args.input, args.output, args.config)

elif args.cmd == "preprocess_radiometric":
reduce_background_radiometric(args.input, args.output, args.metadata, args.config)

elif args.cmd == "track":
detect_and_track(args.input, args.output, args.weights)

Expand Down
2 changes: 1 addition & 1 deletion heatseek/config/preproc_config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# preproc_config.yaml
motion_thresh: 2.75
motion_thresh: 0.5
flow_params:
pyr_scale: 0.5
levels: 3
Expand Down
Loading