diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..54b5483 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,5 @@ +sshpass -p Bookshelf scp -r src/ username@10.42.0.1:~/ + +echo "########## Starting #########" + +sshpass -p Bookshelf ssh username@10.42.0.1 "cd src && python3 main.py" diff --git a/pygame-video.py b/pygame-video.py new file mode 100644 index 0000000..c0f4722 --- /dev/null +++ b/pygame-video.py @@ -0,0 +1,162 @@ +from pygame.locals import KEYDOWN, K_ESCAPE, K_q +import pygame +import cv2 +import sys + +size = [400, 300] + +camera = cv2.VideoCapture(0) +pygame.init() +pygame.display.set_caption("OpenCV camera stream on Pygame") +screen = pygame.display.set_mode(size) + +font = pygame.font.SysFont('Arial', 12) + +MAJOR_TICK_LENGTH = 15 +MINOR_TICK_LENGTH = 8 +LABEL_OFFSET = 20 + +WHITE = (255, 255, 255) +GRAY = (128, 128, 128) + + +YAW_HEIGHT = 1 +PITCH_WIDTH = 10 + +class PitchYawHUD: + def __init__(self, screen_width=800, screen_height=600): + # pygame.init() + # screen = pygame.display.set_mode((screen_width, screen_height)) + # pygame.display.set_caption("Pitch & Yaw HUD") + + # Colors + self.WHITE = (255, 255, 255) + self.GRAY = (128, 128, 128) + + # HUD dimensions + self.YAW_HEIGHT = 1 + self.PITCH_WIDTH = 10 + + # Tick marks + self.MAJOR_TICK_LENGTH = 15 + self.MINOR_TICK_LENGTH = 8 + self.LABEL_OFFSET = 20 + + # Font + self.font = pygame.font.SysFont('Arial', 12) + + # Cardinal directions for yaw + self.cardinal_directions = { + 0: "N", 45: "NE", 90: "E", 135: "SE", + 180: "S", 225: "SW", 270: "W", 315: "NW" + } + + def draw_yaw_indicator(self, screen, yaw_angle): + # Normalize yaw angle to 0-360 + yaw_angle = yaw_angle % 360 + + + # Calculate pixel per degree for yaw + pixels_per_degree = screen.get_width() / 360 + + # Draw tick marks + for angle in range(0, 360, 5): # Draw every 5 degrees + x_pos = (angle - yaw_angle) * pixels_per_degree + x_pos = x_pos % screen.get_width() + + # Determine tick length + if angle % 45 == 0: # Cardinal and intercardinal directions + tick_length = self.MAJOR_TICK_LENGTH + # Draw direction label + if angle in self.cardinal_directions: + label = self.font.render(self.cardinal_directions[angle], True, self.WHITE) + screen.blit(label, (x_pos - label.get_width()/2, self.LABEL_OFFSET)) + else: + tick_length = self.MINOR_TICK_LENGTH + + # Draw tick + pygame.draw.line(screen, self.WHITE, + (x_pos, 0), + (x_pos, tick_length)) + + def draw_pitch_indicator(self, screen, pitch_angle): + # Normalize pitch angle to -180 to 180 + pitch_angle = max(-180, min(180, pitch_angle)) + + # Draw background + # pygame.draw.rect(screen, self.GRAY, + # (screen.get_width() - self.PITCH_WIDTH, 0, + # self.PITCH_WIDTH, screen.get_height())) + + # Calculate pixel per degree for pitch + pixels_per_degree = screen.get_height() / 360 + + # Draw tick marks + for angle in range(-180, 181, 10): # Draw every 10 degrees + y_pos = screen.get_height()/2 + (angle - pitch_angle) * pixels_per_degree + + # Skip if outside screen + if y_pos < 0 or y_pos > screen.get_height(): + continue + + # Determine tick length + if angle % 30 == 0: # Major ticks + tick_length = self.MAJOR_TICK_LENGTH + # Draw angle label + label = self.font.render(str(angle), True, self.WHITE) + screen.blit(label, + (screen.get_width() - self.PITCH_WIDTH - label.get_width() - 5, + y_pos - label.get_height()/2)) + else: + tick_length = self.MINOR_TICK_LENGTH + + # Draw tick + pygame.draw.line(screen, self.WHITE, + (screen.get_width() - self.PITCH_WIDTH, y_pos), + (screen.get_width() - self.PITCH_WIDTH + tick_length, y_pos)) + + def update(self, screen, pitch, yaw): + # screen.fill((0, 0, 0)) # Clear screen + self.draw_yaw_indicator(screen, yaw) + self.draw_pitch_indicator(screen, pitch) + pygame.display.flip() + + + +hud = PitchYawHUD() + +pitch = 0 +yaw = 0 + + + +try: + while True: + + ret, frame = camera.read() + + if not ret: continue + + screen.fill([0, 0, 0]) + frame = cv2.resize(frame, dsize=(size[0], size[1]), interpolation=cv2.INTER_CUBIC) + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + frame = frame.swapaxes(0, 1) + pygame.surfarray.blit_array(screen, frame) + pygame.display.update() + + hud.update(screen, pitch, yaw) + + pitch += 1 + yaw += 1 + + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit(0) + elif event.type == KEYDOWN: + if event.key == K_ESCAPE or event.key == K_q: + sys.exit(0) + +except (KeyboardInterrupt, SystemExit): + pygame.quit() + cv2.destroyAllWindows() \ No newline at end of file diff --git a/raspberry-pi-logo.png b/raspberry-pi-logo.png new file mode 100644 index 0000000..0d715c5 Binary files /dev/null and b/raspberry-pi-logo.png differ diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..a1afa86 --- /dev/null +++ b/src/main.py @@ -0,0 +1,240 @@ +import numpy as np +import fcntl +import mmap +import struct +import time +import os + +class FrameBuffer: + """Class to handle direct framebuffer access on Raspberry Pi.""" + + def __init__(self, device='/dev/fb0'): + # Open the framebuffer device + self.fb = open(device, 'rb+') + + # Get fixed screen information + fix_info = struct.unpack('IIIIHH', + fcntl.ioctl(self.fb.fileno(), + 0x4602, # FBIOGET_FSCREENINFO + struct.pack('IIIIHH', 0, 0, 0, 0, 0, 0))) + + # Get variable screen information + var_info = struct.unpack('IIIIIIIIHHHHHH', + fcntl.ioctl(self.fb.fileno(), + 0x4600, # FBIOGET_VSCREENINFO + struct.pack('IIIIIIIIHHHHHH', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))) + + self.xres = var_info[0] + self.yres = var_info[1] + self.bits_per_pixel = var_info[6] + self.bytes_per_pixel = self.bits_per_pixel // 8 + + print(f"Screen size: {self.xres}x{self.yres}") + + # Map the framebuffer to memory + self.fb_size = fix_info[1] # Length of framebuffer memory + self.fb_map = mmap.mmap(self.fb.fileno(), self.fb_size, mmap.MAP_SHARED, mmap.PROT_WRITE|mmap.PROT_READ) + + def display_frame(self, frame): + """Display a numpy array as a frame. + + Args: + frame: numpy array of shape (height, width, 3) with uint8 RGB values + """ + # Ensure frame matches framebuffer dimensions + if frame.shape[:2] != (self.yres, self.xres): + frame = cv2.resize(frame, (self.xres, self.yres)) + + # Convert frame to correct format (BGR888 to RGB565) + if self.bits_per_pixel == 16: + # Convert RGB888 to RGB565 + r = (frame[:, :, 0] >> 3).astype(np.uint16) << 11 + g = (frame[:, :, 1] >> 2).astype(np.uint16) << 5 + b = (frame[:, :, 2] >> 3).astype(np.uint16) + frame_rgb565 = (r | g | b).astype(np.uint16) + buffer = frame_rgb565.tobytes() + else: + # Assume 24/32 bit color + buffer = frame.tobytes() + + # Write to framebuffer + self.fb_map.seek(0) + self.fb_map.write(buffer) + + def __del__(self): + self.fb_map.close() + self.fb.close() + + + + +class PitchYawHUD: + def __init__(self, width=800, height=600): + self.width = width + self.height = height + + # Colors (in BGR for OpenCV) + self.WHITE = (255, 255, 255) + self.GRAY = (128, 128, 128) + self.BLACK = (0, 0, 0) + + # HUD dimensions + self.YAW_HEIGHT = 40 + self.PITCH_WIDTH = 40 + + # Tick marks + self.MAJOR_TICK_LENGTH = 15 + self.MINOR_TICK_LENGTH = 8 + self.LABEL_OFFSET = 20 + + # Font + self.font = cv2.FONT_HERSHEY_SIMPLEX + self.font_scale = 0.4 + + # Cardinal directions for yaw + self.cardinal_directions = { + 0: "N", 45: "NE", 90: "E", 135: "SE", + 180: "S", 225: "SW", 270: "W", 315: "NW" + } + + def draw_text(self, img, text, pos, color=None): + if color is None: + color = self.WHITE + cv2.putText(img, text, pos, self.font, self.font_scale, color, 1, cv2.LINE_AA) + + def draw_yaw_indicator(self, img, yaw_angle): + # Normalize yaw angle to 0-360 + yaw_angle = yaw_angle % 360 + + # Draw background + # cv2.rectangle(img, (0, 0), (self.width, self.YAW_HEIGHT), self.GRAY, -1) + + # Calculate pixel per degree for yaw + pixels_per_degree = self.width / 360 + + # Draw tick marks + for angle in range(0, 360, 5): # Draw every 5 degrees + x_pos = int((angle - yaw_angle) * pixels_per_degree) + x_pos = x_pos % self.width + + # Determine tick length + if angle % 45 == 0: # Cardinal and intercardinal directions + tick_length = self.MAJOR_TICK_LENGTH + # Draw direction label + if angle in self.cardinal_directions: + text = self.cardinal_directions[angle] + text_size = cv2.getTextSize(text, self.font, self.font_scale, 1)[0] + text_x = int(x_pos - text_size[0]/2) + self.draw_text(img, text, (text_x, self.LABEL_OFFSET + text_size[1])) + else: + tick_length = self.MINOR_TICK_LENGTH + + # Draw tick + cv2.line(img, (x_pos, 0), (x_pos, tick_length), self.WHITE, 1, cv2.LINE_AA) + + def draw_pitch_indicator(self, img, pitch_angle): + # Normalize pitch angle to -180 to 180 + pitch_angle = max(-180, min(180, pitch_angle)) + + # Draw background + # cv2.rectangle(img, + # (self.width - self.PITCH_WIDTH, 0), + # (self.width, self.height), + # self.GRAY, -1) + + # Calculate pixel per degree for pitch + pixels_per_degree = self.height / 360 + + # Draw tick marks + for angle in range(-180, 181, 10): # Draw every 10 degrees + y_pos = int(self.height/2 + (angle - pitch_angle) * pixels_per_degree) + + # Skip if outside screen + if y_pos < 0 or y_pos > self.height: + continue + + # Determine tick length + if angle % 30 == 0: # Major ticks + tick_length = self.MAJOR_TICK_LENGTH + # Draw angle label + text = str(angle) + text_size = cv2.getTextSize(text, self.font, self.font_scale, 1)[0] + text_x = self.width - self.PITCH_WIDTH - text_size[0] - 5 + text_y = int(y_pos + text_size[1]/2) + self.draw_text(img, text, (text_x, text_y)) + else: + tick_length = self.MINOR_TICK_LENGTH + + # Draw tick + start_point = (self.width - self.PITCH_WIDTH, y_pos) + end_point = (self.width - self.PITCH_WIDTH + tick_length, y_pos) + cv2.line(img, start_point, end_point, self.WHITE, 1, cv2.LINE_AA) + + def update(self, img, pitch, yaw): + # Create blank image + # img = np.zeros((self.height, self.width, 3), dtype=np.uint8) + + # Draw indicators + self.draw_yaw_indicator(img, yaw) + self.draw_pitch_indicator(img, pitch) + + return img + + + + + +# Example usage +if __name__ == "__main__": + print("Importing...") + import cv2 + import time + + # Initialize framebuffer + print("Framebuffer...") + fb = FrameBuffer() + + hud = PitchYawHUD(width=fb.xres, height=fb.yres) + + # Create a test pattern + print("Creating test pattern...") + test_frame = np.zeros((fb.yres, fb.xres, 3), dtype=np.uint8) + + cap = cv2.VideoCapture(0) + + if not cap.isOpened(): + print("Error: Could not open camera.") + exit() + + + + cap.set(cv2.CAP_PROP_FRAME_WIDTH, fb.xres) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, fb.yres) + time.sleep(2) + + + # cap.set(cv2.CV_CAP_PROP_FRAME_WIDTH, fb.xres) + # cap.set(cv2.CV_CAP_PROP_FRAME_HEIGHT, fb.yres) + # cap.set(cv2.CV_CAP_PROP_EXPOSURE, 0.1) + + while True: + ret, frame = cap.read() + if not ret: + # print("err") + continue + + print(frame.shape) + + # frame = frame[:fb.xres, :fb.yres] + frame = cv2.resize(frame, dsize=(fb.xres, fb.yres), interpolation=cv2.INTER_CUBIC) + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # test_frame[y_offset:y_offset+frame.shape[0], x_offset:x_offset+frame.shape[1]] = frame + + frame = hud.update(frame, 0, 0) + + # fb.display_frame(frame) + cv2.imshow("e", frame) + cv2.waitKey(1) + +# convert "/usr/share/rpd-wallpaper/raspberry-pi-logo.png"\["$fbw"x"$fbh"^\] +flip -strip -define bmp:subtype=RGB565 bmp2:- | tail -c $(( fbw * fbh * fbd / 8 )) > /dev/fb0 \ No newline at end of file