0

I wish to modify an image by moving columns of pixels up or down such that each column offset follows a curve.

I wish for the curve to intersect 6 or so points in some smooth way. I Imagine looping over image x co-ordinates and calling a curve function that returns the y co-ordinate for the curve at that offset, thus telling me how much to move each column of pixels.

I have investigated various types of curves but frankly I am a bit lost, I was hoping there would be a ready made solution that would allow me to plug in my point co-ords and spit out the data that I need. I'm not too fussed what kind of curve is used, as long as it looks "smooth".

Can anyone help me with this?

I am using HTML5 and canvas. The answer given here looks like the sort of thing I am after, but it refers to an R library (I guess) which is Greek to me!

Testic
  • 157
  • 1
  • 1
  • 9
  • *(Sidenote, [tag:r] is not a library, it's a language. Usually used in scientific or statistics related environments.)* What I don't understand about your request is, wether the curve should connect the points in a smooth way, or wether the points/columns should be modified to fit the curve. Basically, how is the curve defined? – Thomas Nov 22 '17 at 18:47
  • The curve is defined simply by the points, the columns are then moved vertically to fit the curve. – Testic Nov 22 '17 at 18:53
  • https://stackoverflow.com/a/15528789/1693593 –  Nov 22 '17 at 21:10
  • Thanks K3N, I'm probably going to go with something like that. Seems seems there isn't a way to step along the curve pixel-by-pixel, rather I will have to guess values for t (the distance along the curve) until I get each pixel y value. – Testic Nov 22 '17 at 23:10
  • If one of the axis is constant, say x, you can use the cardinal spline and use a segment number that corresponds to each pixel. That way you'll get a single point based on the x position (or very close to) representing the y. There is btw an updated version of the spline [here](https://github.com/epistemex/cardinal-spline-js). An alternative could perhaps be to traverse each segment and find if there is an [intersection](https://stackoverflow.com/a/37143217/1693593) point with that segment. –  Nov 22 '17 at 23:53

1 Answers1

0

Sigmoid curve

A very simple solution if you only want the curve in the y direction is to use a sigmoid curve to interpolate the y pos between control points

// where 0 <= x <= 1 and p > 1
// return value between 0 and 1 inclusive.
// p is power and determines the amount of curve
function sigmoidCurve(x, p){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    var xx = Math.pow(x, p);
    return xx / (xx + Math.pow(1 - x, p))    
}

If you want the y pos at x coordinate px that is between two control points x1,y1 and x2, y2

First find the normalized position of px between x1,x2

var nx = (px - x1) / (x2 - x1); // normalised dist between points

Plug the value into sigmoidCurve

var c = sigmoidCurve(nx, 2); // curve power 2

The use that value to calculate y

var py = (y2 - y1) * c + y1;

And you have a point on the curve between the points.

As a single expression

var py = (y2 - y1) *sigmoidCurve((px - x1) / (x2 - x1), 2) + y1;

If you set the power for the sigmoid curve to 1.5 then it is almost a perfect match for a cubic bezier curve

Example

This example shows the curve animated. The function getPointOnCurve will get the y pos of any point on the curve at position x

const ctx = canvas.getContext("2d");
const curve = [[10, 0], [120, 100], [200, 50], [290, 150]];
const pos = {};
function  cubicInterpolation(x, p0, p1, p2, p3){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));   
}
function sigmoidCurve(x, p){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
 var xx = Math.pow(x, p);
 return xx / (xx + Math.pow(1 - x, p))    
}
// functional for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
// functional iterator 
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };


// find index for x in curve
// returns pos{ index, y }
// if x is at a control point then return the y value and index set to -1
// if not at control point the index is of the point befor x
function getPosOnCurve(x,curve, pos = {}){
  var len = curve.length;
  var i;
  pos.index = -1;
  pos.y = null;
  if(x <= curve[0][0]) { return (pos.y = curve[0][1], pos) }
  if(x >= curve[len - 1][0]) { return  (pos.y = curve[len - 1][1], pos) }
  i = 0;
  var found = false;
  while(!found){  // some JS optimisers will mark "Do not optimise" 
                  // code that do not have an exit clause.
    if(curve[i++][0] <x && curve[i][0] >= x) { break }
  }
  i -= 1;
  if(x === curve[i][0]) { return (pos.y = curve[i][1], pos) }
  pos.index =i
  return pos;
}
// Using Cubic interpolation to create the curve
function getPointOnCubicCurve(x, curve, power){
  getPosOnCurve(x, curve, pos);
  if(pos.index === -1) { return pos.y };
  var i = pos.index;
  // get interpolation values for points around x
  var p0,p1,p2,p3;
  p1 = curve[i][1];
  p2 = curve[i+1][1];
  p0 = i === 0 ? p1 : curve[i-1][1];
  p3 = i === curve.length - 2 ? p2 : curve[i+2][1];
  // get unit distance of x between curve i, i+1
  var ux = (x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]);
  return cubicInterpolation(ux, p0, p1, p2, p3);
}




// Using Sigmoid function to get curve.
// power changes curve power = 1 is line power > 1 tangents become longer
// With the power set to 1.5 this is almost a perfect match for
// cubic bezier solution.
function getPointOnCurve(x, curve, power){
  getPosOnCurve(x, curve, pos);
  if(pos.index === -1) { return pos.y };
  var i = pos.index;
  var p = sigmoidCurve((x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]) ,power);
  return curve[i][1] + (curve[i + 1][1] - curve[i][1]) * p;
}

const step = 2;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center width and height
var ch = h / 2;
function update(timer){
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
  ctx.clearRect(0,0,w,h);
    eachOf(curve, (point) => {
      point[1] = Math.sin(timer / (((point[0] + 10) % 71) * 100) ) * ch * 0.8 + ch;
    });
    
    ctx.strokeStyle = "black";
    ctx.beginPath();
    doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCurve(x * step, curve, 1.5) - 10)});
    ctx.stroke();
    ctx.strokeStyle = "blue";
    ctx.beginPath();
    doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCubicCurve(x * step, curve, 1.5) + 10)});
    ctx.stroke();    

    
    ctx.strokeStyle = "black";
    eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 - 10, 4, 4) );
    eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 + 10, 4, 4) );
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>

Update

I have added a second curve type to the above demo as the blue curve offset from the original sigmoid curve in black.

Cubic polynomial

The above function can be adapted to a variety of interpolation methods. I have added the function

function  cubicInterpolation(x, p0, p1, p2, p3){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));   
}

Which produces a curve based on the slope of the line at two points either side of x. This method is intended for evenly spaced points but still works if you have uneven spacing (such as this example). If the spacing gets too uneven you can notice a bit of a kink in the curve at that point.

Also the curve over and under shoot may be an issue.

For more on the Maths of cubic interpolation.

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Just be aware of that this won't produce a "correct" smooth curve based on consideration of the *series* of points as entry/exit angle always will be "flat" (i.e. "S"-curve). May or may not be good enough for OP though... –  Nov 23 '17 at 08:33
  • As K3N says this method produces a flatter curve. However, given the frankly excellent code example and the ease with which I can implement this, I am going to accept this answer. – Testic Nov 25 '17 at 21:07