1) I have a list of line segments (defined by their two endpoints and a width.)
2) These line segments are drawn in a panel.
3) When my mouse moves (Panel.MouseMove event), I loop over the list of line segments.
4) Foreach:
gPath.Reset();
Pen Pen = new Pen(Color.White, 20);
gPath.AddLine(P1, P2);
return gPath.IsOutlineVisible(cursorPos, Pen);
5) If I get true, then I know my cursor is hovering over the current line segment.
This works fine for about... 300 lines or so. When I reach 1,000 my program slows to a halt (profiling shows that it's caused by IsOutlineVisible). So, is there any way I could increase performance of my hit-testing algorithm? I don't know how efficient IsOutlineVisible is, so I don't want to implement any optimizations that the method already uses. Any ideas?
EDIT:
After digging into my data I noticed some of the lines were extremely large. For example:
endpoint1 = (16000, -16000)
endpoint2 = (5041448, -32868734)
(yes, one of the coordinates is in the negative tens of millions...)
I verified that hit-testing against just one such line segment is enough to bring my program to a halt, (IsOutlineVisible takes 2-3 seconds to do the test, and the test is run whenever the cursor moves...).
I should have tested this more thoroughly before posting. Thanks to all that responded (a 2d spatial index is a great suggestion if I end-up handling thousands of lines).
p.s. If anyone knows why a large line-segment is that big a problem for IsOutlineVisible, that would be great.
Try this:
public Line GetHotSpottedLine(Point mousePos){
var line = lines.Where(line =>
{
Point p1 = new Point(Math.Min(line.P1.X, line.P2.X), Math.Min(line.P1.Y, line.P2.Y));
Point p2 = new Point(Math.Max(line.P1.X, line.P2.X), Math.Max(line.P1.Y, line.P2.Y));
return mousePos.X >= p1.X && mousePos.X <= p2.X && mousePos.Y >= p1.Y && mousePos.Y <= p2.Y;
}).FirstOrDefault(line => {
using (GraphicsPath gp = new GraphicsPath())
{
gp.AddLine(line.P1, line.P2);
//You can declare your pen outside and this pen should be used to draw your lines.
using (Pen p = new Pen(Color.Red, 20))
{
return gp.IsOutlineVisible(mousePos, p);
}
}
});
return line;
}
public class Line{
public Point P1 { get; set; }
public Point P2 { get; set; }
}
List<Line> lines = new List<Line>();
It depends on how you want to use your lines, if you want to draw them, we have to notice the performance of drawing not detecting the hovered line, yes in that case, drawing is your problem. I think we can use some Thread here. Anyway, I tested with 1000 lines and it works OK (with drawing all the lines in Form Paint) without using any thread.
IsOutlineVisible calls the Gdi+, maybe this slows it down a bit.
public bool GraphicsPath.IsOutlineVisible(PointF pt, Pen pen, Graphics graphics)
{
int num;
if (pen == null)
{
throw new ArgumentNullException("pen");
}
int status = SafeNativeMethods.Gdip.GdipIsOutlineVisiblePathPoint(new HandleRef(this, this.nativePath), pt.X, pt.Y, new HandleRef(pen, pen.NativePen), new HandleRef(graphics, (graphics != null) ? graphics.NativeGraphics : IntPtr.Zero), out num);
if (status != 0)
{
throw SafeNativeMethods.Gdip.StatusException(status);
}
return (num != 0);
}
Beside that, these hit testing do not use optimizations like building an 2d index of all graphical elements used. To improve the performance I would try to
implement hit testing yourself and iterating over all elements
maybe use a 2D index, although for < 1000 elements I hardly believe this is necessary.
Related
In the app I'm trying to develop a key part is getting the position of where the user has touched. First I thought of using a tap gesture recognizer but after a quick google search I learned that was useless (See here for an example).
Then I believe I discovered SkiaSharp and after learning how to use it, at least somewhat, I'm still not sure how I get the proper coordinates of a touch. Here are sections of the code in my project that are relevant to the problem.
Canvas Touch Function
private void canvasView_Touch(object sender, SKTouchEventArgs e)
{
// Only carry on with this function if the image is already on screen.
if(m_isImageDisplayed)
{
// Use switch to get what type of action occurred.
switch (e.ActionType)
{
case SKTouchAction.Pressed:
TouchImage(e.Location);
// Update simply tries to draw a small square using double for loops.
m_editedBm = Update(sender);
// Refresh screen.
(sender as SKCanvasView).InvalidateSurface();
break;
default:
break;
}
}
}
Touch Image
private void TouchImage(SKPoint point)
{
// Is the point in range of the canvas?
if(point.X >= m_x && point.X <= (m_editedCanvasSize.Width + m_x) &&
point.Y >= m_y && point.Y <= (m_editedCanvasSize.Height + m_y))
{
// Save the point for later and set the boolean to true so the algorithm can begin.
m_clickPoint = point;
m_updateAlgorithm = true;
}
}
Here I'm just seeing or TRYING to see if the point clicked was in range of the image and I made a different SkSize variable to help. Ignore the boolean, not that important.
Update function (function that attempts to draw ON the point pressed so it's the most important)
public SKBitmap Update(object sender)
{
// Create the default test color to replace current pixel colors in the bitmap.
SKColor color = new SKColor(255, 255, 255);
// Create a new surface with the current bitmap.
using (var surface = new SKCanvas(m_editedBm))
{
/* According to this: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/finger-paint ,
the points I have to start are in Xamarin forms coordinates, but I need to translate them to SkiaSharp coordinates which are in
pixels. */
Point pt = new Point((double)m_touchPoint.X, (double)m_touchPoint.Y);
SKPoint newPoint = ConvertToPixel(pt);
// Loop over the touch point start, then go to a certain value (like x + 100) just to get a "block" that's been altered for pixels.
for (int x = (int)newPoint.X; x < (int)newPoint.X + 200.0f; ++x)
{
for (int y = (int)newPoint.Y; y < (int)newPoint.Y + 200.0f; ++y)
{
// According to the x and y, change the color.
m_editedBm.SetPixel(x, y, color);
}
}
return m_editedBm;
}
}
Here I'm THINKING that it'll start, you know, at the coordinate I pressed (and these coordinates have been confirmed to be within the range of the image thanks to the function "TouchImage". And when it does get the correct coordinates (or at least it SHOULD of done that) the square will be drawn one "line" at a time. I have a game programming background so this kind of sounds simple but I can't believe I didn't get this right the first time.
Also I have another function, it MIGHT prove worthwhile because the original image is rotated and then put on screen. Why? Well by default the image, after taking the picture, and then displayed, is rotated to the left. I had no idea why but I corrected it with the following function:
// Just rotate the image because for some reason it's titled 90 degrees to the left.
public static SKBitmap Rotate()
{
using (var bitmap = m_bm)
{
// The new ones width IS the old ones height.
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
using (var surface = new SKCanvas(rotated))
{
surface.Translate(rotated.Width, 0.0f);
surface.RotateDegrees(90);
surface.DrawBitmap(bitmap, 0, 0);
}
return rotated;
}
}
I'll keep reading and looking up stuff on what I'm doing wrong, but if any help is given I'm grateful.
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 years ago.
Improve this question
I am coding my own Paint in C# using the System.Drawing namespace and basically everything is going well so far except for one thing, the eraser tool.
Right now I the eraser works just by drawing a line with the same color as the background so it appears like it's erasing something.
However, I want to make a new Eraser tool, perhaps improved. This new Eraser would be able to DELETE an element with a single click if the click is within it's bounds.
I know that if one thing has already been drawn it's there and nothing can be done but I was thinking about creating a string array and I'm going to add new elements to the array. For example when I add a line and a rectangle the first two elements of array would be:
line startPoint endPoint
rectangle height width x y
Something like that. And when the Erase tool is used, just compare the coordinates.
Is there an easier approach to this?
Thanks a lot!
Yes, there is. What you're planning to do is essentially retained mode rendering in a way. You keep a list (or other data structure) of objects that are on the canvas and you can reorder or change that list in any way, e.g. by removing or adding objects. After that you just re-create the drawing, that is, clear your drawing area and then draw each object in your list in order. This is necessary because once you have drawn something you only have pixels, and if your line and rectangle intersect, you may have trouble separating line pixels from rectangle pixels.
With GDI+ this is the only approach, since you don't get much more than a raw drawing surface. However, other things exist which already provide that rendering model for you, e.g. WPF.
However, a string[] is a horrible way of solving this. Usually you would have some kind of interface, e.g.
public interface IShape {
public void Draw(Graphics g);
public bool IsHit(PointF p);
}
which your shapes implement. A line would keep its stroke colour and start/end coordinates as state which it would then use to draw itself in the Draw method. Furthermore, when you want to click on a shape with the eraser, you'd have the IsHit method to determine whether a shape was hit. That way each shape is responsible for its own hit-testing. E.g a line could implement a little fuzziness so that you can click a little next to the line instead of having to hit a single pixel exactly.
That's the general idea, anyway. You could expand this as necessary to other ideas. Note that by using this approach your core code doesn't have to know anything about the shapes that are possible (comparing coordinates can be a bit cumbersome if you have to maintain an ever-growing switch statement of different shapes). Of course, for drawing those shapes you still need a bit more code because lines may need a different interaction than rectangles, ellipses or text objects.
I created a small sample that outlines above approach here. The interesting parts are as follows:
interface IShape
{
Pen Pen { get; set; }
Brush Fill { get; set; }
void Draw(Graphics g);
bool IsHit(PointF p);
}
class Line : IShape
{
public Brush Fill { get; set; }
public Pen Pen { get; set; }
public PointF Start { get; set; }
public PointF End { get; set; }
public void Draw(Graphics g)
{
g.DrawLine(Pen, Start, End);
}
public bool IsHit(PointF p)
{
// Find distance to the end points
var d1 = Math.Sqrt((Start.X - p.X) * (Start.X - p.X) + (Start.Y - p.Y) * (Start.Y - p.Y));
var d2 = Math.Sqrt((End.X - p.X) * (End.X - p.X) + (End.Y - p.Y) * (End.Y - p.Y));
// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
var dx = End.X - Start.X;
var dy = End.Y - Start.Y;
var length = Math.Sqrt(dx * dx + dy * dy);
var distance = Math.Abs(dy * p.X - dx * p.Y + End.X * Start.Y - End.Y * Start.X) / Math.Sqrt(dy * dy + dx * dx);
// Make sure the click was really near the line because the distance above also works beyond the end points
return distance < 3 && (d1 < length + 3 && d2 < length + 3);
}
}
public partial class Form1 : Form
{
private ObservableCollection<IShape> shapes = new ObservableCollection<IShape>();
private static Random random = new Random();
public Form1()
{
InitializeComponent();
pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
shapes.CollectionChanged += Shapes_CollectionChanged;
}
private void Shapes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Redraw();
}
public void Redraw()
{
using (var g = Graphics.FromImage(pictureBox1.Image))
{
foreach (var shape in shapes)
{
shape.Draw(g);
}
}
pictureBox1.Invalidate();
}
private void button1_Click(object sender, EventArgs e)
{
shapes.Add(new Line
{
Pen = Pens.Red,
Start = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height)),
End = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height))
});
}
private void pictureBox1_SizeChanged(object sender, EventArgs e)
{
pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Redraw();
}
private IShape FindShape(PointF p)
{
// Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
foreach (var shape in shapes.Reverse())
{
if (shape.IsHit(p))
return shape;
}
return null;
}
private void button1_MouseClick(object sender, MouseEventArgs e)
{
var shape = FindShape(e.Location);
if (shape != null)
{
shape.Pen = Pens.Blue;
Redraw();
}
}
}
Clicking the button creates a random line and redraws. Redrawing is as simple as
public void Redraw()
{
using (var g = Graphics.FromImage(pictureBox1.Image))
{
foreach (var shape in shapes)
{
shape.Draw(g);
}
}
pictureBox1.Invalidate();
}
Clicking the picture will try finding a shape at the click point, and if it finds one, it colours it blue (along with a redraw). Finding the item works as follows:
private IShape FindShape(PointF p)
{
// Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
foreach (var shape in shapes.Reverse())
{
if (shape.IsHit(p))
return shape;
}
return null;
}
So as you can see, the fundamental parts of actually drawing things and maybe selecting them again, are fairly easy that way. Of course, the different drawing tools are another matter, although that has solutions, too.
When I am drawing line and then iterate in for loop it cause high memory leak then after several line it crashed.
private Pen ReDrawFromGrid(Pen pen)
{
for (int parseDgV = 0; parseDgV < dataGridView1.Rows.Count; parseDgV++)
{
float redrawX1;
float redrawY1;
float redrawX2;
float redrawY2;
float.TryParse(dataGridView1.Rows[parseDgV].Cells[0].Value.ToString(), out redrawX1);
float.TryParse(dataGridView1.Rows[parseDgV].Cells[1].Value.ToString(), out redrawY1);
float.TryParse(dataGridView1.Rows[parseDgV].Cells[2].Value.ToString(), out redrawX2);
float.TryParse(dataGridView1.Rows[parseDgV].Cells[3].Value.ToString(), out redrawY2);
if (dataGridView1.Rows[parseDgV].Cells[5].Value.ToString() == "Solid")
{
dashRedraw = new float[1] { 10 };
}
else if (dataGridView1.Rows[parseDgV].Cells[5].Value.ToString() == "Dash")
{
dashRedraw = new float[2] { 10, 10 };
}
else if (dataGridView1.Rows[parseDgV].Cells[5].Value.ToString() == "Dot")
{
dashRedraw = new float[2] { 3, 5 };
}
else
{
dashRedraw = new float[1] { 10 };
}
pen = new Pen(dataGridView1.Rows[parseDgV].Cells[4].Style.BackColor);
pen.DashPattern = dashRedraw;
g.DrawLine(pen, redrawX1, redrawY1, redrawX2, redrawY2);
this.Refresh();
this.axDNVideoX1.Invalidate();
}
GC.Collect();
return pen;
}
I tried to put dispose before and after in this for loop but it resulted no line drawn. How can I iterate draw line without causing memory leak?
Thanks for your help!
Here's a quick re-write of the basic drawing code with some minor tweaks:
static float[] patSolid = new float[] { 10 };
static float[] patDash = new float[] { 10, 10 };
static float[] patDot = new float[] { 3, 5 };
private void RedrawFromGrid(Graphics g)
{
float X1, Y1, X2, Y2;
for (int i = 0; i < dataGridView1.Rows.Count; i++)
{
var cells = dataGridView1.Rows[i].Cells;
if (!float.TryParse(cells[0].Value.ToString(), out X1) ||
!float.TryParse(cells[1].Value.ToString(), out Y1) ||
!float.TryParse(cells[2].Value.ToString(), out X2) ||
!float.TryParse(cells[2].Value.ToString(), out Y2)
)
continue;
var style = cells[5].Value.ToString();
float[] pattern = patSolid;
if (style == "Dash")
pattern = patDash;
else if (style == "Dot")
pattern = patDot;
using (var pen = new Pen(cells[4].Style.BackColor))
{
pen.DashPattern = pattern;
g.DrawLine(pen, X1, Y1, X2, Y2);
}
}
}
It's a bit dirty still, since I wanted to keep the basic functionality close to where you had it.
A few refinements:
The only thing that needs to be allocated inside the loop is the pen object, and you could get rid of that too by caching the pens and reusing them. But since we're doing allocation each time, the pen needs to be disposed of when you're done with it. The using statement does this for you. You should be doing the same for the Graphics object in the method that calls this.
Second: don't allocate the same arrays over and over, allocate them once and reuse them. You already know ahead of time what patterns you are going to be using, so pre-allocating the pattern arrays is a good idea. Especially if this is getting called 30 times per second by your video code or similar.
And finally, this method should have as few side effects as possible. The method that calls it should take care of the side effects - calling this.Refresh() or this.axDNVideoX1.Invalidate() after disposing of the Graphics object when all the drawing is done.
In general a drawing operation should be as fast - and have as few side effects - as it is possible to make it. Allocating arrays, calling Invalidate or Refresh, etc. are for the caller to do. Don't let your drawing method do anything but draw.
edit:
Incidentally, calling GC.Collect() will not solve a memory leak. In fact it's quite likely that it's not going to do anything at all in this context, since none of the allocations happening inside this loop will have reached termination anyway. When you find yourself directly invoking the garbage collector it is almost always a mistake.
First, the parameter "pen" does not have sense to me. You are passing a Pen to your method and then in your method you are creating a new instance of the Pen and then you are returning a Pen. I would cache all required Pen instances and reuse them. Once you don't need a particular Pen any more, Dispose it.
The only 'Memory leak' worrying part of the code here that I see is a 'axDNVideoX1' which is it seems an ActiveX control and it has to do something with Video. Try to comment out that part of the code and see if you still have a memory leak.
I would not say that the 'DrawLine' causes a memory leak.
Keep your eye on the count of the 'GDI objects' created and destroyed. You can see this value in the Task Manager.
You are creating a Pen per loop iteration. You need to dispose those objects properly. The pen you pass to the function is lost, the pens you create in the loop are lost except for the last which is returned.
using blocks are your friend. Use them.
I'm hoping someone can help me out here. My ultimate goal with this code is to extract the color of the sweater I am wearing. Like the title suggests, I'm trying to exctract RBG values from a certain Skeleton point (ie. skeleton.Joint[JointType.Spine].Position). I do this using the following mapping:
All of the following code is within the SensorAllFramesReady event:
private void SensorAllFramesReady(object sender, AllFramesReadyEventArgs e)
{
Skeleton[] skeletons = new Skeleton[0];
using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
{
if (skeletonFrame != null)
{
skeletons = new Skeleton[skeletonFrame.SkeletonArrayLength];
skeletonFrame.CopySkeletonDataTo(skeletons);
}
}
if (skeletons.Length != 0)
{
foreach (Skeleton skel in skeletons)
{
if (skel.TrackingState == SkeletonTrackingState.Tracked)
{
colorPoint = this.SkeletonPointToColor(skel.Joints[JointType.Spine].Position);
}
}
}
}
private Point SkeletonPointToColor(SkeletonPoint skelpoint)
{
ColorImagePoint colorPoint = this.sensor.CoordinateMapper.MapSkeletonPointToColorPoint(skelpoint, ColorImageFormat.RgbResolution640x480Fps30);
return new Point(colorPoint.X, colorPoint.Y);
}
I assign the returned Point to a variable "ColorPoint", and here is how I (somewhat successful) extract the RBG values:
using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
{
if (colorFrame != null)
{
int arrayLength = colorFrame.PixelDataLength;
this.colorPixelData = new byte[arrayLength];
colorFrame.CopyPixelDataTo(this.colorPixelData);
blue = (int)colorPixelData[(int)(4* (colorPoint.X + (colorPoint.Y * colorFrame.Width)))+0];
green = (int)colorPixelData[(int)(4 * (colorPoint.X + (colorPoint.Y * colorFrame.Width))) + 1];
red = (int)colorPixelData[(int)(4 * (colorPoint.X + (colorPoint.Y * colorFrame.Width))) + 2];
}
}
I then draw an ellipse on my Windows Form window using the retrieved RBG values. Now this works, kind of. I do get a color which resembles the color of the sweater I'm wearing, but even if I do my best to stand very still the color is always changing. It's almost as if I'm getting random RBG values within a certain range, and only the range is dictated by the color of my sweater. Why is this? Is there another way I should be solving this problem?
Thank you for reading!
EDIT: I apologise for the formatting, this is my firs time submitting a question and I realise the formatting in the first code block is a bit off. The SkeletonPointToColor method is naturally not within the SensorAllFramesReady method. My apologies
First of all I really recomend you have a look at the Coding4Fun Kinect Toolkit which provides many useful functions for dealing with the Kinect data. There is a for instance an extension for returning a bitmap named ToBitmapSource() which should be of use.
Another observation is that without using some algorithm to get an average of the color values received it's quite normal to have the color values jump around. Also not confident that you can expect the skeleton and image frames to be 100% in sync.
First time I ever ask a question here so correct me if I´m doing it wrong.
Picture of my chess set:
Every time I move a piece it lags for about 1 second. Every piece and tile has an Image and there is exactly 96 Images. Every time I move a piece it clears everything with black and then update the graphics.
In the early stages of the chess I didn't have any Images and used different colors instead and only a few pieces there was no noticeable lag and the piece moved in an instant.
public void updateGraphics(PaintEventArgs e, Graphics g, Bitmap frame)
{
g = Graphics.FromImage(frame);
g.Clear(Color.Black);
colorMap(g);
g.Dispose();
e.Graphics.DrawImageUnscaled(frame, 0, 0);
}
The function colorMap(g) looks like this:
private void colorMap(Graphics g)
{
for (int y = 0; y < SomeInts.amount; y++)
{
for (int x = 0; x < SomeInts.amount; x++)
{
//Tiles
Bundle.tile[x, y].colorBody(g, x, y);
//Pieces
player1.colorAll(g);
player2.colorAll(g);
}
}
}
The colorAll function executes every pieces colorBody(g) function which look like this:
public void colorBody(Graphics g)
{
//base.colorBody() does the following: body = new Rectangle(x * SomeInts.size + SomeInts.size / 4, y * SomeInts.size + SomeInts.size / 4, size, size);
base.colorBody();
if (team == 1)
{
//If its a white queen
image = Image.FromFile("textures/piece/white/queen.png");
}
if (team == 2)
{
//If its a black queen
image = Image.FromFile("textures/piece/black/queen.png");
}
g.DrawImage(image, body);
}
and finaly the function that moves the piece:
public void movePiece(MouseEventArgs e)
{
for (int y = 0; y < SomeInts.amount; y++)
{
for (int x = 0; x < SomeInts.amount; x++)
{
if (Bundle.tile[x, y].body.Contains(e.Location))
{
//Ignore this
for (int i = 0; i < queens.Count; i++)
{
Queen temp = queens.ElementAt<Queen>(i);
temp.move(x, y);
}
//Relevant
player1.move(x, y);
player2.move(x, y);
}
}
}
}
Thank you for reading all this! I could make a link to the whole program if my coding examples is not enough.
You're calling Image.FromFile in every refresh, for every image - effectively reloading every image file every time from disk.
Have you considered loading the images once, and storing the resulting Images somewhere useful? (say, an array, Image[2,6] would be adequate)
Why do you redraw the board each time? Can't you just leave the board where it is and display an image with transparent background over it? That way you have one image as a background (the board), plus 64 smaller images placed over the board in a grid and just change the image being displayed on each move.
That way, you can let windows handle the drawing...
Also, load the images of the pieces at the start of the application.
In addition to not calling Image.FromFile() inside updateGraphics() (which is definitely your biggest issue), you shouldn't be attempting to redraw the entire board every on every call to updateGraphics() - most of the time, only a small portion of the board will be invalidated.
The PaintEventArgs contains an parameter, ClipRectangle, which specifies which portion of the board needs redrawing. See if you can't figure out which tiles intersect with that rectangle, and only redraw those tiles :)
Hint: Write a function Point ScreenToTileCoords(Point) which takes a screen coordinate and returns which board-tile is at that coordinate. Then the only tiles you need to redraw are
Point upperLeftTileToBeDrawn = ScreenToTileCoords(e.ClipRectangle.Left, e.ClipRectangle.Top);
Point lowerRightTileToBeDrawn = ScreenToTileCoords(e.ClipRectangle.Right - 1, e.ClipRectangle.Bottom- 1);
Also, make sure your control is double-buffered, to avoid tearing. This is much simpler than #Steve B's link in the comments above states; assuming this is a UserControl, simply set
this.DoubleBuffered = true;
Well, what about this:
Do not clear the whole board but only those parts that need to be cleared.
Alternative:
Update to WPF - it moves drawing to the graphics card - and just move pieces around, in a smart way (i.e. have a control / object for every piece).