From ff0bb7c242637f1fc512c2d6f1588c7a5a8472eb Mon Sep 17 00:00:00 2001 From: "kormiltsyn.roman" Date: Mon, 27 Apr 2026 11:50:39 +0300 Subject: [PATCH] feat: add solution of task 1 --- .idea/gradle.xml | 6 ++ build.gradle.kts | 11 +++- settings.gradle.kts | 2 +- .../kotlin/convolution/ConvolutionKernel.kt | 65 +++++++++++++++++++ src/main/kotlin/convolution/Main.kt | 60 +++++++++++++++++ src/main/kotlin/convolution/OpenCvSupport.kt | 37 +++++++++++ .../convolution/SequentialConvolution.kt | 62 ++++++++++++++++++ .../convolution/SequentialConvolutionTest.kt | 48 ++++++++++++++ 8 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/convolution/ConvolutionKernel.kt create mode 100644 src/main/kotlin/convolution/Main.kt create mode 100644 src/main/kotlin/convolution/OpenCvSupport.kt create mode 100644 src/main/kotlin/convolution/SequentialConvolution.kt create mode 100644 src/test/kotlin/convolution/SequentialConvolutionTest.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 9d62f93..2a65317 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,10 +1,16 @@ + diff --git a/build.gradle.kts b/build.gradle.kts index 677cf93..f73ece4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,9 @@ plugins { + application kotlin("jvm") version "2.1.10" } -group = "org.example" +group = "convolution" version = "1.0-SNAPSHOT" repositories { @@ -10,12 +11,18 @@ repositories { } dependencies { + implementation("org.openpnp:opencv:4.9.0-0") testImplementation(kotlin("test")) } tasks.test { useJUnitPlatform() } + +application { + mainClass = "convolution.MainKt" +} + kotlin { jvmToolchain(23) -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a0d31e5..c4b578a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } -rootProject.name = "task_1" +rootProject.name = "convolution" diff --git a/src/main/kotlin/convolution/ConvolutionKernel.kt b/src/main/kotlin/convolution/ConvolutionKernel.kt new file mode 100644 index 0000000..25aba92 --- /dev/null +++ b/src/main/kotlin/convolution/ConvolutionKernel.kt @@ -0,0 +1,65 @@ +package convolution + +data class ConvolutionKernel( + val name: String, + val size: Int, + val coefficients: DoubleArray, +) { + init { + require(size % 2 == 1) { "Kernel size must be odd." } + require(coefficients.size == size * size) { + "Kernel $name must contain exactly ${size * size} coefficients." + } + } +} + +object Kernels { + private val all = listOf( + ConvolutionKernel( + name = "identity3", + size = 3, + coefficients = doubleArrayOf( + 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, + ), + ), + ConvolutionKernel( + name = "box3", + size = 3, + coefficients = doubleArrayOf( + 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, + 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, + 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, + ), + ), + ConvolutionKernel( + name = "gaussian3", + size = 3, + coefficients = doubleArrayOf( + 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0, + 2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0, + 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0, + ), + ), + ConvolutionKernel( + name = "sharpen3", + size = 3, + coefficients = doubleArrayOf( + 0.0, -1.0, 0.0, + -1.0, 5.0, -1.0, + 0.0, -1.0, 0.0, + ), + ), + ) + + private val byName = all.associateBy { it.name } + + fun resolve(name: String): ConvolutionKernel { + return byName[name] ?: error( + "Unknown kernel '$name'. Available kernels: ${availableNames()}." + ) + } + + fun availableNames(): String = all.joinToString(", ") { it.name } +} diff --git a/src/main/kotlin/convolution/Main.kt b/src/main/kotlin/convolution/Main.kt new file mode 100644 index 0000000..8a63714 --- /dev/null +++ b/src/main/kotlin/convolution/Main.kt @@ -0,0 +1,60 @@ +package convolution + +import java.nio.file.Path +import kotlin.system.exitProcess + +private data class CliConfig( + val inputPath: Path, + val outputPath: Path, + val kernelName: String, +) + +fun main(args: Array) { + val config = parseArgs(args) ?: run { + printUsage() + exitProcess(1) + } + + val exitCode = runCatching { + val kernel = Kernels.resolve(config.kernelName) + val source = OpenCvSupport.readGrayscale(config.inputPath) + val result = SequentialConvolution.apply(source, kernel) + OpenCvSupport.write(config.outputPath, result) + println("Saved '${kernel.name}' result to ${config.outputPath}") + }.fold( + onSuccess = { 0 }, + onFailure = { error -> + System.err.println(error.message ?: error.toString()) + 1 + }, + ) + + exitProcess(exitCode) +} + +private fun parseArgs(args: Array): CliConfig? { + if (args.size !in 2..3) { + return null + } + + return CliConfig( + inputPath = Path.of(args[0]), + outputPath = Path.of(args[1]), + kernelName = args.getOrElse(2) { "gaussian3" }, + ) +} + +private fun printUsage() { + println( + """ + Usage: + ./gradlew run --args="input/source.bmp output/result.bmp [kernel]" + + Available kernels: + ${Kernels.availableNames()} + + Example: + ./gradlew run --args="input/source.bmp output/result.bmp sharpen3" + """.trimIndent() + ) +} diff --git a/src/main/kotlin/convolution/OpenCvSupport.kt b/src/main/kotlin/convolution/OpenCvSupport.kt new file mode 100644 index 0000000..bf64969 --- /dev/null +++ b/src/main/kotlin/convolution/OpenCvSupport.kt @@ -0,0 +1,37 @@ +package convolution + +import nu.pattern.OpenCV +import org.opencv.core.Core +import org.opencv.core.Mat +import org.opencv.imgcodecs.Imgcodecs +import java.nio.file.Files +import java.nio.file.Path + +object OpenCvSupport { + private var loaded = false + + fun readGrayscale(path: Path): Mat { + require(Files.exists(path)) { "Input file does not exist: $path" } + ensureLoaded() + + val image = Imgcodecs.imread(path.toString(), Imgcodecs.IMREAD_GRAYSCALE) + require(!image.empty()) { "Failed to read image from $path" } + return image + } + + fun write(path: Path, image: Mat) { + ensureLoaded() + path.parent?.let(Files::createDirectories) + check(Imgcodecs.imwrite(path.toString(), image)) { "Failed to write image to $path" } + } + + private fun ensureLoaded() { + if (loaded) { + return + } + + OpenCV.loadLocally() + Core.setNumThreads(1) + loaded = true + } +} diff --git a/src/main/kotlin/convolution/SequentialConvolution.kt b/src/main/kotlin/convolution/SequentialConvolution.kt new file mode 100644 index 0000000..190a4f5 --- /dev/null +++ b/src/main/kotlin/convolution/SequentialConvolution.kt @@ -0,0 +1,62 @@ +package convolution + +import kotlin.math.roundToInt +import org.opencv.core.CvType +import org.opencv.core.Mat + +object SequentialConvolution { + fun apply(source: Mat, kernel: ConvolutionKernel): Mat { + require(source.type() == CvType.CV_8UC1) { + "Expected a grayscale CV_8UC1 image, got type ${source.type()}." + } + + val width = source.cols() + val height = source.rows() + val inputBytes = ByteArray(width * height) + source.get(0, 0, inputBytes) + + val input = IntArray(inputBytes.size) { index -> + inputBytes[index].toInt() and 0xFF + } + + val output = apply_aux(width, height, input, kernel) + val outputBytes = ByteArray(output.size) { index -> output[index].toByte() } + + return Mat(height, width, CvType.CV_8UC1).apply { + put(0, 0, outputBytes) + } + } + + fun apply_aux(width: Int, height: Int, input: IntArray, kernel: ConvolutionKernel): IntArray { + require(width > 0) { "Image width must be positive." } + require(height > 0) { "Image height must be positive." } + require(input.size == width * height) { + "Input array size ${input.size} does not match image shape ${width}x$height." + } + + val radius = kernel.size / 2 + val output = IntArray(input.size) + + for (y in 0 until height) { + for (x in 0 until width) { + var sum = 0.0 + var kernelIndex = 0 + + for (kernelY in -radius..radius) { + val sourceY = (y + kernelY).coerceIn(0, height - 1) + val rowOffset = sourceY * width + + for (kernelX in -radius..radius) { + val sourceX = (x + kernelX).coerceIn(0, width - 1) + val pixel = input[rowOffset + sourceX] + sum += pixel * kernel.coefficients[kernelIndex++] + } + } + + output[y * width + x] = sum.roundToInt().coerceIn(0, 255) + } + } + + return output + } +} diff --git a/src/test/kotlin/convolution/SequentialConvolutionTest.kt b/src/test/kotlin/convolution/SequentialConvolutionTest.kt new file mode 100644 index 0000000..f211361 --- /dev/null +++ b/src/test/kotlin/convolution/SequentialConvolutionTest.kt @@ -0,0 +1,48 @@ +package convolution + +import kotlin.test.Test +import kotlin.test.assertContentEquals + +class SequentialConvolutionTest { + @Test + fun `identity kernel keeps the image unchanged`() { + val input = intArrayOf( + 10, 20, 30, + 40, 50, 60, + 70, 80, 90, + ) + + val actual = SequentialConvolution.apply_aux( + width = 3, + height = 3, + input = input, + kernel = Kernels.resolve("identity3"), + ) + + assertContentEquals(input, actual) + } + + @Test + fun `box blur uses replicated borders`() { + val input = intArrayOf( + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + ) + + val actual = SequentialConvolution.apply_aux( + width = 3, + height = 3, + input = input, + kernel = Kernels.resolve("box3"), + ) + + val expected = intArrayOf( + 2, 3, 4, + 4, 5, 6, + 6, 7, 8, + ) + + assertContentEquals(expected, actual) + } +}