Zoom in and out and keep current pixel at mouse coordinates - opengl

So... I have this code I wrote to that draws a quad in ortho mode in Opengl.
I know where its located and its size. Sometimes square.. some times its rectangular.
What I'm attempting to do is zoom in and out and stay over the same location on the quad.
I thought it would work but It's not.
Knowing the distance from the mouse location to the corner, offset.x and the difference equaling rect_size.x- old_size_w the basic math would me this:
(offset.x/rect_size.x) * difference. That should give me the scale of how much the location needs to move based on where the mouse is sitting.
I hope someone can sort this out..
Thank you!....
Some numbers...
location = 100,100
old_size_w = 1024
rect_size.x = 1088 (new size old_ * 1.0625)
mouse_delta.x = 425
offset = 100 - 425 (-325)
difference = 1088-1024 (64)
delta_x = 325/1088 (.2987132....)
x_offset = Cint(delta_x * difference) (19) (19.11764...)
SO.... we are only moving by 19 pixels.. If we do the math from the other direction.. the 2 must = the difference from the old zoom and the new zoom
delta_x = (1088-325) /1088 (.701286...)
x_offset2 = Cint(delta_x * difference) (45) (44.88235...)
19 + 45 = 64 <--- this proves out the math
Yet... I am getting a nasty shift that gets worse the closer to the right of the image I move.
Maybe someone can find the problem.. r_x is remaining X in the code below and is for proving the math.
Public Sub img_scale_up()
If Not ready_to_render Then Return
If Zoom_Factor >= 4.0 Then
Zoom_Factor = 4.0
Return 'to big and the t_bmp creation will hammer memory.
End If
Dim amt As Single = 0.0625
Zoom_Factor += amt
Dim offset As New Point
'old_w and old_h are the orginal size of the image.
Dim old_size_w, old_size_h As Double
old_size_w = (old_w * (Zoom_Factor - amt))
old_size_h = (old_h * (Zoom_Factor - amt))
offset = rect_location - (mouse_delta)
rect_size.X = Zoom_Factor * old_w
rect_size.Y = Zoom_Factor * old_h
Dim r_x As Double = ((rect_size.X - -offset.X) / rect_size.X) * (rect_size.X - old_size_w)
Dim delta_x As Double = CSng(offset.X / rect_size.X)
Dim delta_y As Double = CSng(offset.Y / rect_size.Y)
Dim x_offset = delta_x * (rect_size.X - old_size_w)
Dim y_offset = delta_y * (rect_size.Y - old_size_h)
rect_location.X += CInt(x_offset)
rect_location.Y += CInt(y_offset)
draw_(current_image)

Store mouse position as u, v where u and v are on 0.0 - 1.0. Translate your image so that mouse it as 0, 0. Manipulate, in this case by scaling. Then translate back so that 0, 0, goes to u. v.

I usually get lost (doing silly mistakes) in deriving the right equations (as I use many view transform formulas not just one) and mostly I am to lazy to derive so I do it like this instead:
convert mouse position to coordinate system where offset is not scaled
if it is screen or world coordinate system depends on your transformation order. Store the result as mx0,my0
apply zoom change
convert mouse position to coordinate system where offset is not scaled
it is the same as #1 but with updated zoom. Store the result as mx1,my1
update offset
simply add:
offset_x += mx0-mx1;
offset_y += my0-my1;
Where offset_x,offset_y holds your view offset (position).
Also see Zooming graphics based on current mouse position

Ok.. I sorted this out.. I used the a couple of the wrong values for both zooming in and out.
This is the entire code for zooming around the mouse center and maintaining the center of the mouse.
Anyone trying to figure this out.. well here's the code that works perfectly :)
Public Sub img_scale_up()
If Not ready_to_render Then Return
If Zoom_Factor >= 4.0 Then
Zoom_Factor = 4.0
Return 'to big and the t_bmp creation will hammer memory.
End If
Dim amt As Single = 0.0625
Zoom_Factor += amt
'this bit of math zooms the texture around the mouses center during the resize.
'old_w and old_h is the original size of the image in width and height
'mouse_pos is current mouse position in the window.
Dim offset As New Point
Dim old_size_w, old_size_h As Double
old_size_w = (old_w * (Zoom_Factor - amt))
old_size_h = (old_h * (Zoom_Factor - amt))
offset = rect_location - (mouse_pos)
rect_size.X = Zoom_Factor * old_w
rect_size.Y = Zoom_Factor * old_h
Dim delta_x As Double = CDbl(offset.X / old_size_w)
Dim delta_y As Double = CDbl(offset.Y / old_size_h)
Dim x_offset = delta_x * (rect_size.X - old_size_w)
Dim y_offset = delta_y * (rect_size.Y - old_size_h)
rect_location.X += CInt(x_offset)
rect_location.Y += CInt(y_offset)
draw_(current_image)
End Sub
Public Sub img_scale_down()
If Not ready_to_render Then Return
If Zoom_Factor <= 0.25 Then
Zoom_Factor = 0.25
Return
End If
Dim amt As Single = 0.0625
Zoom_Factor -= amt
'this bit of math zooms the texture around the mouses center during the resize.
'old_w and old_h is the original size of the image in width and height
'mouse_pos is current mouse position in the window.
Dim offset As New Point
Dim old_size_w, old_size_h As Double
old_size_w = (old_w * (Zoom_Factor - amt))
old_size_h = (old_h * (Zoom_Factor - amt))
offset = rect_location - (mouse_pos)
rect_size.X = Zoom_Factor * old_w
rect_size.Y = Zoom_Factor * old_h
Dim delta_x As Double = CDbl(offset.X / (rect_size.X + (rect_size.X - old_size_w)))
Dim delta_y As Double = CDbl(offset.Y / (rect_size.Y + (rect_size.Y - old_size_h)))
Dim x_offset = delta_x * (rect_size.X - old_size_w)
Dim y_offset = delta_y * (rect_size.Y - old_size_h)
rect_location.X += -CInt(x_offset)
rect_location.Y += -CInt(y_offset)
draw_(current_image)
End Sub

Related

Meanshift algorithm for tracking objects issue computing centroid update of search window

I have been trying to implement the meanshift algorithm for tracking objects, and have gone through the concepts involved.
As per now I have managed to successfully generate a backprojected stream from my camera with a single channel hue roi histogram and a single channel hue video stream which seems fine, I know there is a meanshift function within the opencv library but I am trying to implement one myself using the data structures provided in opencv, calculating the moments and computing the mean centroid of the search window.
But for some reason I am unable to locate the problem within my code as it keeps on converging to the upper left corner of my video stream for any input roi (region of interest) to be tracked. Following is a code snippet of the function for calculating the centroid of the search window where I feel the problem lies but not sure what it is, I would really appreciate if someone can point me in the right direction:
void moment(Mat &backproj, Rect &win){
int x_c, y_c, x_c_new, y_c_new;
int idx_row, idx_col;
double m00 = 0.0 , m01 = 0.0 , m10 = 0.0 ;
double res = 1.0, TOL = 0.003 ;
//Set the center of search window as the center of the probabilistic image:
y_c = (int) backproj.rows / 2 ;
x_c = (int) backproj.cols / 2 ;
//Centroid search solver until residual below certain tolerance:
while (res > TOL){
win.width = (int) 80;
win.height = (int) 60;
//First array element at position (x,y) "lower left corner" of the search window:
win.x = (int) (x_c - win.width / 2) ;
win.y = (int) (y_c - win.height / 2);
//Modulo correction since modulo of negative integer is negative in C:
if (win.x < 0)
win.x = win.x % backproj.cols + backproj.cols ;
if (win.y < 0)
win.y = win.y % backproj.rows + backproj.rows ;
for (int i = 0; i < win.height; i++ ){
//Traverse along y-axis (height) i.e. rows ensuring wrap around top/bottom boundaries:
idx_row = (win.y + i) % (int)backproj.rows ;
for (int j = 0; j < win.width; j++ ){
//Traverse along x-axis (width) i.e. cols ensuring wrap around left/right boundaries:
idx_col = (win.x + j) % (int)backproj.cols ;
//Compute Moments:
m00 += (double) backproj.at<uchar>(idx_row, idx_col) ;
m10 += (double) backproj.at<uchar>(idx_row, idx_col) * i ;
m01 += (double) backproj.at<uchar>(idx_row, idx_col) * j ;
}
}
//Compute new centroid coordinates of the search window:
x_c_new = (int) ( m10 / m00 ) ;
y_c_new = (int) ( m01 / m00 );
//Compute the residual:
res = sqrt( pow((x_c_new - x_c), 2.0) + pow((y_c_new - y_c), 2.0) ) ;
//Set new search window centroid coordinates:
x_c = x_c_new;
y_c = y_c_new;
}
}
It's my second ever query on stackoverflow so please excuse me for any guidelines that I forgot to follow.
EDIT
changed m00 , m01 , m10 to block level variables within WHILE-LOOP instead of function level variables, thanks to Daniel Strul for pointing it out but the problem still remains. Now the search window jumps around the frame boundaries instead of focusing on the roi.
void moment(Mat &backproj, Rect &win){
int x_c, y_c, x_c_new, y_c_new;
int idx_row, idx_col;
double m00 , m01 , m10 ;
double res = 1.0, TOL = 0.003 ;
//Set the center of search window as the center of the probabilistic image:
y_c = (int) backproj.rows / 2 ;
x_c = (int) backproj.cols / 2 ;
//Centroid search solver until residual below certain tolerance:
while (res > TOL){
m00 = 0.0 , m01 = 0.0 , m10 = 0.0
win.width = (int) 80;
win.height = (int) 60;
//First array element at position (x,y) "lower left corner" of the search window:
win.x = (int) (x_c - win.width / 2) ;
win.y = (int) (y_c - win.height / 2);
//Modulo correction since modulo of negative integer is negative in C:
if (win.x < 0)
win.x = win.x % backproj.cols + backproj.cols ;
if (win.y < 0)
win.y = win.y % backproj.rows + backproj.rows ;
for (int i = 0; i < win.height; i++ ){
//Traverse along y-axis (height) i.e. rows ensuring wrap around top/bottom boundaries:
idx_row = (win.y + i) % (int)backproj.rows ;
for (int j = 0; j < win.width; j++ ){
//Traverse along x-axis (width) i.e. cols ensuring wrap around left/right boundaries:
idx_col = (win.x + j) % (int)backproj.cols ;
//Compute Moments:
m00 += (double) backproj.at<uchar>(idx_row, idx_col) ;
m10 += (double) backproj.at<uchar>(idx_row, idx_col) * i ;
m01 += (double) backproj.at<uchar>(idx_row, idx_col) * j ;
}
}
//Compute new centroid coordinates of the search window:
x_c_new = (int) ( m10 / m00 ) ;
y_c_new = (int) ( m01 / m00 );
//Compute the residual:
res = sqrt( pow((x_c_new - x_c), 2.0) + pow((y_c_new - y_c), 2.0) ) ;
//Set new search window centroid coordinates:
x_c = x_c_new;
y_c = y_c_new;
}
}
The reason your algorithms always converges to the upper left corner independently of the input data is that m00, m10 and m01 are never reset to zero:
On iteration 0, for each moment variable m00, m10 and m01, you compute the right value m0
Between iteration 0 and iteration 1 , the moments variables are not reset and keep their previous value
Thus, on iteration 1, for each moment variable m00, m10 and m01, you actually sum the new moment with the old one and obtain ( m0 + m1 )
On iteration 2, you carry on summing the new moments on top of the previous ones and obtain ( m0 + m1 + m2 )
And so on, iteration by iteration.
At the very least, the moment variables should be reset at the beginning of each iteration.
Ideally, they should not be function-level variables but should rather be block-level variables, as they have no use outside the loop iterations (except for debugging purpose):
while (res > TOL){
...
double m00 = 0.0, m01 = 0.0, m10 = 0.0;
for (int i = 0; i < win.height; i++ ){
...
EDIT 1
The reason for the second problem you encounter (the ROI jumping all around the place) is that the computations of the moments are based on the relative coordinates i and j.
Thus, what you compute is [ avg(j) , avg(i) ], wher as what you really want is [ avg(y) , avg(x) ]. To solve this issue, I had proposed a first solution. I"ve replaced it by a much simpler solution below.
EDIT 2
The simplest solution is to add the coordinates of the ROI corner right at the end of each iteration:
x_c_new = win.x + (int) ( m10 / m00 ) ;
y_c_new = win.y + (int) ( m01 / m00 );

Determining coordinates for mandelbrot zoom

I got a mandelbrot set I want to zoom in. The mandelbrot is calculated around a center coordinate, mandelbrot size and a zoom-level. The original mandelbrot is centered around
real=-0.6 and im=0.4 with a size of 2 in both real and im.
I want to be able to click on a point in the image and calculate a new one, zoomed in around that point
The window containing it is 800x800px, so I figured this would make a click in the lower right corner be equal to a center of real=0.4 and im=-0.6, and a click in the upper left corner be real=-1.6 and im=1.4
I calculated it with:
for the real values
800a+b=0.4 => a=0.0025
0a+b=-1.6 => b=-1.6
for imaginary values
800c+d=-0.6 => c=-0.0025
0c+d=1.4 => d=1.4
However, this does not work if I continue with mandelbrot size of 2 and zoom-level of 2. Am I missing something concerning the coordinates with the zoom-levels?
I had similar problems zooming in my C# Mandelbrot. My solution was to calculate the difference from the click position to the center in percents, multiply this with the maximum of units (width / zoom * 0.5, width = height, zoom = n * 100) from the center and add this to your current value. So My code was this (assuming I get sx and sy as parameters from the click):
double[] o = new double[2];
double digressLRUD = width / zoom * 0.5; //max way up or down from the center in coordinates
double shiftCenterCursor_X = sx - width/2.0; //shift of cursor to center
double shiftCenterCursor_X_percentage = shiftCenterCursor_X / width/2.0; //shift in percentage
o[0] = x + digressLRUD * shiftCenterCursor_X_percentage; //new position
double shiftCenterCursor_Y = sy - width/2.0;
double shiftCenterCursor_Y_percentage = shiftCenterCursor_Y / width/2.0;
o[1] = y - digressLRUD * shiftCenterCursor_Y_percentage;
This works, but you'll have to update the zoom (I use to multiply it with 2).
Another point is to move the selected center to the center of the image. I did this using some calculations:
double maxRe = width / zoom;
double centerRe = reC - maxRe * 0.5;
double maxIm = height / zoom;
double centerIm = -imC - maxIm * 0.5;
This will bring you the coordinates you have to pass your algorithm so it'll render the selected place.

Direction of shortest rotation between two vectors

my question is regarding working out the direction of the smallest angle between two vectors in 2D. I am making a game in C++ where one of the obstacles is a heat seeking missile launcher. I have it working by calculating the vector between the target and bullet, normalising the vector and then multiplying it by its speed. However, I am now coming back to this class to make it better. Instead of instantly locking onto the player I want it to only do so only when the bullets vector is within a certain angle (the angle between the bullets vector and the vector bulletloc->target). Otherwise I want it to slowly pan towards the target by a degrees thus giving the player enough space to avoid it. I have done all this (in a vb.net project so i could simplify the problem, work it out then re write in in C++). However the bullet always rotates clockwise towards the target even if the quickest route would be counter clockwise. So the problem is working out the direction to apply the rotation in so the smallest angle is covered. Here is my code so you can try and see what I am describing:
Function Rotate(ByVal a As Double, ByVal tp As Point, ByVal cp As Point, ByVal cv As Point)
'params a = angle, tp = target point, cp = current point, cv = current vector of bullet'
Dim dir As RotDir 'direction to turn in'
Dim tv As Point 'target vector cp->tp'
Dim d As Point 'destination point (d) = cp + vector'
Dim normal As Point
Dim x1 As Double
Dim y1 As Double
Dim VeritcleResolution As Integer = 600
tp.Y = VeritcleResolution - tp.Y 'modify y parts to exist in plane with origin (0,0) in bottom left'
cp.Y = VeritcleResolution - cp.Y
cv.Y = cv.Y * -1
tv.X = tp.X - cp.X 'work out cp -> tp'
tv.Y = tp.Y - cp.Y
'calculate angle between vertor to target and vecrot currntly engaed on'
Dim tempx As Double
Dim tempy As Double
tempx = cv.X * tv.X
tempy = cv.Y * tv.Y
Dim DotProduct As Double
DotProduct = tempx + tempy 'dot product of cp-> d and cp -> tp'
Dim magCV As Double 'magnitude of current vector'
Dim magTV As Double 'magnitude of target vector'
magCV = Math.Sqrt(Math.Pow(cv.X, 2) + Math.Pow(cv.Y, 2))
magTV = Math.Sqrt(Math.Pow(tv.X, 2) + Math.Pow(tv.Y, 2))
Dim VectorAngle As Double
VectorAngle = Acos(DotProduct / (magCV * magTV))
VectorAngle = VectorAngle * 180 / PI 'angle between cp->d and cp->tp'
If VectorAngle < a Then 'if the angle is small enough translate directly towards target'
cv = New Point(tp.X - cp.X, tp.Y - cp.Y)
magCV = Math.Sqrt((cv.X ^ 2) + (cv.Y ^ 2))
If magCV = 0 Then
x1 = 0
y1 = 0
Else
x1 = cv.X / magCV
y1 = cv.Y / magCV
End If
normal = New Point(x1 * 35, y1 * 35)
normal.Y = normal.Y * -1
cv = normal
ElseIf VectorAngle > a Then 'otherwise smootly translate towards the target'
Dim x As Single
d = New Point(cp.X + cv.X, cp.Y + cv.Y)
a = (a * -1) * PI / 180 'THIS LINE CONTROL DIRECTION a = (a*-1) * PI / 180 would make the rotation counter clockwise'
'rotate the point'
d.X -= cp.X
d.Y -= cp.Y
d.X = (d.X * Cos(a)) - (d.Y * Sin(a))
d.Y = (d.X * Sin(a)) + (d.Y * Cos(a))
d.X += cp.X
d.Y += cp.Y
cv.X = d.X - cp.X
cv.Y = d.Y - cp.Y
cv.Y = cv.Y * -1
End If
Return cv
End Function
One idea I had was to work out the bearing of the two vectors and if the difference is greater than 180 degrees, rotate clockwise otherwise rotate counter clockwise, any ideas would be helpful. Thanks.
EDIT: I would like to add that this site is very helpful. I often use questions posed by others to solve my own problems and I want to take the chance to say thanks.
As you've written in your code, the angle between two (normalized) vectors is the inverse cosine of their dot product.
To get a signed angle, you can use a third vector representing the normal of the plane that the other two vectors lie on -- in your 2D case, this would be a 3D vector pointing straight "up", say (0, 0, 1).
Then, take the cross-product of the first vector (the one you want the angle to be relative to) with the second vector (note cross-product is not commutative). The sign of the angle should be the same as the sign of the dot product between the resulting vector and the plane normal.
In code (C#, sorry) -- note all vectors are assumed to be normalized:
public static double AngleTo(this Vector3 source, Vector3 dest)
{
if (source == dest) {
return 0;
}
double dot; Vector3.Dot(ref source, ref dest, out dot);
return Math.Acos(dot);
}
public static double SignedAngleTo(this Vector3 source, Vector3 dest, Vector3 planeNormal)
{
var angle = source.AngleTo(dest);
Vector3 cross; Vector3.Cross(ref source, ref dest, out cross);
double dot; Vector3.Dot(ref cross, ref planeNormal, out dot);
return dot < 0 ? -angle : angle;
}
This works by taking advantage of the fact that the cross product between two vectors yields a third vector which is perpendicular (normal) to the plane defined by the first two (so it's inherently a 3D operation). a x b = -(b x a), so the vector will always be perpendicular to the plane, but on a different side depending on the (signed) angle between a and b (there's something called the right-hand rule).
So the cross product gives us a signed vector perpendicular to the plane which changes direction when the angle between the vectors passes 180°. If we know in advance a vector perpendicular to the plane which is pointing straight up, then we can tell whether the cross product is in the same direction as that plane normal or not by checking the sign of their dot product.
Based on #Cameron's answer, here is the python translation i've used:
As bonus, i've added the signed_angle_between_headings function to directly return the 'quickest' turn angle between two north-referenced headings.
import math
import numpy as np
def angle_between_vectors(source, dest):
if np.array_equal(source, dest):
return 0
dot = np.dot(source, dest)
return np.arccos(dot)
def signed_angle_from_to_vectors(source, dest, plane_normal):
angle = angle_between_vectors(source, dest)
cross = np.cross(source, dest)
dot = np.dot(cross, plane_normal)
return -angle if dot < 0 else angle
def signed_angle_between_headings(source_heading, destination_heading):
if source_heading == destination_heading:
return 0
RAD2DEGFACTOR = 180 / math.pi
source_heading_rad = source_heading / RAD2DEGFACTOR
dest_heading_rad = destination_heading / RAD2DEGFACTOR
source_vector = np.array([np.cos(source_heading_rad), np.sin(source_heading_rad), 0])
dest_vector = np.array([np.cos(dest_heading_rad), np.sin(dest_heading_rad), 0])
signed_angle_rad = signed_angle_from_to_vectors(source_vector, dest_vector, np.array([0,0,1]))
return signed_angle_rad * RAD2DEGFACTOR

Line-Circle Algorithm not quite working as expected

First, see:
https://math.stackexchange.com/questions/105180/positioning-a-widget-involving-intersection-of-line-and-a-circle
I have an algorithm that solves for the height of an object given a circle and an offset.
It sort of works but the height is always off:
Here is the formula:
and here is a sketch of what it is supposed to do:
And here is sample output from the application:
In the formula, offset = 10 and widthRatio is 3. This is why it is (1 / 10) because (3 * 3) + 1 = 10.
The problem, as you can see is the height of the blue rectangle is not correct. I set the bottom left offsets to be the desired offset (in this case 10) so you can see the bottom left corner is correct. The top right corner is wrong because from the top right corner, I should only have to go 10 pixels until I touch the circle.
The code I use to set the size and location is:
void DataWidgetsHandler::resize( int w, int h )
{
int tabSz = getProportions()->getTableSize() * getProportions()->getScale();
int r = tabSz / 2;
agui::Point tabCenter = agui::Point(
w * getProportions()->getTableOffset().getX(),
h * getProportions()->getTableOffset().getY());
float widthRatio = 3.0f;
int offset = 10;
int height = solveHeight(offset,widthRatio,tabCenter.getX(),tabCenter.getY(),r);
int width = height * widthRatio;
int borderMargin = height;
m_frame->setLocation(offset,
h - height - offset);
m_frame->setSize(width,height);
m_borderLayout->setBorderMargins(0,0,borderMargin,borderMargin);
}
I can assert that the table radius and table center location are correct.
This is my implementation of the formula:
int DataWidgetsHandler::solveHeight( int offset, float widthRatio, float h, float k, float r ) const
{
float denom = (widthRatio * widthRatio) + 1.0f;
float rSq = denom * r * r;
float eq = widthRatio * offset - offset - offset + h - (widthRatio * k);
eq *= eq;
return (1.0f / denom) *
((widthRatio * h) + k - offset - (widthRatio * (offset + offset)) - sqrt(rSq - eq) );
}
It uses the quadratic formula to find what the height should be so that the distance between the top right of the rectangle, bottom left, amd top left are = offset.
Is there something wrong with the formula or implementation? The problem is the height is never long enough.
Thanks
Well, here's my solution, which looks to resemble your solveHeight function. There might be some arithmetic errors in the below, but the method is sound.
You can think in terms of matching the coordinates at the point of the circle across
from the rectangle (P).
Let o_x,o_y be the lower left corner offset distances, w and h be the
height of the rectangle, w_r be the width ratio, dx be the desired
distance between the top right hand corner of the rectangle and the
circle (moving horizontally), c_x and c_y the coordinates of the
circle's centre, theta the angle, and r the circle radius.
Labelling it is half the work! Simply write down the coordinates of the point P:
P_x = o_x + w + dx = c_x + r cos(theta)
P_y = o_y + h = c_y + r sin(theta)
and we know w = w_r * h.
To simplify the arithmetic, let's collect some of the constant terms, and let X = o_x + dx - c_x and Y = o_y - c_y. Then we have
X + w_r * h = r cos(theta)
Y + h = r sin(theta)
Squaring and summing gives a quadratic in h:
(w_r^2 + 1) * h^2 + 2 (X*w_r + Y) h + (X^2+Y^2-r^2) == 0
If you compare this with your effective quadratic, then as long as we made different mistakes :-), you might be able to figure out what's going on.
To be explicit: we can solve this using the quadratic formula, setting
a = (w_r^2 + 1)
b = 2 (X*w_r + Y)
c = (X^2+Y^2-r^2)

Creating a linear gradient in 2D array

I have a 2D bitmap-like array of let's say 500*500 values. I'm trying to create a linear gradient on the array, so the resulting bitmap would look something like this (in grayscale):
(source: showandtell-graphics.com)
The input would be the array to fill, two points (like the starting and ending point for the Gradient tool in Photoshop/GIMP) and the range of values which would be used.
My current best result is this:
alt text http://img222.imageshack.us/img222/1733/gradientfe3.png
...which is nowhere near what I would like to achieve. It looks more like a radial gradient.
What is the simplest way to create such a gradient? I'm going to implement it in C++, but I would like some general algorithm.
This is really a math question, so it might be debatable whether it really "belongs" on Stack Overflow, but anyway: you need to project the coordinates of each point in the image onto the axis of your gradient and use that coordinate to determine the color.
Mathematically, what I mean is:
Say your starting point is (x1, y1) and your ending point is (x2, y2)
Compute A = (x2 - x1) and B = (y2 - y1)
Calculate C1 = A * x1 + B * y1 for the starting point and C2 = A * x2 + B * y2 for the ending point (C2 should be larger than C1)
For each point in the image, calculate C = A * x + B * y
If C <= C1, use the starting color; if C >= C2, use the ending color; otherwise, use a weighted average:
(start_color * (C2 - C) + end_color * (C - C1))/(C2 - C1)
I did some quick tests to check that this basically worked.
In your example image, it looks like you have a radial gradient. Here's my impromtu math explanation for the steps you'll need. Sorry for the math, the other answers are better in terms of implementation.
Define a linear function (like y = x + 1) with the domain (i.e. x) being from the colour you want to start with to the colour your want to end with. You can think of this in terms of a range the within Ox0 to OxFFFFFF (for 24 bit colour). If you want to handle things like brightness, you'll have to do some tricks with the range (i.e. the y value).
Next you need to map a vector across the matrix you have, as this defines the direction that the colours will change in. Also, the colour values defined by your linear function will be assigned at each point along the vector. The start and end point of the vector also define the min and max of the domain in 1. You can think of the vector as one line of your gradient.
For each cell in the matrix, colours can be assigned a value from the vector where a perpendicular line from the cell intersects the vector. See the diagram below where c is the position of the cell and . is the the point of intersection. If you pretend that the colour at . is Red, then that's what you'll assign to the cell.
|
c
|
|
Vect:____.______________
|
|
I'll just post my solution.
int ColourAt( int x, int y )
{
float imageX = (float)x / (float)BUFFER_WIDTH;
float imageY = (float)y / (float)BUFFER_WIDTH;
float xS = xStart / (float)BUFFER_WIDTH;
float yS = yStart / (float)BUFFER_WIDTH;
float xE = xEnd / (float)BUFFER_WIDTH;
float yE = yEnd / (float)BUFFER_WIDTH;
float xD = xE - xS;
float yD = yE - yS;
float mod = 1.0f / ( xD * xD + yD * yD );
float gradPos = ( ( imageX - xS ) * xD + ( imageY - yS ) * yD ) * mod;
float mag = gradPos > 0 ? gradPos < 1.0f ? gradPos : 1.0f : 0.0f;
int colour = (int)( 255 * mag );
colour |= ( colour << 16 ) + ( colour << 8 );
return colour;
}
For speed ups, cache the derived "direction" values (hint: premultiply by the mag).
There are two parts to this problem.
Given two colors A and B and some percentage p, determine what color lies p 'percent of the way' from A to B.
Given a point on a plane, find the orthogonal projection of that point onto a given line.
The given line in part 2 is your gradient line. Given any point P, project it onto the gradient line. Let's say its projection is R. Then figure out how far R is from the starting point of your gradient segment, as a percentage of the length of the gradient segment. Use this percentage in your function from part 1 above. That's the color P should be.
Note that, contrary to what other people have said, you can't just view your colors as regular numbers in your function from part 1. That will almost certainly not do what you want. What you do depends on the color space you are using. If you want an RGB gradient, then you have to look at the red, green, and blue color components separately.
For example, if you want a color "halfway between" pure red and blue, then in hex notation you are dealing with
ff 00 00
and
00 00 ff
Probably the color you want is something like
80 00 80
which is a nice purple color. You have to average out each color component separately. If you try to just average the hex numbers 0xff0000 and 0x0000ff directly, you get 0x7F807F, which is a medium gray. I'm guessing this explains at least part of the problem with your picture above.
Alternatively if you are in the HSV color space, you may want to adjust the hue component only, and leave the others as they are.
void Image::fillGradient(const SColor& colorA, const SColor& colorB,
const Point2i& from, const Point2i& to)
{
Point2f dir = to - from;
if(to == from)
dir.x = width - 1; // horizontal gradient
dir *= 1.0f / dir.lengthQ2(); // 1.0 / (dir.x * dir.x + dir.y * dir.y)
float default_kx = float(-from.x) * dir.x;
float kx = default_kx;
float ky = float(-from.y) * dir.y;
uint8_t* cur_pixel = base; // array of rgba pixels
for(int32_t h = 0; h < height; h++)
{
for(int32_t w = 0; w < width; w++)
{
float k = std::clamp(kx + ky, 0.0f, 1.0f);
*(cur_pixel++) = colorA.r * (1.0 - k) + colorB.r * k;
*(cur_pixel++) = colorA.g * (1.0 - k) + colorB.g * k;
*(cur_pixel++) = colorA.b * (1.0 - k) + colorB.b * k;
*(cur_pixel++) = colorA.a * (1.0 - k) + colorB.a * k;
kx += dir.x;
}
kx = default_kx;
ky += dir.y;
}
}