Files
better-genshin-impact/BetterGenshinImpact/GameTask/Common/Map/Maps/SeaOfBygoneErasMap.cs
2026-01-19 20:12:14 +08:00

254 lines
10 KiB
C#

using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.GameTask.Common.Map.Maps.Base;
using OpenCvSharp;
using Microsoft.Extensions.Logging;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using System.Collections.Generic;
using System.Linq;
using System;
using Newtonsoft.Json.Linq;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
namespace BetterGenshinImpact.GameTask.Common.Map.Maps;
/// <summary>
/// 旧日之海
/// </summary>
public class SeaOfBygoneErasMap : SceneBaseMap
{
#region
static readonly int GameMapRows = 3; // 游戏坐标下地图块的行数
static readonly int GameMapCols = 4; // 游戏坐标下地图块的列数
static readonly int GameMapUpRows = 2; // 游戏坐标下 左上角离地图原点的行数(注意原点在块的右下角)
static readonly int GameMapLeftCols = 5; // 游戏坐标下 左上角离地图原点的列数(注意原点在块的右下角)
#endregion
static readonly int SeaOfBygoneErasMapImageBlockWidth = 1024;
private static Mat TeleportTemplate;
private static Mat TeleportTemplateMask;
private List<Point> MapTeleports;
static SeaOfBygoneErasMap()
{
Mat img = GameTaskManager.LoadAssetImage("QuickTeleport", "TeleportTransparentBackground.png", ImreadModes.Unchanged);
TeleportTemplate = new Mat();
Cv2.CvtColor(img, TeleportTemplate, ColorConversionCodes.BGRA2GRAY);
Mat[] channels = Cv2.Split(img);
TeleportTemplateMask = channels[3];
}
public SeaOfBygoneErasMap() : base(type: MapTypes.SeaOfBygoneEras,
mapSize: new Size(GameMapCols * SeaOfBygoneErasMapImageBlockWidth, GameMapRows * SeaOfBygoneErasMapImageBlockWidth),
mapOriginInImageCoordinate: new Point2f((GameMapLeftCols + 1) * SeaOfBygoneErasMapImageBlockWidth, (GameMapUpRows + 1) * SeaOfBygoneErasMapImageBlockWidth),
mapImageBlockWidth: SeaOfBygoneErasMapImageBlockWidth,
splitRow: 0,
splitCol: 0)
{
ExtractAndSaveFeature(Global.Absolute("Assets/Map/SeaOfBygoneEras/SeaOfBygoneEras_0_1024.png"));
ExtractAndSaveFeature(Global.Absolute("Assets/Map/SeaOfBygoneEras/SeaOfBygoneEras_-1_1024.webp"));
ExtractAndSaveFeature(Global.Absolute("Assets/Map/SeaOfBygoneEras/SeaOfBygoneEras_-2_1024.webp"));
Layers = BaseMapLayer.LoadLayers(this);
MapTeleports = MapLazyAssets.Instance.ScenesDic["SeaOfBygoneEras"].Points.
Where(i => i.Type == "TeleportWaypoint").
Select(i => ConvertGenshinMapCoordinatesToImageCoordinates(new Point2f((float)i.TranX, (float)i.TranY))).
Select(i => new Point(i.X, i.Y)).OrderBy(i => i.X).ThenBy(i => i.Y).ToList();
}
public override Point2f GetBigMapPosition(Mat greyBigMapMat)
{
var rect = GetBigMapRectByTeleports(greyBigMapMat);
if (rect != default)
{
return rect.GetCenterPoint();
}
return base.GetBigMapPosition(greyBigMapMat);
}
public override Rect GetBigMapRect(Mat greyBigMapMat)
{
var rect = GetBigMapRectByTeleports(greyBigMapMat);
if (rect != default)
{
return rect;
}
return base.GetBigMapRect(greyBigMapMat);
}
private Rect GetBigMapRectByTeleports(Mat greyBigMapMat)
{
// It's fine to miss some, but definitely no false positive results.
const double threshold = 0.99;
using Mat result = new Mat();
Cv2.MatchTemplate(greyBigMapMat, TeleportTemplate, result, TemplateMatchModes.CCorrNormed, TeleportTemplateMask);
var teleportPoints = new List<Point>();
// Step 1: Get all teleport positions from current screenshot
for (int i = 1; i < result.Rows - 1; ++i)
{
for (int j = 1; j < result.Cols - 1; ++j)
{
float val = result.At<float>(i, j);
if (val > threshold)
{
if (val >= result.At<float>(i - 1, j - 1) &&
val >= result.At<float>(i - 1, j) &&
val >= result.At<float>(i - 1, j + 1) &&
val >= result.At<float>(i, j - 1) &&
val >= result.At<float>(i, j + 1) &&
val >= result.At<float>(i + 1, j - 1) &&
val >= result.At<float>(i + 1, j) &&
val >= result.At<float>(i + 1, j + 1))
{
var newPoint = new Point(j, i);
bool tooClose = false;
foreach (var p in teleportPoints)
{
if (p.DistanceTo(newPoint) < 50)
{
tooClose = true;
break;
}
}
if (!tooClose)
{
teleportPoints.Add(newPoint);
}
}
}
}
}
if (teleportPoints.Count < 2)
{
return default;
}
teleportPoints = teleportPoints.Select(i => new Point(i.X + 12, i.Y + 12)).
OrderBy(i => i.X).ThenBy(i => i.Y).ToList();
/*
foreach (var p in teleportPoints) {
Logger.LogInformation("Teleport point: {a}", p);
}
Logger.LogInformation("Total telepoints: {c}", teleportPoints.Count);
*/
Func<Point, Point, double> GetAngleOfTwoPoints = (p0, p1) =>
{
double deltaX = p0.X - p1.X;
int deltaY = p0.Y - p1.Y;
if (deltaY == 0) { return 90; }
var val = Math.Atan(deltaX / deltaY);
return val / Math.PI * 180;
};
// Step 2: find a diagonal determined by two of the teleports, let's call these two teleports reference points
Point rp0 = new Point();
Point rp1 = new Point();
double refAngle = 0.0;
{
double minAngleDiff = 180;
for (int i = 0; i < teleportPoints.Count; ++i)
{
for (int j = i + 1; j < teleportPoints.Count; ++j)
{
var p0 = teleportPoints[i];
var p1 = teleportPoints[j];
var angle = GetAngleOfTwoPoints(p0, p1);
if (Math.Abs(Math.Abs(angle) - 45) < minAngleDiff)
{
rp0 = p0;
rp1 = p1;
minAngleDiff = Math.Abs(Math.Abs(angle) - 45);
}
}
}
refAngle = GetAngleOfTwoPoints(rp0, rp1);
// Logger.LogInformation("Reference points {a} and {b}, Angle {c}", rp0, rp1, refAngle);
}
{
/*
var debugMat = new Mat();
Cv2.CvtColor(greyBigMapMat, debugMat, ColorConversionCodes.GRAY2BGR);
foreach (var p in teleportPoints)
{
Cv2.DrawMarker(debugMat, p, new Scalar(255, 0, 0), MarkerTypes.Cross, 20, 2);
}
Cv2.DrawMarker(debugMat, rp0, new Scalar(0, 255, 0), MarkerTypes.TriangleUp, 20, 2);
Cv2.DrawMarker(debugMat, rp1, new Scalar(0, 255, 0), MarkerTypes.TriangleUp, 20, 2);
Cv2.ImWrite(((DateTimeOffset)DateTime.UtcNow).ToUnixTimeMilliseconds().ToString() + ".png", debugMat);
*/
}
// Step 3: For all diagonals determined by pairs of teleports on this map.
var minDeviation = double.MaxValue;
var transformParamScale = 0.0;
var transformParamDeltaX = 0.0;
var transformParamDeltaY = 0.0;
for (int i = 0; i < MapTeleports.Count; ++i)
{
for (int j = i + 1; j < MapTeleports.Count; ++j)
{
var mp0 = MapTeleports[i];
var mp1 = MapTeleports[j];
var angle = GetAngleOfTwoPoints(mp0, mp1);
if (Math.Abs(angle - refAngle) < 5)
{
// Step 4: Assuming this pair corresponds to the reference points
var mpDist = mp0.DistanceTo(mp1);
var rpDist = rp0.DistanceTo(rp1);
var scale = mpDist / rpDist;
var deltaX = mp0.X - rp0.X * scale;
var deltaY = mp0.Y - rp0.Y * scale;
Func<Point, Point> transformPoint = i =>
{
return new Point(i.X * scale + deltaX, i.Y * scale + deltaY);
};
var transformedPoints = teleportPoints.Select(i => transformPoint(i)).ToList();
// Step 5: Check how close the fit is
double totalDeviation = 0;
foreach (var p in transformedPoints)
{
var minDist = MapTeleports.Select(i => i.DistanceTo(p)).Min();
totalDeviation += minDist;
}
if (totalDeviation < minDeviation)
{
minDeviation = totalDeviation;
transformParamScale = scale;
transformParamDeltaX = deltaX;
transformParamDeltaY = deltaY;
}
}
}
}
// Logger.LogInformation("Min deviation: {d}", minDeviation);
if (minDeviation < 200)
{
Func<Point, Point> transformPoint = i =>
{
return new Point(i.X * transformParamScale + transformParamDeltaX, i.Y * transformParamScale + transformParamDeltaY);
};
var pTopLeft = transformPoint(new Point(0, 0));
var pBottomRight = transformPoint(new Point(greyBigMapMat.Width, greyBigMapMat.Height));
// Logger.LogInformation("Rect: {a}, {b}", pTopLeft, pBottomRight);
return new Rect(pTopLeft.X, pTopLeft.Y, pBottomRight.X - pTopLeft.X, pBottomRight.Y - pTopLeft.Y);
}
return default;
}
}