Add $1 uni-stroke recognizer system

This commit is contained in:
2025-01-03 01:50:25 -05:00
parent 3abf8d34f0
commit fbffa6ae71
12 changed files with 783 additions and 0 deletions
@@ -0,0 +1,11 @@
#include "UniStrokeDataTable.h"
FUniStrokeDataTable::FUniStrokeDataTable()
{
// DO NOTHING
}
FUniStrokeDataTable::~FUniStrokeDataTable()
{
// DO NOTHING
}
@@ -0,0 +1,28 @@
// Copyright Team Lumi. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "UniStrokeDataTable.generated.h"
USTRUCT()
struct WIZARDINGCENTRAL_API FUniStrokeDataTable : public FTableRowBase
{
GENERATED_BODY()
FUniStrokeDataTable();
virtual ~FUniStrokeDataTable() override;
/**
* The name of the shape
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Name;
/**
* The points of the shape
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FVector2D> Points;
};
@@ -0,0 +1,404 @@
#include "UniStrokePoint.h"
#include "ScreenPass.h"
FUniStrokePoint::FUniStrokePoint()
{
this->X = 0.0f;
this->Y = 0.0f;
}
FUniStrokePoint::FUniStrokePoint(const float& X, const float& Y)
{
this->X = X;
this->Y = Y;
}
float
FUniStrokePoint::Distance(const FUniStrokePoint& PointA,
const FUniStrokePoint& PointB)
{
return FMath::Sqrt(
// delta X
FMath::Square(PointB.X - PointA.X) +
/// delta Y
FMath::Square(PointB.Y - PointA.Y)
);
}
FUniStrokePoint
FUniStrokePoint::Centroid(const TArray<FUniStrokePoint>& Points)
{
float SumX = 0.0f;
float SumY = 0.0f;
for (const FUniStrokePoint& Point : Points)
{
SumX += Point.X;
SumY += Point.Y;
}
return FUniStrokePoint(
SumX / Points.Num(),
SumY / Points.Num()
);
}
FUniStrokeRectangle
FUniStrokePoint::BoundingBox(const TArray<FUniStrokePoint>& Points)
{
// Edge case
if (Points.Num() == 0)
{
return FUniStrokeRectangle();
}
float MinX = Points[0].X;
float MinY = Points[0].Y;
float MaxX = Points[0].X;
float MaxY = Points[0].Y;
for (const FUniStrokePoint& Point : Points)
{
MinX = FMath::Min(MinX, Point.X);
MinY = FMath::Min(MinY, Point.Y);
MaxX = FMath::Max(MaxX, Point.X);
MaxY = FMath::Max(MaxY, Point.Y);
}
return FUniStrokeRectangle(MinX, MinY, MaxX - MinX, MaxY - MinY);
}
// TODO: Proofread this function
TArray<float>
FUniStrokePoint::Vectorize(const TArray<FUniStrokePoint>& Points)
{
float Sum = 0.0f;
TArray<float> Vector;
for (const FUniStrokePoint& Point : Points)
{
Vector.Add(Point.X);
Vector.Add(Point.Y);
Sum += FMath::Square(Point.X) + FMath::Square(Point.Y);
}
const float Magnitude = FMath::Sqrt(Sum);
for (float& V : Vector)
{
V /= Magnitude;
}
return Vector;
}
// TODO: Proofread this function
float
FUniStrokePoint::OptimalCosineDistance(const TArray<float>& VectorA,
const TArray<float>& VectorB)
{
float A = 0.0f;
float B = 0.0f;
for (int i = 0; i < VectorA.Num(); i += 2)
{
A += VectorA[i] * VectorB[i] + VectorA[i + 1] * VectorB[i + 1];
B += VectorA[i] * VectorB[i + 1] - VectorA[i + 1] * VectorB[i];
}
const float Angle = FMath::Atan(B / A);
return FMath::Acos(A * FMath::Cos(Angle) + B * FMath::Sin(Angle));
}
TArray<FUniStrokePoint>
FUniStrokePoint::From(const TArray<FVector2D>& Points)
{
TArray<FUniStrokePoint> NewPoints;
NewPoints.Reserve(Points.Num());
Algo::Transform(
Points,
NewPoints,
[](const FVector2D& Point)
{
return FUniStrokePoint(Point.X, Point.Y);
}
);
return NewPoints;
}
void
FUniStrokePoint::Resample(TArray<FUniStrokePoint>& Points,
const int& Num)
{
const float I = PathLength(Points) / (Num - 1);
// D <- 0
float D = 0.0f;
// newPoints <- points[0]
TArray<FUniStrokePoint> OldPoints = Points;
Points.SetNum(1);
// foreach point p[i] for i >= 1 in points do
for (int i = 1; i < OldPoints.Num(); ++i)
{
// d <- Distance(p[i - 1], p[i])
const float d = Distance(OldPoints[i - 1], OldPoints[i]);
// if (D + d) >= I then
if (D + d >= I)
{
// q.X <- p[i - 1].X + ((I - D) / d) * (p[i].X - p[i - 1].X)
const float qx = OldPoints[i - 1].X + (I - D) / d * (OldPoints[i].X - OldPoints[i - 1].X);
// q.Y <- p[i - 1].Y + ((I - D) / d) * (p[i].Y - p[i - 1].Y)
const float qy = OldPoints[i - 1].Y + (I - D) / d * (OldPoints[i].Y - OldPoints[i - 1].Y);
// Append(newPoints, q)
FUniStrokePoint NewPoint = FUniStrokePoint(qx, qy);
Points.Add(NewPoint);
// Insert(points, i, q)
OldPoints.Insert(NewPoint, i);
// D <- 0
D = 0.0f;
}
else
{
// D <- D + d
D += d;
}
}
// Edge case
if (Points.Num() == Num - 1)
{
Points.Add(OldPoints.Last());
}
}
float
FUniStrokePoint::PathLength(const TArray<FUniStrokePoint>& Points)
{
// d <- 0
float d = 0;
// for i from 1 to |A| step 1 do
for (auto i = 1; i < Points.Num(); ++i)
{
// d <- d + Distance(A[i - 1], A[i])
d += Distance(Points[i - 1], Points[i]);
}
// return d
return d;
}
void
FUniStrokePoint::RotateToZero(TArray<FUniStrokePoint>& Points)
{
// c <- Centroid(points)
FUniStrokePoint c = Centroid(Points);
// theta <- Atan(c.Y - points[0].Y, c.X - points[0].X)
const float Theta = FMath::Atan2(c.Y - Points[0].Y, c.X - Points[0].X);
// newPoints <- RotateBy(points, -theta)
RotateBy(Points, -Theta);
// return newPoints
}
void
FUniStrokePoint::RotateBy(TArray<FUniStrokePoint>& Points,
const float& Theta)
{
TArray<FUniStrokePoint> OldPoints = Points;
Points.Empty();
// c <- Centroid(points)
const FUniStrokePoint c = Centroid(OldPoints);
const float SinTheta = FMath::Sin(Theta);
const float CosTheta = FMath::Cos(Theta);
// foreach point p in points do
for (const FUniStrokePoint& p : OldPoints)
{
FUniStrokePoint q = FUniStrokePoint(
// q.x <- (p.x - c.x) * Cos(theta) - (p.y - c.y) * Sin(theta) + c.x
(p.X - c.X) * CosTheta - (p.Y - c.Y) * SinTheta + c.X,
// q.y <- (p.x - c.x) * Sin(theta) + (p.y - c.y) * Cos(theta) + c.y
(p.X - c.X) * SinTheta + (p.Y - c.Y) * CosTheta + c.Y
);
// Append(newPoints, q)
Points.Add(q);
}
// return newPoints
}
void
FUniStrokePoint::ScaleToSquare(TArray<FUniStrokePoint>& Points,
const float& Size)
{
TArray<FUniStrokePoint> OldPoints = Points;
Points.Empty();
// B <- BoundingRect(points)
const FUniStrokeRectangle B = BoundingBox(OldPoints);
// foreach point p in points do
for (const FUniStrokePoint& p : OldPoints)
{
const FUniStrokePoint q = FUniStrokePoint(
// q.x <- p.x * (size / B.width)
p.X * (Size / B.Width),
// q.y <- p.y * (size / B.height)
p.Y * (Size / B.Height)
);
// Append(newPoints, q)
Points.Add(q);
}
}
void
FUniStrokePoint::TranslateTo(TArray<FUniStrokePoint>& Points,
const FUniStrokePoint& Point)
{
TArray<FUniStrokePoint> OldPoints = Points;
Points.Empty();
// c <- Centroid(points)
const FUniStrokePoint c = Centroid(OldPoints);
// foreach point p in points do
for (; const FUniStrokePoint& p : OldPoints)
{
const FUniStrokePoint q = FUniStrokePoint(
// q.x <- p.x + point.x - c.x
p.X + Point.X - c.X,
// q.y <- p.y + point.y - c.y.
p.Y + Point.Y - c.Y
);
// Append(newPoints, q)
Points.Add(q);
}
// return newPoints
}
void
FUniStrokePoint::TranslateToOrigin(TArray<FUniStrokePoint>& Points)
{
TArray<FUniStrokePoint> OldPoints = Points;
Points.Empty();
// c <- Centroid(points)
const FUniStrokePoint c = Centroid(OldPoints);
// foreach point p in points do
for (const FUniStrokePoint& p : OldPoints)
{
const FUniStrokePoint q = FUniStrokePoint(
// q.x <- p.x - c.x
p.X - c.X,
// q.y <- p.y - c.y.
p.Y - c.Y
);
// Append(newPoints, q)
Points.Add(q);
}
// return newPoints
}
float
FUniStrokePoint::DistanceAtBestAngle(const TArray<FUniStrokePoint>& Points,
const TArray<FUniStrokePoint>& T,
const float& ThetaFrom,
const float& ThetaTo,
const float& ThetaThreshold)
{
float ThetaA = ThetaFrom;
float ThetaB = ThetaTo;
// x1 <- phi theta_a + (1 - phi) theta_b
float x1 = Phi * ThetaA + (1 - Phi) * ThetaB;
// f1 <- DistanceAtAngle(points, T, x1)
float f1 = DistanceAtAngle(Points, T, x1);
// x2 <- (1 - phi) theta_a + phi theta_b
float x2 = (1 - Phi) * ThetaA + Phi * ThetaB;
// f2 <- DistanceAtAngle(points, T, x2)
float f2 = DistanceAtAngle(Points, T, x2);
// while |theta_b - theta_a| > threshold do
while (FMath::Abs(ThetaB - ThetaA) > ThetaThreshold)
{
// if f1 < f2 then
if (f1 < f2)
{
// theta_b <- x2
ThetaB = x2;
// x2 <- x1
x2 = x1;
// f2 <- f1
f2 = f1;
// x1 <- phi theta_a + (1 - phi) theta_b
x1 = Phi * ThetaA + (1 - Phi) * ThetaB;
// f1 <- DistanceAtAngle(points, T, x1)
f1 = DistanceAtAngle(Points, T, x1);
}
// else
else
{
// theta_a <- x1
ThetaA = x1;
// x1 <- x2
x1 = x2;
// f1 <- f2
f1 = f2;
// x2 <- (1 - phi) theta_a + phi theta_b
x2 = (1 - Phi) * ThetaA + Phi * ThetaB;
// f2 <- DistanceAtAngle(points, T, x2)
f2 = DistanceAtAngle(Points, T, x2);
}
}
// return min(f1, f2)
return FMath::Min(f1, f2);
}
float
FUniStrokePoint::DistanceAtAngle(const TArray<FUniStrokePoint>& Points,
const TArray<FUniStrokePoint>& T,
const float& Theta)
{
// newPoints <- RotateBy(points, theta)
TArray<FUniStrokePoint> NewPoints = Points;
RotateBy(NewPoints, Theta);
// d <- PathDistance(newPoints, T.points)
// return d
return PathDistance(NewPoints, T);
}
float
FUniStrokePoint::PathDistance(const TArray<FUniStrokePoint>& PathA,
const TArray<FUniStrokePoint>& PathB)
{
// Edge case: Different number of points
if (PathA.Num() != PathB.Num())
{
return TNumericLimits<float>::Max();
}
// d <- 0
float d = 0;
// for i from 0 to |A| step 1 do
for (int i = 0; i < PathA.Num(); ++i)
{
// d <- d + Distance(A[i], B[i])
d += Distance(PathA[i], PathB[i]);
}
// return d / |A|
return d / PathA.Num();
}
@@ -0,0 +1,90 @@
// Copyright Team Lumi. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UniStrokeRectangle.h"
#include "UniStrokePoint.generated.h"
static const float Phi = 0.5f * (-1.0f + FMath::Sqrt(5.0f));
USTRUCT()
struct WIZARDINGCENTRAL_API FUniStrokePoint
{
GENERATED_BODY()
FUniStrokePoint();
FUniStrokePoint(const float& X, const float& Y);
~FUniStrokePoint() = default;
static void
Resample(TArray<FUniStrokePoint>& Points,
const int& Num);
static float
PathLength(const TArray<FUniStrokePoint>& Points);
static void
RotateToZero(TArray<FUniStrokePoint>& Points);
static void
RotateBy(TArray<FUniStrokePoint>& Points,
const float& Theta);
static void
ScaleToSquare(TArray<FUniStrokePoint>& Points,
const float& Size);
static void
TranslateTo(TArray<FUniStrokePoint>& Points,
const FUniStrokePoint& Point);
static void
TranslateToOrigin(TArray<FUniStrokePoint>& Points);
static float
DistanceAtBestAngle(const TArray<FUniStrokePoint>& Points,
const TArray<FUniStrokePoint>& T,
const float& ThetaFrom,
const float& ThetaTo,
const float& ThetaThreshold);
static float
DistanceAtAngle(const TArray<FUniStrokePoint>& Points,
const TArray<FUniStrokePoint>& T,
const float& Theta);
static float
PathDistance(const TArray<FUniStrokePoint>& PathA,
const TArray<FUniStrokePoint>& PathB);
/**
* Converts a TArray of FVector2D to a TArray of FUniStrokePoint
*
* @param Points The points to convert
* @return The converted points as a TArray of FUniStrokePoint
*/
static TArray<FUniStrokePoint>
From(const TArray<FVector2D>& Points);
static float
Distance(const FUniStrokePoint& PointA,
const FUniStrokePoint& PointB);
static FUniStrokePoint
Centroid(const TArray<FUniStrokePoint>& Points);
static FUniStrokeRectangle
BoundingBox(const TArray<FUniStrokePoint>& Points);
static float
OptimalCosineDistance(const TArray<float>& VectorA,
const TArray<float>& VectorB);
static TArray<float>
Vectorize(const TArray<FUniStrokePoint>& Points);
private:
float X;
float Y;
};
@@ -0,0 +1,79 @@
#include "UniStrokeRecognizer.h"
FUniStrokeRecognizer::FUniStrokeRecognizer()
{
this->Templates = TArray<FUniStrokeTemplate>();
}
FUniStrokeRecognizer::~FUniStrokeRecognizer()
{
// DO NOTHING
}
// TODO: Review this function
FUniStrokeResult
FUniStrokeRecognizer::Recognize(const TArray<FVector2D>& VectorPoints,
const bool& UseProtractor)
{
TArray<FUniStrokePoint> Points = FUniStrokePoint::From(VectorPoints);
// Edge case: Not enough points
if (Points.Num() < 2 || FUniStrokePoint::PathLength(Points) < 100.0f)
{
return FUniStrokeResult("Too few points made", 0.0);
}
const FUniStrokeTemplate Candidate = FUniStrokeTemplate("", Points);
int TemplateIndex = -1;
// b <- +infty
float b = TNumericLimits<float>::Max();
// foreach template T in templates do
for (int i = 0; i < Templates.Num(); i++)
{
// d <- DistanceAtBestAngle(points, T, -theta, theta, threshold)
const float d = UseProtractor
? FUniStrokePoint::OptimalCosineDistance(
Templates[i].Vector,
Candidate.Vector
)
: FUniStrokePoint::DistanceAtBestAngle(
Candidate.Points,
Templates[i].Points,
-AngleRange,
AngleRange,
AnglePrecision
);
// if d < b then
if (d < b)
{
// b <- d
b = d;
// T' <- T
TemplateIndex = i;
}
}
// score <- 1 b / 0.5 sqrt(size^2 + size^2)
const float Score = UseProtractor ? 1.0 - b : 1.0 - b / HalfDiagonal;
// return <T', score>
return TemplateIndex == -1
? FUniStrokeResult("No match", 0.0)
: FUniStrokeResult(Templates[TemplateIndex].Name, Score);
}
void
FUniStrokeRecognizer::AddTemplate(const FString& Name,
const TArray<FVector2D>& VectorPoints)
{
const TArray<FUniStrokePoint> Points = FUniStrokePoint::From(VectorPoints);
this->Templates.Add(FUniStrokeTemplate(Name, Points));
}
void FUniStrokeRecognizer::Reset()
{
Templates.SetNum(NumTemplates);
}
@@ -0,0 +1,34 @@
// Copyright Team Lumi. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UniStrokeResult.h"
#include "UniStrokeTemplate.h"
#include "UniStrokeRecognizer.generated.h"
static constexpr int NumTemplates = 16;
static const float Diagonal = FMath::Sqrt(2 * FMath::Square(SquareSize));
static const float HalfDiagonal = 0.5 * Diagonal;
static constexpr float AngleRange = FMath::DegreesToRadians(45.0);
static constexpr float AnglePrecision = FMath::DegreesToRadians(2.0);
USTRUCT()
struct WIZARDINGCENTRAL_API FUniStrokeRecognizer
{
GENERATED_BODY()
FUniStrokeRecognizer();
~FUniStrokeRecognizer();
FUniStrokeResult
Recognize(const TArray<FVector2D>& VectorPoints, const bool& UseProtractor);
void
AddTemplate(const FString& Name, const TArray<FVector2D>& VectorPoints);
void
Reset();
TArray<FUniStrokeTemplate> Templates;
};
@@ -0,0 +1,22 @@
#include "UniStrokeRectangle.h"
FUniStrokeRectangle::FUniStrokeRectangle()
{
X = 0.0f;
Y = 0.0f;
Width = 0.0f;
Height = 0.0f;
}
FUniStrokeRectangle::FUniStrokeRectangle(const float& X, const float& Y, const float& Width, const float& Height)
{
this->X = X;
this->Y = Y;
this->Width = Width;
this->Height = Height;
}
FUniStrokeRectangle::~FUniStrokeRectangle()
{
// DO NOTHING
}
@@ -0,0 +1,24 @@
// Copyright Team Lumi. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UniStrokeRectangle.generated.h"
USTRUCT()
struct FUniStrokeRectangle
{
GENERATED_BODY()
FUniStrokeRectangle();
FUniStrokeRectangle(const float& X,
const float& Y,
const float& Width,
const float& Height);
~FUniStrokeRectangle();
float X;
float Y;
float Width;
float Height;
};
@@ -0,0 +1,19 @@
#include "UniStrokeResult.h"
FUniStrokeResult::FUniStrokeResult()
{
this->Name = "No match";
this->Score = 0.0f;
}
FUniStrokeResult::FUniStrokeResult(const FString& Name,
const float& Score)
{
this->Name = Name;
this->Score = Score;
}
FUniStrokeResult::~FUniStrokeResult()
{
// DO NOTHING
}
@@ -0,0 +1,20 @@
// Copyright Team Lumi. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UniStrokeResult.generated.h"
USTRUCT()
struct WIZARDINGCENTRAL_API FUniStrokeResult
{
GENERATED_USTRUCT_BODY()
FUniStrokeResult();
FUniStrokeResult(const FString& Name,
const float& Score);
~FUniStrokeResult();
FString Name;
float Score;
};
@@ -0,0 +1,27 @@
#include "UniStrokeTemplate.h"
FUniStrokeTemplate::FUniStrokeTemplate()
{
this->Name = "";
this->Vector = TArray<float>();
this->Points = TArray<FUniStrokePoint>();
}
FUniStrokeTemplate::FUniStrokeTemplate(const FString& Name,
const TArray<FUniStrokePoint>)
{
this->Name = Name;
this->Points = Points;
FUniStrokePoint::Resample(this->Points, NumPoints);
FUniStrokePoint::RotateToZero(this->Points);
FUniStrokePoint::ScaleToSquare(this->Points, SquareSize);
FUniStrokePoint::TranslateToOrigin(this->Points);
this->Vector = FUniStrokePoint::Vectorize(this->Points);
}
FUniStrokeTemplate::~FUniStrokeTemplate()
{
// DO NOTHING
}
@@ -0,0 +1,25 @@
// Copyright Team Lumi. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UniStrokePoint.h"
#include "UniStrokeTemplate.generated.h"
static constexpr int NumPoints = 64;
static constexpr float SquareSize = 250.0f;
USTRUCT()
struct WIZARDINGCENTRAL_API FUniStrokeTemplate
{
GENERATED_BODY()
FUniStrokeTemplate();
FUniStrokeTemplate(const FString& Name,
const TArray<FUniStrokePoint>);
~FUniStrokeTemplate();
FString Name;
TArray<float> Vector;
TArray<FUniStrokePoint> Points;
};