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);
}