纯前端离线图片嗅探:用MobileNet在浏览器侧边栏自动识别并逆向高清原图 📅 发布时间:2026/9/8 14:27:57 👁 浏览次数: 做这个项目的起因其实很简单我经常在浏览网页时看到特别喜欢的配图右键保存结果发现存下来的只是一张压缩过的小图放大全是马赛克而真正的高清原图往往藏在图库服务器某个角落需要手动去翻页面源码、找资源路径效率太低。后来我干脆写了一个浏览器侧边栏扩展把图片嗅探、离线AI识别和高清图库逆向这几件事全串起来在侧边栏点一下按钮网页里所有图片资源就会被自动扫描交给MobileNet在浏览器本地完成分类识别再按照分辨率、清晰度、文件体积等维度综合打分把高清原图从低质量的缩略图里筛出来。整个过程完全在浏览器里跑不需要后端不需要上传任何图片断网也能用。如果你平时有收集图片素材、扒参考图、整理设计灵感的需求或者你正在研究纯前端如何跑轻量级AI模型那这个项目应该能给你挺多启发。下面我会从整体设计、模型选型、代码实现到踩坑记录完整拆一遍。1. 项目概述与方案拆解1.1 要解决的核心痛点网页里的图片资源远比你看到的要复杂。一个正常的图片页面背后可能同时存在缩略图、中等尺寸图、原图、WebP格式图、带裁剪参数的CDN图甚至还有懒加载时保留的透明占位图。浏览器最终展示给你的只是其中一张。普通用户想拿高清原图最原始的办法是右键“在新标签页中打开图片”但很多网站会把图片地址做一层包装或者原图体积太大、加载太慢展示层直接用了压缩比例很高的版本。这个工具要解决的就是“从一堆隐藏资源里快速找出最优质量版本”的问题。我把它拆成了三个核心动作嗅探拿到当前页面加载过的所有图片URL以及页面中声明的候选图片地址。识别对图片内容做离线AI打分搞清楚这张图是什么类别是不是一张有效内容例如排除纯色块、图标、广告像素图等。逆向根据URL特征和图片头信息推导并验证更高质量的原始资源地址最终给出排序结果。1.2 为什么选纯前端全离线路线有人会问这种图片筛选逻辑放在后端服务里不是更简单吗后端可以批量下载、用OpenCV做清晰度分析还能跑更大的模型。但我在设计初期就定了一条硬规矩整个工具必须能在浏览器扩展的侧边栏里独立运行且严格离线。原因是多方面的。第一是隐私。图片内容往往会泄露你正在浏览的网站、你的兴趣倾向、甚至一些工作相关的敏感信息。把这些图片上传到后端做分析等于让第三方看到了你的完整浏览轨迹这个代价我接受不了。纯前端意味着所有图片都只在本机解码、分析和排序状态完全可控。第二是实时性。后端方案的链路太长了扩展采集URL、请求后端接口、后端下载图片、分析、回传结果。一来一回的延迟在网页这种即时场景里非常明显。而把MobileNet等模型直接塞进扩展里推理发生在同一个浏览器进程内或通过Web Worker并行图片下载成功后几毫秒就能得到分类结果体验完全是另一个层级。第三是成本。不买服务器、不维护推理服务、不怕接口被打爆这也是很多个人工具项目最务实的选择。MobileNet本身的定位就是“面向移动端和嵌入式设备的轻量级网络”一个几兆字节的模型文件放在本地对资源占用几乎可以忽略不计。1.3 整体架构与工作流程整个扩展的逻辑结构可以分成三层采集层通过页面注入脚本和扩展后台配合收集当前标签页的图片资源URL包括页面DOM中img标签的src、srcset属性performance.getEntriesByType(resource)拿到的网络请求记录以及link relpreload asimage等预加载资源。分析层在扩展的后台或侧边栏页面里把候选图片URL逐一下载成Blob读取宽高、体积、格式然后解码成ImageBitmap或Tensor交给离线加载的MobileNet模型做分类推理同时对图片做清晰度指标计算。展示层把分析结果按“高清潜力分”排序在侧边栏展示缩略图列表点击即可跳转到原图地址或直接下载。用户视角的完整流程是打开一个图片很多的网页唤起侧边栏点击“扫描当前页面”等一两秒侧边栏出现排序后的图片列表顶部是高分高清原图候选每张图的缩略图右下角标着分辨率、文件大小和AI分类标签。全程不需要联网请求任何外部API。2. 核心技术原理2.1 MobileNet的轻量秘诀MobileNet是Google提出的一系列面向端侧视觉任务的卷积神经网络从V1到V3迭代了很多版本核心思想一直没有变用极低的参数量换取可接受的精度。它最关键的创新是深度可分离卷积Depthwise Separable Convolution把一次标准卷积拆分成两步先用深度卷积Depthwise Convolution在每一个输入通道内部做空间特征提取再用逐点卷积Pointwise Convolution本质上是一个1x1卷积在通道维度上做线性组合。这样做的效果非常直观假设输入特征图尺寸是Df x Df x M输出通道数为N卷积核大小是Dk x Dk。标准卷积的计算量是Dk * Dk * M * N * Df * Df而深度可分离卷积的计算量是Dk * Dk * M * Df * Df M * N * Df * Df。两者一对比压缩率大约等于1/N 1/Dk^2。以常见的3x3卷积核、256个输出通道为例计算量能直接降到原来的九分之一左右。这就是MobileNet能在浏览器这种没有GPU加速的环境里跑出实时效果的根本原因。MobileNet V1还暴露了两个额外的控制参数宽度系数Width Multiplier记为α和分辨率系数Resolution Multiplier记为ρ。宽度系数控制每一层通道数的缩放比例分辨率系数控制输入图像的分辨率。把这两个参数调低模型体积和推理耗时会进一步下降。在我这个项目里默认使用mobilenet_v2的1.0版本输入分辨率为224x224模型权重经过量化后大约只有十几MB加载进内存毫无压力。2.2 前端推理引擎选型浏览器里跑神经网络主流选项有TensorFlow.js、TFLite Runtime Web以及ONNX Runtime Web。我最后选了TensorFlow.js主要原因是它和tensorflow-models/mobilenet这个官方模型库配合得非常顺畅。这个包内部封装好了预处理、归一化和输出解析不用自己手写softmax和label映射表。TensorFlow.js底层有两套执行后端WebGL后端用GPU做并行计算适合卷积网络这类算子密集型任务CPU后端则用WebAssembly向量化指令加速。在侧边栏这种小型UI页面上WebGL后端通常能把一次MobileNet推理压到五六十毫秒的级别对于批量扫描上百张图片的场景这个速度完全够用。如果你追求极致的模型体积也可以考虑把MobileNet V3导出为TFLite格式再用tensorflow/tfjs-tflite插件加载。我试过用V3 Large int8量化版模型权重能压到三四MBCPU后端跑224x224输入大概八九十毫秒识别准确率比V2略微下降但体积优势巨大。扩展打包发布时如果对商店的体积限制敏感V3量化是更好的选择。2.3 嗅探与“高清逆向”的实现逻辑“图片嗅探”这个词听起来有点神秘其实在纯前端的语境下本质上就是两件事一是拿到页面里所有图片的URL二是过滤掉无效或低价值的URL。URL来源我按可靠性排了个优先级performance.getEntriesByType(resource)浏览器性能API直接返回当前页面发出过的所有资源请求包含完整的name字段可靠性最高而且能拿到资源大小、加载耗时等额外信息。DOM扫描遍历document.images读取currentSrc、src、srcset里的候选地址。srcset尤其重要很多响应式图片的原始大图URL就藏在里面。MutationObserver监听DOM变化一旦页面后续懒加载了新的图片能自动追加到待分析队列。“高清图库逆向”的逻辑则更有意思。很多图片服务商不会直接给出原图URL而是通过URL参数来控制缩放、裁剪、质量。例如某些图库的图片地址带有?w400h300这样的尺寸参数或者路径里包含/thumb/、/small/这种目录层级。逆向的时候可以先基于同一路径尝试去掉这些限制参数、替换成/original/或/large/再通过请求头只下载前几KB数据来验证新地址是否真实存在、体积是否更大。这算是一种“有根据的猜测”不是暴力破解因为目标资源本来就是该网站的公开图片服务没有任何绕权限的行为。下面是我在实际代码里用的一个逆向候选生成器片段function generateHighResCandidates(url) { const candidates []; const u new URL(url); // 去掉常见的尺寸类query参数 const sizeParams [w, h, width, height, size, resize, quality, q]; u.searchParams.forEach((value, key) { if (sizeParams.includes(key.toLowerCase())) { const clone new URL(u.toString()); clone.searchParams.delete(key); candidates.push(clone.toString()); } }); // 替换路径中的尺寸目录 const pathPatterns [ [/(\/thumb\/|\/small\/|\/medium\/)/i, /large/], [/\/resize\/[^/]\//i, /original/], [/\.(jpg|jpeg|png)_?\dx\d\./i, .$1] ]; for (const [regex, replacement] of pathPatterns) { if (regex.test(u.pathname)) { const clone new URL(u.toString()); clone.pathname u.pathname.replace(regex, replacement); candidates.push(clone.toString()); } } return candidates; }生成候选地址之后还需要做两步验证。第一步是HEAD请求或范围请求只拿前512字节检查Content-Length和Content-Type排除404和不存在的资源。第二步才是完整下载读取最终的宽高和体积。这样能避免一次性把所有候选图都拉下来节省流量和内存。3. 实操从零打造一个侧边栏图片逆向工具3.1 扩展基础配置MV3 Side Panel我用的是Manifest V3标准因为Chrome和Edge现在都默认支持side_panelAPI可以在浏览器窗口右侧开一个常驻面板非常适合这种工具型界面。manifest.json的核心配置如下{ manifest_version: 3, name: Offline Image Inspector, version: 1.0.0, minimum_chrome_version: 114, permissions: [ sidePanel, storage, unlimitedStorage, activeTab, scripting ], host_permissions: [all_urls], background: { service_worker: background.js }, action: { default_title: 打开图片嗅探面板 }, side_panel: { default_path: panel.html }, web_accessible_resources: [ { resources: [models/*], matches: [all_urls] } ] }这里有几个容易踩坑的点。sidePanel权限在MV3里必须显式声明panel.html是侧边栏页面的入口它和普通扩展页面一样可以访问扩展的模型资源但默认状态下不能直接读取网页内容需要借助chrome.scripting往目标页面注入采集脚本。host_permissions设为all_urls是为了让扩展后台能够跨源抓取图片二进制数据没有这一步你会碰到各种CORS错误。在background service worker里需要监听用户点击扩展图标然后打开侧边栏chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });3.2 打包与加载离线MobileNet模型把模型文件放进扩展目录是保证离线可用的核心。tensorflow-models/mobilenet的默认加载方式是从Google Cloud Storage下载模型离线环境会直接报错。因此需要先把模型下载到本地再通过相对路径加载。具体做法是使用tf.loadGraphModel配合chrome.runtime.getURL(models/model.json)import * as tf from tensorflow/tfjs; import * as mobilenet from tensorflow-models/mobilenet; let modelPromise null; async function getOfflineModel() { if (modelPromise) return modelPromise; const modelUrl chrome.runtime.getURL(models/mobilenet_v2/model.json); const baseUrl chrome.runtime.getURL(models/mobilenet_v2/); modelPromise mobilenet.load({ version: 2, alpha: 1.0, modelUrl, // 关键必须指定baseUrl否则权重分片文件按相对路径找不到 fromTFHub: false }).catch(err { modelPromise null; throw err; }); return modelPromise; }这里有一个官方文档里语焉不详的细节modelUrl指向model.json后模型内部的权重分片文件例如group1-shard1of1.bin的加载路径取决于model.json里记录的相对路径而TensorFlow.js会用modelUrl的目录作为基准去拼接。所以启动加载之前最好先打开model.json确认里面引用的.bin文件名和你实际下载的文件完全一致大小写不同都可能导致加载失败。模型文件清单主要包括一个model.json和1到5个权重分片。把整个目录放到扩展的models文件夹下并且确保在web_accessible_resources里声明了对应目录否则页面上下文里即使想引用也拿不到。3.3 图片资源嗅探的上游细节嗅探逻辑需要同时跑在页面上下文的注入脚本里和扩展后台里。页面上下文负责DOM扫描扩展后台负责跨源抓取两边通过chrome.runtime.sendMessage通信。注入脚本的采集代码如下(() { const urlSet new Set(); function collectFromPerformance() { const entries performance.getEntriesByType(resource); for (const entry of entries) { if (entry.initiatorType img || /\.(png|jpe?g|webp|gif|avif)(\?|#|$)/i.test(entry.name)) { urlSet.add(entry.name); } } } function collectFromDom() { document.querySelectorAll(img).forEach(img { if (img.currentSrc) urlSet.add(img.currentSrc); if (img.src) urlSet.add(img.src); const srcset img.getAttribute(srcset); if (srcset) { srcset.split(,).forEach(part { const url part.trim().split(/\s/)[0]; if (url) urlSet.add(new URL(url, location.href).href); }); } }); } collectFromPerformance(); collectFromDom(); // 监听懒加载 const observer new MutationObserver(() { collectFromDom(); chrome.runtime.sendMessage({ type: IMG_URLS_UPDATE, urls: [...urlSet] }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); chrome.runtime.sendMessage({ type: IMG_URLS_UPDATE, urls: [...urlSet] }); })();采集完URL之后后台需要按优先级排序currentSrc优先、srcset里的最大分辨率版本优先、performance记录里的体积较大资源优先。排序的目的是避免把时间浪费在扫描几十张按钮图标和背景纹理上。3.4 推理与高清筛选管线拿到一个候选URL后后台流程如下fetch图片为Blob若体积大于某个阈值我设为20MB则跳过避免内存爆炸。createImageBitmap解码读取width和height。如果宽高任一小于640标记为“低分清选”直接过滤。将ImageBitmap绘制到224x224的canvas上再用tf.browser.fromPixels转成Tensor归一化后传给MobileNet做推理。推理结果返回1000个类别的概率分布取top5作为标签。计算清晰度指标包括相邻像素梯度均值拉普拉斯方差和文件体积密度每百万像素对应多少KB。综合得分 分辨率得分 清晰度得分 格式加分PNG/WebP无损通常比高压缩JPG更接近原图最终按得分排序展示。清晰度计算的简化代码function computeSharpnessScore(imageData) { const { data, width, height } imageData; let sum 0; let count 0; // 拉普拉斯梯度的近似中心像素与上下左右像素的差 for (let y 1; y height - 1; y) { for (let x 1; x width - 1; x) { const idx (y * width x) * 4; const center data[idx] * 0.299 data[idx 1] * 0.587 data[idx 2] * 0.114; const up data[idx - width * 4] * 0.299 data[idx - width * 4 1] * 0.587 data[idx - width * 4 2] * 0.114; const down data[idx width * 4] * 0.299 data[idx width * 4 1] * 0.587 data[idx width * 4 2] * 0.114; const left data[idx - 4] * 0.299 data[idx - 3] * 0.587 data[idx - 2] * 0.114; const right data[idx 4] * 0.299 data[idx 5] * 0.587 data[idx 6] * 0.114; sum Math.abs(4 * center - up - down - left - right); count; } } const normalized sum / count / 255; return Math.min(1, normalized * 10); }这里有一个很real的问题拉普拉斯方差对压缩噪声非常敏感JPG质量低时画面边缘会出现振铃效应评分反而可能偏高。所以我并没有单独依赖这个指标而是把它和分辨率、体积一起做了加权求和。实际调参下来权重大概是这样指标权重说明分辨率0.5长边像素越高分越高但超过2000像素后边际收益递减拉普拉斯清晰度0.3评价值建议做log压缩否则噪点图会把正常图压下去体积密度0.2每百万像素的KB数过小说明压缩严重过大可能是噪声图3.5 侧边栏UI与交互侧边栏页面用纯HTMLCSS实现没引入前端框架毕竟侧边栏宽度有限组件复杂度不高。顶部是一个“扫描当前页”按钮和状态指示灯中间是可滚动的结果列表底部是模型加载状态和统计信息。关键交互有三个点击缩略图在新标签页打开原图悬停时显示URL地址和AI分类标签右键菜单提供“复制原图地址”和“下载原图”。结果列表用虚拟滚动优化因为一次扫描可能命中几百张图片全部渲染DOM的话侧边栏会卡成PPT。移动端和桌面端的肩并肩体验差异也值得说一下侧边栏在宽屏浏览器上非常好用但在平板或窗口很窄的场景下会挤压正文区域。所以我加了一个折叠按钮用户可以把侧边栏收起只保留一个悬浮球入口点开再展开。4. 常见问题与排查技巧4.1 一张表定位高发问题症状原因解决思路模型加载一直转圈model.json里的分片路径与本地文件不一致打开model.json核对.bin文件名推理结果全部一样输入Tensor没有做归一化或者通道顺序错误检查像素值是否除以255R/G/B顺序很多图片扫描不出来页面里的图片是canvas.toDataURL动态生成这类图只能拿到base64建议单独走截图识别逻辑点击原图跳转403目标服务器做防盗链验证复制链接到新标签页手动打开或用Referer伪装侧边栏白屏扩展里加载了跨域模块被CSP拦截检查extension pages的content_security_policy配置内存占用暴涨并发下载太多大图加并发控制队列同时最多抓4张4.2 踩坑实录与解决方案第一个印象深刻的坑是tf.loadGraphModel和mobilenet.load的Model URL混用问题。tensorflow-models/mobilenet这个包在加载时底层其实会调用tf.loadGraphModel但它期望的modelUrl和普通的TFJS GraphModel格式略有区别。网上很多教程直接让你传一个model.json路径结果加载成功了但推理结果全是垃圾。后来自查发现是这个包内部默认的预处理方式假设输入是0~1范围而我从canvas转出来的Tensor是0~255整数。加一行tensor.div(255)后结果立刻恢复正常。第二个坑是图片解码的崩溃问题。svg图片和动图GIF对createImageBitmap处理不太友好某些超大GIF解码会导致内存瞬间飙高。我在解码前加了格式和体积预检遇到image/svgxml格式直接丢给img标签渲染不参与离线推理GIF只取第一帧或全部跳过。第三个坑是扩展里的fetch和网页上下文的fetch完全不一样。扩展后台service_worker里的fetch默认携带扩展的origin部分图片服务会返回403。解决办法是给fetch加上credentials: omit并对部分站点强制设置Referer为图片URL自己的来源。第四个坑比较隐蔽是MutationObserver监听DOM导致内存泄漏。在一些单页应用里DOM节点频繁增删如果每次变化都重新扫描document.images并克隆URL性能损耗很大。我的做法是加了一个200ms的防抖节流并且用一个WeakSet记录已经处理过的img元素节点避免重复。5. 性能优化与扩展方向5.1 跑起来以后再优化什么基础版本可用之后最重要的优化目标就是减少主线程卡顿。MobileNet推理本身不慢但批量处理时图片解码、canvas绘制、Tensor转换堆在一起侧边栏一样会卡。优化手段按收益排序如下。第一把所有推理逻辑移到Web Worker里。TensorFlow.js支持在worker里执行输入输出通过postMessage传ArrayBuffer。侧边栏UI只负责渲染结果推理过程中的解码计算全部在后台线程完成。实测下来扫描四五十张图片时UI卡顿从肉眼可见降到基本无感。第二引入并发控制。我在后台实现了一个简单的任务队列同时处理的任务数固定为3。但并发数不是越高越好图片下载是IO密集型并发设太低浪费带宽设太高内存撑不住。经过反复测试3到4是均衡点。第三用IndexedDB缓存结果。第一次扫描某个页面后把URL、图片hash和推理结果缓存起来。下一次再打开同一个页面直接读缓存连网络请求都省了。缓存的key我用的是host pathname 图片URL失效策略是缓存一周后自动清理。5.2 还能怎么玩MobileNet只是这个架构里的第一个模型。顺着同一套离线推理管线后续可以接更多模型人脸检测模型给图片打上“含人脸”标签顺便把活体检测思路套进来通过纹理细节判断是人脸照片还是卡通人脸对设计师找真人素材很有帮助。物体检测模型比如EfficientDet Lite可以更进一步做图内多目标识别输出“一只猫一个沙发”这样的结构化标签。相似度模型算图片特征向量然后在本地做反向索引实现“按图搜图”——虽然不能和云端向量库比规模但在自己收藏的几千张图里搜相似图绰绰有余。OCR模型用TFLite加载离线OCR模型把图片里的文字提取出来对找截图素材、识别产品详情页非常有用。还有一个方向是结合浏览器的downloadAPI做批量下载器把筛选出的高清图生成一个zip包。不过浏览器扩展要打包多个文件并下载需要用JSZip之类的库文件数量多的时候内存压力不小我目前还在测试更好的流式压缩方案。另外提醒一点虽然这个工具的定位是辅助收集自己需要的素材但技术上它毕竟能批量获取网页里的图片原始资源使用的时候还是要注意版权边界。个人收藏和研究用途没问题拿去二次分发或商用最好先确认目标图库的授权协议。根据我个人实际使用的体会这个工具最有价值的场景其实是设计素材收集和前端页面分析。前端拿到一个精美的参考页面原来想看它的背景大图、Logo源文件、图标雪碧图要开DevTools一项项翻现在侧边栏一键扫描标签分类几秒钟就能把所有可用的高清资源理清楚。MobileNet那种“虽然不认识具体物体但能判断大致类别”的能力反而成了惊喜它给每一张素材自动打上的场景标签省掉了大量手动整理时间。最后再分享一个小技巧模型不要贪新求大。我试过用ResNet50跑同样的任务识别精度确实高一点但模型体积多出几百MB推理耗时增加了十倍在端侧场景里完全不划算。MobileNet V2 1.0版本在大多数图片分类场景和ResNet50的差距已经非常小选它做默认推理引擎足够让人安心了。如果哪天你觉得分类不准优先检查的应该是预处理环节而不是急着换更大的模型。