深度拆解 OmniPic:在浏览器沙箱里造一台 0 云端成本的端侧 AI 图像工作站

深度拆解 OmniPic:在浏览器沙箱里造一台 0 云端成本的端侧 AI 图像工作站

这不是一篇产品软文。本文从源码层面拆解 OmniPic 的七层技术架构,涵盖 MV3 安全沙箱突围、端侧卷积推理管线、2D-DCT 频域指纹、CDN 原图逆向规则引擎、跨页面状态机巡检、流式零压缩打包与本地素材库直连协议——每一层都有真实代码路径与实测数据支撑。

引言:浏览器扩展做图像采集,为什么大多数都做不好

如果你用过市面上的图片下载类扩展,大概率遇到过这些场景:

在某电商平台上右键保存商品图,下载到的是 300px 的缩略图,不是 1200px 的主图;

在花瓣或 Pinterest 上想批量保存一个画板的所有图,发现页面是懒加载 + 无限滚动,传统工具只能抓到首屏可见的几十张;

在某个图片站上想搜「夕阳」,但文件名是 f8a2bc81.jpg 这样的随机哈希,关键词搜索完全失效;

想从 2000 张采集结果里去掉重复的,发现同一张图有 5 个不同分辨率的版本,文件名和 URL 都不一样;

你把图片下载到本地后,还要手动拖进 Eagle 素材库,中间隔了一整道手动工序。

这些问题的根源不是「功能没做」,而是技术栈选错了战场——用正则扫 标签的思路,无法应对现代 Web 的 Shadow DOM 封装、CDN 动态缩略图管道、SPA 懒加载渲染、以及跨页面导航时任务状态丢失等工程挑战。

OmniPic 是我用业余时间做的一个开源浏览器扩展,试图从架构层面系统性地解决这些问题。本文是对其技术实现的完整拆解。

一、四层解耦架构:为什么不能「一把梭」写在一个脚本里

浏览器扩展有三种执行环境:Content Script(页面上下文)、Background Service Worker(后台)、Side Panel / Popup(UI)。很多扩展把逻辑全堆在 Content Script 里,导致页面一刷新就丢状态、UI 一卡顿就掉帧。

OmniPic 的架构把整条管线拆成四个完全隔离的层:

┌─────────────────────────────────────────────────────┐│ 嗅探层 (content_crawler.js + content.js) ││ DOM / Shadow DOM / Computed Style / srcset 嗅探 ││ 防盗蒙层穿透 + 右键解锁 + 悬浮快捷胶囊 + 拖框选图 │└──────────────────────┬──────────────────────────────┘ │ PostMessage RPC┌──────────────────────▼──────────────────────────────┐│ 编排层 (task_queue.js @ Background SW) ││ 跨页面导航状态机 | 多分类巡检调度 | 翻页续采 ││ chrome.storage.local 持久化 | MPA 导航漂移防护 │└──────────────────────┬──────────────────────────────┘ │ MessageChannel / PostMessage┌──────────────────────▼──────────────────────────────┐│ 渲染层 (sidepanel.js) ││ 虚拟滚动 60FPS | 分类卡片 | 近重复折叠抽屉 ││ 以图搜图余弦排序 | IndexedDB 向量持久化 │└──────────────────────┬──────────────────────────────┘ │ SharedArrayBuffer / Transferable┌──────────────────────▼──────────────────────────────┐│ 计算核 (ai_worker.js @ Dedicated Worker) ││ MobileNetV1 前向传播 | 1024维嵌入提取 | 余弦相似度 ││ pHash 2D-DCT | K-Means 调色 | Laplacian 模糊检测 ││ 启发式去底 + 双三次放大 │└─────────────────────────────────────────────────────┘

关键设计决策:

所有矩阵运算隔离在 Dedicated Worker——UI 线程零阻塞,Side Panel 滚动恒定 60 FPS。模型推理一次约 180ms,但这 180ms 发生在 Worker 里,用户看到的只是搜索结果「啪」地刷出来。

跨页面导航状态持久化到 chrome.storage.local——这是整条管线里最容易被忽略的工程问题。在多页应用(MPA)网站(比如很多电商老站和图片站),点击「下一页」会触发整页导航,Content Script 的内存状态会全部清零。task_queue.js 在每次导航前把当前进度(已采分类、每分类已采张数、剩余预算、菜单计划快照)序列化到 storage,导航完成后由 Background SW 恢复并驱动下一轮采集。状态机定义了四种状态:IDLE → RUNNING → PAUSED → COMPLETED,并有 LOW_YIELD_THRESHOLD = 3 的安全阀——如果某个导航目标的真实可采图片不足 3 张,判定为非画廊页(设置页/关于页/表单页),安全跳过而非误采。

菜单计划冻结机制——巡检开始时一次性扫描站点导航菜单并冻结快照。这杜绝了巡检过程中因为页面动态注入广告或推荐栏导致菜单漂移到无关页面的风险。引擎永远不会「漂」到计划外的页面。

二、MV3 安全沙箱突围:把 TensorFlow.js 塞进扩展的正确姿势

2.1 问题:Manifest V3 杀掉了 unsafe-eval

Manifest V3 彻底废弃了 unsafe-eval。这意味着任何 new Function(...)、eval(...)、setTimeout(string) 都会被浏览器内核强制拦截:

EvalError: Refused to evaluate a string as JavaScript because'unsafe-eval' is not an allowed source of script in thefollowing Content Security Policy directive.

而 TensorFlow.js 的官方编译版在初始化运行时、为 WebGL/CPU 算子动态生成代码时,大量依赖这类动态包装:

// TensorFlow.js 内部的动态全局查找(MV3 下致命报错)const globalScope = new Function("return this")();const dynamicRequire = new Function('return require("' + mod + '")')();const regeneratorPolyfill = new Function("r", "regeneratorRuntime = r");

照搬官方 tf.min.js 进扩展,第一行就会抛 EvalError。

2.2 解法:AST 静态化 + wasm-unsafe-eval 最小特权

不是「关掉 CSP」——MV3 也不允许关——而是对 tf.min.js 做源码级修补:

第一步:把动态环境侦测替换为静态绑定。 Function("return this")() 的目的是拿到全局对象。在 Web Worker 上下文里,全局对象就是 self。我们直接把这一行替换为 self,消灭了动态代码构造。

第二步:只声明 wasm-unsafe-eval。 MV3 的 CSP 策略支持一个专门为 WebAssembly 开放的豁免:

{ "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" }}

这比 unsafe-eval 的攻击面小一个数量级——它只允许 WebAssembly 模块的即时编译,不允许任意 JavaScript 字符串的 eval。TensorFlow.js 的 WASM 后端依赖这个能力来加载预编译的 SIMD 算子。

2.3 三重降级链:WebGL → WASM → CPU

推理后端不是写死一个,而是做成熔断降级链:

WebGL (GPU 加速) ──失败──▶ WASM (SIMD 汇编) ──失败──▶ CPU (纯原生计算)

任何低端机、无独显的笔记本、甚至禁用了 GPU 加速的虚拟机里,OmniPic 都能稳定运行,只是推理速度不同。这保证了「装上就能用」,而不是「需要独显才能用」。

实测数据(M1 MacBook Air):

分类推理:44 张/秒,单张约 180ms

200 张近重复去重(O(n²)):23ms

语义嵌入索引(500 张图批量):约 12 秒

三、端侧语义搜索:1024 维向量空间里的「以图搜图」

3.1 为什么文件名搜索在网页图片上完全失效

Web 上的图片文件名约 85% 是随机哈希(f8a2bc81.jpg、thumb_04_a1b2c3.webp),alt 文本的覆盖率不足 30%。关键词搜索在这类素材上几乎 100% 失效。

OmniPic 的做法是在端侧运行 MobileNetV1 卷积神经网络,剥离最后的 1000 类 Softmax 分类头,取倒数第二层全局平均池化(Global Average Pooling, GAP)的输出——一个 1024 维的浮点向量。

数学表达:

\mathbf{x} \in \mathbb{R}^{224 \times 224 \times 3} \xrightarrow{\text{MobileNet Backbone}} \mathbf{f} \in \mathbb{R}^{1024}

这个向量就是当前图像的「语义身份证」——同一只狗的不同照片,它们的 1024 维向量在欧氏空间里会很接近;而一只狗和一辆车的向量则会相距很远。

3.2 余弦相似度召回与近重复折叠

对用户拖入的参考图 $\mathbf{A}$ 和候选图库 $\mathbf{B}_i$,在 Worker 中并行计算余弦相似度:

\text{Sim}(\mathbf{A}, \mathbf{B}i) = \frac{\sum^{1024} A_k B_{i,k}}{\sqrt{\sum_{k=1}^{1024} A_k^2} \cdot \sqrt{\sum_{k=1}^{1024} B_{i,k}^2}}

以图搜图:相似度 Top-N 排序返回,拖一张产品参考图,所有同类产品的图排到顶。

近重复变体折叠:当两张图相似度 $\geq 0.92$,判定为同源变体(不同裁剪比或分辨率版本),自动折叠到主图卡片的抽屉里。首屏杂乱度肉眼可见地下降。

3.3 自然语言搜图的实现路径

用户输入「小狗」「单反」「跑车」这类中文词,OmniPic 的做法不是做 NLP,而是利用 ImageNet 1000 类标签的中文词典映射:

把中文词映射到 ImageNet 类别 ID(如「狗」→ 对应的犬种类别)

取该类别的 1024 维 one-hot 权重向量作为查询向量

在已索引的图库向量集合上做余弦排序

这是一种「穷人的 CLIP」——没有真正的跨模态对齐,但在 ImageNet 覆盖的语义空间里,文字搜索的准确率已经远超文件名匹配。关键是100% 离线,不需要任何 API Key。

3.4 向量持久化

向量索引存在 IndexedDB(omnipic_db),不是每次刷新都重新推理。首次打开一个已采图的网站,搜索是即时的——因为向量已经在上次采集时算好并持久化了。

四、感知去重:从像素到 64-bit 指纹的 2D-DCT 频域变换

4.1 为什么二进制哈希去重在 CDN 重分发面前失效

同一张图在不同 CDN 节点、不同压缩率、轻微水印下,二进制完全不同。MD5 和 SHA-256 只能做「精确去重」,对「视觉相同但二进制不同」无能为力。

4.2 pHash:离散余弦变换感知指纹

OmniPic 的感知哈希引擎(background/phash.js)实现了完整的 2D-DCT 算法:

第一步:缩放至 32×32 灰度矩阵。 去掉颜色信息,只保留亮度通道。

第二步:预计算余弦查找表。 DCT 的核心是余弦乘积累加,直接 Math.cos() 调用在 32×32 的双重循环里会产生 1024 次三角函数计算。我们在初始化时预计算 cosTable(一个 Float64Array(32×32)),把运行时计算量压到最低。

第三步:二维离散余弦变换。 对 32×32 的灰度矩阵执行正向 2D-DCT:

F(u, v) = \alpha(u) \alpha(v) \sum_{x=0}^{31} \sum_{y=0}^{31} f(x, y) \cos\left[\frac{(2x+1)u\pi}{64}\right] \cos\left[\frac{(2y+1)v\pi}{64}\right]

其中归一化系数:

\alpha(u) = \begin{cases} \frac{1}{\sqrt{32}}, & u = 0 \ \sqrt{\frac{2}{32}}, & u > 0 \end{cases}

第四步:取左上 8×8 低频系数。 DCT 变换后,图像能量集中在左上角的低频区域。我们只取 8×8 = 64 个低频系数,丢弃高频细节——这正是人眼对亮度变化最敏感的频段。

第五步:中位数二值化生成 64-bit 指纹。 剔除直流分量 $F(0,0)$ 后计算中位数,每个系数与中数比较,大于等于记 1,小于记 0,得到 64 位二进制串,转换为 16 字符十六进制哈希。

比对:两张图的 pHash 做按位异或(XOR),再 popcount 统计 1 的个数——这就是汉明距离。距离 ≤ 5 判定视觉全等。位运算在全库比对时是微秒级的。

4.3 画质竞争:只留最高清母图

去重不是「保留第一个」,而是「保留最好的」。OmniPic 的画质竞争评分公式:

\text{Score} = (\text{Width} \times \text{Height}) \times \text{FormatWeight} \times \text{QualityConfidence}

格式权重:PNG > WebP > JPG。质量置信度通过拉普拉斯方差(Laplacian 算子边缘锐度检测)计算——方差 < 100 判定为模糊废片,直接淘汰。这样在 5 个分辨率变体里自动只保留最高清最清晰的母图版本。

五、CDN 原图逆向:为什么你下载的总是缩略图

5.1 缩略图陷阱

几乎所有云厂商的图片服务都会在 URL 上拼接缩略图参数。你以为右键保存的是原图,实际上拿到的是被压到几百像素的缩略图:

| 云服务 | 缩略图参数特征 | 你下载到的 | 实际母图 |

|:--|:--|:--|:--|

| 阿里云 OSS | ?x-oss-process=image/resize,w_300 | 300px 宽的裁剪版 | 无参数的原始上传图 |

| 腾讯云 COS | ?imageMogr2/thumbnail/300x | 300px 缩放版 | 移除 imageMogr2 管道 |

| 七牛云 | ?imageView2/2/w/200 | 200px 宽度版 | 剥离 imageView2 标志 |

| Unsplash | &w=400&fit=crop&q=60 | 400px 60% 质量版 | &w=3840&q=100 4K 原图 |

| 小红书 | /square/ 或 !nd_dft_wlteh_jpg | 方形缩略图 | /origin/ 无损母图 |

这意味着你在图片站上看到的图,可能只有实际原图体积的 1/10。你的流量在浪费,你的素材质量在被打折。

5.2 OmniPic 的 CDN 逆向规则引擎

OmniPic 的 content.js 里实现了 restoreOriginalUrl() 函数,这是一个模式匹配 + 参数剥离的管道式规则引擎:

// 阿里云 OSS / 腾讯云 COS / 七牛云:剥离处理管道参数if (url.includes('x-oss-process=') || url.includes('imageMogr2') || url.includes('imageView2')) { url = url.replace(/([?&])x-oss-process=[^&#]/gi, '$1') .replace(/([?&])imageMogr2[^&#]/gi, '$1') .replace(/([?&])imageView2[^&#]*/gi, '$1');}// Unsplash:重写为 4K 高质量输出if (url.includes('images.unsplash.com')) { // 排除头像(头像不需要放大到 3840px) if (url.includes('/profile-') || url.includes('avatar')) return url; const u = new URL(url); u.searchParams.delete('w'); u.searchParams.delete('h'); u.searchParams.delete('crop'); u.searchParams.delete('fit'); u.searchParams.delete('dpr'); u.searchParams.set('w', '3840'); u.searchParams.set('q', '100'); url = u.href;}// 小红书:路径替换 + 参数剥离if (url.includes('xhscdn.com') || url.includes('xiaohongshu.com')) { url = url.replace(//(square|small|thumbnail|100_100|200_200)//g, '/origin/') .replace(/!nd_dft_[a-z0-9_]+/gi, '');}

注意这里有一个细节:头像不放大。Unsplash 的 profile 头像本身就是小图,如果一律重写为 w=3840,反而会请求到不存在的尺寸。这是一个在真实测试中发现并修复的 bug——代码注释里标着 BUGFIX #2。

5.3 电商平台的 CDN 清洗

电商平台(淘宝/天猫/1688/京东/亚马逊/Shopee)的图片 URL 有自己的清洗规则。sidepanel/adapters/ecommerce.js 里的 cleanCdnUrl() 专门处理这些平台的 CDN 参数:

function cleanCdnUrl(url) { let cleaned = url.replace(/_\d+x\d+[^.]*.(jpg|png|webp)/gi, '.$1') // 去掉 _400x400 后缀 .replace(/.webp$/i, '') // 统一格式 .replace(/_q\d+.jpg$/i, '') // 去掉质量参数 .replace(///img.alicdn.com/i, 'https://img.alicdn.com'); if (cleaned.startsWith('//')) cleaned = 'https:' + cleaned; return cleaned;}

同时,extractImagesFromSelectors() 会优先读取 data-original、data-src、data-zoom-image、data-large_image 等懒加载属性——这些属性存的是高清原图 URL,而 src 属性里往往是占位缩略图。

六、防盗蒙层穿透与右键解锁:当网站不让你下载时

6.1 透明蒙层拦截

很多图片站会在图片上方覆盖一个全透明的

,把 pointer-events 设为 auto,把图片的 pointer-events 设为 none。这样你的右键点击落在了透明蒙层上,弹出来的是「另存为网页」而不是「另存为图片」。

6.2 OmniPic 的穿透方案

content_crawler.js 里的 unlockRightClickAndAntiTheft() 做了两件事:

第一:拦截禁用事件。 对 contextmenu、dragstart、selectstart、copy、cut 五个事件在 capture 阶段调用 stopPropagation(),阻止网站脚本注册的阻止处理器执行。

第二:识别并禁用透明蒙层。 遍历页面上所有叶子节点

、、,如果满足以下条件,就把它的 pointer-events 强制设为 none:

没有子元素(是叶子节点)

宽高都大于 80px

position: absolute 或 fixed

background-color: rgba(0,0,0,0)(完全透明)

background-image: none

z-index > 0(浮在内容之上)

这组条件精准地命中了防盗透明蒙层的特征,同时不会误伤正常的内容层。

6.3 悬浮快捷胶囊

除了自动嗅探,OmniPic 还注入了一个悬浮在图片右上角的快捷操作胶囊。鼠标 hover 到任意图片上时,胶囊出现,提供三个快捷操作:一键保存 4K 原图、复制图片 URL、加入选中列表。这个胶囊是一个动态注入的 DOM 元素,带 pointer-events: auto,不会被蒙层拦截。

6.4 Alt + 鼠标拖框选图

按住 Alt 键,在页面上拖一个矩形框,框内所有图片会被自动选中。这用到了几何碰撞检测——遍历页面上所有 元素,判断其 getBoundingClientRect() 与拖框矩形是否相交。对于密集图墙(如 Pinterest 瀑布流),这个操作比一张张点选快几十倍。

七、全站多分类自动巡检:跨页面导航状态机

7.1 为什么「全站采集」比「单页采集」难一个量级

单页采集只需要在当前 DOM 里找图片。全站采集要解决的是:

导航菜单识别——网站的分类菜单长什么样?有几个分类?每个分类的 URL 是什么?

跨页面状态持久化——在 MPA(多页应用)网站里,点击导航会触发整页导航,Content Script 的 JS 堆会全部清零。如果不持久化状态,采完第一页就丢了进度。

翻页续采——每个分类内可能有分页(「下一页」按钮或无限滚动),怎么在采完一页后自动翻页并继续采集?

分类归档——采到的图要按网站分类自动归档,不是一堆混在一起。

导航漂移防护——巡检过程中页面可能动态注入广告或推荐栏,导致菜单结构变化,引擎不能跟着漂移到无关页面。

7.2 OmniPic 的巡检状态机

background/task_queue.js 实现了一个四状态有限状态机:

IDLE ──startTask──▶ RUNNING ──pause──▶ PAUSED ──resume──▶ RUNNING │ │ │ └──allCategoriesDone──▶ COMPLETED └──────────────────────────────────────────────────▶

巡检启动时,状态机做以下事情:

第 1 步:冻结菜单计划。 一次性扫描站点导航菜单,提取所有分类名称和 URL,冻结成不可变的 menus 数组快照。在整个巡检过程中,菜单计划永不重新扫描——这杜绝了动态注入的推荐栏把引擎引偏。

第 2 步:逐分类导航。 Background SW 用 chrome.tabs.update() 把标签页导航到第一个分类的 URL。页面加载完成后,Content Script 恢复状态并开始该分类的深度采集(滚屏 + 翻页)。

第 3 步:翻页续采。 Content Script 在采完当前页所有图片后,如果检测到「下一页」链接,不自己导航——而是把 nextUrl 和已采图片通过 PostMessage 发回 Background SW。SW 累积图片、更新进度、导航到 nextUrl、恢复 Content Script 继续采集。每一次 tab 导航都是队列发起的,引擎永远不会漂移到计划外的页面。

第 4 步:低产出安全跳过。 某些导航目标可能是设置页、关于页、表单页,没有图片或只有极少图片。当真实可采图片数 < LOW_YIELD_THRESHOLD(默认 3),安全跳过该分类。失败模式是「少采」(safe-skip),不是「误采」(wrong-crawl)。

第 5 步:状态持久化。 每次导航前把 { taskId, status, currentIndex, menus, categoryResults, menuProgress } 序列化到 chrome.storage.local。即使标签页崩溃或扩展重载,也能从断点恢复。

7.3 分类自动归档

巡检采到的图片按分类名自动归入子文件夹(如 01_壁纸/、02_风景/、03_二次元/),导出 ZIP 时保持目录结构。这个分类不是靠 AI 猜的,而是直接取网站自己的导航菜单文本——网站自己最清楚它的分类逻辑。

八、电商与社交专精解包:不是扫 <img>,而是理解页面结构

8.1 电商商品图的三类拆解

一个电商商品页(淘宝/天猫/1688/京东/亚马逊/Shopee)的图片通常有三类用途:

主图轮播——商品头图,通常是白底高清大图,用于展示商品全貌

SKU 颜色分类图——每个颜色/规格对应一张图,用于选择器交互

详情页长图切片——详情页的一张超长图被切成多段加载

这三类图的 DOM 位置、懒加载策略和 CDN 参数都不同。ecommerce.js 的 unpackEcommerce() 方法会:

检测当前页面属于哪个电商平台(hostname 正则匹配)

对每个平台使用专门的 CSS 选择器集来定位三类图片

优先读取 data-original / data-zoom-image / data-large_image 等懒加载高清属性

清洗 CDN URL,剥离尺寸后缀(如 _400x400.jpg → .jpg)

按分类归入 01_商品主图轮播/、02_SKU颜色分类图/、03_详情页长图切片/

8.2 社交多图帖的解包

社交平台(小红书/微博/Twitter/Instagram)的多图帖有自己的图片加载策略:

小红书:图片可能藏在 window.INITIAL_STATE 的 JSON 里,也可能在 Swiper 轮播组件的 data-src 里。OmniPic 先尝试解析 INITIAL_STATE 脚本块,匹配 "urlDefault": "..." 或 "url": "...",剥离小红书的 CDN 缩略图参数;如果 JSON 解析失败,回退到 DOM Swiper 选择器(.swiper-slide img、.note-slider img)。

微博:图片在 sinaimg.cn 上,URL 路径段 /square/、/thumbnail/ 指示缩略图,替换为 /large/ 或 /origin/ 取原图。

Twitter/X:图片在 twimg.com 上,URL 参数 ?name=small / ?name=thumb 指示缩略图,替换为 ?name=large 或 ?format=jpg&name=4096x4096 取最大尺寸。

Instagram:图片在 cdninstagram.com 上,需要解析 INITIAL_STATE 或从 的 srcset 里取最高分辨率候选。

每种平台的解包逻辑都是一条独立的规则链,不是通用的「扫 img」。

九、内存工程:2000+ 大图秒级流式打包

9.1 传统 jszip 为什么会崩

前端打包 ZIP 文件的流行库 jszip 有两个致命问题:

默认用 DEFLATE 压缩——在主线程做 CPU 密集运算,几千张图同时压缩会把 UI 卡死。

全量加载到 JS 堆内存——几千张大图的 ArrayBuffer 全部堆积在 V8 的 JS 堆里,很容易触发 Maximum call stack size exceeded 或 OOM 崩溃。

9.2 STORE 零压缩 + 异步流式分块

OmniPic 的打包管线做了两层优化:

第一层:STORE 零压缩直写。 ZIP 规范定义了 compressionMethod: 0 (STORE),即不压缩,只做存储。图片(JPG、PNG、WebP)本身已经是高压缩率格式,二次 DEFLATE 的体积缩减不足 1%,但 CPU 开销是 100%。切换到 STORE 模式后,打包过程从「CPU 密集」变成「IO 密集」,完全不再卡 UI。

第二层:异步队列流式分块写入。 构建并发池(Concurrency Pool = 6),用 Blob / ArrayBuffer 分段消费机制:下载一张 → 写入一个分块 → 释放该图片的引用 → 下载下一张。边下载、边写入、边 GC。内存占用恒定在约 80MB 以内,无论打包多少张图都不会涨。

实测:2000+ 张图(总计约 4GB)打包导出约 90 秒,全程 UI 无卡顿,内存峰值 < 100MB。

十、本地素材库直连:Eagle / Billfish REST API 桥接

10.1 为什么不是「下载到文件夹再手动拖」

设计师和素材管理者的典型工作流是:采集 → 整理 → 入库。如果扩展只能「下载到 Downloads 文件夹」,中间就隔了一道手动拖拽的工序。OmniPic 直接对接 Eagle 和 Billfish 的本地 REST API,实现一键入库。

10.2 心跳自动发现

eagle_bridge.js 不会假设 Eagle 一定在运行。它用一个 2 秒超时的 AbortController 去探测 http://localhost:41595/api/application/info(Eagle)和 http://localhost:3150(Billfish)。如果探测成功,标记为已连接,并获取 Eagle 的文件夹列表——用户可以在导出时选择目标文件夹。

心跳每 5 秒轮询一次,自动感知 Eagle 的启停状态。状态变化通过监听器通知 UI 层,UI 实时显示「已连接 Eagle v3.x」或「未检测到 Eagle」。

10.3 并发批量导入

选中一批图 → 选择 Eagle 文件夹 → 点击「同步到 Eagle」。OmniPic 会并发(并发度控制在 4-6)地调用 Eagle 的 POST /api/item/addFromURL 接口,每张图带上来源 URL 和分类标签。Eagle 会自动下载图片并归入库中,无需用户做任何手动操作。

十一、真实性能数据汇总

| 场景 | 数据 |

|:--|:--|

| 分类推理(MobileNet 前向传播) | 44 张/秒,单张约 180ms |

| 200 张近重复去重(O(n²) 余弦比对) | 23ms |

| 500 张图语义嵌入索引 | 约 12 秒 |

| 2000+ 张图流式 ZIP 导出 | 约 90 秒,内存峰值 < 100MB |

| 全站巡检(4 个分类 × 50 张/分类) | 约 8 分钟(含网络等待) |

| Side Panel 虚拟滚动 | 恒定 60 FPS(10000+ 张图不卡) |

| Eagle 桥接心跳探测 | 2 秒超时,5 秒轮询 |

| Unsplash 冒烟测试 | 24 张图,零适配 |

| Wallhaven 冒烟测试 | 28 张图,零适配 |

| M1 语义搜图测试 | 103/103 通过(1 个中等级 UX bug) |

| 巡检 v2 测试 | 49/49 通过 |

十二、与同类工具的差异化对比

| 能力维度 | 传统图片下载扩展 | OmniPic |

|:--|:--|:--|

| 图片发现 | 正则扫 | DOM + Shadow DOM + Computed Style + srcset + data-src |

| 防盗蒙层 | 无法穿透 | 透明蒙层自动识别 + pointer-events: none 穿透 |

| 右键解锁 | 不支持 | 拦截 contextmenu/dragstart/selectstart/copy/cut 的阻止器 |

| 下载原图 | 拿到的是缩略图 | CDN 参数逆向剥离,直取母图 |

| 语义搜索 | 靠文件名/alt 文本 | 1024 维 MobileNet 端侧向量 + 余弦相似度 |

| 视觉去重 | 无 / 靠文件大小 | 2D-DCT 64-bit 感知哈希 + 汉明距离 ≤ 5 |

| 全站采集 | 单页 | 导航菜单识别 + 跨页面状态机 + 翻页续采 + 分类归档 |

| 电商解包 | 无 | 主图/SKU/详情三类自动拆分(淘宝/天猫/1688/京东/亚马逊/Shopee) |

| 社交解包 | 无 | 九宫格/轮播全展开(小红书/微博/Twitter/Instagram) |

| 素材库直连 | 无 | Eagle / Billfish REST API 一键入库 |

| 打包导出 | jszip 全量加载 → OOM | STORE 零压缩 + 流式分块,内存恒定 80MB |

| 隐私 | 可能上传到服务器 | 100% 本地,0 云端成本 |

| 价格 | 免费/订阅 | 永久免费,MIT 开源 |

十三、诚实标注:哪些是真实实现,哪些是启发式近似

技术文章不应该夸大。以下是对每个功能的诚实标注:

| 功能 | 实现方式 | 诚实口径 |

|:--|:--|:--|

| 1024 维以图搜图 | MobileNetV1 GAP 层真实向量 + 余弦相似度 | 真实深度学习模型推理,可对外宣传 |

| ImageNet 分类 | MobileNetV1 1000 类 Softmax 真实推理 | 真实模型,44 张/秒 |

| pHash 感知去重 | 2D-DCT 完整实现,预计算余弦表 | 真实算法,微秒级比对 |

| K-Means 5 色调色盘 | K-Means++ 初始化 + 8 轮迭代 | 真实实现 |

| Laplacian 模糊检测 | 方差 < 100 判定模糊 | 真实实现 |

| CDN 原图逆向 | 参数剥离 + URL 重写规则库 | 真实实现,覆盖主流云厂商 |

| 全站巡检 | 跨页面状态机 + 菜单冻结 + 翻页续采 | 真实实现,49/49 测试通过 |

| 电商/社交解包 | 平台专用选择器 + CDN 清洗 | 真实实现 |

| 流式 ZIP 打包 | STORE 零压缩 + 并发池流式分块 | 真实实现 |

| Eagle/Billfish 直连 | 本地 REST API + 心跳探测 | 真实实现 |

| 智能去底 | 颜色 + 边缘启发式(无模型) | 启发式算法,适合纯色背景,复杂背景非专业级 |

| 4x 放大 | Catmull-Rom 双三次插值 + unsharp 锐化 | 传统插值算法,不是 AI 超分,不重建细节 |

| 自然语言搜图 | ImageNet 中文标签词典映射 | 词典映射,不是真 CLIP 跨模态对齐 |

结语

OmniPic 想解决的核心问题是:在现代 Web 上高效地采集、理解和整理图片,同时不牺牲隐私。

这需要同时攻克几个工程挑战:MV3 安全沙箱对深度学习框架的限制、CDN 缩略图参数对原图下载的干扰、跨页面导航对任务状态的破坏、以及海量图片对浏览器内存的压力。每一个挑战都没有「标准答案」,需要的是针对性的工程设计——AST 修补绕过 CSP 限制、参数剥离规则引擎逆向 CDN、序列化状态机跨导航持久化、STORE 零压缩流式分块控制内存。

这些技术决策的源码全部在 GitHub 上,MIT 协议,欢迎审查、复用和贡献。

如果你是电商运营、设计师、社媒编辑、或者只是经常需要从网页上批量保存图片的人——装上试试。如果你是开发者,对端侧 AI 落地、浏览器扩展架构或离线计算机视觉感兴趣——欢迎来 GitHub 交流。

安装:

Chrome:https://chromewebstore.google.com/detail/omnipic-ai-studio-pro-端侧w/cokoeaaaglgcljahaffjeijomhmkljlc

Firefox:https://addons.mozilla.org/firefox/addon/omnipic-studio/

Edge:https://microsoftedge.microsoft.com/addons/detail/omnipic-bulk-image-downloader/bbf6b99a-cb66-4d1e-b918-aac03dc77a95

GitHub:https://github.com/falsettagirillo-cyber/OmniPic

本文所有性能数据均在 M1 MacBook Air 上实测,测试报告源文件随仓库一同开源。

posted @ 2026-09-07 10:32  SuperEden  阅读(20)  评论(0)    收藏  举报