郭昱辰, 簡子昕
Welcome to the documentation for the Chisel-Based Pixel-Art Scaling Hardware Accelerator. This project represents the final assignment for the Computer Architecture course (113-1 Semester). In its initial version, the accelerator facilitates image conversion and incorporates several fundamental features. Future iterations will focus on enhancing performance through hardware acceleration.
Pixel Art is a digital art form characterized by the meticulous creation and editing of images at the pixel level, commonly utilized in gaming and graphic design. As image resolutions escalate, the efficient scaling of pixel art poses significant challenges. This project endeavors to design a hardware accelerator using the Chisel language to achieve high-quality pixel art scaling. The current version (1.0) encompasses image conversion and several essential functionalities, laying the groundwork for future enhancements aimed at optimizing performance through hardware acceleration.
Before engaging with this project, ensure you possess the following knowledge and tools:
The objective of this project is to design a Chisel-based hardware accelerator capable of efficiently scaling pixel art images. The key functionalities implemented in this version include:
This documentation will delve into the implementation specifics of each module, providing a comprehensive analysis of the project's components.
package pixelart
import chisel3._
import chisel3.util._
/**
* Enhanced Edge Detector Module
* Utilizes the Sobel algorithm to compute gradients and determine edge direction and strength.
*/
class EnhancedEdgeDetector extends Module {
val io = IO(new Bundle {
// Current pixel
val currentPixel = Input(UInt(24.W))
// Neighboring pixels (Top-Left, Top, Top-Right, Left, Right, Bottom-Left, Bottom, Bottom-Right)
val neighbors = Input(Vec(8, UInt(24.W)))
// Output edge direction and strength
val edgeDirection = Output(UInt(2.W)) // 00: None, 01: Horizontal, 10: Vertical, 11: Diagonal
val edgeStrength = Output(UInt(8.W)) // Edge strength
})
// Separate RGB components
def getRGB(pixel: UInt): (UInt, UInt, UInt) = {
val r = pixel(23, 16)
val g = pixel(15, 8)
val b = pixel(7, 0)
(r, g, b)
}
// Calculate grayscale value
def toGray(pixel: UInt): UInt = {
val (r, g, b) = getRGB(pixel)
// Standard grayscale conversion formula
(r * 299.U + g * 587.U + b * 114.U) / 1000.U
}
// Convert all relevant pixels to grayscale
val grayCurrent = toGray(io.currentPixel)
val grayNeighbors = io.neighbors.map(toGray)
// Sobel kernels
val sobelX = Array(
-1, 0, 1,
-2, 0, 2,
-1, 0, 1
)
val sobelY = Array(
-1, -2, -1,
0, 0, 0,
1, 2, 1
)
// Calculate Gx and Gy
// Neighbor pixel order: 0:Top-Left, 1:Top, 2:Top-Right, 3:Left, 4:Right, 5:Bottom-Left, 6:Bottom, 7:Bottom-Right
val gx = (grayNeighbors(0).asSInt * sobelX(0).S +
grayNeighbors(1).asSInt * sobelX(1).S +
grayNeighbors(2).asSInt * sobelX(2).S +
grayNeighbors(3).asSInt * sobelX(3).S +
grayNeighbors(4).asSInt * sobelX(4).S +
grayNeighbors(5).asSInt * sobelX(5).S +
grayNeighbors(6).asSInt * sobelX(6).S +
grayNeighbors(7).asSInt * sobelX(7).S).asUInt
val gy = (grayNeighbors(0).asSInt * sobelY(0).S +
grayNeighbors(1).asSInt * sobelY(1).S +
grayNeighbors(2).asSInt * sobelY(2).S +
grayNeighbors(3).asSInt * sobelY(3).S +
grayNeighbors(4).asSInt * sobelY(4).S +
grayNeighbors(5).asSInt * sobelY(5).S +
grayNeighbors(6).asSInt * sobelY(6).S +
grayNeighbors(7).asSInt * sobelY(7).S).asUInt
// Calculate gradient magnitude |Gx| + |Gy|
val gradient = (gx.asSInt.abs + gy.asSInt.abs).asUInt
// Set gradient threshold
val threshold = 30.U
// Determine if it's an edge
val isEdge = gradient > threshold
// Determine edge direction based on Gx and Gy
val direction = Wire(UInt(2.W))
when(gx > gy) {
direction := 1.U // Horizontal
} .elsewhen(gx < gy) {
direction := 2.U // Vertical
} .otherwise {
direction := 3.U // Diagonal
}
io.edgeDirection := Mux(isEdge, direction, 0.U)
io.edgeStrength := gradient(7,0)
}
RGB Separation and Grayscale Conversion: Segregates the input 24-bit RGB pixel into individual R, G, and B components. Converts these components to grayscale using a standard formula to streamline the subsequent edge detection process.
Sobel Algorithm Application: Implements the Sobel operator to compute horizontal (Gx) and vertical (Gy) gradients, which are essential for edge detection.
Gradient Magnitude and Direction Determination: Calculates the gradient magnitude by summing the absolute values of Gx and Gy. If the magnitude surpasses a predefined threshold, the pixel is classified as an edge, and its direction (horizontal, vertical, diagonal) is ascertained based on the relative magnitudes of Gx and Gy.
/**
* Improved Pixel Interpolator Module
* Performs intelligent interpolation based on edge direction and strength.
*/
class EnhancedPixelInterpolator(scaleFactor: Int) extends Module {
require(scaleFactor >= 1 && scaleFactor <= 4, "Scale factor must be between 1 and 4")
val io = IO(new Bundle {
// Current pixel
val currentPixel = Input(UInt(24.W))
// Edge detection results
val edgeDirection = Input(UInt(2.W))
val edgeStrength = Input(UInt(8.W))
// Interpolated pixel outputs
val outPixels = Output(Vec(scaleFactor * scaleFactor, UInt(24.W)))
})
// Separate RGB components
def getRGB(pixel: UInt): (UInt, UInt, UInt) = {
val r = pixel(23, 16)
val g = pixel(15, 8)
val b = pixel(7, 0)
(r, g, b)
}
val (r, g, b) = getRGB(io.currentPixel)
// Interpolation function: Linear interpolation
def lerp(a: UInt, b: UInt, t: UInt): UInt = {
((a.asUInt * (255.U - t)) + (b.asUInt * t)) / 255.U
}
// Interpolation strategy based on edge direction
for (i <- 0 until (scaleFactor * scaleFactor)) {
// Calculate interpolation position
val row = (i / scaleFactor).U
val col = (i % scaleFactor).U
val t_row = (row * 255.U) / scaleFactor.U
val t_col = (col * 255.U) / scaleFactor.U
io.outPixels(i) := MuxLookup(io.edgeDirection, io.currentPixel) (
Seq(
// Horizontal edge: blend left and right pixels
1.U -> {
// Placeholder for specific interpolation logic
io.currentPixel
},
// Vertical edge: blend top and bottom pixels
2.U -> {
// Placeholder for specific interpolation logic
io.currentPixel
},
// Diagonal edge: bilinear interpolation
3.U -> {
// Placeholder for specific interpolation logic
io.currentPixel
}
)
)
// Fallback: Directly copy the current pixel if no specific strategy is applied
when(io.edgeStrength < 50.U) { // Adjust based on edge strength threshold
io.outPixels(i) := io.currentPixel
}
}
}
Interpolation Strategy: Selects appropriate interpolation methods based on the detected edge direction (horizontal, vertical, diagonal). Currently utilizes linear interpolation as a foundational approach, with placeholders for more sophisticated strategies.
Intelligent Interpolation: Leverages edge strength to determine the necessity of interpolation. If the edge strength is below a certain threshold, the current pixel is directly propagated to preserve image fidelity.
/**
* Extended PixelArtScaler Module with Enhanced Edge Detection and Pixel Interpolation
*/
class EnhancedPixelArtScaler(width: Int, height: Int, scaleFactor: Int) extends Module {
require(scaleFactor >= 1, "Scale factor must be at least 1")
require(scaleFactor <= 4, "Scale factor is limited to a maximum of 4 for this implementation")
val io = IO(new Bundle {
// Input pixel (24-bit RGB)
val inPixel = Input(UInt(24.W))
val inValid = Input(Bool())
// Output pixels (scaleFactor x scaleFactor block)
val outPixels = Output(Vec(scaleFactor * scaleFactor, UInt(24.W)))
val outValid = Output(Bool())
// Neighboring pixels output (Top-Left, Top, Top-Right, Left, Right, Bottom-Left, Bottom, Bottom-Right)
val neighbor = Output(Vec(8, UInt(24.W)))
})
// Line buffers: store previous, current, and next lines
val lineBufferPrev = RegInit(VecInit(Seq.fill(width)(0.U(24.W))))
val lineBufferCurrent = RegInit(VecInit(Seq.fill(width)(0.U(24.W))))
val lineBufferNext = RegInit(VecInit(Seq.fill(width)(0.U(24.W))))
// Current column and row indices
val col = RegInit(0.U(log2Ceil(width + 1).W)) // Increased width to prevent overflow
val row = RegInit(0.U(log2Ceil(height + 1).W)) // Increased width to prevent overflow
// Shift registers for the current line to get left, center, right
val shiftRegPrev = RegInit(VecInit(Seq.fill(3)(0.U(24.W))))
val shiftRegCurrent = RegInit(VecInit(Seq.fill(3)(0.U(24.W))))
val shiftRegNext = RegInit(VecInit(Seq.fill(3)(0.U(24.W))))
// Instantiate Enhanced Edge Detector Module
val edgeDetector = Module(new EnhancedEdgeDetector)
// Instantiate Pixel Interpolator Module
val pixelInterpolator = Module(new EnhancedPixelInterpolator(scaleFactor))
// Update line buffers
when(io.inValid) {
// Shift line buffers
lineBufferPrev := lineBufferCurrent
lineBufferCurrent := lineBufferNext
lineBufferNext(col) := io.inPixel
// Update column and row indices
when(col === (width - 1).U) {
col := 0.U
row := row + 1.U
} .otherwise {
col := col + 1.U
}
// Update shift registers
shiftRegPrev(0) := lineBufferPrev(col)
shiftRegPrev(1) := shiftRegPrev(0)
shiftRegPrev(2) := shiftRegPrev(1)
shiftRegCurrent(0) := lineBufferCurrent(col)
shiftRegCurrent(1) := shiftRegCurrent(0)
shiftRegCurrent(2) := shiftRegCurrent(1)
shiftRegNext(0) := lineBufferNext(col)
shiftRegNext(1) := shiftRegNext(0)
shiftRegNext(2) := shiftRegNext(1)
}
// Extract neighboring pixels
val neighbors = Wire(Vec(8, UInt(24.W)))
neighbors(0) := Mux(row === 0.U || col === 0.U, 0.U, shiftRegPrev(1)) // Top-Left
neighbors(1) := Mux(row === 0.U, 0.U, shiftRegPrev(0)) // Top
neighbors(2) := Mux(row === 0.U || col === (width - 1).U, 0.U, shiftRegPrev(2)) // Top-Right
neighbors(3) := Mux(col === 0.U, 0.U, shiftRegCurrent(1)) // Left
neighbors(4) := Mux(col === (width - 1).U, 0.U, shiftRegCurrent(2)) // Right
neighbors(5) := Mux(row === (height - 1).U || col === 0.U, 0.U, shiftRegNext(1)) // Bottom-Left
neighbors(6) := Mux(row === (height - 1).U, 0.U, shiftRegNext(0)) // Bottom
neighbors(7) := Mux(row === (height - 1).U || col === (width - 1).U, 0.U, shiftRegNext(2)) // Bottom-Right
// Output extracted neighboring pixels
io.neighbor := neighbors
// Connect Enhanced Edge Detector Module
edgeDetector.io.currentPixel := io.inPixel
edgeDetector.io.neighbors := neighbors
// Connect Pixel Interpolator Module
pixelInterpolator.io.currentPixel := io.inPixel
pixelInterpolator.io.edgeDirection := edgeDetector.io.edgeDirection
pixelInterpolator.io.edgeStrength := edgeDetector.io.edgeStrength
// Connect interpolated pixels to module output
io.outPixels := pixelInterpolator.io.outPixels
// Output valid signal
io.outValid := io.inValid
}
Line Buffer Management: Utilizes three line buffers (previous, current, next) to store sequential lines of image data. Manages column and row indices to track the current position within the image matrix.
Neighboring Pixel Extraction: Retrieves the eight neighboring pixels surrounding the current pixel, handling boundary conditions by padding with zeros where necessary to avoid accessing invalid memory regions.
Edge Detection and Interpolation: Feeds the current pixel and its neighbors into the EnhancedEdgeDetector
module to identify edges. The resultant edge direction and strength are then passed to the EnhancedPixelInterpolator
module to perform intelligent interpolation based on the detected edge characteristics.
/**
* Main Program
* Reads an image → Reads scaling factor → Prepares output matrix → Simulates scaling using Chisel → Writes back the image
*/
object PixelArtScale extends App {
//========== (1) Read Scaling Factor ==========
println("Enter scaling factor (e.g., 2 for 2x scaling):")
val scaleFactorInput = StdIn.readInt()
require(scaleFactorInput >= 1 && scaleFactorInput <= 4, "Scaling factor must be between 1 and 4")
//========== (2) Read Image and Convert to 2D Array ==========
/**
* Reads PNG/JPG using Scala and converts it to a 2D array [height][width] (24-bit RGB)
*/
def loadImageToMatrix(path: String): Array[Array[Int]] = {
val img = ImageIO.read(new File(path))
val w = img.getWidth
val h = img.getHeight
val data = Array.ofDim[Int](h, w)
for (y <- 0 until h; x <- 0 until w) {
data(y)(x) = img.getRGB(x, y) & 0xFFFFFF
}
data
}
//========== (3) Write Output Matrix Back to Image ==========
/**
* Writes a 2D array (24-bit RGB) back to an image
*/
def saveMatrixToImage(matrix: Array[Array[Int]], outPath: String): Unit = {
val h = matrix.length
val w = if (h > 0) matrix(0).length else 0
val outImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB)
for (y <- 0 until h; x <- 0 until w) {
outImg.setRGB(x, y, matrix(y)(x) & 0xFFFFFF)
}
ImageIO.write(outImg, "png", new File(outPath))
}
//========== (4) Prepare Data: Read input.png → Create outputMatrix ==========
// Read input image
val inputPath = "/home/yckuo/桌面/chisel-book/src/main/scala/Pixel-art scaling Accelerator/Lenna.png"
val inputMatrix = loadImageToMatrix(inputPath)
val inHeight = inputMatrix.length
val inWidth = if (inHeight > 0) inputMatrix(0).length else 0
// Create scaled output matrix
val outHeight = inHeight * scaleFactorInput
val outWidth = inWidth * scaleFactorInput
val outputMatrix = Array.ofDim[Int](outHeight, outWidth)
println(s"[Info] Input image size: $inWidth x $inHeight")
println(s"[Info] Scale factor: ${scaleFactorInput}x")
println(s"[Info] Output image size: $outWidth x $outHeight")
//========== (5) **At the End of the Program** Use RawTester.test(...) to Perform Simulation ==========
// Use RawTester for simple testing/simulation
RawTester.test(new EnhancedPixelArtScaler(inWidth, inHeight, scaleFactorInput)) { dut =>
// Initialize line buffers and shift registers
for (y <- 0 until inHeight + 2) { // Including two boundary lines
for (x <- 0 until inWidth + 2) { // Including two boundary columns
val rgb24 = if (y >= 1 && y <= inHeight && x >= 1 && x <= inWidth)
inputMatrix(y - 1)(x - 1)
else
0 // Boundary padding with 0
// Poke input
dut.io.inPixel.poke(rgb24.U)
dut.io.inValid.poke(true.B)
// Advance clock by 1 cycle
dut.clock.step(1)
// If outValid is true, extract the corresponding scaleFactor x scaleFactor block
if (dut.io.outValid.peek().litToBoolean) {
for (i <- 0 until (scaleFactorInput * scaleFactorInput)) {
val outP = dut.io.outPixels(i).peek().litValue.toInt & 0xFFFFFF
// Calculate output matrix coordinates
val baseY = (y - 1) * scaleFactorInput
val baseX = (x - 1) * scaleFactorInput
// Ensure within output matrix bounds
val pixelY = baseY + (i / scaleFactorInput)
val pixelX = baseX + (i % scaleFactorInput)
if (pixelY >= 0 && pixelY < outHeight && pixelX >= 0 && pixelX < outWidth) {
outputMatrix(pixelY)(pixelX) = outP
}
}
// Retrieve neighboring pixels
val neighbors = dut.io.neighbor.map(_.peek().litValue.toInt & 0xFFFFFF)
// Further processing of neighboring pixels can be done here, such as applying the xBR algorithm
// Currently, only printing neighboring pixels for reference
/*
println(s"Pixel ($x, $y):")
println(s"Neighbors: ${neighbors.mkString(", ")}")
*/
}
}
}
// If needed, advance the clock by 1 cycle at the end
dut.clock.step(1)
}
//========== (6) Write Back Image File and End Program ==========
val outputPath = "/home/yckuo/桌面/chisel-book/src/main/scala/Pixel-art scaling Accelerator/output.png"
saveMatrixToImage(outputMatrix, outputPath)
println(s"[Info] Processing complete! Scaled image saved to: $outputPath")
}
Read Scaling Factor: Prompts the user to input the desired scaling factor, ensuring it falls within the acceptable range (1 to 4).
Read Image and Convert to 2D Array: Utilizes Scala's ImageIO
to load a PNG/JPG image and transform it into a two-dimensional array representing pixel data in 24-bit RGB format.
Write Output Matrix Back to Image: Converts the processed two-dimensional pixel array back into an image format and saves it to the specified output path.
Prepare Data: Reads the input image, determines the dimensions of the scaled output image based on the scaling factor, and initializes the output matrix accordingly.
Simulation Using RawTester: Employs RawTester
to simulate the EnhancedPixelArtScaler
module. Inputs image data into the module, simulates hardware operations, and aggregates the output pixels into the output matrix.
Write Back Image File: Saves the scaled pixel data as a new image file, completing the scaling process.
Follow these steps to run the project:
Set Up the Development Environment:
Install Scala and SBT (Scala Build Tool)**: Ensure that Scala and SBT are installed on your system. Refer to the Scala installation guide and the SBT installation instructions for detailed steps.
Install Chisel and Dependencies**: Set up Chisel by following the official Chisel installation guide.
Prepare Input Image:
/home/yckuo/桌面/chisel-book/src/main/scala/Pixel-art scaling Accelerator/Lenna.png
.Compile and Run:
sbt run
PixelArtScale
main program and input the desired scaling factor (e.g., 2 for 2x scaling).sbt "runMain pixelart.PixelArtScale"
.\src\main\scala\Pixel Art\PixelArtScale.scala
View Output Results:
/home/yckuo/桌面/chisel-book/src/main/scala/Pixel-art scaling Accelerator/output.png
.After running the program, you will obtain the scaled image. Below is an example comparison of the original and scaled images:
In this project, we successfully designed and implemented a Chisel-based pixel art scaling solution, focusing on the core Art Pixel functionality. The current implementation integrates edge detection and intelligent interpolation techniques to facilitate the scaling of pixel art images effectively. While the foundational features are operational, the present version does not yet leverage hardware acceleration, which remains a critical area for enhancement.
Future Enhancements Include:
Integration of Hardware Accelerators: Developing dedicated hardware accelerators to parallelize computations, thereby significantly accelerating the scaling process and improving overall performance.
Optimization of Interpolation Algorithms: Refining existing interpolation methods and incorporating advanced algorithms, such as the xBR algorithm, to further enhance image quality and scalability.
Expansion of Supported Scaling Factors: Extending the range of supported scaling factors to accommodate a wider variety of application requirements and use cases.
Hardware Resource Optimization: Streamlining module designs to minimize hardware resource consumption, ensuring efficient utilization and maximizing performance.
These enhancements aim to elevate the pixel art scaling solution's performance and versatility, establishing it as a robust and efficient tool for various digital applications. By focusing on hardware acceleration and algorithm optimization, future versions will deliver superior scalability and image fidelity, meeting the demanding needs of modern pixel art processing.