Cropping a cross rectangle from image using c# - c#

What I want to do is basically cropping a rectangle from an image. However, it should satisfy some special cases:
I want to crop an angled rectangle on image.
I don't want to rotate the image and crop a rectangle :)
If cropping exceeds the image size, I don't want to crop an empty background color.
I want to crop from back of the starting point, that will end at starting point when rectangle size completed. I know I couldn't explain well so if I show what I want visually:
The blue dot is the starting point there, and the arrow shows cropping direction. When cropping exceeds image borders, it will go back to the back of the starting point as much as, when the rectangle width and height finished the end of the rectangle will be at starting point.
Besides this is the previous question I asked:
How to crop a cross rectangle from an image using c#?
In this question, I couldn't predict that a problem can occur about image dimensions so I didn't ask for it. But now there is case 3. Except case three, this is exactly same question. How can I do this, any suggestions?

What needs to be done is to add offsets to the matrix alignment. In this case I am taking one extra length of the rectangle from each side (total 9 rectangles) and offsetting the matrix each time.
Notice that it is necessary to place offset 0 (the original crop) last, otherwise you will get the wrong result.
Also note that if you specify a rectangle that is bigger than the rotated picture you will still get empty areas.
public static Bitmap CropRotatedRect(Bitmap source, Rectangle rect, float angle, bool HighQuality)
{
int[] offsets = { -1, 1, 0 }; //place 0 last!
Bitmap result = new Bitmap(rect.Width, rect.Height);
using (Graphics g = Graphics.FromImage(result))
{
g.InterpolationMode = HighQuality ? InterpolationMode.HighQualityBicubic : InterpolationMode.Default;
foreach (int x in offsets)
{
foreach (int y in offsets)
{
using (Matrix mat = new Matrix())
{
//create the appropriate filler offset according to x,y
//resulting in offsets (-1,-1), (-1, 0), (-1,1) ... (0,0)
mat.Translate(-rect.Location.X - rect.Width * x, -rect.Location.Y - rect.Height * y);
mat.RotateAt(angle, rect.Location);
g.Transform = mat;
g.DrawImage(source, new Point(0, 0));
}
}
}
}
return result;
}
To recreate your example:
Bitmap source = new Bitmap("C:\\mjexample.jpg");
Bitmap dest = CropRotatedRect(source, new Rectangle(86, 182, 87, 228), -45, true);

Related

Resizing a Bitmap used as watermark the result shows dark borders

Problem:
I have a watermark that I want to print on an Image. The Image varies in size so sometimes the watermark is too big and sometimes it's too small. In order to fix this I calculate the size of the image and resize the watermark. However, after resizing the Image, black borders appear around its margins.
Code
I am on a Mac using .NET Core3.1 and I am using two NuGet packages that helps to draw images / bitmaps. One is System.Drawing.Common and the other one, because I am on macOS is, runtime.osx.10.10x64.CoreCompat.System.Drawing.
The code that I use to resize the watermark founded here:
Bitmap watermarkNew = new Bitmap(watermark, new Size(image.Width / 10 * 3, image.Height / 10 * 3));
I have to use / 10 * 3 because the Bitmap constructor doesn't accept floats values, so I cannot multiply by * 0.3.
Results:
watermark before watermark after
To superimpose an Image on another, it's preferable to use an unscaled Image than generate a new Bitmap based on the desired size beforehand.
▶ The two Image are meant to blend, thus the scaling of one of the Images, in this case the Watermark Image, should be performed while the Image to scale is painted over the other with a SourceOver operation.
This way, the internal GDI+ (well, the GDI+ replica here) functions have means to calculate the blending procedure correctly.
This also prevents the copy to show imperfect semi-transparent pixels (similar to a dark halo) generated when a smaller Image is created using the new Bitmap() method.
▶ Also, we need to be sure that all operations are performed on a 32BitArgb Bitmaps.
It's better to create a 32BitArgb copy of the destination Image and draw the watermark on this copy. This can also ensure a better result. GDI+ function work better on this kind of Images.
Here, the CopyToArgb32() method takes care of this aspect, also applying the DPI resolution of the original Image to the copy.
▶ Furthermore, this produces a distorted Image (unless that's the expected result, that is):
Bitmap watermarkNew = new Bitmap(watermark, new Size(image.Width / 10 * 3, image.Height / 10 * 3));
The watermark Image dimensions should be resized calculating a scale factor that is a desired fraction (a percentage or a fixed measure) or the destination Image.
For example, to occupy a maximum size equals to one third of the destination Bitmap minimum dimension.
In other words, if the destination Bitmap size is 1500x600 px, the watermark Bitmap will be scaled proportionally to have a maximum Height of 200px:
float scale = (Math.Min(original.Width, original.Height) * .33f) /
Math.Min(watermark.Width, watermark.Height);
SizeF watermarkSize = new SizeF(watermark.Width * scale, watermark.Height * scale);
To further improve the blending, the Watermark could be made less opaque (or, more transparent, as you want to see it).
This can be simply achieved using as ColorMatrix as shown here:
How to apply a fade transition effect to Images
All combined in a class object that exposes a Watermark([Bitmap], [Bitmap], [Imageformat]) static method.
In the sample code, the Watermark is scaled to 1/3 of the maximum dimension of destination image and centered (just a generic placement, since the position of the watermark is not specified):
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
public class BitmapOperations
{
public static Bitmap Watermark(Bitmap watermark, Bitmap original, ImageFormat format)
{
var units = GraphicsUnit.Pixel;
float scale = (Math.Max(original.Width, original.Height) * .33f) /
Math.Max(watermark.Width, watermark.Height);
var watermarkSize = new SizeF(watermark.Width * scale, watermark.Height * scale);
var watermarkBounds = CenterRectangleOnRectangle(
new RectangleF(PointF.Empty, watermarkSize), original.GetBounds(ref units));
var workImage = CopyToArgb32(original);
// Using the SetOpacity() extension method described in the linked question
// watermark = watermark.SetOpacity(.5f, 1.05f);
using (var g = Graphics.FromImage(workImage)) {
g.PixelOffsetMode = PixelOffsetMode.Half;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.DrawImage(watermark, watermarkBounds);
return workImage;
}
}
private static Bitmap CopyToArgb32(Bitmap source)
{
var bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
bitmap.SetResolution(source.HorizontalResolution, source.VerticalResolution);
using (var g = Graphics.FromImage(bitmap)) {
g.DrawImage(source, new Rectangle(0, 0, bitmap.Width, bitmap.Height),
new Rectangle(0, 0, bitmap.Width, bitmap.Height), GraphicsUnit.Pixel);
g.Flush();
}
return bitmap;
}
private static RectangleF CenterRectangleOnRectangle(RectangleF source, RectangleF destination)
{
source.Location = new PointF((destination.Width - source.Width) / 2,
(destination.Height - source.Height) / 2);
return source;
}
}
Results:
Applying an opacity level of 50% and small correction in gamma:

Using Win2D, how to crop an image to a circle

It seems like this should be simple enough, but I'm really struggling with finding any documentation on how I can do this. I'm simply looking to crop an image to turn a square into a circle.
There is a lot of discussion about it, but I can't seem to find a good example of how to do this using UWP/Win2D.
Here is a bit of code to illustrate the issue I was trying to describe in my comments:
// draw a 10x10 grid of circles
var bitmap = await CanvasBitmap.LoadAsync(sender, "Assets/ice.png"); // hex-shaped image is 250x220 pixels
var brush = new CanvasImageBrush(sender, bitmap);
for (var i = 0; i < 10; i++)
{
for (var j = 0; j < 10; j++)
{
//_drawingSession.FillCircle(new Vector2(i * 50, j * 50), (float)(25), Colors.Blue);
_drawingSession.FillCircle(new Vector2(i * 50, j * 50), (float)(25), brush);
}
}
The image below shows how the brush is being cut from the same x/y coordinates based on the vector where the target circle is to be drawn.
Note: the same effect occurs with FillEllipse().
You can try to use CanvasImageBrush and CanvasDrawingSession.FillEllipse Method achieve it.
private async void canvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.CanvasControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasDrawEventArgs args)
{
using (CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(sender, "Assets/image.jpg"))
{
CanvasImageBrush canvasImageBrush = new CanvasImageBrush(sender, bitmap);
args.DrawingSession.FillEllipse(new System.Numerics.Vector2(100f), 100, 100, canvasImageBrush);
}
}
------------ Update -------------
If you want to cut a circle out of the image source, you can configure the CanvasImageBrush.Transform property to scale the image, then cut the circle and display it on the canvas.
private async void canvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.CanvasControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasDrawEventArgs args)
{
using (CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(sender, "Assets/image.jpg"))
{
CanvasImageBrush canvasImageBrush = new CanvasImageBrush(sender, bitmap);
System.Numerics.Vector2 center = new System.Numerics.Vector2((float)(bitmap.Size.Width / 2),
(float)(bitmap.Size.Height / 2));
canvasImageBrush.Transform = System.Numerics.Matrix3x2.CreateScale(0.5F, center);
args.DrawingSession.FillEllipse(center, 160, 160, canvasImageBrush);
}
}
You should change some parameters in my above code to satisfy your requirement, such as the scale in the Matrix3x2.CreateScale method.
Okay, after chatting with one of the fellows on the GitHub Win2D project, I finally have a clear answer on how this works - and it works nothing like I would have expected it to work.
First, the bitmap brush image is by default being positioned at 0,0 on the canvas.
In my case, I wanted to cut a circle from the image and draw it someplace else on the canvas. This requires 2 separate bits of math.
First, you need to position the bitmap's top-left-corner (TLC) to where you want the circle to be drawn. This is done by setting the brush's Transform property. In my example, I'm setting the image TLC to 300/300;
// create the brush
var brush = new CanvasImageBrush(sender, _tiles[1]);
brush.Transform = Matrix3x2.CreateTranslation(300, 300);
Now, to cut/draw the circle using the brush image, I have to describe where the center of the image is to be on the canvas. My image is 250x220.
// 300+250/2, 300+220/2 = 425, 410
_args.DrawingSession.FillCircle(new Vector2(425, 410), (float)(110), brush);
This gives the effect of cutting a circle out of my original bitmap and drawing it on the canvas at the desired location.
Hopefully this is clear enough. I know I certainly struggled to find the answer.

C# LinearGradientBrush Bitmap repeating Vertically

I am playing around with the Microsoft Vision API and learning C# as I go, and one of the properties of a Vision object is an "Accent Color" of the image.
From a series of images analysed, I want to show those colors ordered in a Linear Gradient -- because that will be able to show the user that most pictures are (for example) blue, because Blue colors take up half of the gradient etc.
I have this working, in that I am ordering the Colors by Hue, and able to produce a Linear Gradient I am filling into a Bitmap.
BUT, the gradient by default is Horizontal, and I need Vertical -- so I've used LinearGradientBrush.RotateTransform(90) which rotates that actual gradient fine, but doesn't seem to fill the entire Rectangle, and it repeats. This is what I'm getting as a result:
How do I create a Vertical LinearGradient that fills up the entire Height of the Rectangle object for my Bitmap?
Here is my code:
private Bitmap CreateColorGradient(System.Drawing.Rectangle rect, System.Drawing.Color[] colors)
{
Bitmap gradient = new Bitmap(rect.Width, rect.Height);
LinearGradientBrush br = new LinearGradientBrush(rect, System.Drawing.Color.White, System.Drawing.Color.White, 0, false);
ColorBlend cb = new ColorBlend();
// Positions
List<float> positions = new List<float>();
for (int i = 0; i < colors.Length; i++) positions.Add((float)i / (colors.Length - 1));
cb.Positions = positions.ToArray();
cb.Colors = colors;
br.InterpolationColors = cb;
br.RotateTransform(90);
using (Graphics g = Graphics.FromImage(gradient))
g.FillRectangle(br, rect);
return gradient;
}
Thanks for reading and any help -- also if you see something in my code that could be done better please point it out it helps me learn :)
You are ignoring the angle parameter in the constructor. And as you instead do a rotation on the Grahics object your brush rectangle is no longer correctly fits the target bitmap and the gradient can't fill it; so it repeats.
To correct
simply set the angle to 90 and
remove the br.RotateTransform(90); call.
Here this changes the result from the left to the middle version:
While we're looking at it, do take note of the WrapMode property of LinearGradientBrush. What you see in the first image is the default WrapMode.Clamp. Often a changing to one of the Flip mode helps.. So lets have a look at the impact of it one the first version at the right position.
It looks like WrapMode.TileFlipY but since I have brought back the rotation it actually takes a value WrapMode.TileFlipX or WrapMode.TileFlipXY:
br.WrapMode = WrapMode.TileFlipX;

Scale x and y coordinates to Form/pictureBox location. C#

My Form2 is in layman terms an external 'mini-map' off the actual 'mini-map' in-game.
As you can see on my Form2, my drawn red dot does not have the same location for my player when compared to the 'mini-map' in-game which is the yellow dot.
In the DebugView, you can see my characters X and Y location (charX & charY).
The coordinates are passed as int x & int y in a function to my Form2 class file.
The image in my pictureBox1 (which is the image in the current example picture above) is pulled from my server (url= "http://randomspam.co/MAP/103000000.img/miniMap.canvas.png").
Here is the following code with comments to my progress as of now.
Please take note that the pictureBox1 location is set to 0,0.
The errors are as follows;
1) The red dot location on my external mini-map != the location of my character in the mini-map in-game.
2) The red dot consistently flickers (appears & disappears)
3) The tooltip when shown on the pictureBox is really lagging in revealing and dis-revealing itself.
If anyone knows how to help out my current situation (as I am lost), please, anything is appreciated.
Thanks.
Ok, lets split this in topics:
1) Red Dot Location:
Here you have to match red dot position to the new size, this was answered several times before, see this -> How can I transform XY coordinates and height/width on a scaled image to an original sized image?
2) Double Buffer to stop Flickering:
public void DrawWhatever(Graphics graphics, int cx, int cy)
{
Graphics g;
Bitmap buffer = null;
buffer = new Bitmap([image width], [image height], graphics);
g = Graphics.FromImage(buffer);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
// Draw a circle.
Pen p = new Pen(Color.Red,1)
g.DrawEllipse(p,cx,cy,30,30); //example values
graphics.DrawImage(buffer, 0, 0);
g.Dispose();
}
3) Tooltip:
Check double buffer algorithm and let me know
Have a copy from the mini map:
Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Bitmap bmpClone = new Bitmap(pictureBox1.Width, pictureBox1.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Graphics objGraphics = Graphics.FromImage(bmpClone);
objGraphics.DrawImage(pictureBox1.Image, 0, 0);
objGraphics.Dispose();
bmp = (Bitmap)bmpClone.Clone();
pictureBox1.Image = bmp;
Now before any invalidation do:
Graphics objGraphics = Graphics.FromImage(bmp);
objGraphics.SmoothingMode = SmoothingMode.HighQuality;
objGraphics.DrawImage(bmpClone, 0, 0);
objGraphics.FillEllipse(Brushes.Red, cx, cy, 5, 5)
objGraphics.Dispose();
pictureBox1.Invalidate();
You dont need anything inside pictureBox1_Paint
valter

Graphics.DrawImage alternatives for large images

I am trying to draw a crosshair ("plus sign") with inverted colors over an image to show the location of a selected point within the image. This is how I do it:
private static void DrawInvertedCrosshair(Graphics g, Image img, PointF location, float length, float width)
{
float halfLength = length / 2f;
float halfWidth = width / 2f;
Rectangle absHorizRect = Rectangle.Round(new RectangleF(location.X - halfLength, location.Y - halfWidth, length, width));
Rectangle absVertRect = Rectangle.Round(new RectangleF(location.X - halfWidth, location.Y - halfLength, width, length));
ImageAttributes attributes = new ImageAttributes();
float[][] invertMatrix =
{
new float[] {-1, 0, 0, 0, 0 },
new float[] { 0, -1, 0, 0, 0 },
new float[] { 0, 0, -1, 0, 0 },
new float[] { 0, 0, 0, 1, 0 },
new float[] { 1, 1, 1, 0, 1 }
};
ColorMatrix matrix = new ColorMatrix(invertMatrix);
attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
g.DrawImage(img, absHorizRect, absHorizRect.X, absHorizRect.Y, absHorizRect.Width, absHorizRect.Height, GraphicsUnit.Pixel, attributes);
g.DrawImage(img, absVertRect, absVertRect.X, absVertRect.Y, absVertRect.Width, absVertRect.Height, GraphicsUnit.Pixel, attributes);
}
It works as expected, however, it is really slow. I want the user to be able to move the selected location around with their mouse by setting the location to the cursor's location whenever it moves. Unfortunately, on my computer, it can update only around once per second for big images.
So, I am looking for an alternative to using Graphics.DrawImage to invert a region of an image. Are there any ways to do this with speeds proportional to the selected region area rather than the entire image area?
Sounds to me you are focusing on the wrong problem. Painting the image is slow, not painting the "cross-hairs".
Large images can certainly be very expensive when you don't help. And System.Drawing makes it very easy to not help. Two basic things you want to do to make the image paint faster, getting it more than 20 times faster is quite achievable:
avoid forcing the image painting code to rescale the image. Instead do it just once so the image can be drawn directly one-to-one without any rescaling. Best time to do so is when you load the image. Possibly again in the control's Resize event handler.
pay attention to the pixel format of the image. The fastest one by a long shot is the pixel format that's directly compatible with the way the image needs to be stored in the video adapter. So the image data can be directly copied to video RAM without having to adjust each individual pixel. That format is PixelFormat.Format32bppPArgb on 99% of all modern machines. Makes a huge difference, it is ten times faster than all the other ones.
A simple helper method that accomplishes both without otherwise dealing with the aspect ratio:
private static Bitmap Resample(Image img, Size size) {
var bmp = new Bitmap(size.Width, size.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
using (var gr = Graphics.FromImage(bmp)) {
gr.DrawImage(img, new Rectangle(Point.Empty, size));
}
return bmp;
}
Draw the image once on Graphics g, then draw the crosshair on Graphics g directly instead of the image. You can optionally keep track of the places the user clicked so as to save them either in the image or elsewhere as needed.

Categories

Resources