diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3df5234 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: Kotlin CI + +on: [push, pull_request] + +jobs: + build-test-lint: + runs-on: ubuntu-latest + env: + _JAVA_OPTIONS: "-Xmx6g" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 20 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 20 + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission to Gradle wrapper + run: chmod +x ./gradlew + + - name: Run tests + run: ./gradlew test + + - name: Run ktlint + run: ./gradlew ktlintCheck diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf8e212 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Sonia Kozyreva + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d93e823 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Image Convolution in Kotlin + +This project implements and benchmarks sequential and several parallel approaches to applying 2D filters to grayscale images. + +## โœจ Features + +- Sequential and parallel convolution +- Interactive CLI tool for applying filters to images +- Performance benchmarking and analysis +- Predefined filters +- Support for user-supplied or built-in test images +- Convolve several images at once using sequential or asynchronous pipeline + +## Built-in Filters + +All filters are defined as 2D matrices (kernels), with optional normalization (`factor`) and offset (`bias`). + +* `blur_3x3`, `blur_5x5` +* `gaussian_blur_3x3`, `gaussian_blur_5x5` +* `sharpen` +* `edge_detect` +* `motion_blur` +* `identity` +* `emboss` + +## Modes of Convolution + +| Mode | Description | +| -------- | -------------------------------- | +| `seq` | Standard single-threaded version | +| `pixels` | Parallelized per-pixel | +| `rows` | Rows processed in parallel | +| `cols` | Columns processed in parallel | +| `tiles` | Blocks (tiles) of the image | + +## Types of Pipelines + +| Mode | Description | +| -------- | -------------------------------- | +| `seq` | Convolve one image at a time using any mode | +| `async` | Convolve several images simultaneously | + + +## ๐Ÿงช Requirements + +- JDK 17+ +- Kotlin +- Gradle +- OpenCV via [JavaCPP](https://github.com/bytedeco/javacpp) + +## Getting started +Clone the repo: +```bash +git clone git@github.com:sofyak0zyreva/convolution.git +``` +Run the following command to install the dependencies: + +```bash +./gradlew build +``` + +## โ–ถ๏ธ Usage + +To apply a filter to an image via CLI: + +```bash +./gradlew run --quiet --console=plain +``` + +You'll be prompted to: + +1. Enter an image path (or use defaults from _resources/images/_). You can enter path from the repository root or absolute path +2. Select a mode (with optional batch/tile sizes -- they are responsible for how many pixels will be allocated per coroutine) +3. Choose a filter + +The result will be saved as a new `.bmp` file in the project directory's `output` folder. + + +## ๐Ÿงต Performance Benchmarking +You can simply run `main()` in the specified file (simplest for `BenchmarkPipelines.kt`). + +To measure the performance of a specific mode (`BenchmarkSizes.kt`): + +```kotlin +val result = benchmarkSingleMode(inputImage, filter, ConvolutionMode.ParallelRows(8)) +println(result) +``` +Here, you can also use randomly generated images of a chosen size. + +To compare all modes (`BenchmarkAllModes.kt`): + +```kotlin +val results = benchmarkAllModes(image, filter) +results.forEach(::println) +``` + +To benchmark scalability (e.g., for rows/cols/tiles) (`EfficiencyAnalysis.kt`): + +```kotlin +val sizes = listOf(1, 4, 8, 16, 32, 64, 128) +benchmarkSizes(image, filter, { ConvolutionMode.ParallelRows(8) }, sizes) +``` + + + +## โœ… Testing & Correctness + +* Sequential implementation serves as the reference with key points preserved: + - Compositionality: applying filters sequentially should equal applying their composition + (e.g., `apply(filter1, apply(filter2, img)) == apply(filter1 โŠ• filter2, img)`) + - Identity: some filters compose to identity (e.g., _shift-left _then_ shift-right_) + - Zero-padding: expanding filters with zeros shouldn't change results + - Known-output filters: test with trivial filters (_zero filter, identity filter_) +* All modes are tested against the sequential implementation for numerical accuracy +* Standard Error of the Mean (SEM) is reported in benchmarks. See [Performance Analysis](./src/test/kotlin/benchmarks/EfficiencyAnalysis.md), + [Plots](./src/test/kotlin/benchmarks/plots), and [Results](./src/test/kotlin/benchmarks/results) for more + +Run: + +```bash +./gradlew test +``` + +## ๐Ÿ“‚ Directory Structure + +``` +src/ +โ”œโ”€โ”€ main/ +โ”‚ โ”œโ”€โ”€ kotlin/ โ† Core logic, filters, modes, pipelines and CLI +โ”‚ โ””โ”€โ”€ resources/ +โ”‚ โ””โ”€โ”€ images/ โ† Sample input images +โ””โ”€โ”€ test/ + โ””โ”€โ”€ kotlin/ + โ”œโ”€โ”€ benchmarks/ โ† Performance analysis, plots, results + โ””โ”€โ”€ ... โ† Tests + +``` + +## License +This project uses JavaCPP Presets for OpenCV and OpenCV, both licensed under Apache License 2.0. +See [`LICENSE`](LICENSE) for details. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..14c15fd --- /dev/null +++ b/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '2.1.0' + id 'application' + id 'org.jlleitschuh.gradle.ktlint' version '11.6.0' +} + +group = 'org.example' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") + implementation("org.bytedeco:opencv-platform:4.6.0-1.5.8") // Includes all platforms + implementation("org.bytedeco:openblas-platform:0.3.21-1.5.8") // For math optimizations + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") +} +application { + mainClassName = 'convolution.MainKt' +} +run { + standardInput = System.in +} +test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(20) +} + diff --git a/src/main/kotlin/convolution/ConvMode.kt b/src/main/kotlin/convolution/ConvMode.kt new file mode 100644 index 0000000..dcc5ab3 --- /dev/null +++ b/src/main/kotlin/convolution/ConvMode.kt @@ -0,0 +1,29 @@ +package convolution + +import filters.Filter +import org.bytedeco.opencv.opencv_core.Mat + +// options carry different parameters so not enum +sealed class ConvolutionMode { + object Sequential : ConvolutionMode() + object ParallelPixels : ConvolutionMode() + class ParallelRows(val batchSize: Int) : ConvolutionMode() + class ParallelCols(val batchSize: Int) : ConvolutionMode() + class ParallelTiles(val tileWidth: Int, val tileHeight: Int) : ConvolutionMode() + + override fun toString(): String = when (this) { + Sequential -> "seq" + ParallelPixels -> "par_pixels" + is ParallelRows -> "par_rows_$batchSize" + is ParallelCols -> "par_cols_$batchSize" + is ParallelTiles -> "par_tiles_${tileWidth}x$tileHeight" + } +} + +fun Mat.convolveWithMode(filter: Filter, mode: ConvolutionMode): Mat = when (mode) { + is ConvolutionMode.Sequential -> this.seqConvolve(filter) + is ConvolutionMode.ParallelPixels -> this.parallelConvolvePixels(filter) + is ConvolutionMode.ParallelRows -> this.parallelConvolveRows(filter, mode.batchSize) + is ConvolutionMode.ParallelCols -> this.parallelConvolveCols(filter, mode.batchSize) + is ConvolutionMode.ParallelTiles -> this.parallelConvolveTiles(filter, mode.tileWidth, mode.tileHeight) +} diff --git a/src/main/kotlin/convolution/Main.kt b/src/main/kotlin/convolution/Main.kt new file mode 100644 index 0000000..e82992e --- /dev/null +++ b/src/main/kotlin/convolution/Main.kt @@ -0,0 +1,152 @@ +package convolution + +import filters.filterPool +import java.io.File + +fun promptForImageOrDir(): List { + while (true) { + print("Enter path to image or directory: ") + val input = readlnOrNull()?.trim() + + if (input.isNullOrBlank()) { + println("โŒ Path cannot be empty.") + continue + } + + val file = File(input) + + if (!file.exists()) { + println("โŒ Path does not exist: $input") + continue + } + + // Single image file + if (file.isFile) { + try { + loadImage(file.absolutePath) // check if image is valid + return listOf(file.absolutePath) + } catch (e: Exception) { + println("โŒ Cannot load image. Reason: ${e.message}") + continue + } + } + + // Directory + if (file.isDirectory) { + val files = file.listFiles { f -> f.isFile } + ?.map { it.absolutePath } + ?.sorted() + ?: emptyList() + + if (files.isEmpty()) { + println("โŒ Directory is empty. No images to process.") + continue + } + + return files + } + + println("โŒ Not a valid file or directory: $input") + } +} + +fun promptForFilterName(): String { + val filterNames = filterPool.keys.toList() + + while (true) { + println("\nAvailable filters:") + filterNames.forEachIndexed { index, name -> + println(" [$index] $name") + } + + print("Select filter index: ") + val index = readlnOrNull()?.toIntOrNull() + if (index != null && index in filterNames.indices) { + return filterNames[index] + } else { + println("โŒ Invalid index. Please enter a number from the list.") + } + } +} + +fun promptForMode(): ConvolutionMode { + val options = listOf( + "sequential", + "parallel pixels", + "parallel rows (with batch size)", + "parallel columns (with batch size)", + "parallel tiles (with tile size)" + ) + + println("\nAvailable convolution modes:") + options.forEachIndexed { index, name -> + println(" [$index] $name") + } + + while (true) { + print("Select mode index: ") + when (readlnOrNull()?.toIntOrNull()) { + 0 -> return ConvolutionMode.Sequential + 1 -> return ConvolutionMode.ParallelPixels + 2 -> { + print("Enter batch size [default 1 -- per row]: ") + val batch = readlnOrNull()?.toIntOrNull() ?: 1 + return ConvolutionMode.ParallelRows(batch) + } + 3 -> { + print("Enter batch size [default 1 -- per column]: ") + val batch = readlnOrNull()?.toIntOrNull() ?: 1 + return ConvolutionMode.ParallelCols(batch) + } + 4 -> { + print("Enter tile width [default 32]: ") + val w = readlnOrNull()?.toIntOrNull() ?: 32 + print("Enter tile height [default 32]: ") + val h = readlnOrNull()?.toIntOrNull() ?: 32 + return ConvolutionMode.ParallelTiles(w, h) + } + else -> println("โŒ Invalid index. Please enter a number from the list.") + } + } +} + +fun main() { +// val imagePaths = listOf( +// "src/main/resources/images/bird.bmp", +// "src/main/resources/images/scenery.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp", +// "src/main/resources/images/cat.bmp" +// ) + try { + val imagePaths = promptForImageOrDir() + val imageNum = imagePaths.size + val selectedMode = promptForMode() + val filterName = promptForFilterName() + val filter = filterPool[filterName] ?: throw IllegalArgumentException("Filter should not be null.") + println("Applying filter: $filterName...") + + if (imageNum == 1) { + val imagePath = imagePaths[0] + val inputImage = loadImage(imagePath) + val grayImage = inputImage.toGrayscale() + val filename = File(imagePath).nameWithoutExtension + val resultImage = grayImage.convolveWithMode(filter, selectedMode) + File("output").apply { mkdirs() } + val modeName = selectedMode.toString() + val outputPath = "output${File.separator}${filename}_${filterName}_$modeName.bmp" + saveImage(resultImage, outputPath) + println("โœ… Done! Output saved to: $outputPath") + } else { + runAsyncPipeline(imagePaths, filter, filterName, selectedMode) + println("โœ… Pipeline finished") + } + } catch (e: Exception) { + System.err.println("โŒ Error: ${e.message}") + } +} diff --git a/src/main/kotlin/convolution/ParallelConv.kt b/src/main/kotlin/convolution/ParallelConv.kt new file mode 100644 index 0000000..802ffb9 --- /dev/null +++ b/src/main/kotlin/convolution/ParallelConv.kt @@ -0,0 +1,90 @@ +package convolution + +import filters.Filter +import filters.checkFilterSize +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.bytedeco.opencv.global.opencv_core.CV_8UC1 +import org.bytedeco.opencv.opencv_core.Mat +import kotlin.math.min + +// runBlocking { ... } creates a coroutine scope that blocks the current thread until all launched coroutines inside it finish +fun Mat.parallelConvolvePixels(filter: Filter): Mat = runBlocking { + checkFilterSize(filter) + val output = Mat(rows(), cols(), CV_8UC1) + + for (y in 0 until rows()) { + for (x in 0 until cols()) { + // launch{ ... } launches a new coroutine for this batch + // Dispatchers.Default: + // the coroutine will run on a background thread pool, these threads are shared across the app + launch(Dispatchers.Default) { + val pixelValue = convolvePixel(x, y, filter, this@parallelConvolvePixels) + output.ptr(y, x).put(pixelValue) + } + } + } + return@runBlocking output +} + +fun Mat.parallelConvolveRows(filter: Filter, batchSize: Int): Mat = runBlocking { + checkFilterSize(filter) + val output = Mat(rows(), cols(), CV_8UC1) + + val numBatches = (rows() + batchSize - 1) / batchSize + for (i in 0 until numBatches) { + val startRow = i * batchSize + val endRow = min(startRow + batchSize, rows()) + launch(Dispatchers.Default) { + for (y in startRow until endRow) { + for (x in 0 until cols()) { + val pixelValue = convolvePixel(x, y, filter, this@parallelConvolveRows) + output.ptr(y, x).put(pixelValue) + } + } + } + } + return@runBlocking output +} + +fun Mat.parallelConvolveCols(filter: Filter, batchSize: Int): Mat = runBlocking { + checkFilterSize(filter) + val output = Mat(rows(), cols(), CV_8UC1) + + val numBatches = (cols() + batchSize - 1) / batchSize + for (i in 0 until numBatches) { + val startColumn = i * batchSize + val endColumn = min(startColumn + batchSize, cols()) + launch(Dispatchers.Default) { + for (x in startColumn until endColumn) { + for (y in 0 until rows()) { + val pixelValue = convolvePixel(x, y, filter, this@parallelConvolveCols) + output.ptr(y, x).put(pixelValue) + } + } + } + } + return@runBlocking output +} + +fun Mat.parallelConvolveTiles(filter: Filter, tileWidth: Int, tileHeight: Int): Mat = runBlocking { + checkFilterSize(filter) + val output = Mat(rows(), cols(), CV_8UC1) + + for (tileY in 0 until rows() step tileHeight) { + for (tileX in 0 until cols() step tileWidth) { + launch(Dispatchers.Default) { + val endY = min(tileY + tileHeight, rows()) + val endX = min(tileX + tileWidth, cols()) + for (y in tileY until endY) { + for (x in tileX until endX) { + val result = convolvePixel(x, y, filter, this@parallelConvolveTiles) + output.ptr(y, x).put(result) + } + } + } + } + } + return@runBlocking output +} diff --git a/src/main/kotlin/convolution/PipelineStages.kt b/src/main/kotlin/convolution/PipelineStages.kt new file mode 100644 index 0000000..b589294 --- /dev/null +++ b/src/main/kotlin/convolution/PipelineStages.kt @@ -0,0 +1,58 @@ +package convolution + +import filters.Filter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.withContext +import org.bytedeco.opencv.opencv_core.Mat +import java.io.File + +// --- Reader --- +fun readImages(paths: List): Flow> = flow { + for (path in paths) { + val name = File(path).name + println("Reading: $name") + val mat = loadImage(path).toGrayscale() + emit(name to mat) + println("Read: $name") + } +}.flowOn(Dispatchers.IO) + +// --- Worker (convolution) --- +fun Flow>.convolveImages( + filter: Filter, + mode: ConvolutionMode, + concurrency: Int +): Flow> { + val convDispatcher = newFixedThreadPoolContext( + Runtime.getRuntime().availableProcessors(), + "convPool" + ) + + return this.flatMapMerge(concurrency) { (name, mat) -> + flow { + println("Convolving: $name") + val result = withContext(convDispatcher) { + mat.convolveWithMode(filter, mode) + } + emit(name to result) + println("Convolved: $name") + } + } +} + +// --- Writer --- +fun Flow>.writeImages(filterName: String, mode: ConvolutionMode): Flow = + this.map { (name, mat) -> + val filename = File(name).nameWithoutExtension + val modeName = mode.toString() + val outputPath = "output/${filename}_${filterName}_$modeName.bmp" + saveImage(mat, outputPath) + println("โœ… Saved: $outputPath") + mat.release() + }.flowOn(Dispatchers.IO) diff --git a/src/main/kotlin/convolution/Pipelines.kt b/src/main/kotlin/convolution/Pipelines.kt new file mode 100644 index 0000000..f5ece23 --- /dev/null +++ b/src/main/kotlin/convolution/Pipelines.kt @@ -0,0 +1,49 @@ +package convolution + +import filters.Filter +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking +import java.io.File + +fun runAsyncPipeline( + paths: List, + filter: Filter, + filterName: String, + mode: ConvolutionMode, + concurrency: Int = 8 +) = runBlocking { + File("output").mkdirs() + readImages(paths) + .buffer(5) + .convolveImages(filter, mode, concurrency) + .writeImages(filterName, mode) + .catch { e -> println("Pipeline failed: ${e.message}") } + .collect() +} + +fun runSeqPipeline( + paths: List, + filter: Filter, + filterName: String, + mode: ConvolutionMode +) { + File("output").mkdirs() + + for (path in paths) { + val file = File(path) + val name = file.nameWithoutExtension + println("Reading: ${file.name}") + val img = loadImage(path).toGrayscale() + println("Read: $name") + + println("Convolving: ${file.name}") + val result = img.convolveWithMode(filter, mode) + println("Convolved: ${file.name}") + val outputPath = "output/${name}_${filterName}_$mode.bmp" + saveImage(result, outputPath) + println("Saved: $outputPath") + result.release() + } +} diff --git a/src/main/kotlin/convolution/PixelConv.kt b/src/main/kotlin/convolution/PixelConv.kt new file mode 100644 index 0000000..abc86ad --- /dev/null +++ b/src/main/kotlin/convolution/PixelConv.kt @@ -0,0 +1,25 @@ +package convolution + +import filters.Filter +import org.bytedeco.opencv.opencv_core.Mat +import kotlin.math.roundToInt + +fun convolvePixel(x: Int, y: Int, filter: Filter, input: Mat): Byte { + val kernel = filter.kernel + val kRadius = kernel.size / 2 + var sum = 0.0 + for (ky in -kRadius..kRadius) { + for (kx in -kRadius..kRadius) { + // wrap-around coordinates + val wrappedY = (y + ky + input.rows()) % input.rows() + val wrappedX = (x + kx + input.cols()) % input.cols() + // get pixel value from the input matrix + val pixelValue = (input.ptr(wrappedY, wrappedX).get(0).toInt() and 0xFF).toDouble() + // multiply by kernel + sum += pixelValue * kernel[ky + kRadius][kx + kRadius] + } + } + // clamp to valid 8-bit range and store in output matrix + val clampedValue = (sum * filter.factor + filter.bias).roundToInt().coerceIn(0, 255) + return clampedValue.toByte() +} diff --git a/src/main/kotlin/convolution/SequentialConv.kt b/src/main/kotlin/convolution/SequentialConv.kt new file mode 100644 index 0000000..f92aa22 --- /dev/null +++ b/src/main/kotlin/convolution/SequentialConv.kt @@ -0,0 +1,19 @@ +package convolution + +import filters.Filter +import filters.checkFilterSize +import org.bytedeco.opencv.global.opencv_core.CV_8UC1 +import org.bytedeco.opencv.opencv_core.Mat + +fun Mat.seqConvolve(filter: Filter): Mat { + checkFilterSize(filter) + + val output = Mat(rows(), cols(), CV_8UC1) // 8-bit grayscale output + for (x in 0 until cols()) { + for (y in 0 until rows()) { + val pixelValue = convolvePixel(x, y, filter, this) + output.ptr(y, x).put(pixelValue) + } + } + return output +} diff --git a/src/main/kotlin/convolution/Utils.kt b/src/main/kotlin/convolution/Utils.kt new file mode 100644 index 0000000..7b5422b --- /dev/null +++ b/src/main/kotlin/convolution/Utils.kt @@ -0,0 +1,21 @@ +package convolution + +import org.bytedeco.opencv.global.opencv_imgcodecs +import org.bytedeco.opencv.global.opencv_imgproc +import org.bytedeco.opencv.opencv_core.Mat + +internal fun loadImage(path: String): Mat { + val mat = opencv_imgcodecs.imread(path) + if (mat.empty()) throw IllegalArgumentException("Failed to load image: $path") + return mat +} + +internal fun saveImage(image: Mat, path: String) { + if (!opencv_imgcodecs.imwrite(path, image)) throw IllegalStateException("Failed to save image: $path") +} + +internal fun Mat.toGrayscale(): Mat { + val grayMat = Mat() + opencv_imgproc.cvtColor(this, grayMat, opencv_imgproc.COLOR_BGR2GRAY) // Note: OpenCV uses BGR order by default! + return grayMat +} diff --git a/src/main/kotlin/filters/Filter.kt b/src/main/kotlin/filters/Filter.kt new file mode 100644 index 0000000..24a55cc --- /dev/null +++ b/src/main/kotlin/filters/Filter.kt @@ -0,0 +1,7 @@ +package filters + +class Filter( + val kernel: Array, + val factor: Double = 1.0, + val bias: Double = 0.0 +) diff --git a/src/main/kotlin/filters/Filters.kt b/src/main/kotlin/filters/Filters.kt new file mode 100644 index 0000000..5a88175 --- /dev/null +++ b/src/main/kotlin/filters/Filters.kt @@ -0,0 +1,76 @@ +package filters + +val filterPool = mapOf( + "blur_3x3" to Filter(Array(3) { DoubleArray(3) { 1.0 / 9 } }), + "blur_5x5" to Filter(Array(5) { DoubleArray(5) { 1.0 / 25 } }), + "identity" to Filter( + arrayOf( + doubleArrayOf(0.0, 0.0, 0.0), + doubleArrayOf(0.0, 1.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0) + ) + ), + "sharpen" to Filter( + arrayOf( + doubleArrayOf(0.0, -1.0, 0.0), + doubleArrayOf(-1.0, 5.0, -1.0), + doubleArrayOf(0.0, -1.0, 0.0) + ) + ), + "edge_detect" to Filter( + arrayOf( + doubleArrayOf(-1.0, -1.0, -1.0), + doubleArrayOf(-1.0, 8.0, -1.0), + doubleArrayOf(-1.0, -1.0, -1.0) + ) + ), + "motion_blur" to Filter( + arrayOf( + doubleArrayOf(1.0 / 9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + doubleArrayOf(0.0, 1.0 / 9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 1.0 / 9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0, 1.0 / 9, 0.0, 0.0, 0.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0, 0.0, 1.0 / 9, 0.0, 0.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0, 1.0 / 9, 0.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 / 9, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 / 9, 0.0), + doubleArrayOf(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 / 9) + ) + ), + "gaussian_blur_3x3" to Filter( + arrayOf( + doubleArrayOf(1.0, 2.0, 1.0), + doubleArrayOf(2.0, 4.0, 2.0), + doubleArrayOf(1.0, 2.0, 1.0) + ), + factor = 1.0 / 16 + ), + "gaussian_blur_5x5" to Filter( + arrayOf( + doubleArrayOf(1.0, 4.0, 6.0, 4.0, 1.0), + doubleArrayOf(4.0, 16.0, 24.0, 16.0, 4.0), + doubleArrayOf(6.0, 24.0, 36.0, 24.0, 6.0), + doubleArrayOf(4.0, 16.0, 24.0, 16.0, 4.0), + doubleArrayOf(1.0, 4.0, 6.0, 4.0, 1.0) + ), + factor = 1.0 / 256 + ), + "emboss" to Filter( + arrayOf( + doubleArrayOf(-1.0, -1.0, -1.0, -1.0, 0.0), + doubleArrayOf(-1.0, -1.0, -1.0, 0.0, 1.0), + doubleArrayOf(-1.0, -1.0, 0.0, 1.0, 1.0), + doubleArrayOf(-1.0, 0.0, 1.0, 1.0, 1.0), + doubleArrayOf(0.0, 1.0, 1.0, 1.0, 1.0) + ), + bias = 128.0 + ) +) + +fun createBasicFilter(kernel: Array): Filter { + return Filter(kernel = kernel) +} +fun checkFilterSize(filter: Filter) { + val kernel = filter.kernel + require(kernel.size % 2 == 1 && kernel[0].size % 2 == 1) { "Kernel must have odd dimensions" } +} diff --git a/src/main/resources/images/bird.bmp b/src/main/resources/images/bird.bmp new file mode 100644 index 0000000..12d48a4 Binary files /dev/null and b/src/main/resources/images/bird.bmp differ diff --git a/src/main/resources/images/cat.bmp b/src/main/resources/images/cat.bmp new file mode 100644 index 0000000..0ed7b68 Binary files /dev/null and b/src/main/resources/images/cat.bmp differ diff --git a/src/main/resources/images/ducks.bmp b/src/main/resources/images/ducks.bmp new file mode 100644 index 0000000..d575004 Binary files /dev/null and b/src/main/resources/images/ducks.bmp differ diff --git a/src/main/resources/images/scenery.bmp b/src/main/resources/images/scenery.bmp new file mode 100644 index 0000000..f9f2abc Binary files /dev/null and b/src/main/resources/images/scenery.bmp differ diff --git a/src/main/resources/images/seashell.bmp b/src/main/resources/images/seashell.bmp new file mode 100644 index 0000000..8e9faf8 Binary files /dev/null and b/src/main/resources/images/seashell.bmp differ diff --git a/src/test/kotlin/ParallelConvTests.kt b/src/test/kotlin/ParallelConvTests.kt new file mode 100644 index 0000000..730291f --- /dev/null +++ b/src/test/kotlin/ParallelConvTests.kt @@ -0,0 +1,54 @@ +import convolution.ConvolutionMode +import convolution.convolveWithMode +import convolution.seqConvolve +import java.util.Random +import kotlin.math.min +import kotlin.test.Test + +class ParallelConvTests { + @Test + fun `parallel convolution with random batch and tile sizes is the same as sequential`() { + val imageHeight = Random().nextInt(imageSizeBound) + val imageWidth = Random().nextInt(imageSizeBound) + val image = createRandomImage(imageHeight, imageWidth) + val imageBound = min(image.rows(), image.cols()) + val size = Random().nextInt(1, imageBound) + val modes = listOf( + ConvolutionMode.ParallelPixels, + ConvolutionMode.ParallelRows(batchSize = size), + ConvolutionMode.ParallelCols(batchSize = size), + ConvolutionMode.ParallelTiles(tileWidth = size, tileHeight = size) + ) + val filter = createRandomFilter(setRandomFilterSize()) + for (mode in modes) { + val parResult = image.convolveWithMode(filter, mode) + val seqResult = image.seqConvolve(filter) + assertMatEquals(seqResult, parResult) + } + } + + @Test + fun `parallel convolution with fixed batch and tile sizes is the same as sequential`() { + val image = createRandomImage(128, 128) + val modes = listOf( + ConvolutionMode.ParallelPixels, + ConvolutionMode.ParallelRows(batchSize = 1), + ConvolutionMode.ParallelCols(batchSize = 1), + ConvolutionMode.ParallelTiles(tileWidth = 1, tileHeight = 1), + ConvolutionMode.ParallelRows(batchSize = 32), + ConvolutionMode.ParallelCols(batchSize = 32), + ConvolutionMode.ParallelTiles(tileWidth = 32, tileHeight = 32), + ConvolutionMode.ParallelRows(batchSize = 64), + ConvolutionMode.ParallelCols(batchSize = 64), + ConvolutionMode.ParallelTiles(tileWidth = 64, tileHeight = 64), + ConvolutionMode.ParallelTiles(tileWidth = 32, tileHeight = 64), + ConvolutionMode.ParallelTiles(tileWidth = 64, tileHeight = 16) + ) + val filter = createRandomFilter(setRandomFilterSize()) + for (mode in modes) { + val parResult = image.convolveWithMode(filter, mode) + val seqResult = image.seqConvolve(filter) + assertMatEquals(seqResult, parResult) + } + } +} diff --git a/src/test/kotlin/SequentialConvTests.kt b/src/test/kotlin/SequentialConvTests.kt new file mode 100644 index 0000000..958b54b --- /dev/null +++ b/src/test/kotlin/SequentialConvTests.kt @@ -0,0 +1,82 @@ +import convolution.seqConvolve +import filters.createBasicFilter +import filters.filterPool +import org.bytedeco.opencv.global.opencv_core.countNonZero +import org.bytedeco.opencv.opencv_core.Mat +import org.junit.jupiter.api.Assertions.assertTrue +import java.util.Random +import kotlin.test.Test + +abstract class BaseSequentialConvolutionTests { + protected lateinit var image: Mat + abstract fun setupImage() + + @org.junit.jupiter.api.BeforeEach + fun setup() { + setupImage() + } + + @Test + fun `identity filter should return the same image`() { + val identityFilter = filterPool["identity"] ?: throw IllegalArgumentException("Filter must exist in the pool") + val result = image.seqConvolve(identityFilter) + assertMatEquals(image, result) + } + + @Test + fun `zero kernel should return a black image`() { + val size = setRandomFilterSize() + val zeroKernel = Array(size) { DoubleArray(size) { 0.0 } } + val result = image.seqConvolve(createBasicFilter(zeroKernel)) + assertTrue(countNonZero(result) == 0) + } + + @Test + fun `shift left then right should be identity`() { + val shiftLeft = arrayOf( + doubleArrayOf(0.0, 0.0, 0.0), + doubleArrayOf(1.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 0.0) + ) + val shiftRight = arrayOf( + doubleArrayOf(0.0, 0.0, 0.0), + doubleArrayOf(0.0, 0.0, 1.0), + doubleArrayOf(0.0, 0.0, 0.0) + ) + val result = image.seqConvolve(createBasicFilter(shiftLeft)).seqConvolve(createBasicFilter(shiftRight)) + assertMatEquals(image, result) + } + + @Test + fun `zero-padding a kernel should not change the result`() { + val filter = createRandomFilter(5) + val paddedFilter = padKernelWithZeros(filter, filterSizeBound) + val resultOriginal = image.seqConvolve(filter) + val resultPadded = image.seqConvolve(paddedFilter) + assertMatEquals(resultOriginal, resultPadded) + } + + @Test + fun `composition of two filters is the same as sequential application`() { + val filter1 = createRandomFilter(setRandomFilterSize()) + val filter2 = createRandomFilter(setRandomFilterSize()) + val composedKernel = composeKernels(filter1, filter2) + val seqResult = image.seqConvolve(filter1).seqConvolve(filter2) + val composedResult = image.seqConvolve(composedKernel) + assertMatEquals(seqResult, composedResult, 1) + } +} + +class SequentialConvolutionTestsWithRandomImages : BaseSequentialConvolutionTests() { + override fun setupImage() { + val imageHeight = Random().nextInt(imageSizeBound) + val imageWidth = Random().nextInt(imageSizeBound) + image = createRandomImage(imageHeight, imageWidth) + } +} + +class SequentialConvolutionTestsWithTestImages : BaseSequentialConvolutionTests() { + override fun setupImage() { + image = loadRandomImageFromResources() + } +} diff --git a/src/test/kotlin/TestUtils.kt b/src/test/kotlin/TestUtils.kt new file mode 100644 index 0000000..24daa44 --- /dev/null +++ b/src/test/kotlin/TestUtils.kt @@ -0,0 +1,132 @@ +import filters.Filter +import org.bytedeco.opencv.global.opencv_core +import org.bytedeco.opencv.global.opencv_imgcodecs +import org.bytedeco.opencv.opencv_core.Mat +import java.io.File +import java.util.Random +import kotlin.math.abs + +const val imageSizeBound = 2400 +const val filterSizeBound = 9 + +// create a grayscale Mat filled with random values between 0 and 255 +fun createRandomImage(rows: Int, cols: Int): Mat { + val image = Mat(rows, cols, opencv_core.CV_8UC1) + val rng = java.util.Random() + for (y in 0 until rows) { + for (x in 0 until cols) { + val value = rng.nextInt(256).toByte() + image.ptr(y, x).put(value) + } + } + return image +} + +fun createRandomFilter(size: Int): Filter { + require(size % 2 == 1) { "Kernel size must be odd" } + val kernel = Array(size) { DoubleArray(size) } + val min = 0.0001 + val max = 10.0 + + for (i in 0 until size) { + for (j in 0 until size) { + val value = min + Random().nextDouble() * (max - min) + kernel[i][j] = value + } + } + + val sum = kernel.sumOf { row -> row.sum() } + // normalize if sum is not zero + if (sum != 0.0) { + for (i in 0 until size) { + for (j in 0 until size) { + kernel[i][j] /= sum + } + } + } + val factor = Random().nextDouble() + return Filter(kernel = kernel, factor = factor) +} + +fun setRandomFilterSize(): Int { + val size = Random().nextInt(3, filterSizeBound) + // to get the odd size + return size + (size + 1) % 2 +} + +fun assertMatEquals(expected: Mat, actual: Mat, tolerance: Int = 0) { + require(expected.rows() == actual.rows() && expected.cols() == actual.cols()) + for (y in 0 until expected.rows()) { + for (x in 0 until expected.cols()) { + // fixing the difference between signed and unsigned bytes in Kotlin and OpenCV + val e = expected.ptr(y, x).get(0).toInt() and 0xFF + val a = actual.ptr(y, x).get(0).toInt() and 0xFF + assert(abs(e - a) <= tolerance) { "Mismatch at ($x, $y): $e != $a" } + } + } +} + +fun padKernelWithZeros(filter: Filter, newSize: Int): Filter { + require(newSize % 2 == 1) { "New kernel size must be odd" } + val kernel = filter.kernel + val oldSize = kernel.size + require(kernel[0].size == oldSize) { "Kernel must be square" } + require(newSize >= oldSize) { "New size must be >= old size" } + + val pad = (newSize - oldSize) / 2 + val newKernel = Array(newSize) { y -> + DoubleArray(newSize) { x -> + val ky = y - pad + val kx = x - pad + if (ky in kernel.indices && kx in kernel[0].indices) kernel[ky][kx] else 0.0 + } + } + return Filter(kernel = newKernel, factor = filter.factor, bias = filter.bias) +} + +fun composeKernels(filter1: Filter, filter2: Filter): Filter { + val kernel1 = filter1.kernel + val kernel2 = filter2.kernel + val size1 = kernel1.size + val size2 = kernel2.size + val newSize = size1 + size2 - 1 + val result = Array(newSize) { DoubleArray(newSize) { 0.0 } } + + for (i in 0 until newSize) { + for (j in 0 until newSize) { + var sum = 0.0 + for (ki in 0 until size1) { + for (kj in 0 until size1) { + val i2 = i - ki + val j2 = j - kj + if (i2 in 0 until size2 && j2 in 0 until size2) { + sum += kernel1[ki][kj] * kernel2[i2][j2] + } + } + } + result[i][j] = sum + } + } + return Filter(kernel = result, factor = filter1.factor * filter2.factor, bias = filter1.bias * filter2.factor + filter2.bias) +} + +fun loadRandomImageFromResources(): Mat { + val resourceURL = {}::class.java.getResource("/images") + ?: throw IllegalArgumentException("Resources root not found") + val folder = File(resourceURL.toURI()) + val imageFiles = folder.listFiles { file -> + file.isFile && file.name.endsWith(".bmp") + } ?: throw IllegalStateException("No image files found in resources") + val randomFile = imageFiles.random() + return opencv_imgcodecs.imread(randomFile.absolutePath) +} + +// fun Mat.toArray2D(): Array { +// val result = Array(rows()) { IntArray(cols()) } +// for (y in 0 until rows()) { +// for (x in 0 until cols()) { +// result[y][x] = this.ptr(y, x).get(0).toInt() and 0xFF +// } +// } +// return result +// } diff --git a/src/test/kotlin/benchmarks/BenchmarkAllModes.kt b/src/test/kotlin/benchmarks/BenchmarkAllModes.kt new file mode 100644 index 0000000..c3e4449 --- /dev/null +++ b/src/test/kotlin/benchmarks/BenchmarkAllModes.kt @@ -0,0 +1,70 @@ +package benchmarks + +import assertMatEquals +import convolution.ConvolutionMode +import convolution.parallelConvolveCols +import convolution.parallelConvolvePixels +import convolution.parallelConvolveRows +import convolution.parallelConvolveTiles +import convolution.seqConvolve +import filters.Filter +import filters.filterPool +import org.bytedeco.opencv.global.opencv_imgcodecs +import org.bytedeco.opencv.opencv_core.Mat + +// function that compares execution times of all convolution implementations +private fun benchmarkAllModes( + image: Mat, + filter: Filter, + runs: Int = 10, + verifyCorrectness: Boolean = true +): List { + val modes = listOf( + ConvolutionMode.Sequential, + ConvolutionMode.ParallelPixels, + ConvolutionMode.ParallelRows(batchSize = 4), + ConvolutionMode.ParallelCols(batchSize = 8), + ConvolutionMode.ParallelTiles(tileWidth = 8, tileHeight = 8) + ) + val reference = image.seqConvolve(filter) + return modes.map { mode -> + val times = mutableListOf() + var result = image + repeat(runs) { + val time = when (mode) { + ConvolutionMode.Sequential -> estimateTime { result = image.seqConvolve(filter) } + ConvolutionMode.ParallelPixels -> estimateTime { result = image.parallelConvolvePixels(filter) } + is ConvolutionMode.ParallelRows -> estimateTime { result = image.parallelConvolveRows(filter, mode.batchSize) } + is ConvolutionMode.ParallelCols -> estimateTime { result = image.parallelConvolveCols(filter, mode.batchSize) } + is ConvolutionMode.ParallelTiles -> estimateTime { result = image.parallelConvolveTiles(filter, mode.tileWidth, mode.tileHeight) } + } + times += time + if (verifyCorrectness) { + assertMatEquals(reference, result) + } + } + BenchmarkResult( + mode = mode, + times = times + ) + } +} + +fun main() { + val resource = object {}.javaClass.getResource("/images/cat.bmp") + requireNotNull(resource) { "Image not found!" } + val testImage = opencv_imgcodecs.imread(resource.path) + val allResults = mutableMapOf>() + for ((filterName, filter) in filterPool) { + val results = benchmarkAllModes( + image = testImage, + filter = filter + ) + allResults[filterName] = results + } + + allResults.forEach { (filterName, results) -> + println("=== Filter: $filterName ===") + results.forEach(::println) + } +} diff --git a/src/test/kotlin/benchmarks/BenchmarkPipelines.kt b/src/test/kotlin/benchmarks/BenchmarkPipelines.kt new file mode 100644 index 0000000..986f353 --- /dev/null +++ b/src/test/kotlin/benchmarks/BenchmarkPipelines.kt @@ -0,0 +1,72 @@ +package benchmarks + +import convolution.ConvolutionMode +import convolution.runAsyncPipeline +import convolution.runSeqPipeline +import filters.Filter +import filters.filterPool +import java.io.File + +private fun getAllBmpPathsFromResources(): List { + val resourceURL = {}::class.java.getResource("/images") + ?: throw IllegalArgumentException("Resources folder '/images' not found") + val folder = File(resourceURL.toURI()) + val imageFiles = folder.listFiles { file -> + file.isFile && file.name.endsWith(".bmp", ignoreCase = true) + } ?: emptyArray() + if (imageFiles.isEmpty()) { + println("No BMP images found in resources") + } + return imageFiles.map { it.absolutePath } +} + +private fun benchmarkPipeline( + filter: Filter, + filterName: String, + runs: Int = 3, + concurrency: Int? = null, + pipelineFunc: (List, Filter, String, ConvolutionMode, Int?) -> Unit +): List { + val imagePaths = getAllBmpPathsFromResources() + + val modes = listOf( + ConvolutionMode.Sequential, + ConvolutionMode.ParallelPixels, + ConvolutionMode.ParallelRows(batchSize = 4), + ConvolutionMode.ParallelCols(batchSize = 8), + ConvolutionMode.ParallelTiles(tileWidth = 8, tileHeight = 8) + ) + + return modes.map { mode -> + val times = mutableListOf() + repeat(runs) { + val time = estimateTime { + pipelineFunc(imagePaths, filter, filterName, mode, concurrency) + } + times += time + } + BenchmarkResult(mode = mode, times = times) + } +} + +fun main() { + val allResults = mutableMapOf>() + val filterName = "gaussian_blur_3x3" + val filter = filterPool[filterName] ?: throw IllegalArgumentException("Filter cannot be null") + val numWorkers = 8 + + val asyncResults = benchmarkPipeline(filter, filterName, numWorkers) { paths, f, name, mode, concurrency -> + runAsyncPipeline(paths, f, name, mode, concurrency ?: 1) + } + allResults["async, concurrency = $numWorkers"] = asyncResults + + val seqResults = benchmarkPipeline(filter, filterName) { paths, f, name, mode, _ -> + runSeqPipeline(paths, f, name, mode) + } + allResults["seq"] = seqResults + + allResults.forEach { (pipeline, results) -> + println("=== Pipeline type: $pipeline ===") + results.forEach(::println) + } +} diff --git a/src/test/kotlin/benchmarks/BenchmarkResult.kt b/src/test/kotlin/benchmarks/BenchmarkResult.kt new file mode 100644 index 0000000..cea1881 --- /dev/null +++ b/src/test/kotlin/benchmarks/BenchmarkResult.kt @@ -0,0 +1,22 @@ +package benchmarks + +import convolution.ConvolutionMode +import kotlin.math.sqrt + +internal data class BenchmarkResult( + val mode: ConvolutionMode, + val times: List +) { + private val n: Int get() = times.size + private val average: Double get() = times.average() + private val stdDev: Double get() = sqrt(times.sumOf { (it - average).let { d -> d * d } } / (n - 1)) + private val stdErrorOfTheMean: Double get() = stdDev / sqrt(n.toDouble()) + + override fun toString(): String { + return buildString { + appendLine("Mode: $mode") + appendLine(" Average Time: %.4f s".format(average)) + appendLine(" Std Error Of The Mean: %.4f s".format(stdErrorOfTheMean)) + } + } +} diff --git a/src/test/kotlin/benchmarks/BenchmarkSizes.kt b/src/test/kotlin/benchmarks/BenchmarkSizes.kt new file mode 100644 index 0000000..8b035ca --- /dev/null +++ b/src/test/kotlin/benchmarks/BenchmarkSizes.kt @@ -0,0 +1,55 @@ +package benchmarks + +import convolution.ConvolutionMode +import createRandomFilter +import createRandomImage +import filters.Filter +import org.bytedeco.opencv.opencv_core.Mat + +// function to measure execution time with different batch or tile sizes +// to then choose the most optimal one to compare across all convolution implementations +private fun benchmarkSizes( + image: Mat, + filter: Filter, + modeFactory: (Int) -> ConvolutionMode, + sizesToTry: List, + runs: Int = 10 +): List { + return sizesToTry.map { size -> + val mode = modeFactory(size) + val times = List(runs) { measureSingleModeTime(image, mode, filter) } + BenchmarkResult(mode, times) + } +} + +fun main() { + val testImage = createRandomImage(512, 512) + val testFilter = createRandomFilter(5) + + val testSizes = listOf( + 1, + 4, + 8, + 16, + 32, + 64, + 128, + 256, + 512 + ) + + val benchmarkOptimalSize: ((Int) -> ConvolutionMode) -> List = { modeFactory -> + benchmarkSizes( + image = testImage, + filter = testFilter, + sizesToTry = testSizes, + modeFactory = modeFactory + ) + } + val rowResult = benchmarkOptimalSize { ConvolutionMode.ParallelRows(it) } + val colResult = benchmarkOptimalSize { ConvolutionMode.ParallelCols(it) } + val tileResult = benchmarkOptimalSize { size -> ConvolutionMode.ParallelTiles(size, size) } + rowResult.forEach(::println) + colResult.forEach(::println) + tileResult.forEach(::println) +} diff --git a/src/test/kotlin/benchmarks/EfficiencyAnalysis.kt b/src/test/kotlin/benchmarks/EfficiencyAnalysis.kt new file mode 100644 index 0000000..5ed2768 --- /dev/null +++ b/src/test/kotlin/benchmarks/EfficiencyAnalysis.kt @@ -0,0 +1,97 @@ +package benchmarks + +import convolution.ConvolutionMode +import convolution.loadImage +import convolution.parallelConvolveCols +import convolution.parallelConvolvePixels +import convolution.parallelConvolveRows +import convolution.parallelConvolveTiles +import convolution.promptForFilterName +import convolution.promptForMode +import convolution.seqConvolve +import convolution.toGrayscale +import createRandomImage +import filters.Filter +import filters.filterPool +import imageSizeBound +import org.bytedeco.opencv.opencv_core.Mat +import java.util.Random +import kotlin.time.DurationUnit +import kotlin.time.measureTime + +fun promptForImagePath(): String { + while (true) { + print("Enter path to image (e.g., sample.bmp): ") + val path = readlnOrNull()?.trim() + if (!path.isNullOrBlank()) { + try { + loadImage(path) // check if image is valid + return path + } catch (e: Exception) { + println("โŒ Cannot load image. Reason: ${e.message}") + } + } else { + println("โŒ Path cannot be empty.") + } + } +} + +internal inline fun estimateTime(block: () -> Unit): Double { + return measureTime { block() }.toDouble(DurationUnit.SECONDS) +} + +internal fun measureSingleModeTime(image: Mat, mode: ConvolutionMode, filter: Filter): Double { + return when (mode) { + ConvolutionMode.Sequential -> estimateTime { image.seqConvolve(filter) } + ConvolutionMode.ParallelPixels -> estimateTime { image.parallelConvolvePixels(filter) } + is ConvolutionMode.ParallelRows -> estimateTime { image.parallelConvolveRows(filter, mode.batchSize) } + is ConvolutionMode.ParallelCols -> estimateTime { image.parallelConvolveCols(filter, mode.batchSize) } + is ConvolutionMode.ParallelTiles -> estimateTime { image.parallelConvolveTiles(filter, mode.tileWidth, mode.tileHeight) } + } +} + +private fun benchmarkSingleMode( + image: Mat, + filter: Filter, + mode: ConvolutionMode, + runs: Int = 10 +): BenchmarkResult { + val times = List(runs) { measureSingleModeTime(image, mode, filter) } + return BenchmarkResult(mode, times) +} + +private fun loadOrGenerateImage(): Mat { + println("\nChoose to load or generate an image:") + val options = listOf("load image", "create random image") + + options.forEachIndexed { index, name -> + println(" [$index] $name") + } + while (true) { + print("Select mode index: ") + when (readlnOrNull()?.toIntOrNull()) { + 0 -> return loadImage(promptForImagePath()).toGrayscale() + 1 -> { + print("Enter image size (it's square) [skip to choose random]: ") + val size = readlnOrNull()?.toIntOrNull() ?: Random().nextInt(imageSizeBound) + return createRandomImage(size, size) + } + else -> println("โŒ Invalid index. Please enter a number from the list.") + } + } +} + +// simply measuring execution time +fun main() { + val inputImage = loadOrGenerateImage() + println("Image size is ${inputImage.cols()}x${inputImage.rows()}") + val selectedMode = promptForMode() + + val filterName = promptForFilterName() + val filter = filterPool[filterName] ?: throw IllegalArgumentException("Filter should not be null.") + println("Applying filter: $filterName...") + + val result = benchmarkSingleMode(inputImage, filter, selectedMode) + println("Performance information:") + println(result) +} diff --git a/src/test/kotlin/benchmarks/EfficiencyAnalysis.md b/src/test/kotlin/benchmarks/EfficiencyAnalysis.md new file mode 100644 index 0000000..4196e10 --- /dev/null +++ b/src/test/kotlin/benchmarks/EfficiencyAnalysis.md @@ -0,0 +1,64 @@ +# Performance Analysis + +## 1. Tuning Parallel Parameters + +Before benchmarking on real filters, different configurations of parallel processing were tested to find optimal batch and tile sizes. This was done using a random image and filter, with each configuration measured over 10 runs. + +### Parallel Rows + +Performance improved quickly from `par_rows_1` to `par_rows_4`, and remained very stable through `par_rows_16`. Beyond that, execution time increased steadily, with `par_rows_512` being significantly slower. This suggests that small to moderate batch sizes (4โ€“16 rows) are best, while too many threads introduce overhead or contention. + +### Parallel Columns + +The trend was similar to the row-based implementation. The best results appeared around `par_cols_8` and `par_cols_16`. Larger configurations again showed diminishing returns and eventually degraded performance. Very small batches were also less stable, likely due to overhead. + +### Tiled Parallelism + +Tiled execution was most efficient with `8x8` tiles, closely followed by `4x4` and `16x16`. Extremely small tiles (`1x1`) were less consistent, and very large ones (like `256x256` or `512x512`) led to much longer runtimes. This confirms that medium-sized tiles offer the best balance between concurrency and overhead. + +### Conclusion + +For all strategies, moderate configurations consistently performed best. Too many threads or too large tiles reduced performance due to synchronization and workload imbalance. This impacted the choices in the full benchmark phase. +## 2. Comparison of Implementations + +With optimal parallel parameters established, different implementations were compared: sequential, pixel-wise parallel, row-based, column-based, and tiled. Filters used were from a predefined pool, images were from the test set in the `resources` directory. + +### Performance + +All parallel versions showed clear performance improvements over the sequential baseline. Among them, **row-based** and **tile-based** implementations consistently outperformed others across a range of filters, especially on smaller kernels like `blur_3x3` and `identity`, where they achieved speedups of up to **5 times**. For heavier filters like `motion_blur`, all parallel versions offered massive gains, cutting down execution time by **over 75%**. + +**Pixel-wise** parallelism, while better than sequential, was never the fastest โ€” it generally was behind the more structured row, column, and tile strategies. This is likely due to excessive thread overhead. **Column-based** implementation performed reasonably well, but typically fell short of row- and tile-based counterparts in both speed and stability. + +Performance varied slightly by filter complexity. The `gauss_blur_3ร—3` and `gauss_blur_5ร—5` and simple blur filters benefited more from tile- and row-based execution. The `emboss` and sharpen `filters`, with more pronounced pixel influence, still ran efficiently with all parallel variants, though results were more sensitive to thread scheduling. + +### Correctness + +All implementations produced visually and numerically consistent outputs, compared against the sequential reference. Minor floating-point differences were within acceptable limits and had no seen impact on the image results. + +The standard error of the mean (SEM) values were generally low across all modes, indicating stable and repeatable timing measurements. Larger SEMs appeared occasionally in pixel-wise parallelism, reflecting higher variability likely caused by thread management overhead and less efficient memory access patterns. In contrast, row- and tile-based approaches presented lower SEMs. + +### Conclusion + +Structured parallel approaches โ€” particularly **row-based** and **tile-based** โ€” offered the best performance and reliability across all filters and image types. Pixel-wise parallelism, despite being conceptually simple, proved inefficient in practice. + +# Performance Analysis of Pipelines + +## 1. Tuning Concurrency: Async Pipeline + +The asynchronous pipeline allows multiple worker coroutines to perform image convolutions in parallel. Different values of concurrency (the number of simultaneous convolution workers) were tested, using the same filter (`gaussian_blur_3x3`) across the same [set of images](https://github.com/sofyak0zyreva/convolution/tree/main/src/main/resources/images). + +### Observations + +Increasing the number of workers generally reduces processing time for modes that split work by rows, columns, or tiles. For a typical CPU with 8 cores, **8 concurrent workers** is a reasonable maximum for testing, balancing CPU utilization and memory usage. + +## 2. Comparing Pipelines + +Fully sequential pipeline (reading, convolving, writing images one by one) was compared to the asynchronous pipeline (streaming images through reading โ†’ convolution โ†’ writing, with buffered and parallelized convolution stages). + +### Async vs. Seq Pipeline Performance + +**Async pipeline excels** when convolution can be parallelized across multiple images or within a single image (rows, columns, tiles). **Sequential pipeline** is simpler but suffers from total runtime inflation because the reading, convolution, and writing stages are not overlapped. For highly parallelizable convolution modes, async pipelines with reasonable concurrency (e.g., 8 workers) are **twice or even three times faster** compared to sequential execution. **Parallel pixels mode** can occasionally be slower in async pipelines because it aggressively splits work at the pixel level, increasing memory pressure and overhead from many small tasks. + +### Conclusion + +Asynchronous pipeline with controlled concurrency is optimal for large-scale image processing, while sequential pipelines remain useful for simple or memory-constrained scenarios. diff --git a/src/test/kotlin/benchmarks/pipelines/asyncPipelinePlot.png b/src/test/kotlin/benchmarks/pipelines/asyncPipelinePlot.png new file mode 100644 index 0000000..bbcfac6 Binary files /dev/null and b/src/test/kotlin/benchmarks/pipelines/asyncPipelinePlot.png differ diff --git a/src/test/kotlin/benchmarks/pipelines/results.txt b/src/test/kotlin/benchmarks/pipelines/results.txt new file mode 100644 index 0000000..2ce4a60 --- /dev/null +++ b/src/test/kotlin/benchmarks/pipelines/results.txt @@ -0,0 +1,41 @@ +=== Pipeline type: async === +Mode: seq + Average Time: 23.2424 s + Std Error Of The Mean: 1.2507 s + +Mode: pixels + Average Time: 23.5817 s + Std Error Of The Mean: 0.5109 s + +Mode: rows + Average Time: 8.4324 s + Std Error Of The Mean: 0.4726 s + +Mode: cols + Average Time: 8.6094 s + Std Error Of The Mean: 0.2477 s + +Mode: tiles + Average Time: 8.3390 s + Std Error Of The Mean: 0.0738 s + +=== Pipeline type: seq === +Mode: seq + Average Time: 46.3251 s + Std Error Of The Mean: 1.9338 s + +Mode: pixels + Average Time: 16.1622 s + Std Error Of The Mean: 1.5203 s + +Mode: rows + Average Time: 10.6896 s + Std Error Of The Mean: 0.0435 s + +Mode: cols + Average Time: 10.7209 s + Std Error Of The Mean: 0.0238 s + +Mode: tiles + Average Time: 10.8410 s + Std Error Of The Mean: 0.0368 s diff --git a/src/test/kotlin/benchmarks/pipelines/resultsNWorkers.txt b/src/test/kotlin/benchmarks/pipelines/resultsNWorkers.txt new file mode 100644 index 0000000..41305bc --- /dev/null +++ b/src/test/kotlin/benchmarks/pipelines/resultsNWorkers.txt @@ -0,0 +1,83 @@ +=== Pipeline type: async, concurrency = 8 === +Mode: seq + Average Time: 23.2424 s + Std Error Of The Mean: 1.2507 s + +Mode: pixels + Average Time: 23.5817 s + Std Error Of The Mean: 0.5109 s + +Mode: rows + Average Time: 8.4324 s + Std Error Of The Mean: 0.4726 s + +Mode: cols + Average Time: 8.6094 s + Std Error Of The Mean: 0.2477 s + +Mode: tiles + Average Time: 8.3390 s + Std Error Of The Mean: 0.0738 s + +=== Pipeline type: async, concurrency = 4 === +Mode: seq + Average Time: 17.6897 s + Std Error Of The Mean: 0.6032 s + +Mode: pixels + Average Time: 27.2266 s + Std Error Of The Mean: 2.8882 s + +Mode: rows + Average Time: 11.9267 s + Std Error Of The Mean: 0.6592 s + +Mode: cols + Average Time: 8.6565 s + Std Error Of The Mean: 0.1735 s + +Mode: tiles + Average Time: 8.6625 s + Std Error Of The Mean: 0.1560 s + +=== Pipeline type: async, concurrency = 2 === +Mode: seq + Average Time: 31.6422 s + Std Error Of The Mean: 0.1509 s + +Mode: pixels + Average Time: 18.1427 s + Std Error Of The Mean: 0.7065 s + +Mode: rows + Average Time: 10.1314 s + Std Error Of The Mean: 0.0694 s + +Mode: cols + Average Time: 10.5366 s + Std Error Of The Mean: 0.1753 s + +Mode: tiles + Average Time: 10.7016 s + Std Error Of The Mean: 0.0653 s + +=== Pipeline type: async, concurrency = 1 === +Mode: seq + Average Time: 34.6955 s + Std Error Of The Mean: 0.4686 s + +Mode: pixels + Average Time: 16.2023 s + Std Error Of The Mean: 1.7229 s + +Mode: rows + Average Time: 10.7555 s + Std Error Of The Mean: 0.0260 s + +Mode: cols + Average Time: 11.5219 s + Std Error Of The Mean: 0.0182 s + +Mode: tiles + Average Time: 11.4317 s + Std Error Of The Mean: 0.0404 s \ No newline at end of file diff --git a/src/test/kotlin/benchmarks/pipelines/seqPipelinePlot.png b/src/test/kotlin/benchmarks/pipelines/seqPipelinePlot.png new file mode 100644 index 0000000..d808f1e Binary files /dev/null and b/src/test/kotlin/benchmarks/pipelines/seqPipelinePlot.png differ diff --git a/src/test/kotlin/benchmarks/plots/blur_3x3.png b/src/test/kotlin/benchmarks/plots/blur_3x3.png new file mode 100644 index 0000000..06b014d Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/blur_3x3.png differ diff --git a/src/test/kotlin/benchmarks/plots/blur_5x5.png b/src/test/kotlin/benchmarks/plots/blur_5x5.png new file mode 100644 index 0000000..94663b8 Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/blur_5x5.png differ diff --git a/src/test/kotlin/benchmarks/plots/cols.png b/src/test/kotlin/benchmarks/plots/cols.png new file mode 100644 index 0000000..d83e6de Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/cols.png differ diff --git a/src/test/kotlin/benchmarks/plots/edge_detect.png b/src/test/kotlin/benchmarks/plots/edge_detect.png new file mode 100644 index 0000000..f502d30 Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/edge_detect.png differ diff --git a/src/test/kotlin/benchmarks/plots/emboss.png b/src/test/kotlin/benchmarks/plots/emboss.png new file mode 100644 index 0000000..100aa8c Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/emboss.png differ diff --git a/src/test/kotlin/benchmarks/plots/gaussian_blur_3x3.png b/src/test/kotlin/benchmarks/plots/gaussian_blur_3x3.png new file mode 100644 index 0000000..7289c5a Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/gaussian_blur_3x3.png differ diff --git a/src/test/kotlin/benchmarks/plots/gaussian_blur_5x5.png b/src/test/kotlin/benchmarks/plots/gaussian_blur_5x5.png new file mode 100644 index 0000000..440e14e Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/gaussian_blur_5x5.png differ diff --git a/src/test/kotlin/benchmarks/plots/identity.png b/src/test/kotlin/benchmarks/plots/identity.png new file mode 100644 index 0000000..748f80e Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/identity.png differ diff --git a/src/test/kotlin/benchmarks/plots/motion_blur.png b/src/test/kotlin/benchmarks/plots/motion_blur.png new file mode 100644 index 0000000..6e51a8c Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/motion_blur.png differ diff --git a/src/test/kotlin/benchmarks/plots/rows.png b/src/test/kotlin/benchmarks/plots/rows.png new file mode 100644 index 0000000..1438058 Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/rows.png differ diff --git a/src/test/kotlin/benchmarks/plots/sharpen.png b/src/test/kotlin/benchmarks/plots/sharpen.png new file mode 100644 index 0000000..2910d31 Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/sharpen.png differ diff --git a/src/test/kotlin/benchmarks/plots/tiles.png b/src/test/kotlin/benchmarks/plots/tiles.png new file mode 100644 index 0000000..0662ca7 Binary files /dev/null and b/src/test/kotlin/benchmarks/plots/tiles.png differ diff --git a/src/test/kotlin/benchmarks/results/colResults512.txt b/src/test/kotlin/benchmarks/results/colResults512.txt new file mode 100644 index 0000000..91b3d85 --- /dev/null +++ b/src/test/kotlin/benchmarks/results/colResults512.txt @@ -0,0 +1,35 @@ +Mode: par_cols_1 + Average Time: 0.3195 s + Std Error Of The Mean: 0.0358 s + +Mode: par_cols_4 + Average Time: 0.2608 s + Std Error Of The Mean: 0.0223 s + +Mode: par_cols_8 + Average Time: 0.2381 s + Std Error Of The Mean: 0.0028 s + +Mode: par_cols_16 + Average Time: 0.2387 s + Std Error Of The Mean: 0.0020 s + +Mode: par_cols_32 + Average Time: 0.2436 s + Std Error Of The Mean: 0.0038 s + +Mode: par_cols_64 + Average Time: 0.2468 s + Std Error Of The Mean: 0.0039 s + +Mode: par_cols_128 + Average Time: 0.3221 s + Std Error Of The Mean: 0.0019 s + +Mode: par_cols_256 + Average Time: 0.6102 s + Std Error Of The Mean: 0.0019 s + +Mode: par_cols_512 + Average Time: 1.1986 s + Std Error Of The Mean: 0.0013 s diff --git a/src/test/kotlin/benchmarks/results/resultTables.txt b/src/test/kotlin/benchmarks/results/resultTables.txt new file mode 100644 index 0000000..6769eeb --- /dev/null +++ b/src/test/kotlin/benchmarks/results/resultTables.txt @@ -0,0 +1,98 @@ +=================== blur_3x3 ====================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 1.6081 s | 0.0014 s | +| pixels | 0.5313 s | 0.0580 s | +| rows | 0.3004 s | 0.0021 s | +| cols | 0.3146 s | 0.0027 s | +| tiles | 0.3219 s | 0.0023 s | ++--------+----------------+-----------------------+ + +=================== blur_5x5 ====================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 4.3044 s | 0.0383 s | +| pixels | 1.0216 s | 0.0137 s | +| rows | 0.7981 s | 0.0087 s | +| cols | 1.0822 s | 0.0614 s | +| tiles | 0.9179 s | 0.0203 s | ++--------+----------------+-----------------------+ + +=================== identity ====================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 1.7129 s | 0.0354 s | +| pixels | 0.6443 s | 0.0476 s | +| rows | 0.3786 s | 0.0156 s | +| cols | 0.4169 s | 0.0215 s | +| tiles | 0.3459 s | 0.0058 s | ++--------+----------------+-----------------------+ + +=================== sharpen ======================= ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 2.1195 s | 0.1261 s | +| pixels | 0.5690 s | 0.0144 s | +| rows | 0.3979 s | 0.0100 s | +| cols | 0.4067 s | 0.0151 s | +| tiles | 0.4108 s | 0.0130 s | ++--------+----------------+-----------------------+ + +================ edge_detect ====================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 1.8866 s | 0.0472 s | +| pixels | 0.5391 s | 0.0153 s | +| rows | 0.3836 s | 0.0195 s | +| cols | 0.3488 s | 0.0072 s | +| tiles | 0.3564 s | 0.0046 s | ++--------+----------------+-----------------------+ + +================ motion_blur ====================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 13.6759 s | 0.1060 s | +| pixels | 3.5089 s | 0.1241 s | +| rows | 3.1946 s | 0.0633 s | +| cols | 3.0761 s | 0.0428 s | +| tiles | 3.1471 s | 0.0539 s | ++--------+----------------+-----------------------+ + +============= gaussian_blur_3x3 =================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 1.6853 s | 0.0503 s | +| pixels | 0.5107 s | 0.0230 s | +| rows | 0.3372 s | 0.0043 s | +| cols | 0.3858 s | 0.0137 s | +| tiles | 0.3628 s | 0.0118 s | ++--------+----------------+-----------------------+ + +============= gaussian_blur_5x5 =================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 4.5076 s | 0.0923 s | +| pixels | 1.1993 s | 0.0327 s | +| rows | 0.9540 s | 0.0123 s | +| cols | 1.1097 s | 0.0369 s | +| tiles | 1.2924 s | 0.0477 s | ++--------+----------------+-----------------------+ + +=================== emboss ======================== ++--------+----------------+-----------------------+ +| Mode | Average Time | Std Error Of The Mean | ++--------+----------------+-----------------------+ +| seq | 4.2973 s | 0.0412 s | +| pixels | 1.2003 s | 0.0660 s | +| rows | 1.0467 s | 0.0520 s | +| cols | 0.9425 s | 0.0150 s | +| tiles | 1.0817 s | 0.0644 s | ++--------+----------------+-----------------------+ \ No newline at end of file diff --git a/src/test/kotlin/benchmarks/results/results.txt b/src/test/kotlin/benchmarks/results/results.txt new file mode 100644 index 0000000..107d90d --- /dev/null +++ b/src/test/kotlin/benchmarks/results/results.txt @@ -0,0 +1,188 @@ +=== Filter: blur_3x3 === +Mode: seq + Average Time: 1.6081 s + Std Error Of The Mean: 0.0014 s + +Mode: pixels + Average Time: 0.5313 s + Std Error Of The Mean: 0.0580 s + +Mode: rows + Average Time: 0.3004 s + Std Error Of The Mean: 0.0021 s + +Mode: cols + Average Time: 0.3146 s + Std Error Of The Mean: 0.0027 s + +Mode: tiles + Average Time: 0.3219 s + Std Error Of The Mean: 0.0023 s + +=== Filter: blur_5x5 === +Mode: seq + Average Time: 4.3044 s + Std Error Of The Mean: 0.0383 s + +Mode: pixels + Average Time: 1.0216 s + Std Error Of The Mean: 0.0137 s + +Mode: rows + Average Time: 0.7981 s + Std Error Of The Mean: 0.0087 s + +Mode: cols + Average Time: 1.0822 s + Std Error Of The Mean: 0.0614 s + +Mode: tiles + Average Time: 0.9179 s + Std Error Of The Mean: 0.0203 s + +=== Filter: identity === +Mode: seq + Average Time: 1.7129 s + Std Error Of The Mean: 0.0354 s + +Mode: pixels + Average Time: 0.6443 s + Std Error Of The Mean: 0.0476 s + +Mode: rows + Average Time: 0.3786 s + Std Error Of The Mean: 0.0156 s + +Mode: cols + Average Time: 0.4169 s + Std Error Of The Mean: 0.0215 s + +Mode: tiles + Average Time: 0.3459 s + Std Error Of The Mean: 0.0058 s + +=== Filter: sharpen === +Mode: seq + Average Time: 2.1195 s + Std Error Of The Mean: 0.1261 s + +Mode: pixels + Average Time: 0.5690 s + Std Error Of The Mean: 0.0144 s + +Mode: rows + Average Time: 0.3979 s + Std Error Of The Mean: 0.0100 s + +Mode: cols + Average Time: 0.4067 s + Std Error Of The Mean: 0.0151 s + +Mode: tiles + Average Time: 0.4108 s + Std Error Of The Mean: 0.0130 s + +=== Filter: edge_detect === +Mode: seq + Average Time: 1.8866 s + Std Error Of The Mean: 0.0472 s + +Mode: pixels + Average Time: 0.5391 s + Std Error Of The Mean: 0.0153 s + +Mode: rows + Average Time: 0.3836 s + Std Error Of The Mean: 0.0195 s + +Mode: cols + Average Time: 0.3488 s + Std Error Of The Mean: 0.0072 s + +Mode: tiles + Average Time: 0.3564 s + Std Error Of The Mean: 0.0046 s + +=== Filter: motion_blur === +Mode: seq + Average Time: 13.6759 s + Std Error Of The Mean: 0.1060 s + +Mode: pixels + Average Time: 3.5089 s + Std Error Of The Mean: 0.1241 s + +Mode: rows + Average Time: 3.1946 s + Std Error Of The Mean: 0.0633 s + +Mode: cols + Average Time: 3.0761 s + Std Error Of The Mean: 0.0428 s + +Mode: tiles + Average Time: 3.1471 s + Std Error Of The Mean: 0.0539 s + +=== Filter: gaussian_blur_3x3 === +Mode: seq + Average Time: 1.6853 s + Std Error Of The Mean: 0.0503 s + +Mode: pixels + Average Time: 0.5107 s + Std Error Of The Mean: 0.0230 s + +Mode: rows + Average Time: 0.3372 s + Std Error Of The Mean: 0.0043 s + +Mode: cols + Average Time: 0.3858 s + Std Error Of The Mean: 0.0137 s + +Mode: tiles + Average Time: 0.3628 s + Std Error Of The Mean: 0.0118 s + +=== Filter: gaussian_blur_5x5 === +Mode: seq + Average Time: 4.5076 s + Std Error Of The Mean: 0.0923 s + +Mode: pixels + Average Time: 1.1993 s + Std Error Of The Mean: 0.0327 s + +Mode: rows + Average Time: 0.9540 s + Std Error Of The Mean: 0.0123 s + +Mode: cols + Average Time: 1.1097 s + Std Error Of The Mean: 0.0369 s + +Mode: tiles + Average Time: 1.2924 s + Std Error Of The Mean: 0.0477 s + +=== Filter: emboss === +Mode: seq + Average Time: 4.2973 s + Std Error Of The Mean: 0.0412 s + +Mode: pixels + Average Time: 1.2003 s + Std Error Of The Mean: 0.0660 s + +Mode: rows + Average Time: 1.0467 s + Std Error Of The Mean: 0.0520 s + +Mode: cols + Average Time: 0.9425 s + Std Error Of The Mean: 0.0150 s + +Mode: tiles + Average Time: 1.0817 s + Std Error Of The Mean: 0.0644 s \ No newline at end of file diff --git a/src/test/kotlin/benchmarks/results/rowResults512.txt b/src/test/kotlin/benchmarks/results/rowResults512.txt new file mode 100644 index 0000000..b91305a --- /dev/null +++ b/src/test/kotlin/benchmarks/results/rowResults512.txt @@ -0,0 +1,35 @@ +Mode: par_rows_1 + Average Time: 0.2410 s + Std Error Of The Mean: 0.0092 s + +Mode: par_rows_4 + Average Time: 0.2299 s + Std Error Of The Mean: 0.0021 s + +Mode: par_rows_8 + Average Time: 0.2319 s + Std Error Of The Mean: 0.0008 s + +Mode: par_rows_16 + Average Time: 0.2315 s + Std Error Of The Mean: 0.0013 s + +Mode: par_rows_32 + Average Time: 0.2545 s + Std Error Of The Mean: 0.0050 s + +Mode: par_rows_64 + Average Time: 0.3476 s + Std Error Of The Mean: 0.0340 s + +Mode: par_rows_128 + Average Time: 0.3230 s + Std Error Of The Mean: 0.0018 s + +Mode: par_rows_256 + Average Time: 0.6089 s + Std Error Of The Mean: 0.0016 s + +Mode: par_rows_512 + Average Time: 1.2029 s + Std Error Of The Mean: 0.0010 s diff --git a/src/test/kotlin/benchmarks/results/tileResults512.txt b/src/test/kotlin/benchmarks/results/tileResults512.txt new file mode 100644 index 0000000..0cfd7a3 --- /dev/null +++ b/src/test/kotlin/benchmarks/results/tileResults512.txt @@ -0,0 +1,35 @@ +Mode: par_tiles_1x1 + Average Time: 0.3017 s + Std Error Of The Mean: 0.0221 s + +Mode: par_tiles_4x4 + Average Time: 0.2319 s + Std Error Of The Mean: 0.0017 s + +Mode: par_tiles_8x8 + Average Time: 0.2313 s + Std Error Of The Mean: 0.0005 s + +Mode: par_tiles_16x16 + Average Time: 0.2335 s + Std Error Of The Mean: 0.0011 s + +Mode: par_tiles_32x32 + Average Time: 0.2373 s + Std Error Of The Mean: 0.0009 s + +Mode: par_tiles_64x64 + Average Time: 0.3396 s + Std Error Of The Mean: 0.0350 s + +Mode: par_tiles_128x128 + Average Time: 0.2824 s + Std Error Of The Mean: 0.0063 s + +Mode: par_tiles_256x256 + Average Time: 0.3800 s + Std Error Of The Mean: 0.0088 s + +Mode: par_tiles_512x512 + Average Time: 1.2037 s + Std Error Of The Mean: 0.0042 s