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.
- 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.
.
|-- 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
=======
Install the following tools on Linux/WSL:
sudo apt update
sudo apt install -y g++ qemu-user g++-riscv64-linux-gnu libgtest-devRequired 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.
Run the host unit tests:
make testBuild all binaries:
make allRun the native host scalar version:
make host
./host_canny 256 256 100Run 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"All benchmarks run on QEMU (qemu-riscv64) with a 256×256 grayscale image, averaged over 10 iterations.
| 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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% |
| 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× |
-
__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).
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:
| 01. Gaussian Blur | 02. Sobel Gradients (X / Y) | 03. Gradient Magnitude |
|---|---|---|
![]() |
![]() |
![]() |
| Removes high-frequency noise. | Extracts spatial derivatives. | Maps total edge strength. |
| 04. Non-Maximum Suppression | 05. Double Thresholding | 06. Hysteresis (Final Output) |
|---|---|---|
![]() |
![]() |
![]() |
| Thins edges to 1-pixel lines. | Categorizes edge candidates. | Traces and cleans final edges. |
The main correctness check compares the RISC-V scalar output against the RISC-V RVV output using the same QEMU CPU configuration:
make verifyExpected result:
cmp scalar_output.raw rvv_output.rawprints no differences.sha256sum scalar_output.raw rvv_output.rawprints 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=512These 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 |
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
Run:
make benchmark ARGS="256 256 100"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.
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 testshould finish with34 testspassed.- each
make verify VLEN=<N>command should build both RISC-V binaries. cmp scalar_output.raw rvv_output.rawshould print nothing.sha256sum scalar_output.raw rvv_output.rawshould print matching hashes.make benchmarkshould print two profiles:RISC-V ScalarandRISC-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.
- 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
Gysign 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, orRISC-V RVV. - Raw image I/O checks file read/write failures instead of silently continuing.
src/generate.cppis optional and can compile without RVV because it falls back to the scalar direction kernel when__riscv_vectoris not enabled.
make cleanGenerated output files such as output.raw, scalar_output.raw, and rvv_output.raw can also be removed manually if needed.
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.





