Skip to content

Instantly share code, notes, and snippets.

@kibotu
Last active September 24, 2020 06:35
Show Gist options
  • Save kibotu/0f842ee2c1b2524747ce6529f48b6f42 to your computer and use it in GitHub Desktop.
Save kibotu/0f842ee2c1b2524747ce6529f48b6f42 to your computer and use it in GitHub Desktop.
DeFish

Basic Algorythm

/**
 * 
    input:
        strength as floating point >= 0.  0 = no change, high numbers equal stronger correction.
        zoom as floating point >= 1.  (1 = no change in zoom)

    algorithm:

        set halfWidth = imageWidth / 2
        set halfHeight = imageHeight / 2

        if strength = 0 then strength = 0.00001
        set correctionRadius = squareroot(imageWidth ^ 2 + imageHeight ^ 2) / strength

        for each pixel (x,y) in destinationImage
            set newX = x - halfWidth
            set newY = y - halfHeight

            set distance = squareroot(newX ^ 2 + newY ^ 2)
            set r = distance / correctionRadius

            if r = 0 then
                set theta = 1
            else
                set theta = arctangent(r) / r

            set sourceX = halfWidth + theta * newX * zoom
            set sourceY = halfHeight + theta * newY * zoom

            set color of pixel (x, y) to color of source image pixel at (sourceX, sourceY)
 */

in kotlin

fun removeFishEye(bitmap: Bitmap, strength: Double): Bitmap {

    Log.v("Defishing", "start...")
    val start = System.currentTimeMillis()

    val conf = Bitmap.Config.ARGB_8888 // see other conf types
    val correctedImage = Bitmap.createBitmap(bitmap.width, bitmap.height, conf) // this creates a MUTABLE bitmap

    //The center points of the image
    val xc = bitmap.width / 2.0
    val yc = bitmap.height / 2.0

    var s = strength
    if (strength == 0.0)
        s = 0.00001

    val correctionRadius = Math.sqrt((correctedImage.width * correctedImage.width + correctedImage.height * correctedImage.height).toDouble()) / s

    var theta: Double
    val zoom = 1.0

    val src = DrawableBitmapContainer(bitmap)
    val dst = DrawableBitmapContainer(correctedImage)

    for (x in 0 until correctedImage.width) {
        for (y in 0 until correctedImage.height) {

            val newX = x - xc
            val newY = y - yc

            val distance = Math.sqrt(newX * newX + newY * newY)
            val r = distance / correctionRadius

            theta = if (r == 0.0)
                1.0
            else
                Math.atan(r) / r

            val sourceX = xc + theta * newX * zoom
            val sourceY = yc + theta * newY * zoom

            val xd = Math.max(0.0, Math.min(sourceX, (correctedImage.width - 1).toDouble())).toInt()
            val yd = Math.max(0.0, Math.min(sourceY, (correctedImage.height - 1).toDouble())).toInt()
            dst.setPixel(x, y, src.getPixel(xd, yd))
        }
    }

    Log.v("Defishing", "done after " + (System.currentTimeMillis() - start) + " ms")

    return dst.bimap
}

class DrawableBitmapContainer(private val image: Bitmap) {

    private val width: Int = image.width

    private val height: Int = image.height

    private val pixels: IntArray

    val bimap: Bitmap
        get() {
            image.setPixels(pixels, 0, width, 0, 0, width, height)
            return image
        }

    init {
        pixels = IntArray(width * height)
        image.getPixels(pixels, 0, width, 0, 0, width, height)
    }

    fun getPixel(x: Int, y: Int): Int {
        return pixels[x + y * width]
    }

    fun setPixel(x: Int, y: Int, color: Int) {
        pixels[x + y * width] = color
    }
}

imageView.setImageBitmap(RemoveFishEye(bitmap, 3.5))

with shader

attribute vec4 position;
attribute vec2 texcoord;
varying vec2 texcoordVarying;

void main() {
    gl_Position = position;
    texcoordVarying = texcoord;
}
#ifdef GL_ES
precision highp float;
#endif

varying vec2 texcoordVarying;  // position received from vertex shader
uniform sampler2D texture;

uniform float time;
uniform vec2 textureSize;
uniform float alpha;
uniform float strength;
uniform float zoom;

void main(void) {

    // correction radius
    float cr = sqrt(textureSize.x * textureSize.x + textureSize.y * textureSize.y) / strength;

    // half width
    float hW = textureSize.x / 2.0;

    // half height
    float hH = textureSize.y / 2.0;

    // translate uv by half resolution
    vec2 translatedUv = vec2(
        texcoordVarying.x * textureSize.x - hW,
        texcoordVarying.y * textureSize.y -  hH
    );

    // distance
    float dis = sqrt((translatedUv.x) *
                     (translatedUv.x)
		            +(translatedUv.y) *
                     (translatedUv.y));

    // distance ratio to radius
    float r = dis / cr;

    // avoid null pointer
    float theta = r == 0.0
        ? 1.0
        // arc tangens by corrected distance
        : atan(r) / r;

    // get source coordinate by distance correction and apply zooming
    float sX = hW + theta * translatedUv.x * zoom;
    float sY = hH + theta * translatedUv.y * zoom;

    // use new uv coordinates
    vec2 newUv = vec2(sX /textureSize.x , sY / textureSize.y);
    vec4 color = texture2D(texture, newUv);

    gl_FragColor = vec4(color.r, color.g, color.b, alpha);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment