mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-08 22:51:25 +08:00
重写屏幕找图算法,保证准确性
This commit is contained in:
parent
2476037c09
commit
174d3ed7e7
@ -1,68 +1,10 @@
|
|||||||
const { nativeImage } = require("electron");
|
const { nativeImage } = require("electron");
|
||||||
const { captureScreen } = require("./screenCapture");
|
const { captureScreen } = require("./screenCapture");
|
||||||
|
|
||||||
// 将颜色值映射到8个区间
|
|
||||||
function mapColorValue(val) {
|
|
||||||
if (val > 223) return 7; // [224 ~ 255]
|
|
||||||
if (val > 191) return 6; // [192 ~ 223]
|
|
||||||
if (val > 159) return 5; // [160 ~ 191]
|
|
||||||
if (val > 127) return 4; // [128 ~ 159]
|
|
||||||
if (val > 95) return 3; // [96 ~ 127]
|
|
||||||
if (val > 63) return 2; // [64 ~ 95]
|
|
||||||
if (val > 31) return 1; // [32 ~ 63]
|
|
||||||
return 0; // [0 ~ 31]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算图像特征向量
|
|
||||||
function calculateFeatureVector(
|
|
||||||
buffer,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
startX = 0,
|
|
||||||
startY = 0,
|
|
||||||
w = width,
|
|
||||||
h = height
|
|
||||||
) {
|
|
||||||
// 8^4 = 4096 维向量,表示RGBA各8个区间的组合
|
|
||||||
const vector = new Array(8 * 8 * 8 * 8).fill(0);
|
|
||||||
|
|
||||||
for (let y = startY; y < startY + h; y++) {
|
|
||||||
for (let x = startX; x < startX + w; x++) {
|
|
||||||
const idx = (y * width + x) * 4;
|
|
||||||
// 计算四个通道的量化值
|
|
||||||
const r = mapColorValue(buffer[idx]);
|
|
||||||
const g = mapColorValue(buffer[idx + 1]);
|
|
||||||
const b = mapColorValue(buffer[idx + 2]);
|
|
||||||
const a = mapColorValue(buffer[idx + 3]);
|
|
||||||
|
|
||||||
// 计算在向量中的位置
|
|
||||||
const vectorIdx = r * 512 + g * 64 + b * 8 + a;
|
|
||||||
vector[vectorIdx]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vector;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算余弦相似度
|
|
||||||
function calculateCosineSimilarity(v1, v2) {
|
|
||||||
let dotProduct = 0;
|
|
||||||
let norm1 = 0;
|
|
||||||
let norm2 = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < v1.length; i++) {
|
|
||||||
dotProduct += v1[i] * v2[i];
|
|
||||||
norm1 += v1[i] * v1[i];
|
|
||||||
norm2 += v2[i] * v2[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取显示器缩放比例
|
// 获取显示器缩放比例
|
||||||
function getDisplayScale() {
|
function getDisplayScale() {
|
||||||
|
// MacOS 上要考虑缩放比例
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
// 在 macOS 上,通过比较实际分辨率和报告的分辨率来计算缩放比例
|
|
||||||
const primaryDisplay = utools.getPrimaryDisplay();
|
const primaryDisplay = utools.getPrimaryDisplay();
|
||||||
const { scaleFactor } = primaryDisplay;
|
const { scaleFactor } = primaryDisplay;
|
||||||
return scaleFactor;
|
return scaleFactor;
|
||||||
@ -71,153 +13,94 @@ function getDisplayScale() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 在屏幕上查找图片
|
// 在屏幕上查找图片
|
||||||
async function findImage(targetImageData, options = {}) {
|
async function findImage(subDataURL, options = {}) {
|
||||||
try {
|
const mainDataURL = await captureScreen();
|
||||||
// 获取屏幕截图
|
if (!mainDataURL) return null;
|
||||||
const screenDataUrl = await captureScreen();
|
// 解析主图和子图
|
||||||
if (!screenDataUrl) return null;
|
const mainImg = nativeImage.createFromDataURL(mainDataURL);
|
||||||
|
const subImg = nativeImage.createFromDataURL(subDataURL);
|
||||||
|
|
||||||
// 获取显示器缩放比例
|
// 获取图像基本信息
|
||||||
const scale = getDisplayScale();
|
const mainSize = mainImg.getSize();
|
||||||
|
const subSize = subImg.getSize();
|
||||||
|
|
||||||
// 读取屏幕截图
|
// 获取像素数据(返回Buffer,RGBA格式)
|
||||||
const screenImage = nativeImage.createFromDataURL(screenDataUrl);
|
const mainPixels = mainImg.getBitmap();
|
||||||
const screenBuffer = screenImage.toBitmap();
|
const subPixels = subImg.getBitmap();
|
||||||
const { width: actualWidth, height: actualHeight } = screenImage.getSize();
|
|
||||||
|
|
||||||
// 计算缩放后的实际尺寸
|
// 边界检查
|
||||||
const screenWidth = Math.round(actualWidth / scale);
|
if (subSize.width > mainSize.width || subSize.height > mainSize.height) {
|
||||||
const screenHeight = Math.round(actualHeight / scale);
|
throw new Error("要查找图片尺寸大于屏幕");
|
||||||
|
}
|
||||||
|
|
||||||
// 从 base64 字符串创建目标图片
|
// 预提取子图首像素值(优化点)
|
||||||
const targetImage = nativeImage.createFromDataURL(targetImageData);
|
const firstSubPixel = [
|
||||||
const targetBuffer = targetImage.toBitmap();
|
subPixels[0], // R
|
||||||
const { width: targetWidth, height: targetHeight } = targetImage.getSize();
|
subPixels[1], // G
|
||||||
|
subPixels[2], // B
|
||||||
|
subPixels[3], // A
|
||||||
|
];
|
||||||
|
|
||||||
// 计算目标图片的特征向量
|
// 主图遍历边界
|
||||||
const targetVector = calculateFeatureVector(
|
const maxX = mainSize.width - subSize.width;
|
||||||
targetBuffer,
|
const maxY = mainSize.height - subSize.height;
|
||||||
targetWidth,
|
|
||||||
targetHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
// 设置匹配阈值
|
// 遍历主图每个可能的位置
|
||||||
const threshold = options.threshold || 0.9;
|
for (let y = 0; y <= maxY; y++) {
|
||||||
|
for (let x = 0; x <= maxX; x++) {
|
||||||
|
// 快速检查首像素(性能优化关键)
|
||||||
|
const mainOffset = (y * mainSize.width + x) * 4;
|
||||||
|
if (
|
||||||
|
mainPixels[mainOffset] !== firstSubPixel[0] ||
|
||||||
|
mainPixels[mainOffset + 1] !== firstSubPixel[1] ||
|
||||||
|
mainPixels[mainOffset + 2] !== firstSubPixel[2] ||
|
||||||
|
mainPixels[mainOffset + 3] !== firstSubPixel[3]
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let bestMatch = null;
|
// 完整像素比对
|
||||||
let bestSimilarity = 0;
|
let match = true;
|
||||||
|
for (let subY = 0; subY < subSize.height; subY++) {
|
||||||
|
for (let subX = 0; subX < subSize.width; subX++) {
|
||||||
|
// 计算像素位置
|
||||||
|
const mainPixelPos = ((y + subY) * mainSize.width + (x + subX)) * 4;
|
||||||
|
const subPixelPos = (subY * subSize.width + subX) * 4;
|
||||||
|
|
||||||
// 使用滑动窗口搜索
|
// 精确匹配每个通道
|
||||||
const stepSize = Math.round(8 * scale); // 根据缩放比例调整步长
|
if (
|
||||||
for (let y = 0; y <= actualHeight - targetHeight; y += stepSize) {
|
mainPixels[mainPixelPos] !== subPixels[subPixelPos] ||
|
||||||
for (let x = 0; x <= actualWidth - targetWidth; x += stepSize) {
|
mainPixels[mainPixelPos + 1] !== subPixels[subPixelPos + 1] ||
|
||||||
// 计算当前区域的特征向量
|
mainPixels[mainPixelPos + 2] !== subPixels[subPixelPos + 2] ||
|
||||||
const regionVector = calculateFeatureVector(
|
mainPixels[mainPixelPos + 3] !== subPixels[subPixelPos + 3]
|
||||||
screenBuffer,
|
) {
|
||||||
actualWidth,
|
match = false;
|
||||||
actualHeight,
|
break;
|
||||||
x,
|
|
||||||
y,
|
|
||||||
targetWidth,
|
|
||||||
targetHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算相似度
|
|
||||||
const similarity = calculateCosineSimilarity(
|
|
||||||
targetVector,
|
|
||||||
regionVector
|
|
||||||
);
|
|
||||||
|
|
||||||
// 更新最佳匹配
|
|
||||||
if (similarity > bestSimilarity) {
|
|
||||||
bestSimilarity = similarity;
|
|
||||||
bestMatch = { x: Math.round(x / scale), y: Math.round(y / scale) };
|
|
||||||
|
|
||||||
// 如果相似度已经很高,进行精确搜索
|
|
||||||
if (similarity >= threshold) {
|
|
||||||
// 在周围进行精确搜索,注意搜索范围也要考虑缩放
|
|
||||||
const searchRange = Math.round(4 * scale);
|
|
||||||
for (let dy = -searchRange; dy <= searchRange; dy++) {
|
|
||||||
for (let dx = -searchRange; dx <= searchRange; dx++) {
|
|
||||||
const newX = x + dx;
|
|
||||||
const newY = y + dy;
|
|
||||||
|
|
||||||
if (
|
|
||||||
newX < 0 ||
|
|
||||||
newY < 0 ||
|
|
||||||
newX > actualWidth - targetWidth ||
|
|
||||||
newY > actualHeight - targetHeight
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const preciseVector = calculateFeatureVector(
|
|
||||||
screenBuffer,
|
|
||||||
actualWidth,
|
|
||||||
actualHeight,
|
|
||||||
newX,
|
|
||||||
newY,
|
|
||||||
targetWidth,
|
|
||||||
targetHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
const preciseSimilarity = calculateCosineSimilarity(
|
|
||||||
targetVector,
|
|
||||||
preciseVector
|
|
||||||
);
|
|
||||||
if (preciseSimilarity > bestSimilarity) {
|
|
||||||
bestSimilarity = preciseSimilarity;
|
|
||||||
bestMatch = {
|
|
||||||
x: Math.round(newX / scale),
|
|
||||||
y: Math.round(newY / scale),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!match) break;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果找到足够好的匹配,提前返回
|
if (match) {
|
||||||
if (bestSimilarity >= threshold) {
|
const displayScale = getDisplayScale();
|
||||||
const position = {
|
const position = {
|
||||||
x: bestMatch.x,
|
x: Math.round(x / displayScale),
|
||||||
y: bestMatch.y,
|
y: Math.round(y / displayScale),
|
||||||
width: Math.round(targetWidth / scale),
|
width: Math.round(subSize.width / displayScale),
|
||||||
height: Math.round(targetHeight / scale),
|
height: Math.round(subSize.height / displayScale),
|
||||||
confidence: bestSimilarity,
|
};
|
||||||
};
|
clickImage(position, options.mouseAction);
|
||||||
|
return position;
|
||||||
clickImage(position, options.mouseAction);
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到足够好的匹配,但有最佳匹配且相似度不太低,也返回
|
|
||||||
if (bestMatch && bestSimilarity > threshold * 0.8) {
|
|
||||||
const position = {
|
|
||||||
x: bestMatch.x,
|
|
||||||
y: bestMatch.y,
|
|
||||||
width: Math.round(targetWidth / scale),
|
|
||||||
height: Math.round(targetHeight / scale),
|
|
||||||
confidence: bestSimilarity,
|
|
||||||
};
|
|
||||||
clickImage(position, options.mouseAction);
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("查找图片失败:", error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clickImage = (position, mouseAction) => {
|
const clickImage = (position, mouseAction) => {
|
||||||
// 计算中心点
|
const centerX = Math.round(position.x + position.width / 2);
|
||||||
const centerX = position.x + position.width / 2;
|
const centerY = Math.round(position.y + position.height / 2);
|
||||||
const centerY = position.y + position.height / 2;
|
|
||||||
|
|
||||||
// 根据配置执行鼠标动作
|
// 根据配置执行鼠标动作
|
||||||
switch (mouseAction) {
|
switch (mouseAction) {
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<!-- 配置区域 -->
|
<!-- 配置区域 -->
|
||||||
<div class="col-12 col-sm-4">
|
<div class="col-12 col-sm-4">
|
||||||
<div class="row q-col-gutter-sm">
|
<div class="row">
|
||||||
<!-- 从剪贴板读取按钮 -->
|
<!-- 从剪贴板读取按钮 -->
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-btn
|
<q-btn
|
||||||
@ -53,8 +53,13 @@
|
|||||||
<!-- 匹配阈值设置 -->
|
<!-- 匹配阈值设置 -->
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
v-model="argvs.threshold"
|
v-if="false"
|
||||||
|
:model-value="argvs.threshold"
|
||||||
|
@update:model-value="updateArgvs('threshold', $event)"
|
||||||
label="匹配阈值"
|
label="匹配阈值"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.1"
|
||||||
class="border-primary"
|
class="border-primary"
|
||||||
:command="{
|
:command="{
|
||||||
icon: 'tune',
|
icon: 'tune',
|
||||||
@ -64,20 +69,15 @@
|
|||||||
|
|
||||||
<!-- 鼠标动作选择 -->
|
<!-- 鼠标动作选择 -->
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-select
|
<ButtonGroup
|
||||||
v-model="argvs.mouseAction"
|
:is-collapse="false"
|
||||||
|
:model-value="argvs.mouseAction"
|
||||||
|
@update:model-value="updateArgvs('mouseAction', $event)"
|
||||||
:options="mouseActionOptions"
|
:options="mouseActionOptions"
|
||||||
label="找到后"
|
label="找到后"
|
||||||
class="border-primary"
|
class="border-primary"
|
||||||
dense
|
|
||||||
filled
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
</ButtonGroup>
|
||||||
<q-icon name="mouse" />
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -97,11 +97,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import NumberInput from "components/composer/common/NumberInput.vue";
|
import NumberInput from "components/composer/common/NumberInput.vue";
|
||||||
|
import ButtonGroup from "components/composer/common/ButtonGroup.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ImageSearchEditor",
|
name: "ImageSearchEditor",
|
||||||
components: {
|
components: {
|
||||||
NumberInput,
|
NumberInput,
|
||||||
|
ButtonGroup,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -123,7 +125,7 @@ export default defineComponent({
|
|||||||
],
|
],
|
||||||
defaultArgvs: {
|
defaultArgvs: {
|
||||||
imagePreview: "",
|
imagePreview: "",
|
||||||
threshold: 0.9,
|
threshold: 1,
|
||||||
mouseAction: "none",
|
mouseAction: "none",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -266,8 +268,8 @@ export default defineComponent({
|
|||||||
border: 2px dashed var(--q-primary);
|
border: 2px dashed var(--q-primary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: 128px;
|
max-height: 138px;
|
||||||
min-height: 128px;
|
min-height: 138px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user