When I wrote an application, eDigitizer 2.0 and its new release eDigitizer 2.10,  that allows user to draw lines on images in the PictureBox control, I encountered a problem that lines or shapes I drew on the image are off the place, that is, not positioned in the place I clicked. The problem does not occur if I show images in “normal” SizeMode. I googled a long time and saw people were asking the very question again and again but not any useful solutions. Since the problem irritated me, I decided to write my own method to solve the problem.

The fact is the size of PictureBox control and the size of image are not same. We have to project the x,y coordination of the mouse click on PictureBox control to the x’,y’ coordination of the loaded image. For the different SizeMode type, the projection is different.

For the PictureBoxSizeMode.StretchImage mode, the loaded image is stretched to fill PictureBox control. The ratio to stretch image in the x axis is based on the width of the image and PictureBox control while the ratio to stretch image in the y axis is based on the height of the image and the PictureBox control. The two ratios are independent.

For the PictureBoxSizeMode.CenterImage mode, the center of the loaded iamge will overlap with the center of the PictureBox control. If the image is smaller than the PictureBox control, it will be inside the control. Otherwise, only the center potion with the same size as the control shows. There is no skew and distortion.

For the PictureBoxSizeMode.Zoom mode, it is a little bit complicated. The ratio to shrink or enlarge the image is same in both of the axes. We can get an x_ratio that is the ratio of width of the image and the control and y_ratio that is the ratio of height of the image and the control. The ratio to be used in the zoom is the larger one in the two ratios. If the x_ratio is the larger one, the image will fill the control in the x axis. Otherwise the image will file the control in the y axis. The tricky part is how to get the right projection in the direction where the image does not fill the control. We have to project the width or height of the image in the ratio first and then calculate the border space between the image and the control.

The above ideas were put into one method that needs three input arguments: the image object, and x and y value of the mouse click point in the control. The output is an array with two elements projected x and y values in the image object. The algorithm works well for images loaded in a PictureBox control. Here is the source code.

int [] xy_projection(Bitmap myBitmap2, int x, int y)
{
	int heightB = myBitmap2.Height;
	int heightP = pictureBoxBoard.Height;
	int widthB = myBitmap2.Width;
	int widthP = pictureBoxBoard.Width;
	double xRatio = (double) widthB / (double)widthP;
	double yRatio = (double) heightB / (double)heightP;
	int [] xy = new int[2];
	if (pictureBoxBoard.SizeMode == PictureBoxSizeMode.StretchImage)
	{
		xy[0] = (int) (x * xRatio);
		xy[1] = (int) (y * yRatio);
	}
	else if (pictureBoxBoard.SizeMode == PictureBoxSizeMode.CenterImage)
	{
		int borderHeight = (heightP -heightB) / 2;
		int borderWidth = (widthP - widthB) / 2;
		xy[0] = x - borderWidth;
		xy[1] = y - borderHeight;
	}
	else if (pictureBoxBoard.SizeMode == PictureBoxSizeMode.Zoom)
	{
		double ratio = xRatio;
		bool x_filled = true;
		if ( ratio < yRatio)
		{
			ratio = yRatio;
			x_filled = false;
		}
		if (x_filled)
		{
			heightB = (int) (heightB / ratio);
			int borderHeight = (heightP - heightB) / 2;
			xy[0] = (int) (x * ratio);
			xy[1] = (int) ((y - borderHeight) * ratio);
		}
		else
		{
			widthB = (int) (widthB / ratio);
			int borderWidth = (widthP - widthB) / 2;
			xy[0] =(int) ((x - borderWidth) * ratio);
			xy[1] =(int) (y * ratio);
		}
	}
	else
	{
		xy[0] = x;
		xy[1] = y;
	}
	return xy;
}

To draw on the image in the PictureBox control, you have to get the image from the control. Then generate a Graphics object from it. Now you can draw on the image based on mouse clicks. Whenever you get the mouse point x, y values, we have to use the above function to project the values to the image axes. To be nice, you have to release the memory that the Graphics object uses. In the following code snippet, we draw a cross at the mouse click location and connect the point with the previous mouse click point.

Color color = Color.Black;
Color veryTransparentColor = Color.FromArgb(77,color.R, color.G, color.B);
Bitmap myBitmap2 = (Bitmap)pictureBoxBoard.Image;
Graphics g = Graphics.FromImage(myBitmap2);

//Brush myBrush = new SolidBrush(color);
//g.DrawString(heightB.ToString()+","+widthB.ToString()+";"+heightP.ToString()+widthP.ToString(),this.Font, myBrush, mvPoint);

// draw a small cross
int penWidth = 2;
Pen myPen = new Pen(veryTransparentColor, penWidth);
int [] xy = xy_projection(myBitmap2, mvPoint.X, mvPoint.Y);
g.DrawLine(myPen, new Point(xy[0]-5,xy[1]), new Point(xy[0]+5,xy[1]));
g.DrawLine(myPen, new Point(xy[0],xy[1]-5), new Point(xy[0],xy[1]+5));

// connect two adjacent points if there are more than one points
// the position is not right.
if (myPoints.Count > 1)
{
	int myPos = myPoints.Count-1;
	int [] xy1 = xy_projection(myBitmap2, ((Point)myPoints[myPos-1]).X,((Point)myPoints[myPos-1]).Y);
	int [] xy2 = xy_projection(myBitmap2, ((Point)myPoints[myPos]).X,((Point)myPoints[myPos]).Y);
	g.DrawLine(myPen,xy1[0],xy1[1],xy2[0],xy2[1]);
}
pictureBoxBoard.Invalidate();
g.Dispose();
Share

Tags: , , , , , , , , , , , , ,