138 lines
3.6 KiB
JavaScript
138 lines
3.6 KiB
JavaScript
import GridSizeModel from "../ml/gridSize/GridSizeModel";
|
|
|
|
export function getMapDefaultInset(width, height, gridX, gridY) {
|
|
// Max the width
|
|
const gridScale = width / gridX;
|
|
const y = gridY * gridScale;
|
|
const yNorm = y / height;
|
|
return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: yNorm } };
|
|
}
|
|
|
|
// Get all factors of a number
|
|
function factors(n) {
|
|
const numbers = Array.from(Array(n + 1), (_, i) => i);
|
|
return numbers.filter((i) => n % i === 0);
|
|
}
|
|
|
|
// Greatest common divisor
|
|
// Euclidean algorithm https://en.wikipedia.org/wiki/Euclidean_algorithm
|
|
function gcd(a, b) {
|
|
while (b !== 0) {
|
|
const t = b;
|
|
b = a % b;
|
|
a = t;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// Find all dividers that fit into two numbers
|
|
function dividers(a, b) {
|
|
const d = gcd(a, b);
|
|
return factors(d);
|
|
}
|
|
|
|
// The mean and standard deviation of > 1500 maps from the web
|
|
const gridSizeMean = { x: 31.567792, y: 32.597987 };
|
|
const gridSizeStd = { x: 14.438842, y: 15.582376 };
|
|
|
|
// Most grid sizes are above 10 and below 200
|
|
const minGridSize = 10;
|
|
const maxGridSize = 200;
|
|
|
|
function gridSizeVaild(x, y) {
|
|
return (
|
|
x > minGridSize && y > minGridSize && x < maxGridSize && y < maxGridSize
|
|
);
|
|
}
|
|
|
|
function gridSizeHeuristic(image, candidates) {
|
|
const width = image.width;
|
|
const height = image.height;
|
|
// Find the best candidate by comparing the absolute z-scores of each axis
|
|
let bestX = 1;
|
|
let bestY = 1;
|
|
let bestScore = Number.MAX_VALUE;
|
|
for (let scale of candidates) {
|
|
const x = Math.floor(width / scale);
|
|
const y = Math.floor(height / scale);
|
|
const xScore = Math.abs((x - gridSizeMean.x) / gridSizeStd.x);
|
|
const yScore = Math.abs((y - gridSizeMean.y) / gridSizeStd.y);
|
|
if (xScore < bestScore || yScore < bestScore) {
|
|
bestX = x;
|
|
bestY = y;
|
|
bestScore = Math.min(xScore, yScore);
|
|
}
|
|
}
|
|
|
|
if (gridSizeVaild(bestX, bestY)) {
|
|
return { x: bestX, y: bestY };
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function gridSizeML(image, candidates) {
|
|
const width = image.width;
|
|
const height = image.height;
|
|
const ratio = width / height;
|
|
let canvas = document.createElement("canvas");
|
|
let context = canvas.getContext("2d");
|
|
canvas.width = 2048;
|
|
canvas.height = Math.floor(2048 / ratio);
|
|
|
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
|
|
let imageData = context.getImageData(
|
|
0,
|
|
Math.floor(canvas.height / 2) - 16,
|
|
2048,
|
|
32
|
|
);
|
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
const r = imageData.data[i];
|
|
const g = imageData.data[i + 1];
|
|
const b = imageData.data[i + 2];
|
|
// ITU-R 601-2 Luma Transform
|
|
const luma = (r * 299) / 1000 + (g * 587) / 1000 + (b * 114) / 1000;
|
|
|
|
imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = luma;
|
|
}
|
|
|
|
const model = new GridSizeModel();
|
|
const prediction = await model.predict(imageData);
|
|
|
|
// Find the candidate that is closest to the prediction
|
|
let bestScale = 1;
|
|
let bestScore = Number.MAX_VALUE;
|
|
for (let scale of candidates) {
|
|
const x = Math.floor(width / scale);
|
|
const score = Math.abs(x - prediction);
|
|
if (score < bestScore && x > minGridSize && x < maxGridSize) {
|
|
bestScale = scale;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
|
|
const x = Math.floor(width / bestScale);
|
|
const y = Math.floor(height / bestScale);
|
|
|
|
if (gridSizeVaild(x, y)) {
|
|
return { x, y };
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getGridSize(image) {
|
|
const candidates = dividers(image.width, image.height);
|
|
let prediction = await gridSizeML(image, candidates);
|
|
if (!prediction) {
|
|
prediction = gridSizeHeuristic(image, candidates);
|
|
}
|
|
if (!prediction) {
|
|
prediction = { x: 22, y: 22 };
|
|
}
|
|
|
|
return prediction;
|
|
}
|