Rotate photo with SkiaSharp - c#

I'm trying to rotate photo with SkiaSharp to 90 degrees with following code:
public SKBitmap Rotate()
{
var bitmap = SKBitmap.Decode("test.jpg");
using (var surface = new SKCanvas(bitmap))
{
surface.RotateDegrees(90, bitmap.Width / 2, bitmap.Height / 2);
surface.DrawBitmap(bitmap.Copy(), 0, 0);
}
return bitmap;
}
But when I save bitmap to JPEG file, it has margins both on top and bottom of image.
Original image: http://imgur.com/pGAuko8.
Rotated image: http://imgur.com/bYxpmI7.
What am I doing wrong?

You may want to do something like this:
public static SKBitmap Rotate()
{
using (var bitmap = SKBitmap.Decode("test.jpg"))
{
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
using (var surface = new SKCanvas(rotated))
{
surface.Translate(rotated.Width, 0);
surface.RotateDegrees(90);
surface.DrawBitmap(bitmap, 0, 0);
}
return rotated;
}
}
The reason for this (or yours not working as expected) is that you are rotating the bitmap on itself. You have basically taken an image, and then made a copy on draw it onto the first image. Thus, you still have the margins from the image below.
What I did was to create a NEW bitmap and then draw the decoded bitmap onto that.
The second "issue" is that you are rotating the image, but you are not changing the canvas dimensions. If the bitmap is 50x100, and then you rotate 90 degrees, the bitmap is now 100x50. As you can't actually change the dimensions of a bitmap once created, you have to create a new one. You can see this in the output image as it is actually cropped off a bit.
Hope this helps.

Matthew's solution works for me too, but i had an issue when i tried to rotate bitmaps more than 90° or -90° (bitmap was drawed "out of display"). I highly recommend using this solution. Slightly modified result:
public static SKBitmap Rotate(SKBitmap bitmap, double angle)
{
double radians = Math.PI * angle / 180;
float sine = (float)Math.Abs(Math.Sin(radians));
float cosine = (float)Math.Abs(Math.Cos(radians));
int originalWidth = bitmap.Width;
int originalHeight = bitmap.Height;
int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);
var rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);
using (var surface = new SKCanvas(rotatedBitmap))
{
surface.Translate(rotatedWidth / 2, rotatedHeight / 2);
surface.RotateDegrees((float)angle);
surface.Translate(-originalWidth / 2, -originalHeight / 2);
surface.DrawBitmap(bitmap, new SKPoint());
}
return rotatedBitmap;
}
In my case I used this when I needed rotate picture on the Xamarin.iOS platform (who ever tried this, knows), works like a charm.

Related

Properly set affine matrix and draw it in SkiaSharp

I am trying to scale and skew a bitmap in SkiaSharp with an affine matrix, however; the results always cut part of the resulting bitmap. I am also not sure if my affine matrix has the correct values.
Here is a diagram of what I am trying to accomplish: on the left is the original image. It has a bitmap size of (178x242). On the right is the scaled and skewed image. The bounding box is (273x366), I also know that the the x scale has been skewed -10 pixels and the y scale has been skewed 7 pixels.
Here if my code for applying the affine matrix:
public SKBitmap ApplyAffine(SKBitmap origBitmap, SKSizeI newSize, SKPointI xyRotation)
{
var skewX = 1f / xyRotation.X;
var skewY = 1f / xyRotation.Y;
// Scale transform
var scaleX = (newSize.Width / (float)origBitmap.Width);
var scaleY = (newSize.Height / (float)origBitmap.Height);
// Affine transform
SKMatrix affine = new SKMatrix
{
ScaleX = scaleX,
SkewY = skewY,
SkewX = skewX,
ScaleY = scaleY,
TransX = 0,
TransY = 0,
Persp2 = 1
};
var bitmap = origBitmap.Copy();
var newBitmap = new SKBitmap(newSize.Width, newSize.Height);
using (var canvas = new SKCanvas(newBitmap))
{
canvas.SetMatrix(affine);
canvas.DrawBitmap(bitmap, 0, 0);
canvas.Restore();
}
return newBitmap;
}
The resulting bitmap has the left side cut off. It also appears that it is not translated correctly. How do I properly apply this affine?
If I understood you right and the xyRotation is what I think it is from your description, then I think you were pretty close to the solution :)
public SKBitmap ApplyAffine(SKBitmap origBitmap, SKSizeI newSize, SKPointI xyRotation)
{
// mcoo: skew is the tangent of the skew angle, but since xyRotation is not normalized
// then it should be calculated based on original width/height
var skewX = (float)xyRotation.X / origBitmap.Height;
var skewY = (float)xyRotation.Y / origBitmap.Width;
// Scale transform
// mcoo (edit): we need to account here for the fact, that given skew is known AFTER the scale is applied
var scaleX = (float)(newSize.Width - Math.Abs(xyRotation.X)) / origBitmap.Width;
var scaleY = (float)(newSize.Height - Math.Abs(xyRotation.Y)) / origBitmap.Height;
// Affine transform
SKMatrix affine = new SKMatrix
{
ScaleX = scaleX,
SkewY = skewY,
SkewX = skewX,
ScaleY = scaleY,
//mcoo: we need to account for negative skew moving image bounds towards negative coords
TransX = Math.Max(0, -xyRotation.X),
TransY = Math.Max(0, -xyRotation.Y),
Persp2 = 1
};
var bitmap = origBitmap.Copy();
var newBitmap = new SKBitmap(newSize.Width, newSize.Height);
using (var canvas = new SKCanvas(newBitmap))
{
// canvas.Clear(SKColors.Red);
canvas.SetMatrix(affine);
canvas.DrawBitmap(bitmap, 0, 0);
}
return newBitmap;
}
Now calling ApplyAffine(skBitmap, new SKSizeI(273, 366), new SKPointI(-10,7)) on image of size 178x242 yields somewhat correct result (red background added for reference):

DrawImage resized image too small

When I draw an image using Graphics.DrawImage and draw it at a bigger size than the original image, it ends up being a bit too small. You can see this in the following picture:
The green lines shouldn't be visible and are not part of the image. Rather they get drawn behind the image and the image should cover them.
How can I draw an image with the exact right size?
EDIT: I draw the green part with the same rectangle I pass into the DrawImage call, with the exact dimensions of how big the image should be. So no flaw in my values (I think).
EDIT 2: I draw the green rectangle using FillRectangle, so no pen calculations need to be done. Also, I logged the values that I pass into the rectangle for both the image and the green fill, and the values are correct. It's just the image that's off. I will post code later, as I'm not at my computer at the moment.
EDIT 3: This is the code I use to render the images:
// This is for zooming
public readonly float[] SCALES = { 0.05f, 0.1f, 0.125f, 0.25f, 0.333f, 0.5f, 0.667f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.5f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f, 12.0f, 15.0f, 20.0f, 30.0f, 36.0f };
private int scaleIndex = 8;
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
float ScaleFactor = SCALES[scaleIndex];
e.Graphics.InterpolationMode = ScaleFactor < 1 ? InterpolationMode.Bicubic : InterpolationMode.NearestNeighbor;
Image im = Properties.Resources.TSprite0;
for (int y = 0; y < TilesVertical; y++)
{
for (int x = 0; x < TilesHorizontal; x++)
{
float sx = im.Width * ScaleFactor;
float sy = im.Height * ScaleFactor;
Point p = new Point((int)(-scrollPosition.X + sx * x), (int)(-scrollPosition.Y + sy * y));
Size s = new Size((int)Math.Floor(sx), (int)Math.Floor(sy));
// The green rectangle in the background should be the same size as the image
e.Graphics.FillRectangle(Brushes.Lime, new Rectangle(p, s));
e.Graphics.DrawImage(im, new Rectangle(p, s), 0, 0, 16, 16, GraphicsUnit.Pixel);
}
}
im.Dispose();
}
EDIT 4: Also note that the image seems to be cropped on the left and top instead of resized. Take a look at this comparison of the original image upscaled in Photoshop and then how GDI+ renders it:
The issue happens when scaling to 2x or larger.
Looks like the whole problem is caused by the wrong default PixelOffsetMode.
By offsetting pixels during rendering, you can improve render quality
at the cost of render speed.
Setting it to
g.PixelOffsetMode = PixelOffsetMode.Half;
makes it go away for me.
Setting it to
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
also works fine.
Default, None and HighSpeed cause the image to be rendered a little to the left and up.
Often you will also want to set InterpolationMode.NearestNeighbor.

Rotating image around center C#

I have a small error when trying to rotate an image within a picture box.
It all works. But when rotating, it doesn't rotate perfectly around the center. It's slightly off (not very noticeable) but kinda annoying. Here is my code:
private readonly Bitmap _origPowerKnob = Properties.Resources.PowerKnob;
//CODE WHERE ROTATE METHOD IS CALLED//
using (Bitmap b = new Bitmap(_origPowerKnob))
{
Bitmap newBmp = RotateImage(b, _powerAngle);
PowerKnob.BackgroundImage = newBmp;
}
private Bitmap RotateImage(Bitmap b, float angle)
{
//Create a new empty bitmap to hold rotated image.
Bitmap returnBitmap = new Bitmap(b.Width, b.Height);
//Make a graphics object from the empty bitmap.
Graphics g = Graphics.FromImage(returnBitmap);
//move rotation point to center of image.
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.TranslateTransform((float) b.Width / 2, (float)b.Height / 2);
//Rotate.
g.RotateTransform(angle);
//Move image back.
g.TranslateTransform(-(float)b.Width / 2, -(float)b.Height / 2);
//Draw passed in image onto graphics object.
g.DrawImage(b, new Point(0, 0));
return returnBitmap;
}
Pictures showing what I mean:
It doesn't rotate perfectly. Is there a solution to this? Something I haven't set for my picturebox properties? I've tried alot.
Thanks.
I found a solution to get the rotated image from it's center
[My Testing Conditions]
I'm testing this with the next considerations, if it works for you its ok :3
1.-The source image i used is a perfect square[LxL], taked from a png file, size of the square no matter, just needs to be LxL; //(Width == Height) = true;
2.- The png source image file that i am rotating is transparent and square shaped, i got it from illustrator
3.- I tested files of size 417x417, and 520x520, and 1024x1024
[The code i can share to you]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace YourNamespace
{
public static class TheClassinWhichYouWantToPlaceTheFunction
{
public static Bitmap RotateImageN(Bitmap b, float angle)
{
//Create a new empty bitmap to hold rotated image.
Bitmap returnBitmap = new Bitmap(b.Width, b.Height);
//Make a graphics object from the empty bitmap.
Graphics g = Graphics.FromImage(returnBitmap);
//move rotation point to center of image.
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.TranslateTransform((float)b.Width / 2, (float)b.Height / 2);
//Rotate.
g.RotateTransform(angle);
//Move image back.
g.TranslateTransform(-(float)b.Width / 2, -(float)b.Height / 2);
//Draw passed in image onto graphics object.
//Found ERROR 1: Many people do g.DwarImage(b,0,0); The problem is that you re giving just the position
//Found ERROR 2: Many people do g.DrawImage(b, new Point(0,0)); The size still not present hehe :3
g.DrawImage(b, 0,0,b.Width, b.Height); //My Final Solution :3
return returnBitmap;
}
}
}
I just gived the name "RotateImageN" to the function, because is the 5th solution I have tried :3 i Hope to be helpful
Sorry for my english and/or grammar hehe :3
This is where a simple test form would have helped you a lot.
Take this code and put it in a new WinForms project's Form1.cs file.
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace RotateImage {
public partial class Form1 : Form {
private readonly Graphics gfx;
private readonly Bitmap originalBitmap;
private readonly Bitmap redrawnBitmap;
private readonly Stopwatch sw;
private Timer timer;
public Form1() {
InitializeComponent();
BackColor = Color.White;
timer = new Timer();
timer.Interval = 16;
timer.Enabled = true;
timer.Tick += timer_Tick;
sw = new Stopwatch();
sw.Start();
gfx = CreateGraphics();
originalBitmap = new Bitmap(256, 256);
redrawnBitmap = new Bitmap(256, 256);
using (var bmpGfx = Graphics.FromImage(originalBitmap)) {
DrawCross(bmpGfx, new Point(128, 128), 128D, 0D);
}
}
private void timer_Tick(object sender, EventArgs e) {
// Rotate a full 90 degrees every 4 seconds.
var angle = sw.Elapsed.TotalSeconds * 22.5D;
var newBitmap = RotateImage(originalBitmap, (float)angle);
// Clear the result of the last draw.
gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, 256, 256));
gfx.DrawImageUnscaled(newBitmap, 0, 0);
gfx.DrawEllipse(Pens.Blue, new Rectangle(124, 124, 8, 8));
using (var redrawGfx = Graphics.FromImage(redrawnBitmap)) {
// Clear what we have, we are redrawing on the same surface.
redrawGfx.Clear(Color.White);
DrawCross(redrawGfx, new Point(128, 128), 128D, angle);
}
gfx.DrawImageUnscaled(redrawnBitmap, 256, 0);
gfx.DrawEllipse(Pens.Blue, new Rectangle(256+124, 124, 8, 8));
}
private void DrawCross(Graphics drawGfx, Point center, double radius, double angle) {
// Turn our angle from degrees to radians.
angle *= Math.PI / 180;
// NOTE: Using PointF to lazily "fix" rounding errors and casting (flooring) double to int. When the result of the math below is say 127.9999999...
// then it would get rounded down to 127. There is always Math.Round, which can round to nearest whole (away from .5) integer!
// Draw one line of our cross.
drawGfx.DrawLine(
Pens.Red,
new PointF((float)(Math.Cos(angle) * radius + center.X), (float)(Math.Sin(angle) * radius + center.Y)),
new PointF((float)(Math.Cos(angle - Math.PI) * radius + center.X), (float)(Math.Sin(angle - Math.PI) * radius + center.Y)));
// Rotate our angle 90 degrees.
angle += Math.PI / 2D;
// Draw the other line of our cross.
drawGfx.DrawLine(
Pens.Red,
new PointF((float)(Math.Cos(angle) * radius + center.X), (float)(Math.Sin(angle) * radius + center.Y)),
new PointF((float)(Math.Cos(angle - Math.PI) * radius + center.X), (float)(Math.Sin(angle - Math.PI) * radius + center.Y)));
}
// Your method, not mine.
private Bitmap RotateImage(Bitmap b, float angle)
{
//Create a new empty bitmap to hold rotated image.
Bitmap returnBitmap = new Bitmap(b.Width, b.Height);
//Make a graphics object from the empty bitmap.
Graphics g = Graphics.FromImage(returnBitmap);
//move rotation point to center of image.
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.TranslateTransform((float) b.Width / 2, (float)b.Height / 2);
//Rotate.
g.RotateTransform(angle);
//Move image back.
g.TranslateTransform(-(float)b.Width / 2, -(float)b.Height / 2);
//Draw passed in image onto graphics object.
g.DrawImage(b, new Point(0, 0));
return returnBitmap;
}
}
}
Observe as the two crosses rotate about their center just fine. The left one being a bitmap that is rotated with your method, the right one being redrawn every frame.
That is, there is nothing wrong with your rotation code but it's possible there's something wrong with your source bitmap or your display container.
When you use transparent png with just a portion of the image filled with color for example - the code will rotate transform only the portion of the png that has data in it...
I tried to get a dot in the top center of a png to rotate around the center of the image (500x500 px) and it just recognizes the part that was colored, so it turned into a bounce effect.
I tried with a fully colored 500x500 px image too, and that worked normally..
Hope it helps a little!

Overlaying PNG images KEEPING transparency

So basically what I want to do is overlap two .PNG images with transparent backgrounds. One is with a shotgun which rotates to mouse position, and the other is a cartoon character which I want to put behind the shotgun. Now, the problem is everytime I overlap them, the transparent background of the PNG image gets in the way and I can't see the shooter at all.
I have tried putting the shooter in a panel but placing the shotgun picturebox within it screws up the rotation algorithm (makes it rotate very slowly), I have no idea why.
Any help would be apreciated, thanks.
Coding I used:
Rotation algorithm:
private Bitmap rotateImage(Bitmap b, float angle)
{
//create a new empty bitmap to hold rotated image
Bitmap returnBitmap = new Bitmap(b.Width, b.Height);
//make a graphics object from the empty bitmap
Graphics g = Graphics.FromImage(returnBitmap);
//move rotation point to center of image
g.TranslateTransform((float)b.Width / 2, (float)b.Height / 2);
//rotate
g.RotateTransform((int)angle);
//move image back
g.TranslateTransform(-(float)b.Width / 2, -(float)b.Height / 2);
//draw passed in image onto graphics object
g.DrawImage(b, new Point(0, 0)); //???
return returnBitmap;
}
private float CalcAngle(Point TargetPos)
{
Point ZeroPoint = new Point(pictureBox1.Location.X + pictureBox1.Width / 2, pictureBox1.Location.Y + pictureBox1.Height / 2);
if (TargetPos == ZeroPoint)
{
return 0;
}
double angle;
double deltaX, deltaY;
deltaY = TargetPos.Y - ZeroPoint.Y;
deltaX = TargetPos.X - ZeroPoint.X;
angle = Math.Atan2(deltaY, deltaX) * 180 / Math.PI;
return (float)angle;
}
private void timer1_Tick(object sender, EventArgs e)
{
pictureBox1.Image = (Bitmap)backup.Clone();
//Load an image in from a file
Image image = new Bitmap(pictureBox1.Image);
//Set our picture box to that image
pictureBox1.Image = (Bitmap)backup.Clone();
//Store our old image so we can delete it
Image oldImage = pictureBox1.Image;
//Set angle
angle = CalcAngle(new Point(Cursor.Position.X, Cursor.Position.Y - 10));
//Pass in our original image and return a new image rotated X degrees right
pictureBox1.Image = rotateImage((Bitmap)image, angle);
if (oldImage != null)
{
oldImage.Dispose();
image.Dispose();
}
}
When you are creating a new Bitmap try to use any of the pixel format for 32 bpp or 64 bpp. See the code below:
Bitmap returnBitmap = new Bitmap(b.Width, b.Height, PixelFormat.Format64bppPArgb);
Here I draw three different png files on top of each other onto a panel:
using (Graphics graphic = panel1.CreateGraphics())
{
using (Image image = Image.FromFile(#"D:\tp3.png")) graphic.DrawImage(image, Point.Empty);
using (Image image = Image.FromFile(#"D:\tp2.png")) graphic.DrawImage(image, Point.Empty);
using (Image image = Image.FromFile(#"D:\tp1.png")) graphic.DrawImage(image, Point.Empty);
}
If you create a new BitMap do as #Palak.Maheria said and use a 32bit format with an alpha channel!

Matrix rotateAt c#

Im trying to rotate a image with matrix object and can't get it right
When i rotate the image i got a black spot, it's one pixel wrong and it's the same with 180 angle and 270 angle.
90 angle ex.
A picture of this problem:
http://www.spasm-design.com/rotate/onePixelWrong.jpg
And here is the code:
public System.Drawing.Image Rotate(System.Drawing.Image image, String angle, String direction)
{
Int32 destW, destH;
float destX, destY, rotate;
destW = image.Width;
destH = image.Height;
destX = destY = 0;
if (r == "90" || r == "270")
{
destW = image.Height;
destH = image.Width;
destY = (image.Width - destW) / 2;
destX = (image.Height - destH) / 2;
}
rotate = (direction == "y") ? float.Parse(angle) : float.Parse("-" + angle);
Bitmap b = new Bitmap(destW, destH, PixelFormat.Format24bppRgb);
b.SetResolution(image.HorizontalResolution, image.VerticalResolution);
Matrix x = new Matrix();
x.Translate(destX, destY);
x.RotateAt(rotate, new PointF(image.Width / 2, image.Height / 2));
Graphics g = Graphics.FromImage(b);
g.PageUnit = GraphicsUnit.Pixel;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.Transform = x;
g.DrawImage(image, 0, 0);
g.Dispose();
x.Dispose();
return b;
}
if someone have a good ide why this is happening please tell me.
Have a good day!
I think you're just getting a rounding error on this line:
x.RotateAt(rotate, new PointF(image.Width / 2, image.Height / 2));
Width and Height are both int properties. Try this instead:
x.RotateAt(rotate, new PointF((float)Math.Floor(image.Width / 2),
(float)Math.Floor(image.Height / 2)));
(Not tested, so not sure if this will work.)
Update: I don't think my above fix will work, but it may point you in the direction of the problem. If you can't fix it by adjusting the rounding, you may just need to change destX to -1 to get rid of the black line.
This works:
x.RotateAt(rotate, new PointF(image.Width / 2, image.Height / 2));
this "image.Width / 2" returns float
First i find out what angle is, if it is 90 or 270 flip the image so image.width = image.height and image.height = width
If a do that i get a problem when i rotate the image for the image width can be bigger then height of the image so then i need to reset the image x,y coordinates to 0,0
So this "destY = (image.Width - destW) / 2;" calculate offset of the image to the bitmap
and this "x.Translate(destX, destY);" set the image x equivalent to bitmap x
but something is going wrong for the rotation makes picture 1px to small.
so for my english but im not the best of it, i hope you can read it any why :)
for more questions please send me those and i'm going to try explain what i mean.
A much simpler solution is:
Add one pixel around the image.
Rotate the image.
Remove the one pixel.
Code:
// h and w are the width/height
// cx and cy are the centre of the image
myBitmap = new Bitmap(w + 2, h + 2);
mygraphics = Graphics.FromImage(myBitmap);
mygraphics.TranslateTransform(cx, cy);
mygraphics.RotateTransform(angle);
mygraphics.TranslateTransform(-cx, -cy);
mygraphics.DrawImage(myimage, new Point(1, 1));
// image crop
myBitmap= myBitmap.Clone(new Rectangle(1, 1, (int)w, (int)h), myimage.PixelFormat)
This is the main idea. Hope it helps.

Categories

Resources