view app/src/main/java/com/bartsent/simpleresizer/lib/Resizer.kt @ 13:b1605be35bcc memo.oo

Dumping Bitmap yields 2x improvement!
author David Barts <n5jrn@me.com>
date Thu, 18 Feb 2021 14:15:26 -0800
parents
children 86740f593b6c
line wrap: on
line source

package com.bartsent.simpleresizer.lib

import android.graphics.Bitmap
import android.graphics.Color
import kotlin.math.ceil
import kotlin.math.floor

/**
 * Bitmapped images designed for efficient resampling and per-pixel access via
 * subscripting.
 */
class Resizer(private val bits: IntArray, val width: Int, val height: Int) {
    constructor(width: Int, height: Int) : this(IntArray(width * height), width, height)

    companion object {
        /**
         *  Make a Resizer from a standard Bitmap.
         *  @param      image       Bitmap object.
         *  @return                 A new Resizer.
         */
        fun fromBitmap(image: Bitmap): Resizer {
            val basedOn = IntArray(image.width * image.height)
            image.getPixels(basedOn, 0, image.width, 0, 0, image.width, image.height)
            return Resizer(basedOn, image.width, image.height)
        }
    }

    private interface Band {
        operator fun get(i: Int): Int
        operator fun set(i: Int, v: Int): Unit
        val index: Int
        val size: Int
    }

    private class Row(val basedOn: Resizer, override val index: Int): Band {
        override operator fun get(i: Int) = basedOn[i, index]
        override operator fun set(i: Int, v: Int): Unit {
            basedOn[i, index] = v
        }
        override val size; get() = basedOn.width
    }

    private class Column(val basedOn: Resizer, override val index: Int): Band {
        override operator fun get(i: Int) = basedOn[index, i]
        override operator fun set(i: Int, v: Int): Unit {
            basedOn[index, i] = v
        }
        override val size; get() = basedOn.height
    }

    private data class IndexWeight(var index: Int, var weight: Double)

    operator fun get(x: Int, y: Int): Int = bits[x + y*width]

    operator fun set(x: Int, y: Int, v: Int): Unit {
        bits[x + y*width] = v
    }

    /**
     * Return a standard Bitmap based on this Resizer's image.
     * @return                 A new Bitmap, which may share this Resizer's image storage.
     */
    fun toBitmap(): Bitmap = Bitmap.createBitmap(bits, width, height, Bitmap.Config.ARGB_8888)

    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: Band, i: Int, weights: Array<IndexWeight>, source: Band): Unit {
        var r = 0.0; var g = 0.0; var b = 0.0; var a = 0.0
        weights.forEach {
            val c = source[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[i] = Color.argb(clamp(a), clamp(r/a), clamp(g/a), clamp(b/a))
    }

    private fun resampleBand(target: Band, weights: Array<Array<IndexWeight>>, source: Band) {
        for (i in 0 until target.size) {
            resample(target, i, weights[i], source)
        }
    }

    private val DEFAULT_KERNEL = LanczosKernel()

    /**
     * Resize the horizontal aspect.
     * @param       newWidth    Width of new image
     * @param       kernel      Scaling kernel to use (default: LanczosKernel)
     * @return                  A new Resizer object, resampled per specifications
     */
    fun horizontal(newWidth: Int, kernel: ScalingKernel = DEFAULT_KERNEL): Resizer {
        val dst = Resizer(newWidth, height)
        val weights = precomputeWeights(newWidth, width, kernel)
        for (y in 0 until height) {
            resampleBand(Row(dst, y), weights, Row(this, y))
        }
        return dst
    }

    /**
     * Resize the vertical aspect.
     * @param       newHeight   Height of new image
     * @param       kernel      Scaling kernel to use (default: LanczosKernel)
     * @return                  A new Resizer object, resampled per specifications
     */
    fun vertical(newHeight: Int, kernel: ScalingKernel = DEFAULT_KERNEL): Resizer {
        val dst = Resizer(width, newHeight)
        val weights = precomputeWeights(newHeight, height, kernel)
        for (x in 0 until width) {
            resampleBand(Column(dst, x), weights, Column(this, x))
        }
        return dst
    }
}