# Project export: Edge Detection Accelerator

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2024
- Tagline: AI takes a fraction of the energy when run on application specific hardware. This is a hardware implementation of the Laplace Convolution, an edge detection algorithm.
- Devpost: https://devpost.com/software/hardware-edge-detection
- GitHub: https://github.com/naomirehman1008/CruzHacks2024
- Team: 1 GitHub contributor(s) — naomirehman1008 (2 commits)

## Devpost submission (written by the team)

### Inspiration

I knew I wanted to do a hardware project, and I'm interested in ASIC and heterogeneous computing. I wanted to show how effective and energy efficient application specific hardware can be. This design lacks all the extra control logic that a CPU has, making it significantly more energy efficient. System overview This system performs a Laplace convolution of an input to detect edges. The image is sent from the host computer to the FPGA, which performs the convolution and sends it back to the host. Convolutions are the key operation in CNNs, the state of the art model for image recognition, so with some more work this system could be an AI accelerator. AI consumes a significant amount of energy, and with the surging demand for AI products it is incredibly important to think about how we can make AI sustainable. How I built it The system consists of a host (my laptop) which takes an image, serializes it, and sends it to the FPGA via USB. The FPGA has implemented a few state machines to organize the data and the convolution unit to process it. The host program runs python, and the FPGA implementation is in SystemVerilog.

### Challenges we ran into

I started the FPGA component in Verilog, and quickly realized there is a reason people made new HDLs. SystemVerilog is more powerful and flexible, but it doesn't support 3D RAM, so the synthesis was incorrect. I spent a significant portion of time changing my syntax so the netlist would generate properly. The FPGA I used is a low end product, so it is only able to process an 8x8 image. I originally wanted to do a 14 by 14 image, but even that consumed more block ram than was available. Luckily I anticipated this and parameterized all of my modules so it was easy to change to a smaller design. Unfortunately I was unable to fix the final synthesis errors in my image to UART module, so it the system is not functional. Accomplishments that I'm proud of I'm proud of the sheer number amount of code I churned out. Before this I had barely used behavioral Verilog, and I wrote around a thousand lines of it. I'm also very proud of how modular my code is. All of my modules are parametrized so they can be reused in other projects. What I learned Don't use someone's open source code without testing it!

### What's next

for the Edge Detection Accelerator To be an effective computing system this design needs many changes. Using a faster protocol like SPI or PCIe and a larger FPGA are at the top of the list. With more resources, I would also add a configurable filter so it can be used for other image processing algorithms and CNNs.

## README (from the GitHub repository)

# CruzHacks2024

## Detected evidence (automated analysis)

Indexed codebase: 4 recognized source files, 9 KB.
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (5 of 5)

```
edge_detection_host/generate_tests.py
edge_detection_host/serial_comms.py
edge_detection_host/serial_test.py
LICENSE
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- syntax errors
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### edge_detection_host/generate_tests.py

```python
import numpy as np

conv_filter = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]])



for i in range(10):
    input = np.floor(np.random.rand(3,3) * 255)
    output = np.dot(input, conv_filter)
    print(input)
    print(output)




```

### edge_detection_host/serial_test.py

```python
import serial
import time
import sys
import glob
import serial
import cv2
import numpy as np
import struct

#globals
baud_rate = 115200
IMAGE_WIDTH = 8
IMAGE_HIGHT = 8
PROC_IMAGE_WIDTH = IMAGE_WIDTH - 3 + 1
PROC_IMAGE_HIGHT = IMAGE_HIGHT - 3 + 1
DATA_WIDTH = 8

START_CHAR = 'Z'
STOP_CHAR = 'Z'

DISPLAY_WIDTH = 500
DISPLAY_HIGHT = 500

serial_port = 'COM4'

image_path = "C:\\Users\\naomi\\Documents\\Personal-Projects\\edge_detection_host\\black_and_white.jpg"

def send_image(serial_obj, image):
    #to change data size I'll need to do some bit masking fuckery
    unit8_image = np.int8(image)
    print("uint8 representation")
    print(unit8_image)
    bytes_image = np.ndarray.tobytes(unit8_image)
    print("bytes representation")
    print(bytes_image)

    start_char_bytes = bytes(START_CHAR, 'utf-8')
    stop_char_bytes = bytes(STOP_CHAR, 'utf-8')
    serial_obj.write(start_char_bytes)
    print(start_char_bytes)
    serial_obj.write(bytes_image)
    print(bytes_image)
    serial_obj.write(stop_char_bytes)
    print(stop_char_bytes)
    print("package sent")

def load_image(file_path):
    image = cv2.imread(file_path)
    if image.all() == None:
        err_message = f"Failed to load image from {file_path}. Try again? (y, n)\n"
        try_again = input(err_message)
        if (try_again == 'y'):
            # unimplemented
            print("invalid input, I'd ask you to try again, but I don't have gotos!!")
            exit()
        elif (try_again == 'n'):
            exit()
        else:
            print("invalid input, I'd ask you to try again, but I don't have gotos!!")
            exit()
    return image

def send_image_over_serial(serial_obj, packet):
    serial_obj.write(packet)
    try:
        serial_obj.write(packet)
        print("Packet sent successfully.")
    except serial.SerialException as e:
        print(f"Error: {e}")

def process_input_image(image):
    resized_image =cv2.resize(image, (IMAGE_WIDTH, IMAGE_HIGHT))
    grayscale_img = cv2.cvtColor(resized_image, cv2.COLOR_BGR2GRAY)
    return grayscale_img

def recieve_image_over_serial(serial_obj):
    image_bytes = serial_obj.read(PROC_IMAGE_HIGHT * PROC_IMAGE_WIDTH)
    return image_bytes

if __name__ == '__main__':
    serial_obj = serial.Serial(serial_port, baud_rate, timeout=5)
    image = load_image(image_path)
    image = process_input_image(image)
    send_image(serial_obj, image)
    image_bytes = recieve_image_over_serial(serial_obj)
    if(image_bytes == ''):
        print("didn't get anythin :/")
    print(image_bytes)

```

### edge_detection_host/serial_comms.py

```python
import serial
import time
import sys
import glob
import serial
import cv2
import numpy as np
import struct

# implementation note: USB is half duplex so processing a video is not practical.

#globals
baud_rate = 115200
IMAGE_WIDTH = 8
IMAGE_HIGHT = 8
PROC_IMAGE_WIDTH = IMAGE_WIDTH - 3 + 1
PROC_IMAGE_HIGHT = IMAGE_HIGHT - 3 + 1
DATA_WIDTH = 8

START_CHAR = 'Z'
STOP_CHAR = 'Z'

DISPLAY_WIDTH = 500
DISPLAY_HIGHT = 500

# from https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python
def serial_ports():
    """ Lists serial port names

        :raises EnvironmentError:
            On unsupported or unknown platforms
        :returns:
            A list of the serial ports available on the system
    """
    if sys.platform.startswith('win'):
        ports = ['COM%s' % (i + 1) for i in range(256)]
    elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
        # this excludes your current terminal "/dev/tty"
        ports = glob.glob('/dev/tty[A-Za-z]*')
    elif sys.platform.startswith('darwin'):
        ports = glob.glob('/dev/tty.*')
    else:
        raise EnvironmentError('Unsupported platform')

    result = []
    for port in ports:
        try:
            s = serial.Serial(port)
            s.close()
            result.append(port)
        except (OSError, serial.SerialException):
            pass
    return result

def scan_serial_ports():
    ports = serial_ports()
    if not ports:
        print("No serial ports found.")
        return 0

    print("Available serial ports:")
    for port in ports:
        print(port)
    
    serial_port = input('please select a port\n').strip()
    while(serial_port not in ports):
        serial_port = input("invalid serial port, please try again!\n").strip()

    return serial_port

def take_picture():
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print("Error opening camera")
        exit()
    ret, image = cap.read()
    if(ret):
        print("Image captured successfully")
        return image
    else:
        try_again = input("Failed to cature image. Try again? (y, n)\n")
        if(try_again == 'y'):
            #UNIMPLEMENTED BC NO GOTO!!
            exit()
        elif (try_again == 'n'):
            exit()
        else:
            print("invalid input, I'd ask you to try again, but I don't have gotos!!\n")
            exit()

def load_image(file_path):
    image = cv2.imread(file_path)
    if not image:
        err_message = f"Failed to load image from {file_path}. Try again? (y, n)\n"
        try_again = input(err_message)
        if (try_again == 'y'):
            # unimplemented
            print("invalid input, I'd ask you to try again, but I don't have gotos!!")
            exit()
        elif (try_again == 'n'):
            exit()
        else:
            print("invalid input, I'd ask you to try again, but I don't have gotos!!")
            exit()
    return image

def process_input_image(image):
    resized_image =cv2.resize(image, (IMAGE_WIDTH, IMAGE_HIGHT))
    grayscale_img = cv2.cvtColor(resized_image, cv2.COLOR_BGR2GRAY)
    return grayscale_img

def send_image(serial_obj, image):
    #to change data size I'll need to do some bit masking fuckery
    unit8_image = np.int8(image)
    print("uint8 representation")
    print(unit8_image)
    bytes_image = np.ndarray.tobytes(unit8_image)
    print("bytes representation")
    print(bytes_image)

    start_char_bytes = bytes(START_CHAR, 'utf-8')
    stop_char_bytes = bytes(STOP_CHAR, 'utf-8')
    serial_obj.write(start_char_bytes)
    print(start_char_bytes)
    serial_obj.write(bytes_image)
    print(bytes_image)
    serial_obj.write(stop_char_bytes)
    print(stop_char_bytes)
    print("package sent")

def recieve_image_over_serial(serial_obj):
    image_bytes = serial_obj.read(PROC_IMAGE_HIGHT * PROC_IMAGE_WIDTH)
    return image_bytes

def reconstruct_image(image_bytes):
    rec_image = np.frombuffer(image_bytes, dtype=np.int8).reshape((PROC_IMAGE_WIDTH, PROC_IMAGE_HIGHT))
    return rec_image

def display_image_blocking(image, image_name):
    cv2.namedWindow(image_name, cv2.WINDOW_NORMAL)
    cv2.resizeWindow(image_name, DISPLAY_WIDTH, DISPLAY_HIGHT)
    cv2.imshow(image_name, image)


#MAIN LOOP
if __name__ == '__main__':
    
    # scan for and select serial port
    serial_port = scan_serial_ports()
    if(not serial_port):
        print("no serial ports found. try plugging in the board.")
        exit()
    serial_obj = serial.Serial(serial_port, baud_rate, timeout=5)
    
    # select an image
    input_mode = input("Please enter 'camera' to take a picture, or a path to an image\n")
    if(input_mode == 'camera'):
        input_image = take_picture()
    else:
        input_image = load_image(input_mode)
    
    # pre process image
    processed_image = process_input_image(input_image)

    # display image
    print(processed_image)
    """
    cv2.namedWindow('Grayscale Image', cv2.WINDOW_NORMAL)
    cv2.resizeWindow('Grayscale Image', DISPLAY_WIDTH, DISPLAY_HIGHT)
    cv2.imshow('original', input_image)
    cv2.imshow('Grayscale Image', processed_image)
    # Check for the 'q' key to exit the loop and close the window
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    """
    # prepare image to send
    packet = send_image(serial_obj, processed_image)

    # receive processed image bytes object from fpga
    convolved_image_bytes = recieve_image_over_serial(serial_obj)
    print(convolved_image_bytes)
    # reconstruct image from bytes object
    rec_image = reconstruct_image(convolved_image_bytes)

    #display image
    print("finished")

```