Skip to content

Commit 956fc6e

Browse files
committed
Implement image rotation
1 parent 98955a9 commit 956fc6e

File tree

2 files changed

+127
-17
lines changed

2 files changed

+127
-17
lines changed

src/Aardvark.OpenCV.Tests/Tests/ImageTests.fs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module ``Image Processing Tests`` =
1414
let private randomValues : Type -> (unit -> obj) =
1515
LookupTable.lookup [
1616
typeof<float32>, rnd.UniformFloat >> box
17+
typeof<uint8>, rnd.UniformUInt >> uint8 >> box
1718
typeof<uint16>, rnd.UniformUInt >> uint16 >> box
1819
typeof<int32>, rnd.UniformInt >> box
1920
]
@@ -61,7 +62,7 @@ module ``Image Processing Tests`` =
6162
Aardvark.Init()
6263

6364
[<Theory>]
64-
let ``Scaling`` (filter: ImageInterpolation) (sub: bool) =
65+
let ``Scale`` (filter: ImageInterpolation) (sub: bool) =
6566
let src = PixImage.generate<float32> Col.Format.RGBA sub
6667
let scaleFactor = V2d(0.2345, 1.6789)
6768

@@ -72,7 +73,7 @@ module ``Image Processing Tests`` =
7273
psnr |> should be (greaterThan 20.0)
7374

7475
[<Theory>]
75-
let ``Remapping`` (sub: bool) =
76+
let ``Remap`` (sub: bool) =
7677
let src = PixImage.generate<float32> Col.Format.RGBA sub
7778

7879
let result =
@@ -100,7 +101,7 @@ module ``Image Processing Tests`` =
100101
psnr |> should be (greaterThan 20.0)
101102

102103
[<Theory>]
103-
let ``Remapping (border type)`` (borderType: ImageBorderType) (sub: bool) =
104+
let ``Remap (border type)`` (borderType: ImageBorderType) (sub: bool) =
104105
let src = PixImage.generate<uint16> Col.Format.BGRA sub
105106
let border = [| 1us; 2us; 3us; 4us |]
106107

@@ -137,4 +138,44 @@ module ``Image Processing Tests`` =
137138
| _ -> raise <| NotImplementedException()
138139

139140
let psnr = PixImage.peakSignalToNoiseRatio result expected
140-
psnr |> should equal infinity
141+
psnr |> should equal infinity
142+
143+
[<Theory>]
144+
let ``Rotate`` (resize: bool) (sub: bool) =
145+
let src = PixImage.generate<uint8> Col.Format.BGRA sub
146+
let angle = -Constant.PiTimesFour + rnd.UniformDouble() * Constant.PiTimesFour * 2.0
147+
148+
let rotated =
149+
Aardvark.OpenCV.PixProcessor.Instance.Rotate(src, angle, resize, ImageInterpolation.Linear, border = [| 255uy; 0uy; 0uy; 255uy |])
150+
151+
let remapped =
152+
let dstSize =
153+
if resize then
154+
let srcSize = V2d src.Size.XY
155+
let cos = abs <| cos angle
156+
let sin = abs <| sin angle
157+
158+
V2l(
159+
int64 (srcSize.X * cos + srcSize.Y * sin + 0.5),
160+
int64 (srcSize.X * sin + srcSize.Y * cos + 0.5)
161+
)
162+
163+
else
164+
src.SizeL
165+
166+
let mapX = Matrix<float32>(dstSize)
167+
let mapY = Matrix<float32>(dstSize)
168+
169+
// Note: Positive angle -> clockwise rotation in the image coordinate system
170+
let mat =
171+
let srcCenter = v2f src.Size * 0.5f
172+
let dstCenter = v2f dstSize * 0.5f
173+
M33f.Translation srcCenter * M33f.RotationZ(float32 angle) * M33f.Translation -dstCenter
174+
175+
mapX.SetByCoord(fun x y -> V2l(x, y) |> v2f |> Mat.transformPos mat |> Vec.x) |> ignore
176+
mapY.SetByCoord(fun x y -> V2l(x, y) |> v2f |> Mat.transformPos mat |> Vec.y) |> ignore
177+
178+
Aardvark.OpenCV.PixProcessor.Instance.Remap(src, mapX, mapY, ImageInterpolation.Linear, border = [| 255uy; 0uy; 0uy; 255uy |])
179+
180+
let psnr = PixImage.peakSignalToNoiseRatio rotated remapped
181+
psnr |> should be (greaterThan 20.0)

src/Aardvark.OpenCV/ImageProcessing.cs

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ public static GCHandleDiposable ToMat<T>(this Matrix<T> matrix, MatType matType,
8181

8282
public static GCHandleDiposable ToMat<T>(this Volume<T> volume, MatType matType, int elementSize, out CvMat result)
8383
=> volume.Data.ToMat(volume.Size.XY, volume.Delta.XY, volume.FirstIndex, matType, elementSize, out result);
84+
85+
public static Scalar ToScalar<T>(this T[] color, Col.Format format)
86+
{
87+
var result = new Scalar();
88+
89+
if (color != null)
90+
{
91+
var order = format.ChannelOrder();
92+
for (var i = 0; i < Fun.Min(color.Length, format.ChannelCount()); i++)
93+
result[i] = Convert.ToDouble(color[order[i]]);
94+
}
95+
96+
return result;
97+
}
8498
}
8599

86100
#endregion
@@ -89,7 +103,7 @@ public sealed class PixProcessor : IPixProcessor
89103
{
90104
public string Name => "OpenCV";
91105

92-
public PixProcessorCaps Capabilities => PixProcessorCaps.Scale | PixProcessorCaps.Remap;
106+
public PixProcessorCaps Capabilities => PixProcessorCaps.Scale | PixProcessorCaps.Rotate | PixProcessorCaps.Remap;
93107

94108
[OnAardvarkInit]
95109
public static void Init()
@@ -135,10 +149,74 @@ public PixImage<T> Scale<T>(PixImage<T> image, V2d scaleFactor, ImageInterpolati
135149

136150
#region Rotate
137151

152+
// TODO: Change the interface to accept T[] as border value
153+
/// <inheritdoc cref="Rotate{T}(PixImage{T}, double, bool, ImageInterpolation, ImageBorderType, T)"/>
138154
public PixImage<T> Rotate<T>(PixImage<T> image, double angleInRadians, bool resize, ImageInterpolation interpolation,
139155
ImageBorderType borderType = ImageBorderType.Const,
140-
T border = default)
141-
=> null;
156+
T[] border = default)
157+
{
158+
var src = image.Volume;
159+
160+
if (!src.HasImageWindowLayout())
161+
{
162+
throw new ArgumentException($"Volume must be in image layout (Delta = {src.Delta}).");
163+
}
164+
165+
var matType = typeof(T).ToMatType(image.ChannelCount);
166+
var elementSize = typeof(T).GetCLRSize();
167+
168+
var srcCenter = image.Size.ToV2d() * 0.5;
169+
170+
// This already takes the handedness of the image coordinate system into account.
171+
// See: https://docs.opencv.org/4.10.0/da/d54/group__imgproc__transform.html#gafbbc470ce83812914a70abfb604f4326
172+
var rotMat =
173+
Cv2.GetRotationMatrix2D(
174+
new Point2f((float)srcCenter.X, (float)srcCenter.Y),
175+
-angleInRadians.DegreesFromRadians(),
176+
1.0
177+
);
178+
179+
var dstSize = image.Volume.Size;
180+
if (resize)
181+
{
182+
// Compute bounds of rotated image
183+
// See: https://stackoverflow.com/questions/3231176/how-to-get-size-of-a-rotated-rectangle
184+
var cos = rotMat.At<double>(0, 0);
185+
var sin = rotMat.At<double>(1, 0);
186+
var cosAbs = cos.Abs();
187+
var sinAbs = sin.Abs();
188+
dstSize.X = (long)(image.Width * cosAbs + image.Height * sinAbs + 0.5);
189+
dstSize.Y = (long)(image.Width * sinAbs + image.Height * cosAbs + 0.5);
190+
191+
// Adjust transformation matrix for new center
192+
// We describe the inverse transformation (i.e. from dst to src).
193+
// Shift by -dstCenter -> rotate CW -> shift by srcCenter.
194+
// See: https://math.stackexchange.com/questions/2093314/rotation-matrix-of-rotation-around-a-point-other-than-the-origin
195+
var dstCenter = dstSize.XY.ToV2d() * 0.5;
196+
rotMat.At<double>(0, 2) = -dstCenter.X * cos + dstCenter.Y * sin + srcCenter.X;
197+
rotMat.At<double>(1, 2) = -dstCenter.X * sin - dstCenter.Y * cos + srcCenter.Y;
198+
}
199+
200+
var dst = dstSize.CreateImageVolume<T>();
201+
202+
using var _src = src.ToMat(matType, elementSize, out var srcMat);
203+
using var _dst = dst.ToMat(matType, elementSize, out var dstMat);
204+
205+
Cv2.WarpAffine(
206+
srcMat, dstMat, rotMat,
207+
new Size((int)dst.SX, (int)dst.SY),
208+
interpolation.ToInterpolationFlags(false) | InterpolationFlags.WarpInverseMap,
209+
borderType.ToBorderTypes(),
210+
(borderType == ImageBorderType.Const) ? border.ToScalar(image.Format) : new()
211+
);
212+
213+
return new(image.Format, dst);
214+
}
215+
216+
public PixImage<T> Rotate<T>(PixImage<T> image, double angleInRadians, bool resize, ImageInterpolation interpolation,
217+
ImageBorderType borderType,
218+
T border)
219+
=> Rotate(image, angleInRadians, resize, interpolation, borderType, new T[] { border, border, border, border });
142220

143221
#endregion
144222

@@ -173,20 +251,11 @@ public PixImage<T> Remap<T>(PixImage<T> image, Matrix<float> mapX, Matrix<float>
173251
using var _x = mapX.ToMat(MatType.CV_32FC1, 4, out var mapXMat);
174252
using var _y = mapY.ToMat(MatType.CV_32FC1, 4, out var mapYMat);
175253

176-
Scalar borderValue = new();
177-
if (borderType == ImageBorderType.Const && border != null)
178-
{
179-
var order = image.Format.ChannelOrder();
180-
181-
for (var i = 0; i < Fun.Min(border.Length, 4); i++)
182-
borderValue[i] = Convert.ToDouble(border[order[i]]);
183-
}
184-
185254
Cv2.Remap(
186255
srcMat, dstMat, mapXMat, mapYMat,
187256
interpolation.ToInterpolationFlags(false),
188257
borderType.ToBorderTypes(),
189-
borderValue
258+
(borderType == ImageBorderType.Const) ? border.ToScalar(image.Format) : new()
190259
);
191260

192261
return new (image.Format, dst);

0 commit comments

Comments
 (0)