Skip to content

Commit 02a07f6

Browse files
committed
New module for postprocessing
1 parent 93f4c3c commit 02a07f6

File tree

12 files changed

+165
-9
lines changed

12 files changed

+165
-9
lines changed

.idea/compiler.xml

Lines changed: 0 additions & 8 deletions
This file was deleted.

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44
alias(libs.plugins.kotlin.android) apply false
55
alias(libs.plugins.kotlin.compose) apply false
66
alias(libs.plugins.license)
7+
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
78
}
89

910
license {

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ litert = "1.2.0"
1414
opencv = "4.11.0"
1515
assertj = "3.27.3"
1616
pdfbox = "2.0.27.0"
17+
jetbrainsKotlinJvm = "2.1.0"
1718

1819
[libraries]
1920
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -51,4 +52,5 @@ android-application = { id = "com.android.application", version.ref = "agp" }
5152
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
5253
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
5354
license = { id = "com.github.hierynomus.license", version.ref = "license" }
55+
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
5456

postprocessing/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

postprocessing/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
plugins {
2+
id("java-library")
3+
alias(libs.plugins.jetbrains.kotlin.jvm)
4+
}
5+
java {
6+
sourceCompatibility = JavaVersion.VERSION_11
7+
targetCompatibility = JavaVersion.VERSION_11
8+
}
9+
kotlin {
10+
compilerOptions {
11+
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
12+
}
13+
}
14+
dependencies {
15+
testImplementation(libs.junit)
16+
testImplementation(libs.assertj)
17+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.mydomain.postprocessing
2+
3+
import java.awt.image.BufferedImage
4+
5+
data class RawImage(
6+
val width: Int,
7+
val height: Int,
8+
val pixels: IntArray // each Int represents a pixel as 0xAARRGGBB
9+
) {
10+
11+
init {
12+
require(pixels.size == width * height) { "Pixel array size does not match dimensions" }
13+
}
14+
15+
companion object {
16+
fun fromBufferedImage(image: BufferedImage): RawImage {
17+
val width = image.width
18+
val height = image.height
19+
val pixels = IntArray(width * height)
20+
for (y in 0 until height) {
21+
for (x in 0 until width) {
22+
val rgb = image.getRGB(x, y)
23+
pixels[y * width + x] = rgb
24+
}
25+
}
26+
return RawImage(width, height, pixels)
27+
}
28+
}
29+
30+
fun toBufferedImage(): BufferedImage {
31+
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
32+
for (y in 0 until height) {
33+
for (x in 0 until width) {
34+
val index = y * width + x
35+
image.setRGB(x, y, pixels[index])
36+
}
37+
}
38+
return image
39+
}
40+
41+
fun toGrayscale(): RawImage {
42+
val grayPixels = IntArray(width * height)
43+
for (i in pixels.indices) {
44+
val rgb = pixels[i]
45+
val r = (rgb shr 16) and 0xFF
46+
val g = (rgb shr 8) and 0xFF
47+
val b = rgb and 0xFF
48+
49+
// See https://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
50+
val gray = (0.3 * r + 0.59 * g + 0.11 * b).toInt().coerceIn(0, 255)
51+
52+
grayPixels[i] = (gray shl 16) or (gray shl 8) or gray
53+
}
54+
return RawImage(width, height, grayPixels)
55+
}
56+
57+
fun isColorImage(saturationThreshold: Double = 0.1): Boolean {
58+
var totalSaturation = 0.0
59+
for (rgb in pixels) {
60+
val r = (rgb shr 16) and 0xFF
61+
val g = (rgb shr 8) and 0xFF
62+
val b = rgb and 0xFF
63+
64+
val max = maxOf(r, g, b) / 255.0
65+
val min = minOf(r, g, b) / 255.0
66+
67+
val saturation = if (max == 0.0) 0.0 else (max - min) / max
68+
totalSaturation += saturation
69+
}
70+
71+
val avgSaturation = totalSaturation / pixels.size
72+
println(avgSaturation)
73+
return avgSaturation > saturationThreshold
74+
}
75+
}
76+
77+
fun postProcessDocument(original: RawImage): RawImage {
78+
return if (original.isColorImage()) original else original.toGrayscale()
79+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package org.mydomain.postprocessing
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.assertj.core.api.Assertions.assertThatThrownBy
5+
import org.junit.Test
6+
import java.io.File
7+
import javax.imageio.ImageIO
8+
9+
class PostProcessingTest {
10+
11+
@Test
12+
fun `basic call to RawImage constructor`() {
13+
val image = RawImage(3, 2, IntArray(6))
14+
assertThat(image.width).isEqualTo(3)
15+
assertThat(image.height).isEqualTo(2)
16+
}
17+
18+
@Test
19+
fun `RawImage constructor should detect inconsistency in dimensions`() {
20+
assertThatThrownBy { RawImage(3, 2, IntArray(5)) }
21+
.isInstanceOf(IllegalArgumentException::class.java)
22+
}
23+
24+
@Test
25+
fun grayscale() {
26+
val original = RawImage(1, 1, intArrayOf(0x00102030))
27+
val grayscale = original.toGrayscale()
28+
assertThat(grayscale.pixels).hasSize(1)
29+
assertThat(grayscale.pixels[0].toString(16)).isEqualTo("1c1c1c")
30+
// grayscale conversion formula (applied to 0x00102030)
31+
assertThat((0.3*1*16 + 0.59*2*16 + 0.11*3*16).toInt().toString(16)).isEqualTo("1c")
32+
}
33+
34+
@Test
35+
fun `detect color image`() {
36+
val pink = 0x00FF00FF
37+
val gray = 0x00505050
38+
39+
val grayImage = RawImage(1, 1, intArrayOf(gray))
40+
val pinkImage = RawImage(1, 1, intArrayOf(pink))
41+
42+
assertThat(grayImage.isColorImage()).isFalse()
43+
assertThat(pinkImage.isColorImage()).isTrue()
44+
}
45+
46+
@Test
47+
fun `run post processing on sample files`() {
48+
val inputDir = File("src/test/resources/cropped")
49+
val outputDir = File("build/processed_images")
50+
outputDir.mkdirs()
51+
outputDir.listFiles()?.forEach { f -> f.delete() }
52+
val inputFiles = inputDir.listFiles()
53+
assertThat(inputFiles).isNotNull.isNotEmpty
54+
inputFiles!!.forEach { inputFile ->
55+
println(inputFile)
56+
val inputImage = RawImage.fromBufferedImage(ImageIO.read(inputFile))
57+
val outputImage = postProcessDocument(inputImage)
58+
val outputFile = File(outputDir, inputFile.name)
59+
ImageIO.write(outputImage.toBufferedImage(), "jpg", outputFile)
60+
}
61+
}
62+
63+
}
477 KB
Loading
835 KB
Loading

0 commit comments

Comments
 (0)