Compare commits
	
		
			3 Commits
		
	
	
		
			1ad3ded60d
			...
			08a09e3b15
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 08a09e3b15 | |||
| 00a5bceffb | |||
| 8ba524087d | 
							
								
								
									
										52
									
								
								client.py
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								client.py
									
									
									
									
									
								
							| @@ -5,37 +5,49 @@ import socket | |||||||
| import numpy as np | import numpy as np | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| from common import StdPacket, InterlacedPacket, DoublyInterlacedPacket | from common import StdPacket, InterlacedPacket, DoublyInterlacedPacket, TiledImagePacket | ||||||
|  |  | ||||||
|  |  | ||||||
| def send_packet(sock, packet): | def send_packet(sock, packet): | ||||||
|     sock.sendto(packet, (UDP_IP, UDP_PORT)) |     sock.sendto(packet, (UDP_IP, UDP_PORT)) | ||||||
|  |  | ||||||
| def breakdown_image_norm(frame): | def breakdown_image_norm(frame, last_frame): | ||||||
|     (cols, rows, colors) = frame.shape |     (cols, rows, colors) = frame.shape | ||||||
|     # break the array down into 16x16 chunks, then transmit them as UDP packets |     # break the array down into 16x16 chunks, then transmit them as UDP packets | ||||||
|     for i in range(0, cols, 16): |     for i in range(0, cols, 16): | ||||||
|         for j in range(0, rows, 16): |         for j in range(0, rows, 16): | ||||||
|             # print("Sending frame segment (%d, %d)", i, j) |             # print("Sending frame segment (%d, %d)", i, j) | ||||||
|             pkt = StdPacket(uuid, j, i, frame[i:i + 16, j:j + 16]) |             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()) |                 send_packet(sock, pkt.to_bytestr()) | ||||||
|  |  | ||||||
| def breakdown_image_interlaced(frame): | def breakdown_image_interlaced(frame, last_frame): | ||||||
|     (cols, rows, colors) = frame.shape |     (cols, rows, colors) = frame.shape | ||||||
|     # break the array into 16x32 chunks. we'll split those further into odd and even rows |     # 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 |     # and send each as UDP packets. this should make packet loss less obvious | ||||||
|     for i in range(0, cols, 32): |     for i in range(0, cols, 32): | ||||||
|         for j in range(0, rows, 16): |         for j in range(0, rows, 16): | ||||||
|             # print("Sending frame segment (%d, %d)", i, j) |             # print("Sending frame segment (%d, %d)", i, j) | ||||||
|             pkt = InterlacedPacket(uuid, j, i, False, frame[i:i + 32:2, j:j + 16]) |             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()) |                 send_packet(sock, pkt.to_bytestr()) | ||||||
|  |  | ||||||
|     for i in range(0, cols, 32): |     for i in range(0, cols, 32): | ||||||
|         for j in range(0, rows, 16): |         for j in range(0, rows, 16): | ||||||
|             # print("Sending frame segment (%d, %d)", i, j) |             # print("Sending frame segment (%d, %d)", i, j) | ||||||
|             pkt = InterlacedPacket(uuid, j, i, True, frame[i + 1:i + 32:2, j:j + 16]) |             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()) |                 send_packet(sock, pkt.to_bytestr()) | ||||||
|  |  | ||||||
| def breakdown_image_dint(frame): | def breakdown_image_dint(frame, last_frame): | ||||||
|     (cols, rows, colors) = frame.shape |     (cols, rows, colors) = frame.shape | ||||||
|     # break the array into 16x32 chunks. we'll split those further into odd and even rows |     # 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 |     # and send each as UDP packets. this should make packet loss less obvious | ||||||
| @@ -45,7 +57,25 @@ def breakdown_image_dint(frame): | |||||||
|                 # print("Sending frame segment (%d, %d)", i, j) |                 # print("Sending frame segment (%d, %d)", i, j) | ||||||
|                 i_even = l % 2 == 0 |                 i_even = l % 2 == 0 | ||||||
|                 j_even = l >= 2 |                 j_even = l >= 2 | ||||||
|                 pkt = DoublyInterlacedPacket(uuid, j, i, j_even, i_even, frame[i + i_even:i + 32:2, j + j_even:j + 32: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()) |             send_packet(sock, pkt.to_bytestr()) | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
| @@ -80,7 +110,11 @@ if __name__ == '__main__': | |||||||
|     # create the socket |     # create the socket | ||||||
|     sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |     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: |     while True: | ||||||
|  |         last_frame = frame.copy() | ||||||
|         # Capture frame-by-frame |         # Capture frame-by-frame | ||||||
|         ret, frame = cap.read() |         ret, frame = cap.read() | ||||||
|  |  | ||||||
| @@ -89,7 +123,7 @@ if __name__ == '__main__': | |||||||
|             print("Can't receive frame (stream end?). Exiting ...") |             print("Can't receive frame (stream end?). Exiting ...") | ||||||
|             break |             break | ||||||
|  |  | ||||||
|         breakdown_image_dint(frame) |         breakdown_image_dint(frame, last_frame) | ||||||
|  |  | ||||||
|     # Release the capture and close all windows |     # Release the capture and close all windows | ||||||
|     cap.release() |     cap.release() | ||||||
							
								
								
									
										35
									
								
								common.py
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								common.py
									
									
									
									
									
								
							| @@ -134,3 +134,38 @@ def from_bytes_dint(b: bytes) -> DoublyInterlacedPacket: | |||||||
|     array = np.frombuffer(b[28:], np.uint8).reshape(16, 16, 3) |     array = np.frombuffer(b[28:], np.uint8).reshape(16, 16, 3) | ||||||
|  |  | ||||||
|     return DoublyInterlacedPacket(uuid, x, y, even_x, even_y, array) |     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) | ||||||
| @@ -1,3 +1,4 @@ | |||||||
| opencv-python | opencv-python | ||||||
| numpy | numpy | ||||||
| rich | rich | ||||||
|  | asyncudp | ||||||
							
								
								
									
										69
									
								
								server.py
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								server.py
									
									
									
									
									
								
							| @@ -5,6 +5,7 @@ import socket | |||||||
| import numpy as np | import numpy as np | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| import asyncio | import asyncio | ||||||
|  | import asyncudp | ||||||
| import argparse | import argparse | ||||||
| from rich.console import Console | from rich.console import Console | ||||||
|  |  | ||||||
| @@ -34,33 +35,6 @@ class Client: | |||||||
| # Dictionary of client states stored by UUID | # Dictionary of client states stored by UUID | ||||||
| frames: Dict[str, Client] = {} | frames: Dict[str, Client] = {} | ||||||
|  |  | ||||||
| async def read_packet(): |  | ||||||
|     """Asynchronous coroutine to read UDP packets from the client(s).""" |  | ||||||
|     while True: |  | ||||||
|         # we repeat this a ton of times all at once to hopefully capture all of the image data |  | ||||||
|         for i in range(0, 1600): |  | ||||||
|             try: |  | ||||||
|                 data, addr = sock.recvfrom(DoublyInterlacedPacket.size)  # packet buffer size based on the packet size |  | ||||||
|                 # print("received packet from", addr) |  | ||||||
|                 if data: |  | ||||||
|                     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() |  | ||||||
|                         stat.update(f"[bold yellow]{len(frames.keys())}[/bold yellow] clients connected.") |  | ||||||
|  |  | ||||||
|                     frames[uuid].update(pkt) |  | ||||||
|  |  | ||||||
|             except BlockingIOError: |  | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|         # this is necessary to allow asyncio to swap between reading packets and rendering frames |  | ||||||
|         await asyncio.sleep(0.001) |  | ||||||
|  |  | ||||||
| async def show_frames(): | async def show_frames(): | ||||||
|     """Asynchronous coroutine to display frames in OpenCV debug windows.""" |     """Asynchronous coroutine to display frames in OpenCV debug windows.""" | ||||||
|     while True: |     while True: | ||||||
| @@ -70,20 +44,43 @@ async def show_frames(): | |||||||
|                 console.log(f"Client likely lost connection, dropping [bold red]{id}[/bold red]") |                 console.log(f"Client likely lost connection, dropping [bold red]{id}[/bold red]") | ||||||
|                 cv2.destroyWindow(id) |                 cv2.destroyWindow(id) | ||||||
|                 frames.pop(id) |                 frames.pop(id) | ||||||
|                 stat.update(f"[bold yellow]{len(frames.keys())}[/bold yellow] clients connected.") |  | ||||||
|             else: |             else: | ||||||
|                 # show the latest available frame |                 # show the latest available frame | ||||||
|                 cv2.imshow(id, frames[id].read()) |                 cv2.imshow(id, frames[id].read()) | ||||||
|  |  | ||||||
|         cv2.waitKey(1) |         cv2.waitKey(1) | ||||||
|         # this is necessary to allow asyncio to swap between reading packets and rendering frames |         # this is necessary to allow asyncio to swap between reading packets and rendering frames | ||||||
|         await asyncio.sleep(0.01) |         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__": | if __name__ == "__main__": | ||||||
|     # argument parser |     # argument parser | ||||||
|     parser = argparse.ArgumentParser(description="Proof-of-concept server for sauron-cv") |     parser = argparse.ArgumentParser(description="Proof-of-concept server for sauron-cv") | ||||||
|     parser.add_argument("-p", "--port", type=int, default=5005) |     parser.add_argument("-p", "--port", type=int, default=5005) | ||||||
|     parser.add_argument("-l", "--listen", type=str, default="") |     parser.add_argument("-l", "--listen", type=str, default="0.0.0.0") | ||||||
|     parser.add_argument("-W", "--width", type=int, default=640) |     parser.add_argument("-W", "--width", type=int, default=640) | ||||||
|     parser.add_argument("-H", "--height", type=int, default=480) |     parser.add_argument("-H", "--height", type=int, default=480) | ||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
| @@ -98,22 +95,12 @@ if __name__ == "__main__": | |||||||
|     HEIGHT = args.height |     HEIGHT = args.height | ||||||
|     WIDTH = args.width |     WIDTH = args.width | ||||||
|  |  | ||||||
|     # create the UDP socket |  | ||||||
|     sock = socket.socket(socket.AF_INET,  # Internet |  | ||||||
|                          socket.SOCK_DGRAM)  # UDP |  | ||||||
|     sock.setblocking(False) |  | ||||||
|     sock.bind((UDP_IP, UDP_PORT)) |  | ||||||
|  |  | ||||||
|     console.log("Ready to accept connections.", style="bold green") |  | ||||||
|  |  | ||||||
|     with console.status("[bold yellow]0[/bold yellow] clients connected.", spinner="pong") as stat: |  | ||||||
|  |  | ||||||
|     # create the async event loop |     # create the async event loop | ||||||
|     loop = asyncio.new_event_loop() |     loop = asyncio.new_event_loop() | ||||||
|     asyncio.set_event_loop(loop) |     asyncio.set_event_loop(loop) | ||||||
|  |  | ||||||
|     # create async tasks for reading network packets, displaying windows |     # create async tasks for reading network packets, displaying windows | ||||||
|         loop.create_task(read_packet()) |     loop.create_task(listen(UDP_IP, UDP_PORT)) | ||||||
|     loop.create_task(show_frames()) |     loop.create_task(show_frames()) | ||||||
|     try: |     try: | ||||||
|         loop.run_forever() |         loop.run_forever() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user