Skip to content

Commit c5be1d4

Browse files
authored
Merge pull request #155 from ashtanko/feature/advanced_image_processing
Advanced image processing examples
2 parents 187278c + 1ac4ac0 commit c5be1d4

File tree

5 files changed

+280
-13
lines changed

5 files changed

+280
-13
lines changed

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@
2222

2323
### Metrics
2424
```text
25-
15199 number of properties
26-
10523 number of functions
27-
8940 number of classes
25+
15258 number of properties
26+
10538 number of functions
27+
8941 number of classes
2828
239 number of packages
29-
3531 number of kt files
29+
3533 number of kt files
3030
```
3131

3232

3333
### Complexity Report
3434
```text
35-
266642 lines of code (loc)
36-
165721 source lines of code (sloc)
37-
121060 logical lines of code (lloc)
38-
72547 comment lines of code (cloc)
39-
25022 cyclomatic complexity (mcc)
40-
20410 cognitive complexity
35+
266898 lines of code (loc)
36+
165936 source lines of code (sloc)
37+
121212 logical lines of code (lloc)
38+
72548 comment lines of code (cloc)
39+
25066 cyclomatic complexity (mcc)
40+
20459 cognitive complexity
4141
0 number of total code smells
4242
43 comment source ratio
4343
206 mcc per 1,000 lloc

api/Kotlin-Lab.api

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19686,6 +19686,17 @@ public final class dev/shtanko/concurrency/coroutines/actors/ExampleKt {
1968619686
public static synthetic fun main ([Ljava/lang/String;)V
1968719687
}
1968819688

19689+
public final class dev/shtanko/concurrency/coroutines/examples/AdvancedImageProcessingKt {
19690+
public static final fun applyRegionSpecificFilter (Ljava/awt/image/BufferedImage;)Ljava/awt/image/BufferedImage;
19691+
public static final fun blendSegment (Ljava/awt/image/BufferedImage;Ljava/awt/image/BufferedImage;)Ljava/awt/image/BufferedImage;
19692+
public static final fun detectEdges (Ljava/awt/image/BufferedImage;)Ljava/awt/image/BufferedImage;
19693+
public static final fun main ()V
19694+
public static synthetic fun main ([Ljava/lang/String;)V
19695+
public static final fun mergeSegments (Ljava/util/List;II)Ljava/awt/image/BufferedImage;
19696+
public static final fun processImageParallel (Ljava/awt/image/BufferedImage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
19697+
public static final fun segmentImage (Ljava/awt/image/BufferedImage;I)Ljava/util/List;
19698+
}
19699+
1968919700
public final class dev/shtanko/concurrency/coroutines/examples/AtomicBankTransfer : dev/shtanko/concurrency/coroutines/examples/IAtomicBankTransfer {
1969019701
public static final field Companion Ldev/shtanko/concurrency/coroutines/examples/AtomicBankTransfer$Companion;
1969119702
public fun <init> ()V
@@ -19733,6 +19744,8 @@ public abstract interface class dev/shtanko/concurrency/coroutines/examples/IAto
1973319744
}
1973419745

1973519746
public final class dev/shtanko/concurrency/coroutines/examples/ImageProcessingKt {
19747+
public static final field MAX_COLOR I
19748+
public static final field MAX_COLOR_VALUE I
1973619749
public static final fun adjustBrightness (Ljava/awt/image/BufferedImage;I)Ljava/awt/image/BufferedImage;
1973719750
public static synthetic fun adjustBrightness$default (Ljava/awt/image/BufferedImage;IILjava/lang/Object;)Ljava/awt/image/BufferedImage;
1973819751
public static final fun applySepia (Ljava/awt/image/BufferedImage;)Ljava/awt/image/BufferedImage;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package dev.shtanko.concurrency.coroutines.examples
2+
3+
import dev.shtanko.utils.ResourceFilePath
4+
import dev.shtanko.utils.readImageBytes
5+
import java.awt.Color
6+
import java.awt.image.BufferedImage
7+
import kotlin.math.sqrt
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.async
10+
import kotlinx.coroutines.awaitAll
11+
import kotlinx.coroutines.coroutineScope
12+
import kotlinx.coroutines.runBlocking
13+
14+
private const val KERNEL_RADIUS = 1
15+
16+
private val KERNEL_X = arrayOf(
17+
intArrayOf(-1, 0, 1),
18+
intArrayOf(-2, 0, 2),
19+
intArrayOf(-1, 0, 1),
20+
)
21+
22+
private val KERNEL_Y = arrayOf(
23+
intArrayOf(-1, -2, -1),
24+
intArrayOf(0, 0, 0),
25+
intArrayOf(1, 2, 1),
26+
)
27+
28+
suspend fun processImageParallel(image: BufferedImage): BufferedImage = coroutineScope {
29+
val segments = segmentImage(image, numberOfSegments = 8)
30+
31+
val processedRegions = segments.map { segment ->
32+
async(Dispatchers.Default) {
33+
val edges = detectEdges(segment)
34+
val filtered = applyRegionSpecificFilter(segment)
35+
blendSegment(filtered, edges)
36+
}
37+
}
38+
39+
val finalImage = mergeSegments(processedRegions.awaitAll(), image.width, image.height)
40+
return@coroutineScope finalImage
41+
}
42+
43+
fun segmentImage(image: BufferedImage, numberOfSegments: Int): List<BufferedImage> {
44+
val heightPerSegment = image.height / numberOfSegments
45+
return List(numberOfSegments) { i ->
46+
val y = i * heightPerSegment
47+
val h = if (i == numberOfSegments - 1) image.height - y else heightPerSegment
48+
image.getSubimage(0, y, image.width, h)
49+
}
50+
}
51+
52+
fun detectEdges(image: BufferedImage): BufferedImage {
53+
val w = image.width
54+
val h = image.height
55+
val edgeImage = BufferedImage(w, h, BufferedImage.TYPE_INT_RGB)
56+
57+
for (y in KERNEL_RADIUS until h - KERNEL_RADIUS) {
58+
for (x in KERNEL_RADIUS until w - KERNEL_RADIUS) {
59+
var gx = 0
60+
var gy = 0
61+
62+
for (ky in -KERNEL_RADIUS..KERNEL_RADIUS) {
63+
for (kx in -KERNEL_RADIUS..KERNEL_RADIUS) {
64+
val pixel = Color(image.getRGB(x + kx, y + ky)).red
65+
gx += pixel * KERNEL_X[ky + KERNEL_RADIUS][kx + KERNEL_RADIUS]
66+
gy += pixel * KERNEL_Y[ky + KERNEL_RADIUS][kx + KERNEL_RADIUS]
67+
}
68+
}
69+
70+
val magnitude = sqrt((gx * gx + gy * gy).toDouble())
71+
.toInt()
72+
.coerceIn(0, MAX_COLOR_VALUE)
73+
val gray = Color(magnitude, magnitude, magnitude)
74+
edgeImage.setRGB(x, y, gray.rgb)
75+
}
76+
}
77+
78+
return edgeImage
79+
}
80+
81+
fun applyRegionSpecificFilter(image: BufferedImage): BufferedImage {
82+
val output = BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB)
83+
for (y in 0 until image.height) {
84+
for (x in 0 until image.width) {
85+
val c = Color(image.getRGB(x, y))
86+
val r = MAX_COLOR - c.red
87+
val g = MAX_COLOR - c.green
88+
val b = MAX_COLOR - c.blue
89+
output.setRGB(x, y, Color(r, g, b).rgb)
90+
}
91+
}
92+
return output
93+
}
94+
95+
fun blendSegment(filtered: BufferedImage, edges: BufferedImage): BufferedImage {
96+
val w = filtered.width
97+
val h = filtered.height
98+
val blended = BufferedImage(w, h, BufferedImage.TYPE_INT_RGB)
99+
100+
for (y in 0 until h) {
101+
for (x in 0 until w) {
102+
val c1 = Color(filtered.getRGB(x, y))
103+
val c2 = Color(edges.getRGB(x, y))
104+
105+
val r = ((c1.red + c2.red) / 2).coerceIn(0, MAX_COLOR)
106+
val g = ((c1.green + c2.green) / 2).coerceIn(0, MAX_COLOR)
107+
val b = ((c1.blue + c2.blue) / 2).coerceIn(0, MAX_COLOR)
108+
109+
blended.setRGB(x, y, Color(r, g, b).rgb)
110+
}
111+
}
112+
113+
return blended
114+
}
115+
116+
fun mergeSegments(segments: List<BufferedImage>, totalWidth: Int, totalHeight: Int): BufferedImage {
117+
val result = BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_RGB)
118+
var currentY = 0
119+
for (segment in segments) {
120+
result.graphics.drawImage(segment, 0, currentY, null)
121+
currentY += segment.height
122+
}
123+
return result
124+
}
125+
126+
fun main() = runBlocking {
127+
val path: ResourceFilePath = "images/bd.jpg"
128+
val inputImage = decodeImage(path.readImageBytes())
129+
val result = processImageParallel(inputImage)
130+
131+
val resultBytes = result.toByteArray()
132+
display(resultBytes)
133+
}

src/main/kotlin/dev/shtanko/concurrency/coroutines/examples/ImageProcessing.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,21 @@ private const val SEPIA_BLUE_SCALE_R = 0.272
2727
private const val SEPIA_BLUE_SCALE_G = 0.534
2828
private const val SEPIA_BLUE_SCALE_B = 0.131
2929

30-
private const val MAX_COLOR_VALUE = 255
30+
const val MAX_COLOR_VALUE = 255
3131
private const val BRIGHTNESS_ADJUSTMENT = 40
3232

3333
private const val DEFAULT_THRESHOLD = 128
3434
private const val RGB_COMPONENT_COUNT = 3
3535
private const val MIN_COLOR = 0
36-
private const val MAX_COLOR = 255
36+
const val MAX_COLOR = 255
3737

3838
private const val KERNEL_SIZE = 3
3939
private const val KERNEL_CENTER = KERNEL_SIZE / 2
4040

4141
private val GAUSSIAN_KERNEL = arrayOf(
4242
doubleArrayOf(1.0 / 16, 2.0 / 16, 1.0 / 16),
4343
doubleArrayOf(2.0 / 16, 4.0 / 16, 2.0 / 16),
44-
doubleArrayOf(1.0 / 16, 2.0 / 16, 1.0 / 16)
44+
doubleArrayOf(1.0 / 16, 2.0 / 16, 1.0 / 16),
4545
)
4646

4747
fun toGrayscale(image: BufferedImage): BufferedImage {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package dev.shtanko.concurrency.coroutines.examples
2+
3+
import java.awt.Color
4+
import java.awt.image.BufferedImage
5+
import org.junit.jupiter.api.Assertions.assertEquals
6+
import org.junit.jupiter.api.Assertions.assertTrue
7+
import org.junit.jupiter.api.Test
8+
9+
private const val MAX_COLOR = 255
10+
11+
class AdvancedImageProcessingTest {
12+
private fun createSolidColorImage(width: Int, height: Int, color: Color): BufferedImage {
13+
return BufferedImage(width, height, BufferedImage.TYPE_INT_RGB).apply {
14+
for (x in 0 until width) {
15+
for (y in 0 until height) {
16+
setRGB(x, y, color.rgb)
17+
}
18+
}
19+
}
20+
}
21+
22+
private fun imagesAreEqual(img1: BufferedImage, img2: BufferedImage): Boolean {
23+
if (img1.width != img2.width || img1.height != img2.height) return false
24+
for (x in 0 until img1.width) {
25+
for (y in 0 until img1.height) {
26+
if (img1.getRGB(x, y) != img2.getRGB(x, y)) return false
27+
}
28+
}
29+
return true
30+
}
31+
32+
@Test
33+
fun `applyRegionSpecificFilter inverts colors imagesAreEqual`() {
34+
val original = createSolidColorImage(5, 5, Color(50, 100, 150))
35+
val expected = createSolidColorImage(5, 5, Color(205, 155, 105)) // 255 - original colors
36+
val inverted = applyRegionSpecificFilter(original)
37+
38+
assertTrue(imagesAreEqual(expected, inverted))
39+
}
40+
41+
@Test
42+
fun `detectEdges returns black image for uniform color`() {
43+
val image = createSolidColorImage(10, 10, Color(100, 100, 100))
44+
val edges = detectEdges(image)
45+
46+
for (x in 1 until edges.width - 1) {
47+
for (y in 1 until edges.height - 1) {
48+
val rgb = Color(edges.getRGB(x, y))
49+
assertTrue(rgb.red < 10, "Expected low edge intensity, got ${rgb.red}")
50+
assertEquals(rgb.red, rgb.green)
51+
assertEquals(rgb.green, rgb.blue)
52+
}
53+
}
54+
}
55+
56+
@Test
57+
fun `applyRegionSpecificFilter inverts colors`() {
58+
val original = createSolidColorImage(5, 5, Color(50, 100, 150))
59+
val inverted = applyRegionSpecificFilter(original)
60+
61+
for (x in 0 until original.width) {
62+
for (y in 0 until original.height) {
63+
val originalColor = Color(original.getRGB(x, y))
64+
val invertedColor = Color(inverted.getRGB(x, y))
65+
66+
assertEquals(MAX_COLOR - originalColor.red, invertedColor.red)
67+
assertEquals(MAX_COLOR - originalColor.green, invertedColor.green)
68+
assertEquals(MAX_COLOR - originalColor.blue, invertedColor.blue)
69+
}
70+
}
71+
}
72+
73+
@Test
74+
fun `blendSegment averages colors of two images`() {
75+
val img1 = createSolidColorImage(4, 4, Color(100, 150, 200))
76+
val img2 = createSolidColorImage(4, 4, Color(50, 50, 50))
77+
78+
val blended = blendSegment(img1, img2)
79+
for (x in 0 until blended.width) {
80+
for (y in 0 until blended.height) {
81+
val c = Color(blended.getRGB(x, y))
82+
assertEquals((100 + 50) / 2, c.red)
83+
assertEquals((150 + 50) / 2, c.green)
84+
assertEquals((200 + 50) / 2, c.blue)
85+
}
86+
}
87+
}
88+
89+
@Test
90+
fun `segmentImage splits image into correct number of segments`() {
91+
val image = createSolidColorImage(10, 10, Color.BLACK)
92+
val segments = segmentImage(image, 3)
93+
assertEquals(3, segments.size)
94+
val totalHeight = segments.sumOf { it.height }
95+
assertEquals(image.height, totalHeight)
96+
segments.forEach {
97+
assertEquals(image.width, it.width)
98+
}
99+
}
100+
101+
@Test
102+
fun `mergeSegments recombines images vertically`() {
103+
val img1 = createSolidColorImage(4, 2, Color.RED)
104+
val img2 = createSolidColorImage(4, 3, Color.GREEN)
105+
val merged = mergeSegments(listOf(img1, img2), 4, 5)
106+
107+
assertEquals(4, merged.width)
108+
assertEquals(5, merged.height)
109+
110+
for (x in 0 until 4) {
111+
for (y in 0 until 2) {
112+
assertEquals(Color.RED.rgb, merged.getRGB(x, y))
113+
}
114+
}
115+
for (x in 0 until 4) {
116+
for (y in 2 until 5) {
117+
assertEquals(Color.GREEN.rgb, merged.getRGB(x, y))
118+
}
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)