☰
TensorFlow.js 交互式模型可视化器实战:tfjs-examples 中 interactive-visualizers 的元数据驱动机制与源码解析
2026/10/15 18:55:09 网站建设 项目流程
  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

项目地址:https://gitcode.com/gh_mirrors/tf/tfjs-examples
点击查看免费下载

本篇技术指南聚焦 tfjs-examples 仓库中的interactive-visualizers项目,讲解这套基于 TensorFlow.js 的、可嵌入任意网页的交互式模型可视化器:它如何通过一个「元数据 JSON」文件在运行时描述模型任务类型,进而驱动图像分类、目标检测、图像分割三类感知任务的界面与推理流程。读完本文,你将掌握该可视化器的 URL 查询参数协议、三类模型元数据的 JSON 结构约定、本地开发/构建/测试的完整命令,以及从源码层面理解输入预处理、推理执行、结果后处理与画布叠加渲染的完整调用链。

项目定位:为感知任务提供可嵌入的交互式可视化

interactive-visualizers是 TensorFlow.js 官方示例仓库中的一个 Angular 应用,其定位是:为标准的计算机视觉感知任务提供一套可嵌入到任何网页中的交互式模型可视化器。根据 interactive-visualizers/README.md 的说明,它当前支持三类任务:

  • 图像分类(Image Classification)
  • 目标检测(Object Detection)
  • 图像分割(Image Segmentation)

在仓库根目录的 README.md 示例列表中,该项目同样被标注为 "Image / Multiclass classification, object detection, segmentation / Browser" 类示例,与文档描述一致。

它的核心设计哲学是「一次编写、处处复用」:可视化器本身不绑定任何具体模型,而是在运行时通过 URL 查询参数接收一个模型元数据(metadata JSON)文件的地址,随后依据该元数据中声明的任务类型加载对应的 UI 并执行推理。这意味着同一个可视化器前端可以服务于任何遵守元数据约定的模型。

注意:README 明确指出,这套元数据标准目前仍处于 WIP(Work in progress)状态,是一个不断演进中的约定。文档中给出的示例是一个 AIY 植物分类模型(plants_V1)的metadata.json,托管于 Google 的 tfhub-visualizers 存储桶中。

核心机制:通过 URL 查询参数在运行时注入模型

可视化器与模型之间的唯一耦合点就是一条 URL。README 给出了如下调用形式:

https://visualizerHostedUrl/?modelMetadataUrl=https://myModelMetadataUrl/metadata.json

即:把托管可视化器的地址,加上modelMetadataUrl查询参数(值为模型元数据文件的完整 URL)即可。可视化器启动后会先拉取该元数据,解析其中的任务声明,然后从元数据文件所在的同一目录加载model.json权重文件,构建相应的推理与展示界面。

从源码看,这一流程由 interactive-visualizers/src/app/app.component.ts 中的ngOnInit()完成:

  1. 通过new URLSearchParams(window.location.search)解析全部查询参数;
  2. 若存在tfliteModelUrl(且必须同时提供tfWebApi),则进入 TFLite 模型模式,否则抛出NO_TFWEB_API_ERROR_MESSAGE("No TFWeb API selected. The visualizer could not start");
  3. 若存在modelMetadataUrl,则进入 TF.js GraphModel 模式,调用initAppWithTfjsModel();
  4. 若两者都不存在,直接抛出NO_MODEL_ERROR_MESSAGE("No model URL provided. The visualizer could not start")。

在 TF.js 模式下,initAppWithTfjsModel()会依次执行:

  • fetch(modelMetadataUrl)拉取并解析元数据 JSON;
  • 通过getAssetsUrlPrefix()推导出模型文件目录(取元数据 URL 中去掉文件名后的部分),再拼接model.json,调用tf.loadGraphModel()加载 GraphModel;
  • 根据元数据中携带的任务字段(tfjs_classifier_model_metadata/tfjs_detector_model_metadata/tfjs_segmenter_model_metadata)设置对应的tfWebApiName;
  • 最后调用fetchTestImages()拉取可选测试图片。

这套「同一目录放置metadata.json+model.json」的约定,意味着模型发布方只需要把两类文件放在一个静态目录下,可视化器就能自动发现并加载模型。

元数据 JSON 标准详解:三类任务的声明式描述

虽然元数据标准仍属 WIP,但从源码中可以直接还原出当前版本实际使用的字段结构。这三类任务的元数据分别以如下顶层键声明,且三者互斥——一次只能声明一个任务类型:

任务类型元数据顶层键触发可视化器行为
图像分类tfjs_classifier_model_metadata进入分类器模式(image_classifier)
目标检测tfjs_detector_model_metadata进入检测器模式(object_detector)
图像分割tfjs_segmenter_model_metadata进入分割器模式(image_segmenter)

每一类元数据都包含input_tensor_metadata与output_head_metadata两个部分,其字段在推理管线中被逐一读取:

输入描述(input_tensor_metadata)

  • shape:模型期望的输入张量形状(如[1, 224, 224, 3])。prepareImageInput()会读取shape[1]与shape[2]作为目标宽高,调用tf.image.resizeBilinear()将任意尺寸的输入图片缩放到该尺寸。

输出描述(output_head_metadata[0])——不同任务字段不同:

  • 分类器:
    • score_threshold:结果置信度阈值(可空,缺省视为0.0),低于该值的类别不会被展示;
    • labelmap_path:标签映射文件的相对路径(基于模型文件所在目录)。
  • 检测器:
    • num_detections_tensor_name:检测数量输出张量名;
    • detection_boxes_tensor_name:检测框坐标输出张量名;
    • detection_scores_tensor_name:检测置信度输出张量名;
    • detection_classes_tensor_name:检测类别输出张量名;
    • score_threshold:数组形式,取score_threshold[0]作为默认检测阈值(缺省回退为0.2,对应源码常量DEFAULT_DETECTION_THRESHOLD);
    • labelmap_path:标签映射文件路径。
  • 分割器:
    • semantic_predictions_tensor_name:语义分割预测张量名;
    • labelmap_path:标签映射文件路径。

标签映射(labelmap)文件约定

分类与分割任务依赖labelmap.json将输出索引映射为可读名称。fetchLabelmap()的解析逻辑如下:

  • 文件结构为{"item": [...]}数组,每项可含id、name、display_name字段;
  • 若提供了id,则按id定位标签;否则按数组下标定位;
  • 显示名优先取display_name,其次取name;
  • 未被任何项覆盖的索引统一填充为'unknown'(源码常量UNKNOWN_LABEL_DISPLAY_NAME)。

该行为由 interactive-visualizers/src/app/app.component.spec.ts 中的fetchLabelmap should correctly parse the labelmap JSON用例验证:对于[{id:2, name:'name2'}, {id:1, display_name:'displayName1'}],最终得到['unknown', 'displayName1', 'name2']。

两种模型运行时:TF.js GraphModel 与 TFLite

可视化器内部用modelFormat字段区分两类模型来源(见 app.component.ts):

TF.js GraphModel(modelFormat = 'tfjs')

  • 通过tf.loadGraphModel()加载与元数据同目录的model.json;
  • 推理使用model.executeAsync()执行,可传入单个输出张量名或输出张量名数组(检测器同时取 4 个输出);
  • 输入图片统一经prepareImageInput()转为[0, 1]区间的 float 张量:tf.browser.fromPixels→resizeBilinear→toFloat().div(255).expandDims(0)。

TFLite(modelFormat = 'tflite')

  • 通过tfliteModelUrl查询参数指定.tflite模型地址,且必须同时提供tfWebApi查询参数选择 TFWeb 运行时 API;
  • interactive-visualizers/src/environments/environment.ts 中定义了三个合法的 API 名:image_classifier、image_segmenter、object_detector,分别对应ImageClassifierViz、ImageSegmenterViz、ObjectDetectorViz三个全局类;
  • 这三个全局类来自 interactive-visualizers/src/index.html 中引入的tfweb/viz_bin-bundle.js脚本(一个由 Google 静态托管的 TFWeb 打包脚本),而 TFWeb 的 WASM 资源前缀tfWebWasmFilesPrefix同样配置在环境文件中;
  • 初始化时调用this.tfWebApi.init(prefix + apiName + '/', tfliteModelUrl)加载模型,随后用一张内嵌 base64 小图执行warmUpModel()预热,确保首次真实推理不被编译开销拖累。

两种模式下推理结果的读取方式也不同:TFLite 模式从 TFWeb 返回的 proto 对象(如getClassificationsList()、getDetectionsList()、getSegmentationList())中提取结果;TF.js 模式则从executeAsync()返回的tf.Tensor中取出数组。但两者最终都会统一收敛到相同的内部结果结构(displayName+score/box等),供 UI 层复用。

URL 查询参数完整参考

综合 app.component.ts 的ngOnInit()与后续方法,可视化器当前识别的全部查询参数如下:

参数名用途是否必填
modelMetadataUrlTF.js 模式:指向metadata.json的完整 URL,同目录需有model.json二选一
tfliteModelUrlTFLite 模式:.tflite模型文件完整 URL(需 URL 编码)二选一
tfWebApiTFLite 模式必填,取值image_classifier/image_segmenter/object_detectorTFLite 模式下必填
testImagesIndexUrl可选:指向测试图片索引 JSON(形如["a.jpg", "b.jpg"])的 URL;可视化器会据此生成图片名_thumb.扩展名的缩略图并自动对第一张图执行推理可选
modelDisplayName可选:界面头部展示的模型名称,缺省为 "Interactive model visualizer"可选
publisherName可选:发布者名称,展示在左下角可选
publisherThumbnailUrl可选:发布者缩略图 URL可选

值得注意的一个健壮性细节:testImageSelected()在读取测试图片 blob 时,若 MIME 类型为未知的application/octet-stream,会将其替换为image/jpg再作为 data URL 送入推理管线,避免部分静态服务器对图片类型标注不准确导致的加载失败。

输入方式:测试图片、拖拽上传与多选文件

可视化器的左侧面板提供三种图片输入途径(见 app.component.html 中的inputSection与TestImages区块):

  1. 测试图片:若 URL 中提供了testImagesIndexUrl,界面会渲染一套缩略图列表,点击即触发testImageSelected()执行推理;
  2. 拖拽上传:整个查询区域监听dragover/dragleave/drop事件,dragDrop()会遍历dataTransfer.files中所有图片文件逐一推理;
  3. 文件选择:隐藏的<input type="file" multiple>配合标签触发imageFilesSelected(),同样支持一次选择多张图片批量推理。

图片读取统一走FileReader.readAsDataURL(),上传的图片会追加到uploadedImages列表并与测试图片共享同一套选中/推理逻辑。源码在handleInputImage()中根据tfWebApiName分发到runImageClassifier()、runObjectDetector()或runImageSegmenter()三条独立管线。

三类任务推理管线源码解析

图像分类:软max 置信度 + 阈值过滤 + Top-100 截断

runImageClassifier()的 TF.js 分支执行流程:

  1. 从tfjs_classifier_model_metadata.input_tensor_metadata取输入形状,prepareImageInput()预处理;
  2. model.executeAsync(imageTensor)得到原始输出,记录resultsLatency(毫秒级推理耗时);
  3. 对输出执行squeeze()并转成数组,逐项与score_threshold比较,高于阈值的结果按 labelmap 映射为displayName;
  4. 结果按score降序排序,并截断为最多MAX_NB_RESULTS(100)条,供 UI 渲染带百分比进度条的列表。

由于推理是异步的,代码还做了一个竞态保护:仅当this.imageSelectedIndex === index(当前选中的仍是这张图)时才更新classifierResults,避免用户在推理期间切换图片导致结果错位。

目标检测:四输出张量 + 归一化框 + 阈值滑块

runObjectDetector()的 TF.js 分支一次性请求 4 个输出张量:

executeAsync(imageTensor, [ num_detections_tensor_name, detection_boxes_tensor_name, detection_scores_tensor_name, detection_classes_tensor_name ])

随后从num_detections得到检测数量,遍历每条检测记录,将detectionBoxes[i]([top, left, bottom, right]归一化坐标)、detectionScores[i]、detectionClasses[i]组装为统一结果对象,并通过 labelmap 映射出displayName。检测阈值可从元数据的score_threshold[0]读取(缺省 0.2),且界面提供 0–1 区间的滑块(detectionScoreThresholdChanged()将滑块 0–100 值除以 100)让用户实时过滤结果。

结果绘制由displayRectangles()完成:先在画布上以globalCompositeOperation = 'destination-out'抠出框内区域(配合半透明遮罩形成"高亮窗口"效果),再以虚线(setLineDash([3, 3]))按类别颜色绘制边框,类别颜色取自源码内置的 Pascal VOC 256 色板(COLOR_LIST,注释引用了 Pascal VOC 论文)。悬停某个类别或某个检测框时,fillDetectionCanvas()会只高亮对应的框。

图像分割:逐像素预测 + 类别面积占比 + 悬停高亮

runImageSegmenter()的 TF.js 分支通过semantic_predictions_tensor_name获取逐像素类别矩阵,然后:

  1. 若未提供 labelmap,则扫描预测矩阵的最大类别索引,动态生成Label 0、Label 1… 形式的占位标签;
  2. 统计每个类别出现的像素频率,换算为「占图片面积百分比」(frequencyPercent),过滤掉占比为 0 的类别并按占比降序排列;
  3. 将预测矩阵绘制到覆盖在查询图片上的<canvas>(fillSegmentationCanvas()):非悬停类别像素置为全透明,悬停类别(hoveredSegmentationLabel)以 150 号透明度叠加类别颜色;
  4. 画布监听mousemove/mouseout,按鼠标坐标换算到预测矩阵的宽高网格,实时更新悬停类别。

分割结果列表中的每一项还带颜色标记与面积占比进度条,mouseenter/mouseleave事件会联动画布高亮(segmenterResultHovered()/segmenterResultLeft())。

内存管理

整个推理管线严格遵循 TensorFlow.js 最佳实践:输入张量在推理完成后立即tf.dispose(),输出张量经squeeze()转数组后逐层释放,预处理阶段使用tf.tidy()自动回收中间张量,避免 WebGL 内存泄漏——这对需要连续推理多张图片的可视化器尤为重要。

本地开发、构建与测试

该项目由 Angular CLI 生成,README 标注的生成版本为 10.1.3;当前仓库的 package.json 中实际锁定的依赖已升级到 Angular 11.2.x、TypeScript ~4.0.2,并依赖@tensorflow/tfjs ^2.7.0。以下命令均在interactive-visualizers/目录内执行:

命令作用
npm install(或使用仓库已有的yarn.lock执行yarn)安装依赖
npm start(等价于ng serve)启动开发服务器,访问http://localhost:4200/,修改源码自动热重载
npm run build(等价于ng build)构建应用,产物输出到dist/目录
npm run prod-build(等价于ng build --prod)生产构建:开启代码优化、输出哈希、移除 sourceMap,并启用 AOT 编译
npm run playground-prod-build生产构建 Playground 子应用(输出到dist/playground)
npm test(等价于ng test)运行单元测试;Karma 提供测试环境,Jasmine 负责用例编写
npm run lint(等价于ng lint)检查 lint 警告/错误
npm run e2e(等价于ng e2e)运行 Protractor 端到端测试

在本地调试时,由于可视化器需要从 URL 注入模型地址,你可以直接在浏览器地址栏构造带参数的 URL,例如将?modelMetadataUrl=或?tfliteModelUrl=&tfWebApi=追加到http://localhost:4200/之后。

构建配置详见 interactive-visualizers/angular.json:工作区包含interactive-visualizers(默认项目)与playground两个应用;生产构建对包体设置了预算约束(初始包 2MB 警告、5MB 报错,单组件样式 6KB 警告、10KB 报错)。生产环境通过fileReplacements将environment.ts替换为 environment.prod.ts。

测试架构与覆盖面

单元测试的运行时配置在 interactive-visualizers/karma.conf.js:使用 Jasmine + ChromeHeadless 无头浏览器,singleRun: false、autoWatch: true,并接入karma-coverage-istanbul-reporter生成代码覆盖率报告。

app.component.spec.ts 是核心测试文件,它通过fetch-mock拦截网络请求、用自定义tf.io.IOHandler注入简易 GraphModel(SIMPLE_MODEL),覆盖了以下关键行为:

  • 无modelMetadataUrl参数时ngOnInit应抛出错误;
  • 元数据解析与测试图片索引加载(含_thumb.缩略图 URL 拼接约定);
  • 图片文件读取与上传列表维护;
  • labelmap JSON 解析(含id/name/display_name优先级);
  • 分类器、检测器、分割器三条推理管线对模型输出的解析与结果排序/过滤逻辑(如分类结果按分数降序、检测结果按score_threshold过滤、分割类别面积占比计算)。

端到端测试(e2e/src/app.e2e-spec.ts)则验证应用能在浏览器中正常渲染标题且控制台无 SEVERE 级日志。

Playground:一个即选即用的模型预览门户

除可视化器本体外,仓库还包含一个playground子应用(源码在 interactive-visualizers/projects/playground),其角色是一个「模型预览门户」:左侧列出预置模型清单,点击后通过DomSanitizer生成经过安全校验的 iframe URL,把可视化器以?modelMetadataUrl=...的形式嵌入右侧面板。

预置模型在 projects/playground/src/environments/environment.ts 中定义,包括:AIY 鸟类分类模型(Birds V1)、昆虫分类模型(Insects V1,均为图像分类)和 Mobile Object Localizer V1(目标检测)。这些模型的metadataUrl均指向 Google 托管的 tfhub-visualizers 模型目录,展示了「同一可视化器 + 不同元数据 → 不同任务 UI」的复用效果。生产环境中 Playground 通过interactiveVisualizerUrl: '../index.html'引用同目录下的可视化器构建产物。

部署方面,scripts/upload_gcp.sh 提供了发布脚本:读取package.json的版本号,通过gsutil -m cp将dist/interactive-visualizers/*与dist/playground/*上传到gs://interactive_visualizer/<version>存储桶,实现版本化的托管发布。

如何接入你自己的模型

综合以上机制,将一个 TF.js GraphModel 接入该可视化器的完整步骤为:

  1. 导出模型:用 TensorFlow.js 工具链(或 tfjs-converter)将模型导出为 GraphModel 格式,得到model.json与权重文件;
  2. 编写元数据:在与model.json相同的目录下创建metadata.json,按任务类型填入tfjs_classifier_model_metadata、tfjs_detector_model_metadata或tfjs_segmenter_model_metadata,其中包含input_tensor_metadata.shape、output_head_metadata所需的张量名/阈值/labelmap 路径等字段;
  3. 提供 labelmap(可选):分类/分割任务可另附labelmap.json(结构为{"item": [{id, name, display_name}]});
  4. 测试图片(可选):准备index.json图片清单,通过testImagesIndexUrl参数注入,即可在界面中直接点选验证;
  5. 托管与调用:将模型目录部署到任意静态服务器,然后访问可视化器地址/?modelMetadataUrl=https://你的模型目录/metadata.json即可。

对于 TFLite 模型,则需同时提供tfliteModelUrl(URL 编码)与合法的tfWebApi参数(image_classifier/image_segmenter/object_detector),并确保 TFWeb 运行时(viz_bin-bundle.js)与 WASM 资源可访问。

小结

interactive-visualizers演示了 TensorFlow.js 生态中一种高复用性的模型展示方案:以元数据为契约、以 URL 为接口,将「模型推理」与「可视化 UI」彻底解耦。无论你的模型是分类、检测还是分割,只要产出符合约定的元数据文件,即可获得一套包含图片上传、测试集浏览、置信度/占比列表、画布叠加渲染、延迟统计与 Embed 分享功能的完整交互界面。对希望快速向社区或团队展示 TensorFlow.js 模型效果、或构建统一模型评测门户的开发者而言,这套设计与源码实现都具有直接的参考价值。

  • 示例工程

【免费下载链接】tfjs-examples

Examples built with TensorFlow.js

项目地址:https://gitcode.com/gh_mirrors/tf/tfjs-examples
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询