dsh-copilot-vision 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/diff.js +6 -7
- package/src/plugin.js +42 -13
- package/src/render.js +33 -20
- package/src/shot-index.js +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
### 动态视觉感知:不传全屏,传差分
|
|
12
12
|
|
|
13
|
-
`vp_screenshot_diff` 用 pixelmatch 逐像素比对,4 邻域连通块合并,小于 100 像素的噪声(光标、闪烁)直接丢弃,把「哪里变了」而不是「整屏是什么」交还给模型。`vp_wait_change` 更进一步:每 100ms 在内存里截屏比对,**轮询期零落盘**,变化出现的瞬间才落一张正式截图。「等页面加载」从反复截图碰运气,变成一次有明确语义的等待。换显示器或改缩放后坐标即不可信——宽高或 scaleFactor
|
|
13
|
+
`vp_screenshot_diff` 用 pixelmatch 逐像素比对,4 邻域连通块合并,小于 100 像素的噪声(光标、闪烁)直接丢弃,把「哪里变了」而不是「整屏是什么」交还给模型。`vp_wait_change` 更进一步:每 100ms 在内存里截屏比对,**轮询期零落盘**,变化出现的瞬间才落一张正式截图。「等页面加载」从反复截图碰运气,变成一次有明确语义的等待。换显示器或改缩放后坐标即不可信——宽高或 scaleFactor 不一致直接报错,绝不重采样硬算。网格放大图不再固定 4 倍:按区域面积自适应倍率——≤50,000 物理像素 4x、≤200,000 2x、更大 1x 只画网格,大区域不再产出巨图;同时以「放大后单边 ≤2,000 像素」兜底,4,344×4 这类细长条不会被放大成 17,376 宽的畸形长图。每个区域以 `gridScale` 字段标明实际倍数。`coverage` 只是变色像素的比例,白底同类元素位移时可能仅 2% 却已改天换地——判断变化要看 `regions` 与 `gridPath` 的实际内容,不能只看覆盖率。上下文策略同样内置在工具 description 里(「长程上下文纪律」):旧图会被清理、历史路径随时可能失效,模型必须以最新一次观察为准,偏离预期就直接重新截图,不翻历史记录。
|
|
14
14
|
|
|
15
15
|
### 人机共驾安全
|
|
16
16
|
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
### 位置编码注入
|
|
22
22
|
|
|
23
|
-
大模型在原尺寸小图上数错行、估偏上百像素是常态。Vision-Pilot 把坐标计算交还给代码:每个差分区域附一张 `gridPath`
|
|
23
|
+
大模型在原尺寸小图上数错行、估偏上百像素是常态。Vision-Pilot 把坐标计算交还给代码:每个差分区域附一张 `gridPath` 放大图——按区域面积与单边长度自适应倍率(4x / 2x / 1x,放大后单边 ≤2,000 像素)的最近邻放大,每 20 物理像素一条半透明红线,网格原点就是区域左上角(pngjs + 手写 Bresenham,零新增依赖)。模型只负责数格子、报百分比,`vp_click_relative` 完成精确换算。实测:模型按网格估点落进文字区,误差 ≤ 2.5px;同一场景靠肉眼估坐标偏过 111px。
|
|
24
24
|
|
|
25
|
-
<!-- 配图位 2:vp-grid-*.png
|
|
25
|
+
<!-- 配图位 2:vp-grid-*.png 网格放大图示例(自适应倍率 4x/2x/1x 且单边 ≤2,000 像素,红线每格 = 20 物理像素) -->
|
|
26
26
|
|
|
27
27
|
## 工具集
|
|
28
28
|
|
|
@@ -57,7 +57,7 @@ DSH_HOME=<DSH_HOME> dsh plugin --profile web add dsh-copilot-vision
|
|
|
57
57
|
|
|
58
58
|
# 或从本地 tarball 安装(开发调试)
|
|
59
59
|
npm pack
|
|
60
|
-
DSH_HOME=<DSH_HOME> dsh plugin --profile web add file:<绝对路径>/dsh-copilot-vision-1.
|
|
60
|
+
DSH_HOME=<DSH_HOME> dsh plugin --profile web add file:<绝对路径>/dsh-copilot-vision-1.1.0.tgz
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
- **宿主**:web(问答卡片授权)/ headless(无 UI 答者,只能 Shift+Space,需真人在场)/ desktop(待 DSH 市场审核后内置验证)。
|
package/package.json
CHANGED
package/src/diff.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
// 边界(有意为之,不是欠债):
|
|
5
5
|
// - localPath 仍是原尺寸裁剪:局部坐标 + bbox 左上角 = 全局物理坐标,模型可以直接换算。
|
|
6
|
-
// - 额外产出的 gridPath
|
|
7
|
-
//
|
|
6
|
+
// - 额外产出的 gridPath 是按面积与单边长度自适应倍率的「网格放大镜」(小区域 4x / 中区域 2x / 大区域 1x,
|
|
7
|
+
// 放大后单边不超过 2000 像素,见 render.js),
|
|
8
|
+
// 它只用于目测/相对点击,不改变 localPath 的 1:1 语义;倍数随区域以 gridScale 字段返回。
|
|
8
9
|
// - 不做高频抽帧,只在调用时截一张当前图。
|
|
9
10
|
// - 尺寸/缩放不一致时直接抛错,不做任何重采样或硬算 —— 显示器或缩放变了,
|
|
10
11
|
// 旧截图的坐标系已经不可信,勉强对齐只会给出错误坐标。
|
|
@@ -15,12 +16,12 @@ import pixelmatch from 'pixelmatch'
|
|
|
15
16
|
import { screenshotAction } from './actions.js'
|
|
16
17
|
import { get, recordRegions } from './shot-index.js'
|
|
17
18
|
import { resolveShotsDir } from './paths.js'
|
|
18
|
-
import { generateGridImage,
|
|
19
|
+
import { generateGridImage, resolveGridScale } from './render.js'
|
|
19
20
|
|
|
20
21
|
// 小于这个像素数的连通区域按噪声丢弃(光标闪烁、 blinking caret、亚像素抖动)。
|
|
21
22
|
export const DEFAULT_MIN_REGION_AREA = 100
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
+
// 网格图的成本是「每个区域一次放大 + PNG 编码」,差异块很多时(实测出现过单次 105 块)
|
|
24
25
|
// 全量生成只会拖慢工具并堆满临时目录,所以只给前 N 个(按面积降序)配网格图。
|
|
25
26
|
export const MAX_GRID_REGIONS = 12
|
|
26
27
|
|
|
@@ -148,10 +149,8 @@ export function diffPair({
|
|
|
148
149
|
}
|
|
149
150
|
if (i >= MAX_GRID_REGIONS) {
|
|
150
151
|
region.gridSkipped = `只给前 ${MAX_GRID_REGIONS} 个区域生成网格图(本区域排第 ${i + 1})`
|
|
151
|
-
} else if (bbox.width * bbox.height > MAX_GRID_SOURCE_AREA) {
|
|
152
|
-
region.gridSkipped =
|
|
153
|
-
`区域 ${bbox.width}x${bbox.height} 超过网格图上限 ${MAX_GRID_SOURCE_AREA} 物理像素,直接看全屏更合适`
|
|
154
152
|
} else {
|
|
153
|
+
region.gridScale = resolveGridScale(bbox.width, bbox.height)
|
|
155
154
|
region.gridPath = generateGridImage(currentMeta.path, bbox, currentMeta.scaleFactor, {
|
|
156
155
|
shotId: currentShotId,
|
|
157
156
|
index: i + 1,
|
package/src/plugin.js
CHANGED
|
@@ -21,6 +21,8 @@ const AGENT_PROTOCOL =
|
|
|
21
21
|
'2. 行动(Act):点击目标(绝对坐标或相对坐标)。' +
|
|
22
22
|
'3. 等待(Wait):点击后必须立刻调用 vp_wait_change,确认界面给出了反馈。' +
|
|
23
23
|
'4. 验证(Verify):结合 wait_change 的返回结果(变化区域、超时),判断是否达到了本步目标;' +
|
|
24
|
+
'注意 coverage 低不代表界面没变——相同底色的元素位移或扩展可能覆盖率极低但布局已巨变,' +
|
|
25
|
+
'判断要以 regions 与 gridPath 的实际内容为准;' +
|
|
24
26
|
'若超时未变,重新截图全局(Observe)再规划。' +
|
|
25
27
|
'你的“工作日志”就是你自己每一次工具调用的原始 JSON 返回,请在每次决策前仔细阅读上一步的返回。'
|
|
26
28
|
|
|
@@ -28,6 +30,22 @@ const AGENT_PROTOCOL =
|
|
|
28
30
|
const BLIND_CLICK_WARNING =
|
|
29
31
|
'绝对禁止连续两次点击(盲点)。每次点击后,必须立刻调用 vp_wait_change 等待界面响应。'
|
|
30
32
|
|
|
33
|
+
// 长程上下文纪律:只追加在三个「视觉产出」工具(screenshot/diff/wait)的 description 末尾,
|
|
34
|
+
// 防止模型反复翻历史截图路径把上下文越滚越长。
|
|
35
|
+
const CONTEXT_DISCIPLINE =
|
|
36
|
+
'【长程上下文纪律】' +
|
|
37
|
+
'1. 你的物理工作目录中,历史的差分图和网格图会被定期自动清理。' +
|
|
38
|
+
'请永远只依赖最近一次调用返回的 shotId 和 path。' +
|
|
39
|
+
'2. 不要去回忆或引用十分钟前的截图路径,如果当前状态偏离预期,请直接重新截图(vp_screenshot),' +
|
|
40
|
+
'不要试图在历史记录里找答案。' +
|
|
41
|
+
'3. 历史步骤的文本结论可以留在你的记忆里,但旧图像文件随时可能失效,以最新观察为准。'
|
|
42
|
+
|
|
43
|
+
// coverage 误导:白底同类元素位移/扩展时,覆盖率可能只有百分之几,但布局已经巨变。
|
|
44
|
+
const COVERAGE_WARNING =
|
|
45
|
+
'【重要:关于 coverage】coverage 只是变色像素的面积比例。' +
|
|
46
|
+
'如果界面是相同背景色(如白底)的元素位移或扩展,coverage 会非常低,但界面布局可能已经发生巨变。' +
|
|
47
|
+
'判断界面是否发生重要改变,不要只看 coverage,要以返回的 regions 和 gridPath 的实际内容为准。'
|
|
48
|
+
|
|
31
49
|
const SCREENSHOT_DESCRIPTION =
|
|
32
50
|
'截取主屏全屏并保存为 PNG 文件,返回文件路径(不是 base64,避免占用 token)。' +
|
|
33
51
|
'返回的图像尺寸为物理像素,坐标原点为主屏左上角。' +
|
|
@@ -36,7 +54,8 @@ const SCREENSHOT_DESCRIPTION =
|
|
|
36
54
|
'返回值中的 scaleFactor 用于把图像坐标换算成鼠标注入所需的逻辑坐标;' +
|
|
37
55
|
'调用 vp_click 时直接传图像上的物理像素坐标即可,换算由 vp_click 内部完成。' +
|
|
38
56
|
'返回的 shotId 可用于 vp_screenshot_diff 的 baseline 参数。' +
|
|
39
|
-
AGENT_PROTOCOL
|
|
57
|
+
AGENT_PROTOCOL +
|
|
58
|
+
CONTEXT_DISCIPLINE
|
|
40
59
|
|
|
41
60
|
const DIFF_DESCRIPTION =
|
|
42
61
|
'把当前屏幕与一张历史截图做逐像素差分,返回连通差异区域及其局部裁剪图。' +
|
|
@@ -45,17 +64,22 @@ const DIFF_DESCRIPTION =
|
|
|
45
64
|
'regions[].bbox 与 regions[].offset 都是物理像素,原点为截图左上角,与 vp_click 的坐标系一致:' +
|
|
46
65
|
'如果在局部图 localPath 上看到目标位于 (lx, ly),要点击它就传全局坐标 (bbox.x + lx, bbox.y + ly)。' +
|
|
47
66
|
'localPath 是原尺寸裁剪,局部图 1 像素等于全局 1 像素。' +
|
|
48
|
-
'每个区域还附带 gridPath
|
|
49
|
-
'
|
|
67
|
+
'每个区域还附带 gridPath:该区域的最近邻放大网格图,放大倍数按区域面积自适应' +
|
|
68
|
+
'(≤50000 物理像素 4 倍、≤200000 2 倍、更大 1 倍),且放大后任何单边不超过 2000 像素' +
|
|
69
|
+
'(长边 >500 不做 4 倍、>1000 不做 2 倍),实际倍数见 regions[].gridScale;' +
|
|
70
|
+
'红色网格横向/纵向每一格 = 20 物理像素,网格原点就是 bbox 左上角;' +
|
|
71
|
+
'配合 vp_click_relative 可直接按相对百分比点击,不必自己算全局坐标。' +
|
|
50
72
|
'找目标时,优先使用此工具生成局部放大网格图(gridPath)。不要在全屏图上瞎猜坐标,尽量使用 vp_click_relative 汇报百分比。' +
|
|
51
|
-
'
|
|
73
|
+
'排在第 12 个之后的区域不生成 gridPath,该区域带 gridSkipped 说明原因(bbox 与 localPath 仍可用)。' +
|
|
52
74
|
'小于 100 像素的差异块按噪声(光标、闪烁)丢弃。' +
|
|
53
75
|
'coverage 是差异像素占整屏像素的比例(0~1);coverage 大于 0.4 时局部图已经没有意义,建议直接看全屏截图。' +
|
|
76
|
+
COVERAGE_WARNING +
|
|
54
77
|
'changed=false 表示没有超过噪声阈值的差异区域,不等于屏幕完全没变;要确认刚才的操作是否生效,建议看全屏。' +
|
|
55
78
|
'baseline 与当前截图的宽高或 scaleFactor 不一致时直接报错(多半是换了显示器或改了缩放比例),' +
|
|
56
79
|
'不做重采样硬算,请重新调用 vp_screenshot 取新的 baseline。' +
|
|
57
80
|
'每次调用都会真实截一张新屏幕并写入索引,返回的 currentShotId 可以继续当 baseline 用。' +
|
|
58
|
-
AGENT_PROTOCOL
|
|
81
|
+
AGENT_PROTOCOL +
|
|
82
|
+
CONTEXT_DISCIPLINE
|
|
59
83
|
|
|
60
84
|
const WAIT_DESCRIPTION =
|
|
61
85
|
'等待屏幕相对某张历史截图发生变化:工具内部循环截屏并逐像素比对,一旦变化面积超过阈值就立刻返回,' +
|
|
@@ -70,16 +94,19 @@ const WAIT_DESCRIPTION =
|
|
|
70
94
|
'变化判据:过滤噪声(抗锯齿像素、以及小于 100 像素的连通块)后,差异像素占整屏比例 > 0.001 才算变了。' +
|
|
71
95
|
'检测到变化返回 {changed:true, waitedMs, currentShotId, regions, coverage},' +
|
|
72
96
|
'其中 currentShotId 是那张正式截图,可以继续当后续 baseline 用。' +
|
|
97
|
+
COVERAGE_WARNING +
|
|
73
98
|
'超时未变返回 {changed:false, waitedMs, timeout:true},没有 currentShotId 也没有 regions;' +
|
|
74
99
|
'说明这段时间屏幕真的没动。如果返回 timeout: true,代表操作可能失败或界面卡死:' +
|
|
75
100
|
'绝对不要盲目重试,必须先调用 vp_screenshot 获取全局截图、重新规划,再决定下一步(包括是否继续等待)。' +
|
|
76
101
|
'坐标系与 vp_screenshot_diff 完全一致:regions[].bbox 与 offset 都是物理像素、原点为截图左上角,' +
|
|
77
102
|
'局部图 localPath 未放大;在局部图上看到目标位于 (lx, ly),要点它就传全局坐标 (bbox.x + lx, bbox.y + ly)。' +
|
|
78
|
-
'每个区域还附带 gridPath
|
|
79
|
-
'
|
|
80
|
-
'
|
|
103
|
+
'每个区域还附带 gridPath:按面积自适应放大的网格图(≤50000 物理像素 4 倍、≤200000 2 倍、更大 1 倍;' +
|
|
104
|
+
'放大后任何单边不超过 2000 像素,长边 >500 不做 4 倍、>1000 不做 2 倍,实际倍数见 regions[].gridScale),' +
|
|
105
|
+
'每一格 = 20 物理像素,原点为 bbox 左上角;配合 vp_click_relative 可直接按相对百分比点击。' +
|
|
106
|
+
'排在第 12 个之后的区域没有 gridPath,会带 gridSkipped 说明。' +
|
|
81
107
|
'它只负责「等」,不会替你制造变化:需要交互就先 vp_click,再用本工具等界面响应。' +
|
|
82
|
-
AGENT_PROTOCOL
|
|
108
|
+
AGENT_PROTOCOL +
|
|
109
|
+
CONTEXT_DISCIPLINE
|
|
83
110
|
|
|
84
111
|
const CLICK_DESCRIPTION =
|
|
85
112
|
'在主屏上点击一次鼠标。入参 x/y 为物理像素坐标,原点为主屏左上角,' +
|
|
@@ -95,7 +122,7 @@ const CLICK_RELATIVE_DESCRIPTION =
|
|
|
95
122
|
'在某个差分区域内部按相对位置点击,不需要自己计算全局像素坐标。' +
|
|
96
123
|
'shotId 用 vp_screenshot_diff / vp_wait_change 返回的 currentShotId;index 是 regions 数组里的 1-based 序号。' +
|
|
97
124
|
'xPercent/yPercent 是 0.0~1.0 的相对位置:0 表示区域左/上边缘,1 表示右/下边缘。' +
|
|
98
|
-
'建议先看该区域的 gridPath
|
|
125
|
+
'建议先看该区域的 gridPath:那是按面积自适应放大的网格图(倍数见 regions[].gridScale),横向/纵向每一小格 = 20 物理像素,' +
|
|
99
126
|
'网格原点为 bbox 左上角;数好格子后,用「格数 × 20 ÷ bbox 宽(或高)」换算成百分比,也可以直接目测。' +
|
|
100
127
|
'换算由本工具完成:physical.x = bbox.x + round(xPercent × (bbox.width - 1)),y 同理;不要自己算。' +
|
|
101
128
|
'index 越界、百分比不在 0~1、shotId 没有差分区域时直接抛错。' +
|
|
@@ -374,6 +401,7 @@ export function createPlugin(defineTool, ctx, hotkeyState = { available: false,
|
|
|
374
401
|
bbox: BBOX_SCHEMA,
|
|
375
402
|
localPath: { type: 'string', required: true },
|
|
376
403
|
gridPath: { type: 'string' },
|
|
404
|
+
gridScale: { type: 'number' },
|
|
377
405
|
gridSkipped: { type: 'string' },
|
|
378
406
|
offset: OFFSET_SCHEMA,
|
|
379
407
|
},
|
|
@@ -399,7 +427,7 @@ export function createPlugin(defineTool, ctx, hotkeyState = { available: false,
|
|
|
399
427
|
`全局坐标 = (bbox.x + lx, bbox.y + ly) = (${region.bbox.x} + lx, ${region.bbox.y} + ly);` +
|
|
400
428
|
`局部图 ${region.localPath}` +
|
|
401
429
|
(region.gridPath !== undefined
|
|
402
|
-
? `;网格图 ${region.gridPath}
|
|
430
|
+
? `;网格图 ${region.gridPath}(${region.gridScale} 倍放大,每格 20 物理像素)`
|
|
403
431
|
: `;无网格图:${region.gridSkipped}`),
|
|
404
432
|
).join('\n')
|
|
405
433
|
: '未发现超过噪声阈值的差异区域;要确认操作是否生效,建议看全屏截图') +
|
|
@@ -453,6 +481,7 @@ export function createPlugin(defineTool, ctx, hotkeyState = { available: false,
|
|
|
453
481
|
bbox: BBOX_SCHEMA,
|
|
454
482
|
localPath: { type: 'string', required: true },
|
|
455
483
|
gridPath: { type: 'string' },
|
|
484
|
+
gridScale: { type: 'number' },
|
|
456
485
|
gridSkipped: { type: 'string' },
|
|
457
486
|
offset: OFFSET_SCHEMA,
|
|
458
487
|
},
|
|
@@ -473,12 +502,12 @@ export function createPlugin(defineTool, ctx, hotkeyState = { available: false,
|
|
|
473
502
|
`全局坐标 = (${region.bbox.x} + lx, ${region.bbox.y} + ly);` +
|
|
474
503
|
`局部图 ${region.localPath}` +
|
|
475
504
|
(region.gridPath !== undefined
|
|
476
|
-
? `;网格图 ${region.gridPath}
|
|
505
|
+
? `;网格图 ${region.gridPath}(${region.gridScale} 倍放大,每格 20 物理像素)`
|
|
477
506
|
: `;无网格图:${region.gridSkipped}`),
|
|
478
507
|
).join('\n') +
|
|
479
508
|
`\n` + JSON.stringify(value) + noticeLine()
|
|
480
509
|
: `等待 ${value.waitedMs}ms 超时:屏幕相对 baseline #${_args.baseline} 没有可见变化,` +
|
|
481
|
-
`本次未落盘、未消耗 shotId
|
|
510
|
+
`本次未落盘、未消耗 shotId。超时未检测到变化。请先调用 vp_screenshot 重新观察全屏,切勿盲目重试。\n` +
|
|
482
511
|
JSON.stringify(value) + noticeLine(),
|
|
483
512
|
}],
|
|
484
513
|
},
|
package/src/render.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 位置编码渲染:把差分区域裁出来、按面积自适应倍率最近邻放大、叠加半透明红色网格,
|
|
2
2
|
// 让模型在「带标尺的放大镜」上数格子,而不是在全屏小图上目测偏移。
|
|
3
3
|
//
|
|
4
4
|
// 选型(不引新依赖):只用 pngjs + 手写 Bresenham。
|
|
@@ -14,11 +14,29 @@ import { resolveShotsDir } from './paths.js'
|
|
|
14
14
|
|
|
15
15
|
export const GRID_SCALE = 4
|
|
16
16
|
export const GRID_STEP_PHYSICAL = 20
|
|
17
|
+
// 4x 档的图内步距(放大图上每格像素数);其余档位 = GRID_STEP_PHYSICAL × 倍数。
|
|
17
18
|
export const GRID_STEP_IMAGE = GRID_SCALE * GRID_STEP_PHYSICAL // 80
|
|
18
19
|
|
|
19
|
-
// 4x
|
|
20
|
-
//
|
|
21
|
-
|
|
20
|
+
// 自适应倍率阈值(物理像素面积):小区域 4x 保极限精度,中区域 2x 体积减半,
|
|
21
|
+
// 大区域(整页刷新级)1x 只画网格 —— 1920x1200 全屏若 4x = 7680x4800 ≈ 147MB RGBA,绝不可接受。
|
|
22
|
+
// 1x 后输出不可能超过源截图本身,所以不再需要旧的 250k 物理像素 gridSkipped 上限。
|
|
23
|
+
export const GRID_AREA_MAX_4X = 50_000
|
|
24
|
+
export const GRID_AREA_MAX_2X = 200_000
|
|
25
|
+
|
|
26
|
+
// 面积小但极细长的区域(4344x4 的边框/分隔线)会骗过面积档位:4x 后变成 17376x16 的超长图。
|
|
27
|
+
// 因此再按单边长度封顶:放大后任何一边都不得超过 2000 像素(即 4x 要求长边 <=500,2x 要求 <=1000)。
|
|
28
|
+
// 1x 不再放大,允许输出等于源区域本身,即使源区域长边超过 2000(裁剪尺寸不能凭空压缩)。
|
|
29
|
+
export const GRID_MAX_OUTPUT_EDGE = 2000
|
|
30
|
+
|
|
31
|
+
/** 按区域物理像素尺寸选放大倍数:面积定基础档位,单边上限再降档。 */
|
|
32
|
+
export function resolveGridScale(width, height) {
|
|
33
|
+
const area = width * height
|
|
34
|
+
const maxEdge = Math.max(width, height)
|
|
35
|
+
let scale = area <= GRID_AREA_MAX_4X ? 4 : area <= GRID_AREA_MAX_2X ? 2 : 1
|
|
36
|
+
if (scale === 4 && maxEdge * 4 > GRID_MAX_OUTPUT_EDGE) scale = 2
|
|
37
|
+
if (scale === 2 && maxEdge * 2 > GRID_MAX_OUTPUT_EDGE) scale = 1
|
|
38
|
+
return scale
|
|
39
|
+
}
|
|
22
40
|
|
|
23
41
|
const LINE_R = 255
|
|
24
42
|
const LINE_G = 0
|
|
@@ -59,7 +77,8 @@ function drawLine(png, x0, y0, x1, y1, alpha = GRID_ALPHA) {
|
|
|
59
77
|
}
|
|
60
78
|
|
|
61
79
|
/**
|
|
62
|
-
* 从源截图裁剪 bbox
|
|
80
|
+
* 从源截图裁剪 bbox、按 resolveGridScale(宽, 高) 自适应倍率最近邻放大、
|
|
81
|
+
* 每 GRID_STEP_PHYSICAL 物理像素画一条半透明红线,
|
|
63
82
|
* 落盘为 vp-grid-<shotId>-<index>.png。网格原点 = bbox 左上角(bbox 内第 0 个物理像素)。
|
|
64
83
|
*
|
|
65
84
|
* 文件名里的 shotId/index 不在规格签名里,但文件名要求它们,所以通过第 4 个参数传入;
|
|
@@ -85,13 +104,7 @@ export function generateGridImage(sourcePngPath, bbox, scaleFactor, { shotId, in
|
|
|
85
104
|
`收到 shotId=${shotId}, index=${index}`,
|
|
86
105
|
)
|
|
87
106
|
}
|
|
88
|
-
const
|
|
89
|
-
if (area > MAX_GRID_SOURCE_AREA) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`generateGridImage: 区域 ${bbox.width}x${bbox.height}(${area} 像素)超过网格图上限 ` +
|
|
92
|
-
`${MAX_GRID_SOURCE_AREA},不放大(放大后缓冲为源面积的 16 倍)`,
|
|
93
|
-
)
|
|
94
|
-
}
|
|
107
|
+
const scale = resolveGridScale(bbox.width, bbox.height)
|
|
95
108
|
|
|
96
109
|
const source = PNG.sync.read(readFileSync(sourcePngPath))
|
|
97
110
|
if (bbox.x + bbox.width > source.width || bbox.y + bbox.height > source.height) {
|
|
@@ -101,28 +114,28 @@ export function generateGridImage(sourcePngPath, bbox, scaleFactor, { shotId, in
|
|
|
101
114
|
)
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
const outWidth = bbox.width *
|
|
105
|
-
const outHeight = bbox.height *
|
|
117
|
+
const outWidth = bbox.width * scale
|
|
118
|
+
const outHeight = bbox.height * scale
|
|
106
119
|
const out = new PNG({ width: outWidth, height: outHeight })
|
|
107
120
|
const rowBuf = Buffer.alloc(outWidth * 4)
|
|
108
121
|
|
|
109
122
|
for (let sy = 0; sy < bbox.height; sy++) {
|
|
110
123
|
const srcRow = ((bbox.y + sy) * source.width + bbox.x) * 4
|
|
111
124
|
for (let dx4 = 0; dx4 < outWidth * 4; dx4 += 4) {
|
|
112
|
-
// dx4/4 = 目标列,再除以
|
|
113
|
-
const from = srcRow + ((dx4 / (4 *
|
|
125
|
+
// dx4/4 = 目标列,再除以 scale 才是源列(最近邻:目标 scale 列共用 1 源像素)。
|
|
126
|
+
const from = srcRow + ((dx4 / (4 * scale)) | 0) * 4
|
|
114
127
|
rowBuf[dx4] = source.data[from]
|
|
115
128
|
rowBuf[dx4 + 1] = source.data[from + 1]
|
|
116
129
|
rowBuf[dx4 + 2] = source.data[from + 2]
|
|
117
130
|
rowBuf[dx4 + 3] = source.data[from + 3]
|
|
118
131
|
}
|
|
119
|
-
for (let k = 0; k <
|
|
120
|
-
rowBuf.copy(out.data, (sy *
|
|
132
|
+
for (let k = 0; k < scale; k++) {
|
|
133
|
+
rowBuf.copy(out.data, (sy * scale + k) * outWidth * 4)
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
for (let x = 0; x < outWidth; x +=
|
|
125
|
-
for (let y = 0; y < outHeight; y +=
|
|
137
|
+
for (let x = 0; x < outWidth; x += GRID_STEP_PHYSICAL * scale) drawLine(out, x, 0, x, outHeight - 1)
|
|
138
|
+
for (let y = 0; y < outHeight; y += GRID_STEP_PHYSICAL * scale) drawLine(out, 0, y, outWidth - 1, y)
|
|
126
139
|
|
|
127
140
|
const dir = resolveShotsDir()
|
|
128
141
|
mkdirSync(dir, { recursive: true })
|
package/src/shot-index.js
CHANGED
|
@@ -118,7 +118,7 @@ export function recordRegions(shotId, regions) {
|
|
|
118
118
|
entry.regions = regions.map((region) => ({
|
|
119
119
|
bbox: { ...region.bbox },
|
|
120
120
|
localPath: region.localPath,
|
|
121
|
-
...(region.gridPath !== undefined ? { gridPath: region.gridPath } : {}),
|
|
121
|
+
...(region.gridPath !== undefined ? { gridPath: region.gridPath, gridScale: region.gridScale } : {}),
|
|
122
122
|
}))
|
|
123
123
|
flush()
|
|
124
124
|
return true
|