specific-ai-cli 0.0.0-stage → 0.1.1

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.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +177 -0
  3. package/README.ja.md +177 -0
  4. package/README.ko.md +177 -0
  5. package/README.md +174 -2
  6. package/README.zh-TW.md +177 -0
  7. package/assets/banner.txt +3 -0
  8. package/dist/cli/argv.js +98 -0
  9. package/dist/cli/commands/auth.js +11 -0
  10. package/dist/cli/commands/config.js +23 -0
  11. package/dist/cli/commands/doctor.js +134 -0
  12. package/dist/cli/commands/platforms.js +24 -0
  13. package/dist/cli/commands/version.js +4 -0
  14. package/dist/cli/confirm.js +22 -0
  15. package/dist/cli/dispatch.js +440 -0
  16. package/dist/cli/help.js +223 -0
  17. package/dist/cli/hints.js +60 -0
  18. package/dist/cli/main.js +18 -0
  19. package/dist/cli/output.js +66 -0
  20. package/dist/core/auth-handlers.js +41 -0
  21. package/dist/core/auth-store.js +119 -0
  22. package/dist/core/capabilities.js +163 -0
  23. package/dist/core/config.js +121 -0
  24. package/dist/core/cookies.js +164 -0
  25. package/dist/core/errors.js +37 -0
  26. package/dist/core/files.js +89 -0
  27. package/dist/core/fsutil.js +52 -0
  28. package/dist/core/hash.js +4 -0
  29. package/dist/core/http.js +331 -0
  30. package/dist/core/image.js +280 -0
  31. package/dist/core/log.js +40 -0
  32. package/dist/core/login.js +191 -0
  33. package/dist/core/mp4.js +130 -0
  34. package/dist/core/normalize.js +207 -0
  35. package/dist/core/options.js +110 -0
  36. package/dist/core/paths.js +38 -0
  37. package/dist/core/pb.js +59 -0
  38. package/dist/core/py.js +181 -0
  39. package/dist/core/rand.js +131 -0
  40. package/dist/core/registry.js +129 -0
  41. package/dist/core/schemas.js +41 -0
  42. package/dist/core/stream.js +104 -0
  43. package/dist/core/toolkit.js +35 -0
  44. package/dist/core/vm.js +73 -0
  45. package/dist/core/vocab.js +157 -0
  46. package/dist/platforms/_shared/impaas.js +410 -0
  47. package/dist/platforms/bilibili/index.js +216 -0
  48. package/dist/platforms/bilibili/web/api.js +806 -0
  49. package/dist/platforms/bilibili/web/client.js +311 -0
  50. package/dist/platforms/bilibili/web/commands.js +783 -0
  51. package/dist/platforms/bilibili/web/ddddocr.js +342 -0
  52. package/dist/platforms/bilibili/web/gaia.js +130 -0
  53. package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
  54. package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
  55. package/dist/platforms/bilibili/web/geetest.js +450 -0
  56. package/dist/platforms/bilibili/web/normalize.js +234 -0
  57. package/dist/platforms/bilibili/web/profile.js +135 -0
  58. package/dist/platforms/bilibili/web/resolve.js +112 -0
  59. package/dist/platforms/bilibili/web/sign.js +223 -0
  60. package/dist/platforms/bilibili/web/upos.js +102 -0
  61. package/dist/platforms/douyin/index.js +142 -0
  62. package/dist/platforms/douyin/web/api.js +863 -0
  63. package/dist/platforms/douyin/web/client.js +413 -0
  64. package/dist/platforms/douyin/web/commands.js +712 -0
  65. package/dist/platforms/douyin/web/creator.js +601 -0
  66. package/dist/platforms/douyin/web/crypto.js +247 -0
  67. package/dist/platforms/douyin/web/dtrait.js +190 -0
  68. package/dist/platforms/douyin/web/im.js +502 -0
  69. package/dist/platforms/douyin/web/image.js +5 -0
  70. package/dist/platforms/douyin/web/live.js +225 -0
  71. package/dist/platforms/douyin/web/mssdk.js +148 -0
  72. package/dist/platforms/douyin/web/normalize.js +326 -0
  73. package/dist/platforms/douyin/web/passport.js +1247 -0
  74. package/dist/platforms/douyin/web/profile.js +221 -0
  75. package/dist/platforms/douyin/web/proto.js +26 -0
  76. package/dist/platforms/douyin/web/resolve.js +88 -0
  77. package/dist/platforms/douyin/web/sign.js +436 -0
  78. package/dist/platforms/douyin/web/tos.js +78 -0
  79. package/dist/platforms/index.js +17 -0
  80. package/dist/platforms/jd/index.js +67 -0
  81. package/dist/platforms/jd/web/aks.js +115 -0
  82. package/dist/platforms/jd/web/api.js +549 -0
  83. package/dist/platforms/jd/web/chat.js +132 -0
  84. package/dist/platforms/jd/web/client.js +470 -0
  85. package/dist/platforms/jd/web/commands.js +557 -0
  86. package/dist/platforms/jd/web/jcap/helper.js +90 -0
  87. package/dist/platforms/jd/web/jcap/host.js +107 -0
  88. package/dist/platforms/jd/web/jcap/lsd.js +300 -0
  89. package/dist/platforms/jd/web/jcap/solver.js +995 -0
  90. package/dist/platforms/jd/web/jcap.js +150 -0
  91. package/dist/platforms/jd/web/js.js +420 -0
  92. package/dist/platforms/jd/web/login.js +775 -0
  93. package/dist/platforms/jd/web/normalize.js +297 -0
  94. package/dist/platforms/jd/web/profile.js +256 -0
  95. package/dist/platforms/jd/web/util.js +175 -0
  96. package/dist/platforms/jd/web/webm.js +133 -0
  97. package/dist/platforms/kuaishou/index.js +98 -0
  98. package/dist/platforms/kuaishou/web/api.js +694 -0
  99. package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
  100. package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
  101. package/dist/platforms/kuaishou/web/captcha.js +273 -0
  102. package/dist/platforms/kuaishou/web/client.js +487 -0
  103. package/dist/platforms/kuaishou/web/commands.js +863 -0
  104. package/dist/platforms/kuaishou/web/gap.js +144 -0
  105. package/dist/platforms/kuaishou/web/gdfp.js +376 -0
  106. package/dist/platforms/kuaishou/web/gql.js +11 -0
  107. package/dist/platforms/kuaishou/web/live.js +125 -0
  108. package/dist/platforms/kuaishou/web/normalize.js +241 -0
  109. package/dist/platforms/kuaishou/web/oracle.js +329 -0
  110. package/dist/platforms/kuaishou/web/profile.js +259 -0
  111. package/dist/platforms/kuaishou/web/resolve.js +84 -0
  112. package/dist/platforms/kuaishou/web/session.js +592 -0
  113. package/dist/platforms/kuaishou/web/sign.js +568 -0
  114. package/dist/platforms/taobao/index.js +56 -0
  115. package/dist/platforms/taobao/web/api.js +96 -0
  116. package/dist/platforms/taobao/web/client.js +102 -0
  117. package/dist/platforms/taobao/web/commands.js +172 -0
  118. package/dist/platforms/taobao/web/im.js +68 -0
  119. package/dist/platforms/taobao/web/normalize.js +103 -0
  120. package/dist/platforms/taobao/web/profile.js +85 -0
  121. package/dist/platforms/taobao/web/resolve.js +51 -0
  122. package/dist/platforms/taobao/web/sign.js +35 -0
  123. package/dist/platforms/tiktok/index.js +120 -0
  124. package/dist/platforms/tiktok/web/api.js +1234 -0
  125. package/dist/platforms/tiktok/web/client.js +362 -0
  126. package/dist/platforms/tiktok/web/commands.js +821 -0
  127. package/dist/platforms/tiktok/web/im.js +133 -0
  128. package/dist/platforms/tiktok/web/jsrun.js +126 -0
  129. package/dist/platforms/tiktok/web/normalize.js +286 -0
  130. package/dist/platforms/tiktok/web/profile.js +169 -0
  131. package/dist/platforms/tiktok/web/resolve.js +156 -0
  132. package/dist/platforms/tiktok/web/sign.js +585 -0
  133. package/dist/platforms/tiktok/web/upload.js +805 -0
  134. package/dist/platforms/tiktok/web/wire.js +490 -0
  135. package/dist/platforms/weibo/index.js +91 -0
  136. package/dist/platforms/weibo/web/api.js +206 -0
  137. package/dist/platforms/weibo/web/client.js +155 -0
  138. package/dist/platforms/weibo/web/commands.js +202 -0
  139. package/dist/platforms/weibo/web/normalize.js +137 -0
  140. package/dist/platforms/weibo/web/profile.js +246 -0
  141. package/dist/platforms/weibo/web/resolve.js +60 -0
  142. package/dist/platforms/weibo/web/sign.js +85 -0
  143. package/dist/platforms/x/index.js +107 -0
  144. package/dist/platforms/x/web/api.js +443 -0
  145. package/dist/platforms/x/web/article.js +212 -0
  146. package/dist/platforms/x/web/client.js +202 -0
  147. package/dist/platforms/x/web/commands.js +363 -0
  148. package/dist/platforms/x/web/graphql.js +108 -0
  149. package/dist/platforms/x/web/normalize.js +235 -0
  150. package/dist/platforms/x/web/profile.js +99 -0
  151. package/dist/platforms/x/web/resolve.js +44 -0
  152. package/dist/platforms/x/web/transaction.js +171 -0
  153. package/dist/platforms/xhs/index.js +101 -0
  154. package/dist/platforms/xhs/web/api.js +278 -0
  155. package/dist/platforms/xhs/web/client.js +540 -0
  156. package/dist/platforms/xhs/web/commands.js +1046 -0
  157. package/dist/platforms/xhs/web/creator-api.js +609 -0
  158. package/dist/platforms/xhs/web/creator.js +203 -0
  159. package/dist/platforms/xhs/web/js.js +194 -0
  160. package/dist/platforms/xhs/web/login.js +230 -0
  161. package/dist/platforms/xhs/web/normalize.js +166 -0
  162. package/dist/platforms/xhs/web/profile.js +127 -0
  163. package/dist/platforms/xhs/web/push.js +361 -0
  164. package/dist/platforms/xhs/web/resolve.js +69 -0
  165. package/dist/platforms/xhs/web/state.js +368 -0
  166. package/dist/platforms/xianyu/index.js +59 -0
  167. package/dist/platforms/xianyu/web/api.js +312 -0
  168. package/dist/platforms/xianyu/web/client.js +134 -0
  169. package/dist/platforms/xianyu/web/commands.js +305 -0
  170. package/dist/platforms/xianyu/web/im.js +73 -0
  171. package/dist/platforms/xianyu/web/normalize.js +123 -0
  172. package/dist/platforms/xianyu/web/profile.js +146 -0
  173. package/dist/platforms/xianyu/web/resolve.js +48 -0
  174. package/dist/platforms/xianyu/web/sign.js +79 -0
  175. package/package.json +77 -4
  176. package/static/douyin/Live.proto +194 -0
  177. package/static/douyin/Request.proto +89 -0
  178. package/static/douyin/Response.proto +46 -0
  179. package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
  180. package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
  181. package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
  182. package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
  183. package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
  184. package/static/douyin/challenge_profile.json +801 -0
  185. package/static/douyin/challenge_template_runner.js +217 -0
  186. package/static/douyin/dtrait_profile.json +66 -0
  187. package/static/douyin/mstoken_common_profile.json +1 -0
  188. package/static/douyin/mstoken_profile.json +214 -0
  189. package/static/jd/h5st5_env.js +750 -0
  190. package/static/jd/h5st5_lib.js +5 -0
  191. package/static/jd/h5st5_server.js +49 -0
  192. package/static/jd/jcap/env/env_core.js +512 -0
  193. package/static/jd/jcap/env/run.js +1307 -0
  194. package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
  195. package/static/jd/package.json +4 -0
  196. package/static/jd/pc_tk_lib.js +1 -0
  197. package/static/jd/pc_tk_server.js +63 -0
  198. package/static/jd/summer_cryptico_runner.js +50 -0
  199. package/static/jd/webm/env/run.js +453 -0
  200. package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
  201. package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
  202. package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
  203. package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
  204. package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
  205. package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
  206. package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
  207. package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
  208. package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
  209. package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
  210. package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
  211. package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
  212. package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
  213. package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
  214. package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
  215. package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
  216. package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
  217. package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
  218. package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
  219. package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
  220. package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
  221. package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
  222. package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
  223. package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
  224. package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
  225. package/static/kuaishou/js/cp-kwf.js +1 -0
  226. package/static/kuaishou/tools/kwf_oracle.js +799 -0
  227. package/static/kuaishou/tools/kws_oracle.js +885 -0
  228. package/static/kuaishou/tools/like_token_oracle.js +70 -0
  229. package/static/taobao/taobao_js_20260407.js +570 -0
  230. package/static/tiktok/Tiktok_Request.json +597 -0
  231. package/static/tiktok/package.json +3 -0
  232. package/static/tiktok/shop_bsid/core.js +1 -0
  233. package/static/tiktok/shop_bsid/env_core.js +512 -0
  234. package/static/tiktok/shop_bsid/loader.js +1 -0
  235. package/static/tiktok/shop_bsid/run.js +753 -0
  236. package/static/tiktok/shop_bsid/sign.js +100 -0
  237. package/static/tiktok/shop_bsid/webmssdk.js +1 -0
  238. package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
  239. package/static/tiktok/signing/env/env_core.js +512 -0
  240. package/static/tiktok/signing/env/sign.js +455 -0
  241. package/static/tiktok/signing/sdk_config.json +230 -0
  242. package/static/tiktok/signing/source/webmssdk.js +1 -0
  243. package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
  244. package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
  245. package/static/x/graphql.json +7777 -0
  246. package/static/x/transaction_l1.json +12 -0
  247. package/static/xhs/package.json +3 -0
  248. package/static/xhs/xhs_core/js/b1.js +210 -0
  249. package/static/xhs/xhs_core/js/mns.js +340 -0
  250. package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
  251. package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
  252. package/static/xhs/xhs_core/js/sign.js +225 -0
  253. package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
  254. package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
  255. package/static/xhs/xhs_core/js/xs_common.js +79 -0
  256. package/static/xhs/xhs_creator/js/profile.js +115 -0
  257. package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
  258. package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
  259. package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
  260. package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
  261. package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
  262. package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
  263. package/static/xhs/xhs_pc/js/b1.js +9 -0
  264. package/static/xhs/xhs_pc/js/deflate.js +320 -0
  265. package/static/xhs/xhs_pc/js/mns.js +4 -0
  266. package/static/xhs/xhs_pc/js/profile.js +141 -0
  267. package/static/xhs/xhs_pc/js/rap.js +173 -0
  268. package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
  269. package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
  270. package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
  271. package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
  272. package/static/xhs/xhs_pc/js/sign.js +6 -0
  273. package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
  274. package/static/xhs/xhs_pc/js/xs_common.js +4 -0
  275. package/static/xianyu/et_f.js +2 -0
  276. package/static/xianyu/gen_tfstk.js +410 -0
  277. package/static/xianyu/goofish_js_version_2.js +568 -0
@@ -0,0 +1,995 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { mulberry32 } from '../../../../core/rand.js';
3
+ import { crop, fromMat, img, loadCv, toMat, withMats } from '../../../../core/image.js';
4
+ import { pyRound } from '../../../../core/py.js';
5
+ import { detectLines } from './lsd.js';
6
+ /**
7
+ * JCAP 验证码的本地求解(上游 static/jcap/run/captcha_solver.py 的移植):
8
+ * tp=2 点选(U2Net 显著性 + 带掩码模板匹配)、tp=3 轨迹(U2Net + 骨架 + 四角几何拟合)、
9
+ * tp=26 旋转(方向分类模型 + LSD 直线轴向)、tp=30 滑块(轮廓与纹理相关)。
10
+ * numpy 的 float32 运算用 Math.fround 模拟;scipy 的 differential_evolution 换成同算法的本地实现(随机序列不同)。
11
+ */
12
+ const f = Math.fround;
13
+ /** CPython 的 math.degrees / math.radians 用的常量。 */
14
+ const RAD_TO_DEG = 180 / Math.PI;
15
+ const DEG_TO_RAD = Math.PI / 180;
16
+ const f32 = (v) => f(v);
17
+ // ---------------------------------------------------------------- 数值工具
18
+ /** numpy 对 float32 数组的 pairwise 求和(np.sum / np.mean 的累加顺序)。 */
19
+ export function pairwiseSum(a, start = 0, n = a.length) {
20
+ if (n < 8) {
21
+ let res = 0;
22
+ for (let i = 0; i < n; i++)
23
+ res = f(res + a[start + i]);
24
+ return res;
25
+ }
26
+ if (n <= 128) {
27
+ const r = [0, 1, 2, 3, 4, 5, 6, 7].map((j) => f(a[start + j]));
28
+ let i = 8;
29
+ for (; i < n - (n % 8); i += 8)
30
+ for (let j = 0; j < 8; j++)
31
+ r[j] = f(r[j] + a[start + i + j]);
32
+ let res = f(f(f(r[0] + r[1]) + f(r[2] + r[3])) + f(f(r[4] + r[5]) + f(r[6] + r[7])));
33
+ for (; i < n; i++)
34
+ res = f(res + a[start + i]);
35
+ return res;
36
+ }
37
+ let n2 = Math.floor(n / 2);
38
+ n2 -= n2 % 8;
39
+ return f(pairwiseSum(a, start, n2) + pairwiseSum(a, start + n2, n - n2));
40
+ }
41
+ export const mean32 = (a) => f(pairwiseSum(a) / a.length);
42
+ const clip = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
43
+ function reflect101(p, len) {
44
+ if (len === 1)
45
+ return 0;
46
+ while (p < 0 || p >= len)
47
+ p = p < 0 ? -p : 2 * len - p - 2;
48
+ return p;
49
+ }
50
+ /**
51
+ * cv2.remap(image, x, y, INTER_LINEAR, BORDER_REFLECT_101) 对一组点采样(float32 图像)。
52
+ * OpenCV 5 的 remap 直接用浮点坐标做两次线性插值(SIMD 下为 FMA):t0 = a + fx·(b − a),t1 = c + fx·(d − c),
53
+ * v = t0 + fy·(t1 − t0)。float 乘积在 double 里是精确的,先在 double 里算再舍入一次即模拟 FMA。
54
+ */
55
+ export function remap(im, xs, ys) {
56
+ const { width, height, channels: cn, data } = im;
57
+ const out = new Float32Array(xs.length * cn);
58
+ const fma = (a, b, c) => f(a * b + c);
59
+ for (let k = 0; k < xs.length; k++) {
60
+ const x = f(xs[k]);
61
+ const y = f(ys[k]);
62
+ const sx = Math.floor(x);
63
+ const sy = Math.floor(y);
64
+ const fx = f(x - sx);
65
+ const fy = f(y - sy);
66
+ const x0 = reflect101(sx, width);
67
+ const x1 = reflect101(sx + 1, width);
68
+ const y0 = reflect101(sy, height);
69
+ const y1 = reflect101(sy + 1, height);
70
+ for (let c = 0; c < cn; c++) {
71
+ const a = data[(y0 * width + x0) * cn + c];
72
+ const b = data[(y0 * width + x1) * cn + c];
73
+ const d0 = data[(y1 * width + x0) * cn + c];
74
+ const d1 = data[(y1 * width + x1) * cn + c];
75
+ const t0 = fma(fx, f(b - a), a);
76
+ const t1 = fma(fx, f(d1 - d0), d0);
77
+ out[k * cn + c] = fma(fy, f(t1 - t0), t0);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ // ---------------------------------------------------------------- OpenCV 包装
83
+ async function cvOps() {
84
+ const cv = await loadCv();
85
+ const run = (fn) => withMats(fn);
86
+ const resize = (im, w, h, interp) => run((keep) => {
87
+ const dst = keep(new cv.Mat());
88
+ cv.resize(keep(toMat(cv, im)), dst, new cv.Size(w, h), 0, 0, interp);
89
+ return fromMat(cv, dst);
90
+ });
91
+ const cvtColor = (im, code) => run((keep) => {
92
+ const dst = keep(new cv.Mat());
93
+ cv.cvtColor(keep(toMat(cv, im)), dst, code);
94
+ return fromMat(cv, dst);
95
+ });
96
+ const medianBlur = (im, k) => run((keep) => {
97
+ const dst = keep(new cv.Mat());
98
+ cv.medianBlur(keep(toMat(cv, im)), dst, k);
99
+ return fromMat(cv, dst);
100
+ });
101
+ const distanceTransform = (binary, mask) => run((keep) => {
102
+ const dst = keep(new cv.Mat());
103
+ cv.distanceTransform(keep(toMat(cv, binary)), dst, cv.DIST_L2, mask);
104
+ return fromMat(cv, dst);
105
+ });
106
+ const sobel = (im, dx, dy, ksize = 3) => run((keep) => {
107
+ const dst = keep(new cv.Mat());
108
+ cv.Sobel(keep(toMat(cv, im)), dst, cv.CV_32F, dx, dy, ksize);
109
+ return fromMat(cv, dst);
110
+ });
111
+ const canny = (im, t1, t2) => run((keep) => {
112
+ const dst = keep(new cv.Mat());
113
+ cv.Canny(keep(toMat(cv, im)), dst, t1, t2);
114
+ return fromMat(cv, dst);
115
+ });
116
+ const erode3 = (im) => run((keep) => {
117
+ const dst = keep(new cv.Mat());
118
+ const kernel = keep(cv.Mat.ones(3, 3, cv.CV_8U));
119
+ cv.erode(keep(toMat(cv, im)), dst, kernel, new cv.Point(-1, -1), 1);
120
+ return fromMat(cv, dst);
121
+ });
122
+ const gaussian = (im, sigma) => run((keep) => {
123
+ const dst = keep(new cv.Mat());
124
+ cv.GaussianBlur(keep(toMat(cv, im)), dst, new cv.Size(0, 0), sigma);
125
+ return fromMat(cv, dst);
126
+ });
127
+ const components = (binary) => run((keep) => {
128
+ const labels = keep(new cv.Mat());
129
+ const stats = keep(new cv.Mat());
130
+ const centroids = keep(new cv.Mat());
131
+ const count = cv.connectedComponentsWithStats(keep(toMat(cv, binary)), labels, stats, centroids, 8, cv.CV_32S);
132
+ return { count, labels: Int32Array.from(labels.data32S), stats: Int32Array.from(stats.data32S) };
133
+ });
134
+ const matchTemplate = (image, templ, mask) => run((keep) => {
135
+ const dst = keep(new cv.Mat());
136
+ cv.matchTemplate(keep(toMat(cv, image)), keep(toMat(cv, templ)), dst, cv.TM_CCORR_NORMED, keep(toMat(cv, mask)));
137
+ return fromMat(cv, dst);
138
+ });
139
+ const approxPolyDP = (points, eps) => run((keep) => {
140
+ const src = keep(cv.matFromArray(points.length / 2, 1, cv.CV_32FC2, Array.from(points)));
141
+ const dst = keep(new cv.Mat());
142
+ cv.approxPolyDP(src, dst, eps, false);
143
+ return new Float32Array(dst.data32F);
144
+ });
145
+ return { cv, resize, cvtColor, medianBlur, distanceTransform, sobel, canny, erode3, gaussian, components, matchTemplate, approxPolyDP };
146
+ }
147
+ let opsPromise = null;
148
+ const ops = () => (opsPromise ??= cvOps());
149
+ // ---------------------------------------------------------------- ONNX
150
+ const sessions = new Map();
151
+ async function session(modelPath) {
152
+ let s = sessions.get(modelPath);
153
+ if (!s) {
154
+ s = (async () => {
155
+ const ort = await import('onnxruntime-web');
156
+ ort.env.wasm.numThreads = 1;
157
+ return ort.InferenceSession.create(readFileSync(modelPath));
158
+ })();
159
+ sessions.set(modelPath, s);
160
+ }
161
+ return s;
162
+ }
163
+ async function runModel(modelPath, tensor, dims) {
164
+ const ort = await import('onnxruntime-web');
165
+ const sess = await session(modelPath);
166
+ const out = await sess.run({ [sess.inputNames[0]]: new ort.Tensor('float32', tensor, dims) });
167
+ const t = out[sess.outputNames[0]];
168
+ return { data: t.data, dims: t.dims };
169
+ }
170
+ const MEAN = [f(0.485), f(0.456), f(0.406)];
171
+ const STD = [f(0.229), f(0.224), f(0.225)];
172
+ /** U2Net 显著性图,缩放回原图大小(_u2net_saliency)。 */
173
+ export async function u2netSaliency(image, modelPath) {
174
+ const o = await ops();
175
+ const rgb = o.cvtColor(image, o.cv.COLOR_BGR2RGB);
176
+ const resized = o.resize(rgb, 320, 320, o.cv.INTER_LANCZOS4);
177
+ let max = 0;
178
+ for (const v of resized.data)
179
+ if (v > max)
180
+ max = v;
181
+ const m = Math.max(max, 1e-6);
182
+ const tensor = new Float32Array(3 * 320 * 320);
183
+ for (let i = 0; i < 320 * 320; i++) {
184
+ for (let c = 0; c < 3; c++)
185
+ tensor[c * 320 * 320 + i] = f(f(f(f(resized.data[i * 3 + c]) / m) - MEAN[c]) / STD[c]);
186
+ }
187
+ const { data } = await runModel(modelPath, tensor, [1, 3, 320, 320]);
188
+ const pred = new Float32Array(data.subarray(0, 320 * 320));
189
+ let lo = Infinity;
190
+ let hi = -Infinity;
191
+ for (const v of pred)
192
+ lo = Math.min(lo, v);
193
+ for (let i = 0; i < pred.length; i++)
194
+ pred[i] = f(pred[i] - lo);
195
+ for (const v of pred)
196
+ hi = Math.max(hi, v);
197
+ const div = Math.max(hi, 1e-6);
198
+ for (let i = 0; i < pred.length; i++)
199
+ pred[i] = f(pred[i] / div);
200
+ return o.resize(img(320, 320, 1, pred), image.width, image.height, o.cv.INTER_LANCZOS4);
201
+ }
202
+ // ---------------------------------------------------------------- 骨架与路径
203
+ /** 2D 骨架化(scikit-image 的 _fast_skeletonize,Zhang-Suen 查表法)。 */
204
+ const SKELETON_LUT = [
205
+ 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 3, 1, 1, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 3, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
206
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
207
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0,
208
+ 2, 0, 0, 0, 3, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0,
209
+ 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 1, 3, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
210
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 1, 0, 0, 0, 0, 2, 2, 0, 0,
211
+ 2, 0, 0, 0,
212
+ ];
213
+ export function skeletonize(mask, width, height) {
214
+ const W = width + 2;
215
+ const H = height + 2;
216
+ const sk = new Uint8Array(W * H);
217
+ for (let y = 0; y < height; y++)
218
+ for (let x = 0; x < width; x++)
219
+ sk[(y + 1) * W + x + 1] = mask[y * width + x] ? 1 : 0;
220
+ const cleaned = sk.slice();
221
+ let removed = true;
222
+ while (removed) {
223
+ removed = false;
224
+ for (let pass = 0; pass < 2; pass++) {
225
+ const first = pass === 0;
226
+ for (let r = 1; r < H - 1; r++) {
227
+ for (let c = 1; c < W - 1; c++) {
228
+ if (!sk[r * W + c])
229
+ continue;
230
+ const n = SKELETON_LUT[sk[(r - 1) * W + c - 1] + 2 * sk[(r - 1) * W + c] + 4 * sk[(r - 1) * W + c + 1] + 8 * sk[r * W + c + 1] +
231
+ 16 * sk[(r + 1) * W + c + 1] + 32 * sk[(r + 1) * W + c] + 64 * sk[(r + 1) * W + c - 1] + 128 * sk[r * W + c - 1]];
232
+ if (n === 0)
233
+ continue;
234
+ if (n === 3 || (n === 1 && first) || (n === 2 && !first)) {
235
+ cleaned[r * W + c] = 0;
236
+ removed = true;
237
+ }
238
+ }
239
+ }
240
+ sk.set(cleaned);
241
+ }
242
+ }
243
+ const out = new Uint8Array(width * height);
244
+ for (let y = 0; y < height; y++)
245
+ for (let x = 0; x < width; x++)
246
+ out[y * width + x] = sk[(y + 1) * W + x + 1];
247
+ return out;
248
+ }
249
+ const key = (y, x) => y * 65536 + x;
250
+ /** 最小堆,按 (distance, y, x) 字典序(与 heapq 的元组比较一致)。 */
251
+ class Heap {
252
+ items = [];
253
+ less(a, b) {
254
+ return a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || (a[1] === b[1] && a[2] < b[2])));
255
+ }
256
+ push(v) {
257
+ const h = this.items;
258
+ h.push(v);
259
+ let i = h.length - 1;
260
+ while (i > 0) {
261
+ const p = (i - 1) >> 1;
262
+ if (!this.less(h[i], h[p]))
263
+ break;
264
+ [h[i], h[p]] = [h[p], h[i]];
265
+ i = p;
266
+ }
267
+ }
268
+ pop() {
269
+ const h = this.items;
270
+ const top = h[0];
271
+ const last = h.pop();
272
+ if (h.length) {
273
+ h[0] = last;
274
+ let i = 0;
275
+ for (;;) {
276
+ const l = 2 * i + 1;
277
+ const r = l + 1;
278
+ let m = i;
279
+ if (l < h.length && this.less(h[l], h[m]))
280
+ m = l;
281
+ if (r < h.length && this.less(h[r], h[m]))
282
+ m = r;
283
+ if (m === i)
284
+ break;
285
+ [h[i], h[m]] = [h[m], h[i]];
286
+ i = m;
287
+ }
288
+ }
289
+ return top;
290
+ }
291
+ }
292
+ function farthest(graph, nodes, start) {
293
+ const distances = new Map([[start, 0]]);
294
+ const parents = new Map();
295
+ const heap = new Heap();
296
+ const [sy, sx] = nodes.get(start);
297
+ heap.push([0, sy, sx]);
298
+ while (heap.items.length) {
299
+ const [d, y, x] = heap.pop();
300
+ const k = key(y, x);
301
+ if (d !== distances.get(k))
302
+ continue;
303
+ for (const [nb, w] of graph.get(k)) {
304
+ const cand = d + w;
305
+ if (cand < (distances.get(nb) ?? Infinity)) {
306
+ distances.set(nb, cand);
307
+ parents.set(nb, k);
308
+ const [ny, nx] = nodes.get(nb);
309
+ heap.push([cand, ny, nx]);
310
+ }
311
+ }
312
+ }
313
+ let best = start;
314
+ let bestD = -Infinity;
315
+ for (const [k, d] of distances)
316
+ if (d > bestD)
317
+ (best = k), (bestD = d);
318
+ return { node: best, distances, parents };
319
+ }
320
+ /** 骨架上最长的测地线路径(_longest_skeleton_path),点为 (y, x)。 */
321
+ export function longestSkeletonPath(skel, width, height) {
322
+ const nodes = new Map();
323
+ const order = [];
324
+ for (let y = 0; y < height; y++)
325
+ for (let x = 0; x < width; x++)
326
+ if (skel[y * width + x]) {
327
+ nodes.set(key(y, x), [y, x]);
328
+ order.push(key(y, x));
329
+ }
330
+ const graph = new Map();
331
+ for (const k of order) {
332
+ const [y, x] = nodes.get(k);
333
+ const nbs = [];
334
+ for (const dy of [-1, 0, 1])
335
+ for (const dx of [-1, 0, 1]) {
336
+ if (!(dx || dy))
337
+ continue;
338
+ const nk = key(y + dy, x + dx);
339
+ if (y + dy < 0 || x + dx < 0 || !nodes.has(nk))
340
+ continue;
341
+ nbs.push([nk, Math.sqrt(dx * dx + dy * dy)]);
342
+ }
343
+ graph.set(k, nbs);
344
+ }
345
+ if (!graph.size)
346
+ return { path: [], metrics: {} };
347
+ const endpoints = order.filter((k) => graph.get(k).length === 1);
348
+ const firstStart = endpoints[0] ?? order[0];
349
+ const first = farthest(graph, nodes, firstStart).node;
350
+ const { node: second, distances, parents } = farthest(graph, nodes, first);
351
+ const path = [second];
352
+ while (path[path.length - 1] !== first) {
353
+ const p = parents.get(path[path.length - 1]);
354
+ if (p === undefined)
355
+ break;
356
+ path.push(p);
357
+ }
358
+ path.reverse();
359
+ let branches = 0;
360
+ for (const k of order)
361
+ if (graph.get(k).length >= 4)
362
+ branches++;
363
+ return {
364
+ path: path.map((k) => nodes.get(k)),
365
+ metrics: { pixels: order.length, endpoints: endpoints.length, branches, length: distances.get(second) ?? 0 },
366
+ };
367
+ }
368
+ /** 沿路径重采样 count 个点(_resample_path),点为 [x, y],保留两位小数。 */
369
+ export async function resamplePath(path, count = 64) {
370
+ const o = await ops();
371
+ const pts = new Float32Array(path.length * 2);
372
+ path.forEach(([y, x], i) => {
373
+ pts[i * 2] = x;
374
+ pts[i * 2 + 1] = y;
375
+ });
376
+ let simp = o.approxPolyDP(pts, 1.6);
377
+ if (simp.length < 4)
378
+ simp = pts;
379
+ const n = simp.length / 2;
380
+ const dists = [];
381
+ for (let i = 0; i < n - 1; i++) {
382
+ const dx = f(simp[(i + 1) * 2] - simp[i * 2]);
383
+ const dy = f(simp[(i + 1) * 2 + 1] - simp[i * 2 + 1]);
384
+ dists.push(f(Math.sqrt(f(f(dx * dx) + f(dy * dy)))));
385
+ }
386
+ const cum = [0];
387
+ let acc = 0;
388
+ for (const d of dists)
389
+ cum.push((acc = f(acc + d)));
390
+ const total = cum[cum.length - 1];
391
+ if (total <= 0)
392
+ return Array.from({ length: n }, (_, i) => [pyRound(simp[i * 2], 2), pyRound(simp[i * 2 + 1], 2)]);
393
+ const step = total / (count - 1);
394
+ const out = [];
395
+ let seg = 0;
396
+ for (let t = 0; t < count; t++) {
397
+ const target = t === count - 1 ? total : t * step;
398
+ while (seg + 1 < cum.length - 1 && cum[seg + 1] < target)
399
+ seg++;
400
+ const span = Math.max(cum[seg + 1] - cum[seg], 1e-6);
401
+ const ratio = (target - cum[seg]) / span;
402
+ const px = simp[seg * 2] + ratio * f(simp[(seg + 1) * 2] - simp[seg * 2]);
403
+ const py = simp[seg * 2 + 1] + ratio * f(simp[(seg + 1) * 2 + 1] - simp[seg * 2 + 1]);
404
+ out.push([pyRound(px, 2), pyRound(py, 2)]);
405
+ }
406
+ return out;
407
+ }
408
+ /** 从显著性图里找一条可信的轨迹(_extract_confident_path)。 */
409
+ export async function extractConfidentPath(saliency) {
410
+ const o = await ops();
411
+ const { width, height, data } = saliency;
412
+ let best = null;
413
+ for (const threshold of [0.88, 0.82, 0.76, 0.7, 0.64]) {
414
+ const t = f(threshold);
415
+ const binary = new Uint8Array(width * height);
416
+ for (let i = 0; i < binary.length; i++)
417
+ binary[i] = data[i] >= t ? 1 : 0;
418
+ const { count, labels, stats } = o.components(img(width, height, 1, binary));
419
+ for (let label = 1; label < count; label++) {
420
+ const [x, y, bw, bh, area] = [0, 1, 2, 3, 4].map((j) => stats[label * 5 + j]);
421
+ if (area < 250 || bw < width * 0.35 || bh < height * 0.18)
422
+ continue;
423
+ const comp = new Uint8Array(width * height);
424
+ for (let i = 0; i < comp.length; i++)
425
+ comp[i] = labels[i] === label ? 1 : 0;
426
+ const { path, metrics } = longestSkeletonPath(skeletonize(comp, width, height), width, height);
427
+ if (!path.length)
428
+ continue;
429
+ const thickness = area / Math.max(metrics.length, 1);
430
+ const coverage = metrics.length / Math.max(Math.hypot(bw, bh), 1);
431
+ const confidence = metrics.length - 7 * metrics.endpoints - 2 * metrics.branches - 12 * Math.max(0, thickness - 18);
432
+ const cx = x + bw / 2;
433
+ const cy = y + bh / 2;
434
+ const c = {
435
+ ...metrics,
436
+ threshold,
437
+ area,
438
+ width: bw,
439
+ height: bh,
440
+ thickness,
441
+ coverage,
442
+ confidence,
443
+ center_ok: width * 0.22 <= cx && cx <= width * 0.78 && height * 0.15 <= cy && cy <= height * 0.85,
444
+ };
445
+ if (!best || c.confidence > best.c.confidence)
446
+ best = { path, c };
447
+ }
448
+ }
449
+ if (!best)
450
+ return { retry: true, reason: 'no-path', score: 0 };
451
+ const m = best.c;
452
+ const accepted = m.confidence >= 100 && m.length >= 150 && m.thickness <= 22 && m.coverage >= 1.05 && m.endpoints <= 5 && m.center_ok;
453
+ if (!accepted)
454
+ return { retry: true, reason: 'low-confidence', score: pyRound(m.confidence, 2) };
455
+ return { retry: false, score: pyRound(m.confidence, 2), points: await resamplePath(best.path) };
456
+ }
457
+ // ---------------------------------------------------------------- tp=2 点选
458
+ /** 在主图里找提示图中的那个物体(solve_click)。 */
459
+ export async function solveClick(image, tip, modelPath) {
460
+ const o = await ops();
461
+ const sal = await u2netSaliency(tip, modelPath);
462
+ for (let i = 0; i < sal.data.length; i++)
463
+ sal.data[i] = clip(sal.data[i], 0, 1);
464
+ let max = 0;
465
+ for (const v of sal.data)
466
+ if (v > max)
467
+ max = v;
468
+ const threshold = Math.max(0.25, max * 0.35);
469
+ const t32 = f(threshold);
470
+ const binary = new Uint8Array(sal.data.length);
471
+ for (let i = 0; i < binary.length; i++)
472
+ binary[i] = sal.data[i] >= t32 ? 1 : 0;
473
+ const { count, labels, stats } = o.components(img(tip.width, tip.height, 1, binary));
474
+ const candidates = [];
475
+ for (let idx = 1; idx < count; idx++) {
476
+ const [x, y, w, h, area] = [0, 1, 2, 3, 4].map((j) => stats[idx * 5 + j]);
477
+ if (area < 12 || w < 3 || h < 3)
478
+ continue;
479
+ const vals = [];
480
+ for (let i = 0; i < labels.length; i++)
481
+ if (labels[i] === idx)
482
+ vals.push(sal.data[i]);
483
+ candidates.push([pairwiseSum(vals), x, y, w, h]);
484
+ }
485
+ if (!candidates.length)
486
+ return { retry: true, reason: 'tip-object-not-found', score: 0 };
487
+ candidates.sort((a, b) => b[0] - a[0] || b[1] - a[1] || b[2] - a[2] || b[3] - a[3] || b[4] - a[4]);
488
+ let [, x, y, width, height] = candidates[0];
489
+ const pad = 2;
490
+ x = Math.max(0, x - pad);
491
+ y = Math.max(0, y - pad);
492
+ width = Math.min(tip.width - x, width + 2 * pad);
493
+ height = Math.min(tip.height - y, height + 2 * pad);
494
+ const template = crop(tip, x, y, width, height);
495
+ const tmask = new Uint8Array(width * height);
496
+ for (let r = 0; r < height; r++)
497
+ for (let c = 0; c < width; c++)
498
+ tmask[r * width + c] = sal.data[(y + r) * tip.width + x + c] >= t32 ? 255 : 0;
499
+ let best = null;
500
+ for (let i = 0; i < 15; i++) {
501
+ const scale = i === 14 ? 1.4 : i * ((1.4 - 0.7) / 14) + 0.7;
502
+ const sw = Math.max(4, pyRound(width * scale));
503
+ const sh = Math.max(4, pyRound(height * scale));
504
+ if (sw >= image.width || sh >= image.height)
505
+ continue;
506
+ const scaled = o.resize(template, sw, sh, o.cv.INTER_CUBIC);
507
+ const mask = o.resize(img(width, height, 1, tmask), sw, sh, o.cv.INTER_NEAREST);
508
+ const scores = o.matchTemplate(image, scaled, mask);
509
+ let score = -Infinity;
510
+ let loc = [0, 0];
511
+ for (let r = 0; r < scores.height; r++)
512
+ for (let c = 0; c < scores.width; c++) {
513
+ let v = scores.data[r * scores.width + c];
514
+ if (!Number.isFinite(v))
515
+ v = -1;
516
+ if (v > score)
517
+ (score = v), (loc = [c, r]);
518
+ }
519
+ if (!best || score > best[0])
520
+ best = [score, loc, [sw, sh]];
521
+ }
522
+ if (!best || best[0] < 0.72)
523
+ return { retry: true, reason: 'click-match-low-confidence', score: pyRound(best ? best[0] : 0, 4) };
524
+ const [score, loc, size] = best;
525
+ return { retry: false, solver: 'u2net-masked-template', x: pyRound(loc[0] + size[0] / 2, 2), y: pyRound(loc[1] + size[1] / 2, 2), score: pyRound(score, 4) };
526
+ }
527
+ /** Python 的 int(round(x))(银行家舍入)。 */
528
+ // ---------------------------------------------------------------- tp=26 旋转
529
+ const circularDistance = (a, b) => {
530
+ const d = Math.abs((((a - b) % 360) + 360) % 360);
531
+ return Math.min(d, 360 - d);
532
+ };
533
+ /** 旋转图的纠正角度(solve_rotation):方向分类得到粗角度,LSD 直线的四倍角加权均值给出精确轴向。 */
534
+ export async function solveRotation(image, modelPath) {
535
+ const o = await ops();
536
+ const resized = o.resize(image, 416, 416, o.cv.INTER_CUBIC);
537
+ const tensor = new Float32Array(3 * 384 * 384);
538
+ for (let yy = 0; yy < 384; yy++)
539
+ for (let xx = 0; xx < 384; xx++) {
540
+ const s = ((yy + 16) * 416 + xx + 16) * 3;
541
+ for (let c = 0; c < 3; c++)
542
+ tensor[c * 384 * 384 + yy * 384 + xx] = f(f(f(resized.data[s + 2 - c]) / 255) - MEAN[c]) / STD[c];
543
+ }
544
+ for (let i = 0; i < tensor.length; i++)
545
+ tensor[i] = f(tensor[i]);
546
+ const { data } = await runModel(modelPath, tensor, [1, 3, 384, 384]);
547
+ const logits = Array.from(data.subarray(0, 4), f32);
548
+ const lmax = Math.max(...logits);
549
+ const probs = logits.map((v) => f(Math.exp(f(v - lmax))));
550
+ const psum = Math.max(pairwiseSum(probs), 1e-6);
551
+ const p = probs.map((v) => f(v / psum));
552
+ let cls = 0;
553
+ for (let i = 1; i < p.length; i++)
554
+ if (p[i] > p[cls])
555
+ cls = i;
556
+ const coarse = { 0: 0, 1: 270, 2: 180, 3: 90 }[cls];
557
+ const gray = o.cvtColor(image, o.cv.COLOR_BGR2GRAY);
558
+ const lines = detectLines(o.cv, gray);
559
+ let re = 0;
560
+ let im = 0;
561
+ let total = 0;
562
+ for (const [x1, y1, x2, y2] of lines) {
563
+ const dx = f(x2 - x1);
564
+ const dy = f(y2 - y1);
565
+ const length = Math.hypot(dx, dy);
566
+ if (length < 12)
567
+ continue;
568
+ const theta = Math.atan2(dy, dx);
569
+ const w = length * length;
570
+ re += w * Math.cos(4 * theta);
571
+ im += w * Math.sin(4 * theta);
572
+ total += w;
573
+ }
574
+ let cvAngle;
575
+ let strength;
576
+ if (total <= 0) {
577
+ cvAngle = coarse;
578
+ strength = 0;
579
+ }
580
+ else {
581
+ const axis = (((Math.atan2(im, re) * RAD_TO_DEG) / 4) % 90 + 90) % 90;
582
+ const cands = [0, 1, 2, 3].map((i) => (axis + 90 * i) % 360);
583
+ cvAngle = cands.reduce((a, b) => (circularDistance(b, coarse) < circularDistance(a, coarse) ? b : a));
584
+ strength = Math.hypot(re, im) / total;
585
+ }
586
+ const css = (((360 - cvAngle) % 360) + 360) % 360;
587
+ return {
588
+ retry: false,
589
+ solver: 'orientation-classifier-axis',
590
+ angle: pyRound(css, 2),
591
+ cvAngle: pyRound(cvAngle, 2),
592
+ orientationClass: cls,
593
+ score: pyRound(p[cls], 4),
594
+ axisStrength: pyRound(strength, 4),
595
+ };
596
+ }
597
+ // ---------------------------------------------------------------- tp=30 滑块
598
+ /** 用拼图块的透明轮廓定位缺口(solve_slider)。 */
599
+ export async function solveSlider(image, slot) {
600
+ if (slot.channels !== 4)
601
+ return { retry: true, reason: 'slot-alpha-missing', score: 0 };
602
+ if (slot.height !== image.height || slot.width >= image.width)
603
+ return { retry: true, reason: 'slot-size-mismatch', score: 0 };
604
+ const o = await ops();
605
+ const W = slot.width;
606
+ const H = slot.height;
607
+ const mask = new Uint8Array(W * H);
608
+ let maskSum = 0;
609
+ for (let i = 0; i < mask.length; i++)
610
+ maskSum += mask[i] = slot.data[i * 4 + 3] >= 48 ? 1 : 0;
611
+ if (maskSum < 200)
612
+ return { retry: true, reason: 'slot-mask-empty', score: 0 };
613
+ const mask255 = img(W, H, 1, mask.map((v) => v * 255));
614
+ const contour = o.canny(mask255, 50, 150);
615
+ const ys = [];
616
+ const xs = [];
617
+ for (let y = 0; y < H; y++)
618
+ for (let x = 0; x < W; x++)
619
+ if (contour.data[y * W + x] > 0)
620
+ ys.push(y), xs.push(x);
621
+ const interior = o.erode3(img(W, H, 1, mask));
622
+ const iy = [];
623
+ const ix = [];
624
+ for (let y = 0; y < H; y++)
625
+ for (let x = 0; x < W; x++)
626
+ if (interior.data[y * W + x] > 0)
627
+ iy.push(y), ix.push(x);
628
+ const gray = o.cvtColor(image, o.cv.COLOR_BGR2GRAY);
629
+ const gx = o.sobel(gray, 1, 0);
630
+ const gy = o.sobel(gray, 0, 1);
631
+ const IW = image.width;
632
+ const gradient = new Float32Array(gx.data.length);
633
+ for (let i = 0; i < gradient.length; i++)
634
+ gradient[i] = f(Math.sqrt(f(f(gx.data[i] * gx.data[i]) + f(gy.data[i] * gy.data[i]))));
635
+ const edges = o.canny(gray, 45, 120);
636
+ const distance = o.distanceTransform(img(IW, H, 1, edges.data.map((v) => (v === 0 ? 1 : 0))), 3);
637
+ const inside = o.distanceTransform(img(W, H, 1, mask), 3);
638
+ const outside = o.distanceTransform(img(W, H, 1, mask.map((v) => 1 - v)), 3);
639
+ const signed = img(W, H, 1, inside.data.map((v, i) => f(v - outside.data[i])));
640
+ const sx = o.sobel(signed, 1, 0);
641
+ const sy = o.sobel(signed, 0, 1);
642
+ const nx = new Float32Array(ys.length);
643
+ const ny = new Float32Array(ys.length);
644
+ for (let k = 0; k < ys.length; k++) {
645
+ const a = sx.data[ys[k] * W + xs[k]];
646
+ const b = sy.data[ys[k] * W + xs[k]];
647
+ const len = f(Math.max(f(Math.hypot(a, b)), 1e-6));
648
+ nx[k] = f(a / len);
649
+ ny[k] = f(b / len);
650
+ }
651
+ const slotGrayImg = o.cvtColor(img(W, H, 3, Uint8Array.from({ length: W * H * 3 }, (_, i) => slot.data[Math.floor(i / 3) * 4 + (i % 3)])), o.cv.COLOR_BGR2GRAY);
652
+ const slotGray = new Float32Array(iy.length);
653
+ for (let k = 0; k < iy.length; k++)
654
+ slotGray[k] = slotGrayImg.data[iy[k] * W + ix[k]];
655
+ const slotMean = mean32(slotGray);
656
+ for (let k = 0; k < slotGray.length; k++)
657
+ slotGray[k] = f(slotGray[k] - slotMean);
658
+ const slotScale = Math.max(normF32(slotGray), 1e-6);
659
+ const scores = [];
660
+ const corrs = [];
661
+ const a1 = new Float32Array(ys.length);
662
+ const a3 = new Float32Array(ys.length);
663
+ const cand = new Float32Array(iy.length);
664
+ for (let off = 0; off <= IW - W; off++) {
665
+ let covered = 0;
666
+ for (let k = 0; k < ys.length; k++) {
667
+ const idx = ys[k] * IW + xs[k] + off;
668
+ const cd = distance.data[idx];
669
+ const cg = gradient[idx];
670
+ const align = f(f(Math.abs(f(f(gx.data[idx] * nx[k]) + f(gy.data[idx] * ny[k])))) / f(Math.max(cg, 1e-6)));
671
+ a1[k] = f(Math.exp(f(f(-cd) / f(1.7))));
672
+ if (cd <= f(2.2))
673
+ covered++;
674
+ a3[k] = f(clip(f(cg / 500), 0, 1) * align);
675
+ }
676
+ const chamfer = mean32(a1);
677
+ const coverage = covered / ys.length;
678
+ const oriented = mean32(a3);
679
+ for (let k = 0; k < iy.length; k++)
680
+ cand[k] = gray.data[iy[k] * IW + ix[k] + off];
681
+ const cm = mean32(cand);
682
+ for (let k = 0; k < cand.length; k++)
683
+ cand[k] = f(cand[k] - cm);
684
+ const corr = f(dotF32(slotGray, cand) / f(slotScale * Math.max(normF32(cand), 1e-6)));
685
+ scores.push(1.2 * chamfer + 0.9 * coverage + 0.55 * oriented + 0.9 * Math.max(corr, 0));
686
+ corrs.push(corr);
687
+ }
688
+ const order = scores.map((s, i) => [s, i]).sort((a, b) => a[0] - b[0] || a[1] - b[1]).map(([, i]) => i).reverse();
689
+ const bestIndex = order[0];
690
+ const separated = order.filter((i) => Math.abs(i - bestIndex) >= 8);
691
+ const runnerUp = separated.length ? scores[separated[0]] : 0;
692
+ const accepted = scores[bestIndex] >= 1.9 && corrs[bestIndex] >= 0.6;
693
+ return {
694
+ retry: !accepted,
695
+ reason: accepted ? '' : 'low-slider-confidence',
696
+ solver: 'slider-contour-texture',
697
+ offset: pyRound(bestIndex, 2),
698
+ score: pyRound(scores[bestIndex], 4),
699
+ margin: pyRound(scores[bestIndex] - runnerUp, 4),
700
+ correlation: pyRound(corrs[bestIndex], 4),
701
+ };
702
+ }
703
+ function normF32(a) {
704
+ const sq = new Float32Array(a.length);
705
+ for (let i = 0; i < a.length; i++)
706
+ sq[i] = f(a[i] * a[i]);
707
+ return f(Math.sqrt(pairwiseSum(sq)));
708
+ }
709
+ function dotF32(a, b) {
710
+ let s = 0;
711
+ for (let i = 0; i < a.length; i++)
712
+ s = f(s + f(a[i] * b[i]));
713
+ return s;
714
+ }
715
+ // ---------------------------------------------------------------- tp=3 轨迹
716
+ /** 宽笔画中心的似然(_stroke_likelihood)。 */
717
+ export async function strokeLikelihood(lab, background) {
718
+ const o = await ops();
719
+ const n = lab.width * lab.height;
720
+ const residual = new Float32Array(n);
721
+ for (let i = 0; i < n; i++) {
722
+ let s = 0;
723
+ for (let c = 0; c < 3; c++) {
724
+ const d = f(lab.data[i * 3 + c] - background.data[i * 3 + c]);
725
+ s = f(s + f(d * d));
726
+ }
727
+ residual[i] = f(Math.sqrt(s));
728
+ }
729
+ const likelihood = new Float32Array(n);
730
+ for (const t of [10, 15, 20, 25]) {
731
+ const binary = new Uint8Array(n);
732
+ for (let i = 0; i < n; i++)
733
+ binary[i] = residual[i] >= t ? 1 : 0;
734
+ const dist = o.distanceTransform(img(lab.width, lab.height, 1, binary), 5);
735
+ for (let i = 0; i < n; i++)
736
+ likelihood[i] = f(likelihood[i] + clip(f(dist.data[i] / 8), 0, 1));
737
+ }
738
+ for (let i = 0; i < n; i++)
739
+ likelihood[i] = f(likelihood[i] / 4);
740
+ return { residual, likelihood };
741
+ }
742
+ /** 四个角点(_corners):两条带角度的短边,返回 [左上, 右上, 左下, 右下]。 */
743
+ export function corners(p) {
744
+ const end = (cx, cy, hw, deg) => {
745
+ const a = deg * DEG_TO_RAD;
746
+ const vx = hw * Math.cos(a);
747
+ const vy = hw * Math.sin(a);
748
+ return [
749
+ [cx - vx, cy - vy],
750
+ [cx + vx, cy + vy],
751
+ ];
752
+ };
753
+ const [tl, tr] = end(p[0], p[1], p[2], p[3]);
754
+ const [bl, br] = end(p[4], p[5], p[6], p[7]);
755
+ return [tl, tr, bl, br];
756
+ }
757
+ export const traceChain = (p, topology) => {
758
+ const c = corners(p);
759
+ return topology.map((i) => [f(c[i][0]), f(c[i][1])]);
760
+ };
761
+ /** 一组四角参数的得分(_trace_score);不合法的几何返回 -1000。 */
762
+ export function traceScore(maps, params, topology) {
763
+ const { width, height } = maps.lab;
764
+ const chain = traceChain(params, topology);
765
+ const cxs = chain.map((c) => c[0]);
766
+ const cys = chain.map((c) => c[1]);
767
+ if (Math.min(...cxs) < 4 || Math.min(...cys) < 4 || Math.max(...cxs) >= width - 4 || Math.max(...cys) >= height - 4)
768
+ return -1e3;
769
+ const sc = corners(params);
770
+ const topMax = Math.max(sc[0][1], sc[1][1]);
771
+ const bottomMin = Math.min(sc[2][1], sc[3][1]);
772
+ if (topMax + 18 >= bottomMin)
773
+ return -1e3;
774
+ if (topMax > height * 0.43 || bottomMin < height * 0.45 || sc[3][1] > height * 0.72)
775
+ return -1e3;
776
+ if (Math.max(sc[0][0], sc[2][0]) > width * 0.45 || Math.min(sc[1][0], sc[3][0]) < width * 0.65)
777
+ return -1e3;
778
+ if (Math.abs(sc[1][0] - sc[3][0]) > width * 0.12)
779
+ return -1e3;
780
+ const vectors = [0, 1, 2].map((i) => [f(chain[i + 1][0] - chain[i][0]), f(chain[i + 1][1] - chain[i][1])]);
781
+ const norms = vectors.map(([x, y]) => f(Math.sqrt(f(f(x * x) + f(y * y)))));
782
+ if (f(f(norms[0] + norms[1]) + norms[2]) < width * 0.98)
783
+ return -1e3;
784
+ let turnPenalty = 0;
785
+ for (let i = 0; i < 2; i++) {
786
+ const [a, b] = [vectors[i], vectors[i + 1]];
787
+ const denom = Math.max(f(norms[i] * norms[i + 1]), 1e-6);
788
+ const cosine = clip(f(f(f(a[0] * b[0]) + f(a[1] * b[1])) / f(denom)), -1, 1);
789
+ const angle = Math.acos(cosine) * RAD_TO_DEG;
790
+ if (angle < 28)
791
+ turnPenalty += (28 - angle) * 4;
792
+ else if (angle > 152)
793
+ turnPenalty += (angle - 152) * 4;
794
+ }
795
+ const segScores = [];
796
+ for (let s = 0; s < 3; s++) {
797
+ const start = chain[s];
798
+ const vec = vectors[s];
799
+ const length = norms[s];
800
+ if (length < 35)
801
+ return -1e3;
802
+ const unit = [f(vec[0] / length), f(vec[1] / length)];
803
+ const normal = [f(-unit[1]), unit[0]];
804
+ const num = Math.max(12, Math.trunc(f(length / f(2.5))));
805
+ const cx = new Float64Array(num);
806
+ const cy = new Float64Array(num);
807
+ const step = (0.98 - 0.02) / (num - 1);
808
+ for (let i = 0; i < num; i++) {
809
+ const r = i === num - 1 ? 0.98 : i * step + 0.02;
810
+ cx[i] = start[0] + r * vec[0];
811
+ cy[i] = start[1] + r * vec[1];
812
+ }
813
+ const shifted = (k) => {
814
+ const nxk = f(normal[0] * f(k));
815
+ const nyk = f(normal[1] * f(k));
816
+ return [Array.from(cx, (v) => v + nxk), Array.from(cy, (v) => v + nyk)];
817
+ };
818
+ const sample = (m, k) => {
819
+ const [xs, ys] = shifted(k);
820
+ return remap(m, xs, ys);
821
+ };
822
+ let bestEdge = -1e3;
823
+ for (const radius of [5, 7, 9, 11, 13]) {
824
+ const inner = Math.max(1, radius - 2);
825
+ const outer = radius + 4;
826
+ const li = sample(maps.lab, inner);
827
+ const lo = sample(maps.lab, outer);
828
+ const ri = sample(maps.lab, -inner);
829
+ const ro = sample(maps.lab, -outer);
830
+ const vals = new Float32Array(num);
831
+ for (let i = 0; i < num; i++) {
832
+ const nrm = (a, b) => {
833
+ let q = 0;
834
+ for (let c = 0; c < 3; c++) {
835
+ const d = f(a[i * 3 + c] - b[i * 3 + c]);
836
+ q = f(q + f(d * d));
837
+ }
838
+ return f(Math.sqrt(q));
839
+ };
840
+ vals[i] = clip(Math.min(nrm(li, lo), nrm(ri, ro)), 0, 45);
841
+ }
842
+ bestEdge = Math.max(bestEdge, mean32(vals));
843
+ }
844
+ const r1 = sample(maps.residual, -4);
845
+ const r2 = sample(maps.residual, 0);
846
+ const r3 = sample(maps.residual, 4);
847
+ const resid = new Float32Array(num).map((_, i) => clip(Math.max(r1[i], r2[i], r3[i]), 0, 45));
848
+ const residualScore = mean32(resid);
849
+ const ct = sample(maps.texture, 0);
850
+ const t1 = sample(maps.texture, 15);
851
+ const t2 = sample(maps.texture, -15);
852
+ const tex = new Float32Array(num).map((_, i) => clip(f(f(0.5 * f(t1[i] + t2[i])) - ct[i]), -30, 30));
853
+ const textureScore = mean32(tex);
854
+ const cs = sample(maps.saliency, 0);
855
+ const s1 = sample(maps.saliency, 16);
856
+ const s2 = sample(maps.saliency, -16);
857
+ const sal = new Float32Array(num).map((_, i) => clip(f(cs[i] - f(0.5 * f(s1[i] + s2[i]))), f(-0.45), 1));
858
+ const saliencyScore = mean32(sal) * 45;
859
+ segScores.push(bestEdge + 1.15 * residualScore + 0.75 * textureScore + 1.35 * saliencyScore);
860
+ }
861
+ return (segScores[0] + segScores[1] + segScores[2]) / 3 - turnPenalty;
862
+ }
863
+ /** 与 scipy differential_evolution 同算法(best1bin、拉丁超立方初始化、抖动变异、tol 收敛、局部精修)。 */
864
+ export function differentialEvolution(fn, bounds, options) {
865
+ const rng = mulberry32(options.seed);
866
+ const N = bounds.length;
867
+ const NP = options.popsize * N;
868
+ const tol = options.tol ?? 0.01;
869
+ const CR = options.recombination ?? 0.7;
870
+ const [mLo, mHi] = options.mutation ?? [0.5, 1];
871
+ const scale = (u) => u.map((v, i) => bounds[i][0] + v * (bounds[i][1] - bounds[i][0]));
872
+ // 拉丁超立方
873
+ const pop = Array.from({ length: NP }, (_, i) => Array.from({ length: N }, () => (rng() + i) / NP));
874
+ for (let j = 0; j < N; j++) {
875
+ for (let i = NP - 1; i > 0; i--) {
876
+ const k = Math.floor(rng() * (i + 1));
877
+ [pop[i][j], pop[k][j]] = [pop[k][j], pop[i][j]];
878
+ }
879
+ }
880
+ const energies = pop.map((u) => fn(scale(u)));
881
+ let best = energies.indexOf(Math.min(...energies));
882
+ for (let gen = 0; gen < options.maxiter; gen++) {
883
+ const F = mLo + rng() * (mHi - mLo);
884
+ for (let i = 0; i < NP; i++) {
885
+ const pick = () => {
886
+ let r;
887
+ do
888
+ r = Math.floor(rng() * NP);
889
+ while (r === i);
890
+ return r;
891
+ };
892
+ let r0 = pick();
893
+ let r1 = pick();
894
+ while (r1 === r0)
895
+ r1 = pick();
896
+ const b = pop[best];
897
+ const trial = pop[i].slice();
898
+ const fill = Math.floor(rng() * N);
899
+ for (let j = 0; j < N; j++) {
900
+ if (j === fill || rng() < CR)
901
+ trial[j] = b[j] + F * (pop[r0][j] - pop[r1][j]);
902
+ if (trial[j] < 0 || trial[j] > 1)
903
+ trial[j] = rng();
904
+ }
905
+ const e = fn(scale(trial));
906
+ if (e <= energies[i]) {
907
+ pop[i] = trial;
908
+ energies[i] = e;
909
+ if (e <= energies[best])
910
+ best = i;
911
+ }
912
+ }
913
+ const m = energies.reduce((a, b) => a + b, 0) / NP;
914
+ const sd = Math.sqrt(energies.reduce((a, b) => a + (b - m) ** 2, 0) / NP);
915
+ if (sd <= tol * Math.abs(m))
916
+ break;
917
+ }
918
+ // 局部精修:有界的坐标搜索(代替 L-BFGS-B)
919
+ let x = pop[best].slice();
920
+ let fx = energies[best];
921
+ let stepSize = 0.02;
922
+ for (let iter = 0; iter < 60 && stepSize > 1e-4; iter++) {
923
+ let improved = false;
924
+ for (let j = 0; j < N; j++) {
925
+ for (const dir of [1, -1]) {
926
+ const y = x.slice();
927
+ y[j] = Math.min(1, Math.max(0, y[j] + dir * stepSize));
928
+ const fy = fn(scale(y));
929
+ if (fy < fx) {
930
+ x = y;
931
+ fx = fy;
932
+ improved = true;
933
+ }
934
+ }
935
+ }
936
+ if (!improved)
937
+ stepSize /= 2;
938
+ }
939
+ return { x: scale(x), fun: fx };
940
+ }
941
+ /** 轨迹打分用的几张图(solve_trace 的前半段):LAB、残差、纹理、几何显著性。 */
942
+ export async function traceMaps(image, saliency) {
943
+ const o = await ops();
944
+ const labU8 = o.cvtColor(image, o.cv.COLOR_BGR2Lab);
945
+ const lab = img(image.width, image.height, 3, Float32Array.from(labU8.data));
946
+ const background = o.medianBlur(labU8, 31);
947
+ const { residual, likelihood } = await strokeLikelihood(lab, background);
948
+ const geo = new Float32Array(likelihood.length);
949
+ for (let i = 0; i < geo.length; i++)
950
+ geo[i] = f(f(0.25 * saliency.data[i]) + f(0.75 * likelihood[i]));
951
+ const gray = o.cvtColor(image, o.cv.COLOR_BGR2GRAY);
952
+ const grayF = img(gray.width, gray.height, 1, Float32Array.from(gray.data));
953
+ const gx = o.sobel(grayF, 1, 0);
954
+ const gy = o.sobel(grayF, 0, 1);
955
+ const mag = img(gray.width, gray.height, 1, gx.data.map((v, i) => f(Math.sqrt(f(f(v * v) + f(gy.data[i] * gy.data[i]))))));
956
+ return {
957
+ lab,
958
+ residual: img(image.width, image.height, 1, residual),
959
+ texture: o.gaussian(mag, 3),
960
+ saliency: img(image.width, image.height, 1, geo),
961
+ stroke: img(image.width, image.height, 1, likelihood),
962
+ };
963
+ }
964
+ /** 四角轨迹(solve_trace):显著性 + 笔画似然 + 纹理,拟合完整的四顶点形状。 */
965
+ export async function solveTrace(image, modelPath) {
966
+ const saliency = await u2netSaliency(image, modelPath);
967
+ const skeletonSolution = await extractConfidentPath(saliency);
968
+ const maps = await traceMaps(image, saliency);
969
+ const { width, height } = image;
970
+ const bounds = [
971
+ [width * 0.32, width * 0.68],
972
+ [height * 0.1, height * 0.48],
973
+ [width * 0.12, width * 0.43],
974
+ [-35, 35],
975
+ [width * 0.32, width * 0.68],
976
+ [height * 0.45, height * 0.9],
977
+ [width * 0.12, width * 0.43],
978
+ [-35, 35],
979
+ ];
980
+ let best = null;
981
+ for (const topology of [
982
+ [0, 1, 2, 3],
983
+ [0, 2, 1, 3],
984
+ ]) {
985
+ const r = differentialEvolution((p) => -traceScore(maps, p, topology), bounds, { seed: 17, popsize: 8, maxiter: 32 });
986
+ const cand = { score: -r.fun, chain: traceChain(r.x, topology) };
987
+ if (!best || cand.score > best.score)
988
+ best = cand;
989
+ }
990
+ if (!best || best.score < 55) {
991
+ return { retry: true, reason: 'low-geometric-confidence', score: pyRound(best?.score ?? 0, 2), saliencyScore: skeletonSolution.score ?? 0 };
992
+ }
993
+ const path = best.chain.map(([x, y]) => [pyRound(y), pyRound(x)]);
994
+ return { retry: false, solver: 'four-corner-geometric', score: pyRound(best.score, 2), points: await resamplePath(path) };
995
+ }