using System.Collections.Generic;
using System.Threading.Tasks;
var (topLeft, topRight, bottomRight, bottomLeft) =
(new Point(1224, 197), new Point(1915, 72), new Point(1915, 662), new Point(1229, 638));
const string frameImageUrl =
"https://raw.githubusercontent.com/ProductiveRage/NaivePerspectiveCorrection/main/" +
"Samples/Frames/frame_2837.jpg";
using var httpClient = new HttpClient();
using var downloadStream = await httpClient.GetStreamAsync(frameImageUrl);
using var image = SKBitmap.Decode(downloadStream);
using var extractedContent = SimplePerspectiveCorrection.ExtractAndPerspectiveCorrect(
image, topLeft, topRight, bottomRight, bottomLeft
const int resizeLargestSideTo = 150;
Console.Write("Below is a data url that is a shrunken version of the slide as it was shown");
Console.Write(" projected onto a wall in the original video, contorted back into a rectangle");
Console.Write(" (undoing the distortion of perspective) so that it will be easier to compare");
Console.WriteLine(" it to the original slides and try to work out which one is being shown:");
Console.WriteLine(GenerateDataUrlForResizedVersion(extractedContent, resizeLargestSideTo));
Console.WriteLine("The original (full size) version of the frame is available here:");
Console.WriteLine(frameImageUrl);
static string GenerateDataUrlForResizedVersion(SKBitmap image, int resizeLargestSideTo)
using var shrunken = image.CopyAndResize(resizeLargestSideTo);
using var shrunkenData = shrunken.Encode(SKEncodedImageFormat.Jpeg, 80);
using var memoryStream = new MemoryStream();
shrunkenData.SaveTo(memoryStream);
var shrunkenBytes = memoryStream.ToArray();
return "data:image/jpg;base64," + Convert.ToBase64String(shrunkenBytes);
static class BitmapExtensions
public static SKBitmap CopyAndResize(this SKBitmap image, int resizeLargestSideTo)
var (width, height) = (image.Width > image.Height)
? (resizeLargestSideTo, (int)((double)image.Height / image.Width * resizeLargestSideTo))
: ((int)((double)image.Width / image.Height * resizeLargestSideTo), resizeLargestSideTo);
var resized = new SKBitmap(width, height);
image.ScalePixels(resized, SKFilterQuality.High);
public static DataRectangle<SKColor> GetAllPixels(this SKBitmap image)
var values = new SKColor[image.Width, image.Height];
var pixels = image.Pixels;
for (var y = 0; y < image.Height; y++)
for (var x = 0; x < image.Width; x++)
values[x, y] = pixels[(y * image.Width) + x];
return DataRectangle.For(values);
static class DataRectangle
public static DataRectangle<T> For<T>(T[,] values) => new DataRectangle<T>(values);
sealed class DataRectangle<T>
public DataRectangle(T[,] values)
if ((values.GetLowerBound(0) != 0) || (values.GetLowerBound(1) != 0))
throw new ArgumentException("Both dimensions must have lower bound zero");
var arrayWidth = values.GetUpperBound(0) + 1;
var arrayHeight = values.GetUpperBound(1) + 1;
if ((arrayWidth == 0) || (arrayHeight == 0))
throw new ArgumentException("zero element arrays are not supported");
_values = new T[Width, Height];
Array.Copy(values, _values, Width * Height);
public int Width { get; }
public int Height { get; }
public T this[int x, int y]
if ((x < 0) || (x >= Width))
throw new ArgumentOutOfRangeException(nameof(x));
if ((y < 0) || (y >= Height))
throw new ArgumentOutOfRangeException(nameof(y));
static class SimplePerspectiveCorrection
public static SKBitmap ExtractAndPerspectiveCorrect(
var (projectionSize, sourceSliceLocations, sliceRenderer) =
GetProjectionDetails(topLeft, topRight, bottomRight, bottomLeft);
var pixels = image.GetAllPixels();
var projection = new SKBitmap(projectionSize.Width, projectionSize.Height);
foreach (var (lineToTrace, index) in sourceSliceLocations)
var lengthOfLineToTrace = LengthOfLine(lineToTrace);
var pixelsOnLine = Enumerable
.Range(0, lengthOfLineToTrace)
var fractionOfProgressAlongLineToTrace = (float)j / lengthOfLineToTrace;
var point = GetPointAlongLine(lineToTrace, fractionOfProgressAlongLineToTrace);
return GetAverageColour(pixels, point);
sliceRenderer(projection, pixelsOnLine, index);
static SKColor GetAverageColour(DataRectangle<SKColor> pixels, PointF point)
var (integralX, fractionalX) = GetIntegralAndFractional(point.X);
var x1 = Math.Min(integralX + 1, pixels.Width);
var (integralY, fractionalY) = GetIntegralAndFractional(point.Y);
var y1 = Math.Min(integralY + 1, pixels.Height);
var (topColour0, topColour1) = GetColours(new Point(x0, y0), new Point(x1, y0));
var (bottomColour0, bottomColour1) = GetColours(new Point(x0, y1), new Point(x1, y1));
CombineColours(topColour0, topColour1, fractionalX),
CombineColours(bottomColour0, bottomColour1, fractionalX),
(SKColor c0, SKColor c1) GetColours(Point p0, Point p1)
var c0 = pixels[p0.X, p0.Y];
var c1 = (p0 == p1) ? c0 : pixels[p1.X, p1.Y];
static (int Integral, float Fractional) GetIntegralAndFractional(float value)
var integral = (int)Math.Truncate(value);
var fractional = value - integral;
return (integral, fractional);
static SKColor CombineColours(SKColor x, SKColor y, float proportionOfSecondColour)
if ((proportionOfSecondColour == 0) || (x == y))
if (proportionOfSecondColour == 1)
red: CombineComponent(x.Red, y.Red),
green: CombineComponent(x.Green, y.Green),
blue: CombineComponent(x.Blue, y.Blue),
alpha: CombineComponent(x.Alpha, y.Alpha)
byte CombineComponent(byte x, byte y) =>
(byte)Math.Round((x * (1 - proportionOfSecondColour)) + (y * proportionOfSecondColour)),
delegate void SliceRenderer(SKBitmap image, IEnumerable<SKColor> pixelsOnLine, int index);
sealed record ProjectionDetails(
IEnumerable<((PointF From, PointF To) Line, int Index)> SourceSliceLocations,
SliceRenderer SliceRenderer
static ProjectionDetails GetProjectionDetails(
var edgeToStartFrom = (From: topLeft, To: topRight);
var edgeToConnectTo = (From: bottomLeft, To: bottomRight);
var lengthOfEdgeToStartFrom = LengthOfLine(edgeToStartFrom);
var dimensions = new Size(
GetProjectionHeight(topLeft, topRight, bottomRight, bottomLeft)
return new ProjectionDetails(dimensions, GetSourceSliceLocationEnumerator(), RenderSlice);
IEnumerable<((PointF From, PointF To) Line, int Index)> GetSourceSliceLocationEnumerator() =>
.Range(0, lengthOfEdgeToStartFrom)
var fractionOfProgressAlongPrimaryEdge = (float)i / lengthOfEdgeToStartFrom;
GetPointAlongLine(edgeToStartFrom, fractionOfProgressAlongPrimaryEdge),
GetPointAlongLine(edgeToConnectTo, fractionOfProgressAlongPrimaryEdge)
static void RenderSlice(SKBitmap bitmap, IEnumerable<SKColor> pixelsOnLine, int index)
var pixelsOnLineArray = pixelsOnLine.ToArray();
using var slice = new SKBitmap(1, pixelsOnLineArray.Length);
for (var j = 0; j < pixelsOnLineArray.Length; j++)
slice.SetPixel(0, j, pixelsOnLineArray[j]);
using var canvas = new SKCanvas(bitmap);
canvas.DrawBitmap(slice, dest: new SKRect(index, 0, index + 1, bitmap.Height));
static PointF GetPointAlongLine((PointF From, PointF To) line, float fraction)
var deltaX = line.To.X - line.From.X;
var deltaY = line.To.Y - line.From.Y;
(deltaX * fraction) + line.From.X,
(deltaY * fraction) + line.From.Y
static int LengthOfLine((PointF From, PointF To) line)
var deltaX = line.To.X - line.From.X;
var deltaY = line.To.Y - line.From.Y;
return (int)Math.Round(Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY)));
static int GetProjectionHeight(Point topLeft, Point topRight, Point bottomRight, Point bottomLeft)
var leftVerticalEdgeLength = LengthOfLine((topLeft, bottomLeft));
var rightVerticalEdgeLength = LengthOfLine((topRight, bottomRight));
var shorterVerticalEdgeLength = Math.Min(leftVerticalEdgeLength, rightVerticalEdgeLength);
var largerVerticalEdgeLength = Math.Max(leftVerticalEdgeLength, rightVerticalEdgeLength);
return (shorterVerticalEdgeLength + largerVerticalEdgeLength) / 2;