Убрать эффект рыбьего глаза

1,00
р.
Есть видео с action-камеры, которая при записи применяет эффект типа рыбьего глаза. Область, которая должна быть прямоугольной, имеет несколько скруглённую форму - в сниппете она отмечена красным. Хотелось бы растянуть её обратно до синего прямоугольника. Я могу нарисовать 2 вертикальные или даже все 4 линии, по которым можно определить искажение. Как выполнить трансформацию?


html, body, svg { height: 100% margin: auto display: block }

Если я правильно понимаю, то можно использовать фильтр lenscorrection в ffmpeg, но я не знаю, как подобрать параметры. Попробовал взять один кадр и поиграться с ним в Gimp'е, но из этого ничего не вышло.
В принципе, меня устраивает алгоритм, а точнее формула, по которой для каждого пикселя нового изображения можно вычислить координаты в исходном. При этом входными данными являются кривые безье, которые я могу нарисовать.
Гарантируется, что рамка будет иметь именно такой вид: 4 ломаных точки с направляющими (в идеале обойтись только двумя вертикальными линиями):

Другой пример:


html, body, svg { height: 100% margin: auto display: block }

Ответом на вопрос может быть любой вариант из следующих:
Готовая программа, которая выполняет задачу для видео или кадра. Способ подобрать корректные параметры для ffmpeg. Формула для вычисления старых координат пикселя по координатом на изображении-результате. Другое представление графической трансформации. ...

Ответ
Это задача может быть решена при помощи трансформации текстурных координат во фрагментном шейдере.


let inputs = ['fisheye:321', 'cX:495', 'cY:334', 'rY:258', 'rZ:562', 'zoom:581'] let input = (id, val) => ` ` inputs.forEach(i => inp.innerHTML += input(...i.split(':'))) let gl = canvas.getContext('webgl') let loader = new Image() loader.crossOrigin = "anonymous" loader.src = "https://i.imgur.com/G9H683l.jpg" loader.onload = function() { canvas.width = loader.width canvas.height = loader.height pid = gl.createProgram() shader(` float perspective = 1.0 attribute vec2 coords uniform float rY varying vec2 uv void main(void) { mat3 rotY = mat3(vec3(cos(rY), 0.0, sin(rY)), vec3(0.0, 1.0, 0.0), vec3(-sin(rY), 0.0, cos(rY))) vec3 p = vec3(coords.xy, 0.) * rotY uv = coords.xy*0.5 + 0.5 gl_Position = vec4(p, 1.0 + p.z * perspective) } `, gl.VERTEX_SHADER) shader(` precision highp float const vec2 res = vec2(${canvas.width}., ${canvas.height}.) varying vec2 uv uniform float fisheye uniform float cX uniform float cY uniform float rZ uniform float zoom uniform sampler2D texture // http:/tackoverflow.com/questions/6030814 void main(void) { float prop = res.x / res.y vec2 center = vec2(cX, cY) vec2 p = vec2(uv.x,uv.y/prop) vec2 m = vec2(0.5, 0.5 / prop) vec2 d = p - m float r = sqrt(dot(d, d)) float power = (2.0 * 3.141592 / (2.0 * sqrt(dot(m, m)))) * fisheye float bind if (power > 0.0) { bind = sqrt(dot(m, m)) } else { if (prop < 1.0) bind = m.x else bind = m.y } vec2 uv = p if (power > 0.0) uv = m + normalize(d) * tan(r * power) * bind / tan( bind * power) else if (power < 0.0) uv = m + normalize(d) * atan(r * -power * 10.0) * bind / atan(-power * bind * 10.0) uv -= vec2(0.5, 0.5/prop) vec2 sc = vec2(sin(rZ), cos(rZ)) uv *= mat2(sc.y, -sc.x, sc.xy) uv *= zoom+1. uv -= center uv += vec2(0.5, 0.5/prop) uv = vec2(uv.x, 1.-uv.y * prop) gl_FragColor = texture2D(texture, uv) } `, gl.FRAGMENT_SHADER) gl.linkProgram(pid) gl.useProgram(pid) gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), gl.STATIC_DRAW) let al = gl.getAttribLocation(pid, "coords") gl.vertexAttribPointer(al, 2, gl.FLOAT, false, 0, 0) gl.enableVertexAttribArray(al) let texture = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, texture) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, loader) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.uniform1i(gl.getUniformLocation(pid, "texture"), 0) inputs = inputs.map(i => document.querySelector('#' + i.split(':')[0])) inputs.forEach(i => i.uniform = gl.getUniformLocation(pid, i.id)) draw() } function draw() { inputs.forEach(i => { gl.uniform1f(i.uniform, i.value/5000-0.1) document.querySelector(`label[for="${i.id}"]`) .textContent = `${i.value} ${i.id}: ${(i.value/5000-0.1).toFixed(4)}` }) gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) gl.clearColor(0, 0, 0, 0) gl.drawArrays(gl.TRIANGLES, 0, 6) } function shader(src, type) { let sid = gl.createShader(type) gl.shaderSource(sid, src) gl.compileShader(sid) var message = gl.getShaderInfoLog(sid) gl.attachShader(pid, sid) if (message.length > 0) { console.log(src.split('
').map(function (str, i) { return ("" + (1 + i)).padStart(4, "0") + ": " + str }).join('
')) throw message } } input{width:calc(100% - 190px)} label{display:inline-block width:180px}


PS: после недолгих исследований удалось собрать ffmpeg, прикрутив к нему данный шейдер, подробности тут
код на github

Результат (не сильно старался с коэффициентами)
До

После

UPD: изменения в сниппете