Skip to content

About

High-performance Canny Edge Detection pipeline for RISC-V (RV64GCV), optimized with Vector Intrinsics (RVV) and verified with GoogleTest.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

RISC-V Canny Edge Detection Pipeline 🚀

C++ RISC-V RVV QEMU GoogleTest Tests VLEN License

A C++ implementation of the Canny Edge Detection pipeline with scalar and RISC-V Vector (RVV) code paths. The project targets RV64GC/RV64GCV builds and uses QEMU user-mode emulation for functional verification.

Features

  • Complete Canny pipeline: Gaussian Blur, Sobel Gradients, Magnitude, Direction Quantization, Non-Maximum Suppression, Double Thresholding, and Hysteresis.
  • Native host scalar build for development and unit testing.
  • RISC-V scalar build using rv64gc.
  • RISC-V RVV build using rv64gcv.
  • RVV implementations for Gaussian Blur, Sobel, Magnitude, Direction, and NMS.
  • Bit-exact verification between RISC-V scalar output and RISC-V RVV output.
  • GoogleTest test suite with 34 passing tests.

Repository Structure

.
|-- assets/
|   `-- final_edges.png       # Example edge-detection output
|-- docs/
|   `-- RISCV_Canny_Documentation_v4.pdf
|-- images/                   # Automated pipeline verification assets
|   |-- raw/                  # Pure 8-bit raw pixel dumps per stage
|   `-- png/                  # Viewable converted PNG artifacts
|-- include/                  # Pipeline headers
|   |-- gaussian.h            # Gaussian Blur, scalar + RVV
|   |-- sobel.h               # Sobel Gradients, scalar + RVV
|   |-- magnitude.h           # Gradient Magnitude, scalar + RVV
|   |-- direction.h           # Direction Quantization, scalar + RVV
|   |-- nms.h                 # Non-Maximum Suppression, scalar + RVV
|   |-- threshold.h           # Double Thresholding
|   |-- hysteresis.h          # Hysteresis Edge Tracing
|   `-- image_io.h            # Raw image I/O interface
|-- src/
|   |-- main.cpp              # Pipeline entry point and timing harness
|   |-- image_io.cpp          # Raw image load/save implementation
|   `-- generate.cpp          # Optional one-shot output generator
|-- tests/
|   `-- test_pipeline.cpp     # GoogleTest unit and integration tests
|-- tools/                    # Utility scripts
|   |-- fix_image.py          # Image preprocessing utility
|   `-- view.py               # Visualize raw output
|-- Makefile                  # Host, RISC-V scalar, and RISC-V RVV targets
|-- input.raw                 # 256x256 grayscale test image
|-- LICENSE
|-- .gitignore
`-- README.md
=======

Prerequisites

Install the following tools on Linux/WSL:

sudo apt update
sudo apt install -y g++ qemu-user g++-riscv64-linux-gnu libgtest-dev

Required commands:

  • g++
  • riscv64-linux-gnu-g++
  • qemu-riscv64
  • GoogleTest libraries: -lgtest -lgtest_main

The project guide recommends a custom RVV-capable RISC-V GNU toolchain built with --with-arch=rv64gcv. This repository uses the Linux user-mode QEMU flow, so RV_CXX defaults to riscv64-linux-gnu-g++. If the system package does not provide <riscv_vector.h> or cannot compile -march=rv64gcv, point RV_CXX to a custom RVV-capable compiler.

Build And Run

Run the host unit tests:

make test

Build all binaries:

make all

Run the native host scalar version:

make host
./host_canny 256 256 100

Run the RISC-V scalar binary under QEMU:

make run_scalar ARGS="256 256 100"

Run the RISC-V RVV binary under QEMU:

make run_rvv ARGS="256 256 100"

📊 Optimization Results

All benchmarks run on QEMU (qemu-riscv64) with a 256×256 grayscale image, averaged over 10 iterations.

Overall Optimization Summary

Build Type Flags VLEN Runtime (ms) Binary Size Speedup vs -O0
Scalar -O0 -O0 — 9,414.03 612,251 B 1.0×
Scalar -O2 -O2 — 5,888.67 596929 B 1.6×
Scalar -Ofast -Ofast — 5962.0 601877 B 1.58×
Scalar -Os -Os -- 5771.6 594777 B 1.63x
scalar -O3 -O3 -- 5614.9 601921 B 1.67x
RVV -O3 -O3 -march=rv64gcv -mabi=lp64d 128 659.78 597,167 B 14.27×

Note on QEMU timing: QEMU is not cycle-accurate and does not model vector execution units. Wall-clock time on QEMU does not reflect real hardware speedup. On actual RISC-V silicon, RVV kernels process 16–64 pixels per cycle (depending on VLEN) versus 1 pixel per cycle scalar. The instruction count reduction is real; QEMU just cannot reflect the throughput benefit.


Stage Breakdown at -O0 (Scalar Baseline)

Stage Time (ms) Percentage
Gaussian Blur 7,437.91 79.01%
Sobel Gradients 1,292.12 13.73%
Magnitude 545.42 5.79%
Direction 9.76 0.1%
NMS 10.75 0.11%
Thresholding 7.82 0.08%
Hysteresis 110.26 1.17%
Total 9,414.03 100%

Stage Breakdown at -Os (Scalar)

Stage Time (ms) Percentage
Gaussian Blur 2438.41 42.25%
Sobel Gradients 945.63 16.38%
Magnitude 563.66 9.77%
Direction 768.82 13.32%
NMS 928.33 16.08%
Thresholding 63.25 1.10%
Hysteresis 63.50 1.10%
Total 5771.60 100%

Stage Breakdown at -O2 (Scalar, Compiler Optimized)

Stage Time (ms) Percentage
Gaussian Blur 2744.71 46.61%
Sobel Gradients 849.79 14.43%
Magnitude 503.57 8.55%
Direction 747.6 12.7%
NMS 806.67 13.7%
Thresholding 80.80 1.37%
Hysteresis 155.52 2.64%
Total 631.77 100%

-O2 gives a free 10.3× speedup with no code changes. The compiler auto-vectorized some inner loops and eliminated redundant memory accesses. Gaussian + Sobel still dominate at 77% combined — confirming they are the right targets for manual RVV.


Stage Breakdown at -Ofast (Scalar)

Stage Time (ms) Percentage
Gaussian Blur 2574.13 43.18%
Sobel Gradients 874.78 14.67%
Magnitude 509.90 8.55%
Direction 737.11 12.36%
NMS 849.67 14.25%
Thresholding 254.39 4.27%
Hysteresis 162.01 2.72%
Total 5962.00 100%
.

Stage Breakdown at -Os (Scalar)

Stage Time (ms) Percentage
Gaussian Blur 2379.43 42.38%
Sobel Gradients 821.13 14.62%
Magnitude 497.80 8.87%
Direction 738.83 13.16%
NMS 798.69 14.22%
Thresholding 235.58 4.20%
Hysteresis 143.47 2.56%
Total 5614.94 100%

Stage Breakdown at -O3 with RVV Intrinsics (VLEN=128)

Stage Time (ms) Percentage
Gaussian Blur 280.73 42.55%
Sobel Gradients 106.89 16.20%
Magnitude 43.05 6.52%
Direction 4.37 0.66%
NMS 179.97 27.28%
Thresholding 20.68 3.13%
Hysteresis 24.10 3.65%
Total 659.78 100%

Stage Breakdown at RVV O3 (VLEN=256)

Stage Time (ms) Percentage
Gaussian Blur 239.47 37.45%
Sobel Gradients 176.53 27.61%
Magnitude 40.86 6.39%
Direction 4.18 0.65%
NMS 135.56 21.20%
Thresholding 19.79 3.09%
Hysteresis 23.04 3.60%
Total 639.44 100%

Stage Breakdown at RVV O3 (VLEN=512)

Stage Time (ms) Percentage
Gaussian Blur 247.62 29.74%
Sobel Gradients 339.27 40.75%
Magnitude 40.71 4.89%
Direction 4.39 0.53%
NMS 142.80 17.15%
Thresholding 27.50 3.30%
Hysteresis 30.35 3.64%
Total 832.63 100%

Final Optimization Summary (Phase 7)

Build Type Flags VLEN Runtime (ms) Binary Size Speedup vs O0
Scalar O0 -O0 — 9414.03 612,251 B 1.0×
RVV O3 -O3 128 659.78 597,167 B 14.3×
RVV O3 -O3 256 639.44 597,167 B 14.7×
RVV O3 -O3 512 832.63 597,167 B 11.3×

RVV Intrinsic Annotations

  • __riscv_vsetvl_e16m4 → Sets vector length for 16‑bit elements (VLEN‑agnostic strip‑mining).

  • __riscv_vle16_v_i16m4 → Loads 16‑bit pixels into vector registers (bulk memory access).

  • __riscv_vwmaccu_vx_u32m4 → Widening multiply‑accumulate (prevents overflow in Gaussian/Sobel).

  • __riscv_vabs_v_i16m4 → Absolute value per element (needed for |Gx| + |Gy|).

  • __riscv_vadd_vv_u16m4 → Vector addition (combine gradients).

  • __riscv_vmax_vv_u32m1 → Element‑wise max (used in reduction for global max).

  • __riscv_vmv_x_s_u32m1_u32 → Extract scalar from vector (final reduction step).

  • __riscv_vmslt_vv_i16m4_b16 → Mask compare (gradient direction classification).

  • __riscv_vmerge_vvm_u8m1 → Masked merge (thresholding/hysteresis decisions).


🖼️ Pipeline Visual Verification

The profiling harness isolates and outputs visual artifacts for each core mathematical stage of the Canny edge detection pipeline. Below is the sequential progression from the raw source data to the finalized edge map:

1. Pre-Processing & Gradient Extraction

01. Gaussian Blur 02. Sobel Gradients (X / Y) 03. Gradient Magnitude
Gaussian Blur Sobel X Magnitude
Removes high-frequency noise. Extracts spatial derivatives. Maps total edge strength.

2. Edge Thinning & Final Suppression

04. Non-Maximum Suppression 05. Double Thresholding 06. Hysteresis (Final Output)
NMS Threshold Final Output
Thins edges to 1-pixel lines. Categorizes edge candidates. Traces and cleans final edges.

Correctness Verification

The main correctness check compares the RISC-V scalar output against the RISC-V RVV output using the same QEMU CPU configuration:

make verify

Expected result:

  • cmp scalar_output.raw rvv_output.raw prints no differences.
  • sha256sum scalar_output.raw rvv_output.raw prints the same hash for both files.

Latest verified result:

[  PASSED  ] 34 tests.

RISC-V Scalar SHA-256:
502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f

RISC-V RVV SHA-256:
502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f

This confirms that the RVV implementation is functionally correct and produces byte-identical final edge output compared with the RISC-V scalar implementation.

VLEN sweep commands:

make verify VLEN=128
make verify VLEN=256
make verify VLEN=512

These runs use QEMU's vlen=<N> CPU option. Matching output at all three VLEN values demonstrates that the RVV loops are vector-length agnostic.

Latest VLEN sweep result:

VLEN Result SHA-256
128 Scalar and RVV outputs matched 502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f
256 Scalar and RVV outputs matched 502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f
512 Scalar and RVV outputs matched 502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f

Final Results To Report

The main validated results are:

Item Result
Unit tests 34/34 passed
Host build Passed
RISC-V scalar build Passed
RISC-V RVV build Passed
Scalar vs RVV output comparison Byte-identical
SHA-256 comparison Matching hashes
Performance environment QEMU user-mode emulation

The most important correctness evidence is:

cmp scalar_output.raw rvv_output.raw

This command produced no output, which means the two files are identical.

The SHA-256 hashes were also identical:

502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f  scalar_output.raw
502e32bd63ca1f3a446055085d2c916f7979516924d40fa943ad8046f24ece8f  rvv_output.raw

Benchmarking

Run:

make benchmark ARGS="256 256 100"

Important Note About QEMU Timing

QEMU user-mode emulation is useful for validating that the RISC-V scalar and RVV binaries run correctly and produce matching output. However, QEMU emulates RVV instructions in software, so the wall-clock runtime is not a reliable measurement of real RVV hardware performance.

For this reason, the QEMU benchmark should be reported as an emulation result only. The strongest validated result in this project is correctness: the RVV binary produces byte-identical output compared with the RISC-V scalar binary.

Accurate RVV performance evaluation requires real RVV-capable hardware or a simulator/tooling setup intended for performance measurement.

Reproducibility Commands

Use this exact sequence to reproduce the final validation:

make clean
make test
make verify VLEN=128
make verify VLEN=256
make verify VLEN=512
make benchmark ARGS="256 256 100"

Expected validation result:

  • make test should finish with 34 tests passed.
  • each make verify VLEN=<N> command should build both RISC-V binaries.
  • cmp scalar_output.raw rvv_output.raw should print nothing.
  • sha256sum scalar_output.raw rvv_output.raw should print matching hashes.
  • make benchmark should print two profiles: RISC-V Scalar and RISC-V RVV.

Do not use native host timing as a direct comparison against QEMU RISC-V timing. Host timing runs on the x86 machine, while RISC-V timing runs inside emulation.

Implementation Notes

  • Gaussian Blur uses zero-padding at the image boundary, matching the project guide.
  • Border handling in the RVV Gaussian and Sobel kernels was matched to the scalar implementation.
  • The RVV Sobel Gy sign was corrected to match the scalar Sobel-Y kernel.
  • Gaussian normalization uses exact division by 273 in the RVV path to preserve scalar-equivalent output.
  • RVV code is guarded with __riscv_vector, so scalar RISC-V builds do not require RVV intrinsics.
  • The main program validates command-line arguments and reports the active mode: Host Scalar, RISC-V Scalar, or RISC-V RVV.
  • Raw image I/O checks file read/write failures instead of silently continuing.
  • src/generate.cpp is optional and can compile without RVV because it falls back to the scalar direction kernel when __riscv_vector is not enabled.

Clean Generated Files

make clean

Generated output files such as output.raw, scalar_output.raw, and rvv_output.raw can also be removed manually if needed.

Report Summary

The project implements a full Canny Edge Detection pipeline in C++ with scalar and RISC-V Vector code paths. Gaussian Blur uses zero-padding at image boundaries, matching the project guide. The implementation was tested using 34 GoogleTest tests, all of which passed. The RISC-V scalar and RVV binaries were executed under QEMU at VLEN=128, VLEN=256, and VLEN=512. For all three VLEN values, the final output files were compared with cmp and SHA-256, and both outputs were byte-identical.

The RVV version is slower than the scalar version under QEMU because QEMU emulates vector instructions in software. Therefore, the QEMU runtime is not used as evidence of real hardware speedup. It is used mainly to validate functional correctness of the RVV implementation.

About

High-performance Canny Edge Detection pipeline for RISC-V (RV64GCV), optimized with Vector Intrinsics (RVV) and verified with GoogleTest.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages