changeset 6:e8059b166de1

Lanczos works, but is painfully slow.
author David Barts <n5jrn@me.com>
date Tue, 16 Feb 2021 17:29:52 -0800
parents 247e03baf77c
children 9374d044a132 6ae738b8a814 73beb7c973ae
files app/src/main/java/com/bartsent/simpleresizer/EditImage.kt app/src/main/java/com/bartsent/simpleresizer/lib/LanczosKernel.kt app/src/main/java/com/bartsent/simpleresizer/lib/ScalingKernel.kt app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt
diffstat 4 files changed, 145 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Wed Feb 10 16:31:01 2021 -0800
+++ b/app/src/main/java/com/bartsent/simpleresizer/EditImage.kt	Tue Feb 16 17:29:52 2021 -0800
@@ -17,6 +17,7 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.widget.PopupMenu
 import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding
+import com.bartsent.simpleresizer.lib.getScaledInstance
 import java.io.File
 import java.io.IOException
 import kotlin.math.roundToInt
@@ -135,17 +136,15 @@
 
     fun doScale(newMax: Int): Unit {
         val oldBitmap = State.bitmap!!
-        val factor = newMax.toFloat() / maxOf(oldBitmap.width, oldBitmap.height).toFloat()
+        val factor = newMax.toDouble() / maxOf(oldBitmap.width, oldBitmap.height).toDouble()
         if (factor >= 1.0) {
             throw IllegalArgumentException("can only scale down")
         }
         val newBitmap = Bitmap.createBitmap((oldBitmap.width * factor).roundToInt(), (oldBitmap.height * factor).roundToInt(), oldBitmap.config)
         copyColorSpace(oldBitmap, newBitmap)
-        val scaler = Matrix().apply { setScale(factor, factor) }
-        Canvas(newBitmap).run {
-            drawBitmap(oldBitmap, scaler, null)
-        }
-        setImage(newBitmap)
+        setImage(oldBitmap.getScaledInstance(
+                (oldBitmap.width.toDouble() * factor + 0.5).toInt(),
+                (oldBitmap.height.toDouble() * factor + 0.5).toInt()))
     }
 
     // is there any way to remember the last scale value?
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/LanczosKernel.kt	Tue Feb 16 17:29:52 2021 -0800
@@ -0,0 +1,19 @@
+package com.bartsent.simpleresizer.lib
+
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.sin
+
+object LanczosKernel: ScalingKernel {
+    override val size = 3.0
+
+    private fun sinc(x: Double): Double {
+        if (x == 0.0)
+            return 1.0
+        val pix = PI * x
+        return sin(pix) / pix
+    }
+
+    override fun weight(x: Double): Double =
+        if (abs(x) < size) sinc(x) * sinc(x/size) else 0.0
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/ScalingKernel.kt	Tue Feb 16 17:29:52 2021 -0800
@@ -0,0 +1,8 @@
+package com.bartsent.simpleresizer.lib
+
+import android.graphics.Bitmap
+
+interface ScalingKernel {
+    val size: Double
+    fun weight(x: Double): Double
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/src/main/java/com/bartsent/simpleresizer/lib/getScaledInstance.kt	Tue Feb 16 17:29:52 2021 -0800
@@ -0,0 +1,113 @@
+package com.bartsent.simpleresizer.lib
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Matrix
+import kotlin.math.ceil
+import kotlin.math.floor
+
+private data class IndexWeight(var index: Int, var weight: Double)
+
+fun Bitmap.getScaledInstance(newWidth: Int, newHeight: Int, kernel: ScalingKernel = LanczosKernel): Bitmap {
+    if (newWidth <= 0)
+        throw IllegalArgumentException("invalid width: $newWidth")
+    if (newHeight <= 0)
+        throw IllegalArgumentException("invalid height: $newHeight")
+    if (width == newWidth && height == newHeight)
+        return Bitmap.createBitmap(this)
+    val input = if (config == Bitmap.Config.ARGB_8888)
+        this
+    else {
+        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
+            Canvas(it).drawBitmap(this, Matrix(), null)
+        }
+    }
+    if (width != newWidth) {
+        if (height != newHeight)
+            return resizeVertical(resizeHorizontal(input, newWidth, kernel), newHeight, kernel)
+        else
+            return resizeHorizontal(input, newWidth, kernel)
+    } else {
+        return resizeVertical(input, newHeight, kernel)
+    }
+}
+
+private fun precomputeWeights(dstSize: Int, srcSize: Int, filter: ScalingKernel): Array<Array<IndexWeight>> {
+    val du = srcSize.toDouble() / dstSize.toDouble()
+    val scale = maxOf(1.0, du)
+    val ru = ceil(scale * filter.size)
+    val TEMPLATE = arrayOf<IndexWeight>()
+    val out = Array<Array<IndexWeight>>(dstSize) { TEMPLATE }
+    val tmp = ArrayList<IndexWeight>((ru.toInt()+2)*2)
+    val emax = srcSize - 1
+
+    for (v in 0 until dstSize) {
+        val fu = (v.toDouble()+0.5)*du - 0.5
+        val begin = maxOf(0, ceil(fu - ru).toInt())
+        val end = minOf(emax, floor(fu + ru).toInt())
+        var sum: Double = 0.0
+        for (u in begin..end) {
+            val w = filter.weight((u.toDouble() - fu) / scale)
+            if (w != 0.0) {
+                sum += w
+                tmp.add(IndexWeight(u, w))
+            }
+        }
+        if (sum != 0.0) {
+            tmp.forEach {
+                it.weight /= sum
+            }
+        }
+        out[v] = tmp.toArray(TEMPLATE)
+        tmp.clear()
+    }
+    return out
+}
+
+private fun clamp(v: Double): Int = minOf(255, maxOf(0, (v + 0.5).toInt()))
+
+private fun resample(target: Bitmap, x: Int, y: Int, weights: Array<IndexWeight>, scanLine: IntArray): Unit {
+    var r = 0.0; var g = 0.0; var b = 0.0; var a = 0.0
+    weights.forEach {
+        val c = scanLine[it.index]
+        val aw = Color.alpha(c).toDouble() * it.weight
+        r += Color.red(c).toDouble() * aw
+        g += Color.green(c).toDouble() * aw
+        b += Color.blue(c).toDouble() * aw
+        a += aw
+    }
+    if (a == 0.0)
+        return
+    target.setPixel(x, y, Color.argb(clamp(a), clamp(r/a), clamp(g/a), clamp(b/a)))
+}
+
+private fun resizeHorizontal(image: Bitmap, newWidth: Int, kernel: ScalingKernel): Bitmap {
+    val dst = Bitmap.createBitmap(newWidth, image.height, Bitmap.Config.ARGB_8888)
+    val weights = precomputeWeights(newWidth, image.width, kernel)
+    val scanLine = IntArray(image.width)
+    for (y in 0 until image.height) {
+        for (x in 0 until image.width) {
+            scanLine[x] = image.getPixel(x, y)
+        }
+        for (x in weights.indices) {
+            resample(dst, x, y, weights[x], scanLine)
+        }
+    }
+    return dst
+}
+
+private fun resizeVertical(image: Bitmap, newHeight: Int, kernel: ScalingKernel): Bitmap {
+    val dst = Bitmap.createBitmap(image.width, newHeight, Bitmap.Config.ARGB_8888)
+    val weights = precomputeWeights(newHeight, image.height, kernel)
+    val scanLine = IntArray(image.height)
+    for (x in 0 until image.width) {
+        for (y in 0 until image.height) {
+            scanLine[y] = image.getPixel(x, y)
+        }
+        for (y in weights.indices) {
+            resample(dst, x, y, weights[y], scanLine)
+        }
+    }
+    return dst
+}