move the original python code to legacy subdirectory

This commit is contained in:
2025-03-25 15:51:16 -05:00
parent 08a09e3b15
commit d4aa4eabb4
5 changed files with 0 additions and 0 deletions

58
legacy/README.md Normal file
View File

@ -0,0 +1,58 @@
# Video Streaming Proof of Concept
This project is a demo for streaming video output from multiple "client" devices to one "server". This is a basic demo
of what sauron-cv seeks to accomplish.
## Installation
It is strongly recommended that you use a virtual environment. These instructions will assume you are using venv, you
can substitute this with your preferred environment management. You will need to make sure that the `virtualenv` package
is installed globally, either via pip or the `python3-virtualenv` package in your system package manager.
When first cloning this repo, run the following:
```shell
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
This will create a virtual environment, enter that virtual environment, and install the required packages.
If you start a new shell, you will need to re-enter the virtual environment:
```shell
source .venv/bin/activate
```
## Running
### Client
To run the client with a localhost target:
```shell
python -m client
```
To target an external server, provide its IP address using the `-s` flag:
```shell
python -m client -s [server IP address]
```
### Server
```shell
python -m server
```
### Common Flags
Make sure that these match between your client and server!!
| Short | Long | Description | Default |
|-------|---|---|---|
| `-p` | `--port` | The port for the client / server to communicate on. | `5005` |
| `-W` | `--width` | Image width in pixels. | `640` |
| `-H` | `--height` | Image height in pixels. | `480` |

129
legacy/client.py Normal file
View File

@ -0,0 +1,129 @@
import argparse
import cv2
import socket
import numpy as np
import uuid
from common import StdPacket, InterlacedPacket, DoublyInterlacedPacket, TiledImagePacket
def send_packet(sock, packet):
sock.sendto(packet, (UDP_IP, UDP_PORT))
def breakdown_image_norm(frame, last_frame):
(cols, rows, colors) = frame.shape
# break the array down into 16x16 chunks, then transmit them as UDP packets
for i in range(0, cols, 16):
for j in range(0, rows, 16):
# print("Sending frame segment (%d, %d)", i, j)
arr = frame[i:i + 16, j:j + 16]
last_arr = last_frame[i:i + 16, j:j + 16]
# only update if image segments are different
if not np.allclose(arr, last_arr):
pkt = StdPacket(uuid, j, i, arr)
send_packet(sock, pkt.to_bytestr())
def breakdown_image_interlaced(frame, last_frame):
(cols, rows, colors) = frame.shape
# break the array into 16x32 chunks. we'll split those further into odd and even rows
# and send each as UDP packets. this should make packet loss less obvious
for i in range(0, cols, 32):
for j in range(0, rows, 16):
# print("Sending frame segment (%d, %d)", i, j)
arr = frame[i:i + 32:2, j:j + 16]
last_arr = last_frame[i:i + 32:2, j:j + 16]
if not np.allclose(arr, last_arr):
pkt = InterlacedPacket(uuid, j, i, False, arr)
send_packet(sock, pkt.to_bytestr())
for i in range(0, cols, 32):
for j in range(0, rows, 16):
# print("Sending frame segment (%d, %d)", i, j)
arr = frame[i + 1:i + 32:2, j:j + 16]
last_arr = last_frame[i + 1:i + 32:2, j:j + 16]
# only update if image segments are different
if not np.allclose(arr, last_arr):
pkt = InterlacedPacket(uuid, j, i, True, arr)
send_packet(sock, pkt.to_bytestr())
def breakdown_image_dint(frame, last_frame):
(cols, rows, colors) = frame.shape
# break the array into 16x32 chunks. we'll split those further into odd and even rows
# and send each as UDP packets. this should make packet loss less obvious
for l in range(0, 4):
for i in range(0, cols, 32):
for j in range(0, rows, 32):
# print("Sending frame segment (%d, %d)", i, j)
i_even = l % 2 == 0
j_even = l >= 2
# breakdown image
arr = frame[i + i_even:i + 32:2, j + j_even:j + 32:2]
last_arr = last_frame[i + i_even:i + 32:2, j + j_even:j + 32:2]
# only update if image segments are different
if not np.allclose(arr, last_arr):
pkt = DoublyInterlacedPacket(uuid, j, i, j_even, i_even, arr)
send_packet(sock, pkt.to_bytestr())
def breakdown_image_tiled(frame):
(cols, rows, colors) = frame.shape
# break the array into 16x32 chunks. we'll split those further into odd and even rows
# and send each as UDP packets. this should make packet loss less obvious
xslice = cols // 16
yslice = rows // 16
for i in range(0, xslice):
for j in range(0, yslice):
# print("Sending frame segment (%d, %d)", i, j)
pkt = TiledImagePacket(uuid, j, i, rows, cols, frame[i:cols:xslice, j:rows:yslice])
send_packet(sock, pkt.to_bytestr())
if __name__ == '__main__':
# argument parser
parser = argparse.ArgumentParser(description="Proof-of-concept client for sauron-cv")
parser.add_argument("-p", "--port", type=int, default=5005)
parser.add_argument("-s", "--server", type=str, default="127.0.0.1")
parser.add_argument("-W", "--width", type=int, default=640)
parser.add_argument("-H", "--height", type=int, default=480)
parser.add_argument("-d", "--device", type=int, default=0)
args = parser.parse_args()
# give this client its very own UUID
uuid = uuid.uuid4()
# give target IP address
UDP_IP = args.server
UDP_PORT = args.port
WIDTH = args.width
HEIGHT = args.height
DEVICE = args.device
# Create a VideoCapture object
cap = cv2.VideoCapture(DEVICE) # 0 represents the default camera
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
# Check if camera opened successfully
if not cap.isOpened():
print("Error opening video stream or file")
# create the socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
frame = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
last_frame = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
while True:
last_frame = frame.copy()
# Capture frame-by-frame
ret, frame = cap.read()
# If frame is read correctly, ret is True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
breakdown_image_dint(frame, last_frame)
# Release the capture and close all windows
cap.release()

171
legacy/common.py Normal file
View File

@ -0,0 +1,171 @@
from abc import ABC, abstractmethod
import numpy as np
from uuid import UUID
# The basic structure of the packet is as follows:
# FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF ... FFFF
# | uuid | | | |
# | | | | image data: contains the 16x16 image slice bitpacked from a NumPy array
# | | | y: the y position of this packet in the original image
# | | x: the x position of this packet in the original image
# | uuid: matches the packet to the requesting client
# Other packet types may change this structure. Need to standardize this somehow.
class Packet(ABC):
"""Generic structure for a video streaming packet. Contains a slice of the full image, which will be reconstructed
by the server."""
size: int
def __init__(self, uuid: UUID, x: int, y: int, array: np.ndarray):
self.uuid = uuid
self.x = x
self.y = y
self.array = array
@abstractmethod
def to_bytestr(self) -> bytes:
"""Convert a packet object into a bytestring."""
pass
@abstractmethod
def apply(self, image: np.ndarray) -> np.ndarray:
"""Apply this packet to an image."""
pass
class StdPacket(Packet):
"""A standard packet with no interlacing. Sends a 16x16 chunk of an image."""
size = 16 + 4 + 4 + 768
def __init__(self, uuid: UUID, x: int, y: int, array: np.ndarray):
super().__init__(uuid, x, y, array)
def to_bytestr(self) -> bytes:
bytestr = b""
bytestr += self.uuid.bytes
bytestr += self.x.to_bytes(length=4, signed = False)
bytestr += self.y.to_bytes(length=4, signed = False)
bytestr += self.array.tobytes()
return bytestr
def apply(self, image: np.ndarray) -> np.ndarray:
x = self.x
y = self.y
arr = self.array
image[y:y + 16, x:x + 16] = arr
return image
def from_bytes_std(b: bytes) -> StdPacket:
"""Convert a byte string obtained via UDP into a packet object."""
uuid = UUID(bytes = b[0:16])
x = int.from_bytes(b[16:20], signed = False)
y = int.from_bytes(b[20:24], signed = False)
array = np.frombuffer(b[24:], np.uint8).reshape(16, 16, 3)
return StdPacket(uuid, x, y, array)
class InterlacedPacket(Packet):
"""A packet with horizontal interlacing. Sends half of a 16x32 chunk of an image, every other row"""
size = 16 + 4 + 4 + 4 + 768
def __init__(self, uuid: UUID, x: int, y: int, even: bool, array: np.ndarray):
super().__init__(uuid, x, y, array)
self.even = even
def to_bytestr(self) -> bytes:
bytestr = b""
bytestr += self.uuid.bytes
bytestr += self.x.to_bytes(length=4, signed=False)
bytestr += self.y.to_bytes(length=4, signed=False)
bytestr += self.even.to_bytes(length=4)
bytestr += self.array.tobytes()
return bytestr
def apply(self, image: np.ndarray) -> np.ndarray:
x = self.x
y = self.y
arr = self.array
image[y + self.even:y + 32:2, x:x + 16] = arr
return image
def from_bytes_int(b: bytes) -> InterlacedPacket:
"""Convert a byte string obtained via UDP into a packet object."""
uuid = UUID(bytes=b[0:16])
x = int.from_bytes(b[16:20], signed=False)
y = int.from_bytes(b[20:24], signed=False)
even = bool.from_bytes(b[24:28])
array = np.frombuffer(b[28:], np.uint8).reshape(16, 16, 3)
return InterlacedPacket(uuid, x, y, even, array)
class DoublyInterlacedPacket(Packet):
"""A packet with horizontal interlacing. Sends one quarter of a 32x32 chunk of an image. This will alternate rows
and columns based on the value of even_x and even_y."""
size = 16 + 4 + 4 + 4 + 768
def __init__(self, uuid: UUID, x: int, y: int, even_x: bool, even_y: bool, array: np.ndarray):
super().__init__(uuid, x, y, array)
self.even_x = even_x
self.even_y = even_y
def to_bytestr(self) -> bytes:
bytestr = b""
bytestr += self.uuid.bytes
bytestr += self.x.to_bytes(length=4, signed=False)
bytestr += self.y.to_bytes(length=4, signed=False)
bytestr += self.even_x.to_bytes(length=2)
bytestr += self.even_y.to_bytes(length=2)
bytestr += self.array.tobytes()
return bytestr
def apply(self, image: np.ndarray) -> np.ndarray:
x = self.x
y = self.y
arr = self.array
image[y + self.even_y:y + 32:2, x + self.even_x:x + 32:2] = arr
return image
def from_bytes_dint(b: bytes) -> DoublyInterlacedPacket:
"""Convert a byte string obtained via UDP into a packet object."""
uuid = UUID(bytes=b[0:16])
x = int.from_bytes(b[16:20], signed=False)
y = int.from_bytes(b[20:24], signed=False)
even_x = bool.from_bytes(b[24:26])
even_y = bool.from_bytes(b[26:28])
array = np.frombuffer(b[28:], np.uint8).reshape(16, 16, 3)
return DoublyInterlacedPacket(uuid, x, y, even_x, even_y, array)
class TiledImagePacket(Packet):
"""Distributed selection from image."""
size = 16 + 4 + 4 + 768
def __init__(self, uuid: UUID, x: int, y: int, width: int, height: int, array: np.ndarray):
super().__init__(uuid, x, y, array)
self.width = width
self.height = height
self.xslice = width // 16
self.yslice = height // 16
def to_bytestr(self) -> bytes:
bytestr = b""
bytestr += self.uuid.bytes
bytestr += self.x.to_bytes(length=4, signed=False)
bytestr += self.y.to_bytes(length=4, signed=False)
bytestr += self.array.tobytes()
return bytestr
def apply(self, image: np.ndarray) -> np.ndarray:
x = self.x
y = self.y
arr = self.array
image[y:self.height:self.yslice, x:self.width:self.xslice] = arr
return image
def from_bytes_tiled(b: bytes) -> TiledImagePacket:
"""Convert a byte string obtained via UDP into a packet object."""
uuid = UUID(bytes = b[0:16])
x = int.from_bytes(b[16:20], signed = False)
y = int.from_bytes(b[20:24], signed = False)
array = np.frombuffer(b[24:], np.uint8).reshape(16, 16, 3)
return TiledImagePacket(uuid, x, y, 640, 480, array)

4
legacy/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
opencv-python
numpy
rich
asyncudp

112
legacy/server.py Normal file
View File

@ -0,0 +1,112 @@
from typing import Dict
import cv2
import socket
import numpy as np
from datetime import datetime
import asyncio
import asyncudp
import argparse
from rich.console import Console
from common import Packet, DoublyInterlacedPacket, from_bytes_dint
class Client:
"""Class for tracking client state, including current frame data and time since last update."""
def __init__(self):
self.last_updated = datetime.now()
self.frame = np.ndarray((HEIGHT, WIDTH, 3), dtype=np.uint8)
def update(self, pkt: Packet):
"""Apply a packet to the client frame. Update last client update to current time."""
self.frame = pkt.apply(self.frame)
self.last_updated = datetime.now()
def latency(self) -> float:
"""Return the time since the last client update."""
return (datetime.now() - self.last_updated).total_seconds()
def read(self) -> np.ndarray:
"""Return the current frame."""
return self.frame
# Dictionary of client states stored by UUID
frames: Dict[str, Client] = {}
async def show_frames():
"""Asynchronous coroutine to display frames in OpenCV debug windows."""
while True:
# drop clients that have not sent packets for > 5 seconds
for id in list(frames.keys()):
if frames[id].latency() >= 5:
console.log(f"Client likely lost connection, dropping [bold red]{id}[/bold red]")
cv2.destroyWindow(id)
frames.pop(id)
else:
# show the latest available frame
cv2.imshow(id, frames[id].read())
cv2.waitKey(1)
# this is necessary to allow asyncio to swap between reading packets and rendering frames
await asyncio.sleep(0.05)
async def listen(ip: str, port: int):
"""Asynchronous coroutine to listen for / read client connections."""
sock = await asyncudp.create_socket(local_addr=(ip, port))
console.log("Ready to accept connections.", style="bold green")
while True:
# receive packets
data, addr = await sock.recvfrom()
if data:
# convert the byte string into a packet object
pkt = from_bytes_dint(data)
uuid = str(pkt.uuid)
# if this is a new client, give it a new image
if uuid not in frames.keys():
console.log(f"New client acquired, naming [bold cyan]{uuid}[bold cyan]")
frames[uuid] = Client()
frames[uuid].update(pkt)
if __name__ == "__main__":
# argument parser
parser = argparse.ArgumentParser(description="Proof-of-concept server for sauron-cv")
parser.add_argument("-p", "--port", type=int, default=5005)
parser.add_argument("-l", "--listen", type=str, default="0.0.0.0")
parser.add_argument("-W", "--width", type=int, default=640)
parser.add_argument("-H", "--height", type=int, default=480)
args = parser.parse_args()
# console
console = Console()
# assign constants based on argument parser
UDP_IP = args.listen
UDP_PORT = args.port
HEIGHT = args.height
WIDTH = args.width
# create the async event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# create async tasks for reading network packets, displaying windows
loop.create_task(listen(UDP_IP, UDP_PORT))
loop.create_task(show_frames())
try:
loop.run_forever()
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
# Release the capture and close all windows
cv2.destroyAllWindows()