13.1.13. 设置编辑器#

许多相机应用需要一些非程序员也能更改的设置 -- 例如阈值、模式或 Wi-Fi 密码 -- 而无需改动脚本。OpenMV Cam Settings Editor 就是 IDE 为此提供的工具:一个 JSON 文件描述这些控件,编辑器将该描述转换成表单,你的脚本再用 json.load() 读回保存的值。该文件既是配置,也是其自身编辑器的定义,因此存储这些值的同一个配置文件也定义了编辑它们的 GUI 布局。

工具 → OpenMV Cam Settings Editor 子菜单包含其三个操作。

  • Open OpenMV Cam Settings Config File 会从已连接相机的驱动器读取配置并在编辑器中打开;保存时会将其写回相机。

  • Open Config File 对你计算机上的 .json 文件执行相同操作,用于离线准备配置。

  • Create Default Config 会将一个起始配置写入磁盘并将其打开 -- 这个示例展示了每一种控件类型,可随时精简为你自己的应用所需的设置。

The OpenMV Cam Settings Editor showing the default demo configuration -- Camera, Processing, Network, and System tabs, with the Camera tab's exposure and gain sliders, resolution and pixel-format dropdowns, mirror and flip checkboxes, and a digital-zoom slider, above the Save and Cancel buttons

打开默认配置的设置编辑器 -- JSON 文件的各项控件以表单形式布局(选项卡、滑块、下拉菜单、复选框),底部为 Save 和 Cancel。#

13.1.13.1. 配置文件剖析#

配置文件是一个包含两个键的单一 JSON 对象:

  • title(字符串,可选)-- 编辑器窗口的标题。默认为 OpenMV Cam Settings Editor

  • controls(数组,必需)-- 编辑器按顺序渲染为表单各行的控件对象列表。这就是整个 GUI。

{
  "title": "Camera Settings",
  "controls": [
    { "type": "slider", "name": "threshold", "label": "Threshold", "value": 50 },
    { "type": "checkbox", "name": "draw_overlays", "label": "Draw Overlays", "value": true }
  ]
}

controls 中的每一项都是一个控件 -- 文本字段、数字、复选框、下拉菜单、滑块、group、选项卡条、静态 label 等等。其 type 决定使用哪种控件;对象的其余部分对其进行配置。编辑器无法识别的任何键在保存文件时都会原样保留,因此你可以在文件中与控件一起保存你自己的元数据。

13.1.13.2. 每个控件共有的键#

无论 type 是什么,每个控件对象都接受以下这些键:

  • type*(字符串,必需)-- 要构建哪种控件,例如 ``"slider"`` 或 ``"combobox"``。缺失或无法识别的 *type 会渲染为一个禁用的 Unknown control 占位符,而不会使整个文件失败,因此一处拼写错误只会损失一个控件,而不是整个表单。

  • name*(字符串)-- 供 *你 自己使用的标识符。编辑器从不读取它,只会原样保留。为每个控件指定一个唯一的 name,你的脚本就能用它来查找该控件所保存的值。

  • label(字符串)-- 显示在控件旁边或控件上的文本。对大多数控件而言这是富文本 -- HTML 标签和 <a href> 链接都有效。

  • tooltip(字符串)-- 控件的悬停提示文本。

  • enabled(布尔值,默认 true)-- 设为 false 可将控件显示为灰色且只读。

容器控件(grouptabs)的标题使用 title 而非 label,而 tabs 的提示文本是按每个选项卡分别设置的;这些差异在各控件的页面中均有说明。

13.1.13.3. 保存与读取值#

保存值的控件 -- 复选框、滑块、文本字段 -- 会将该值保存在 value 键中。当你点击保存时,编辑器会将每个控件当前的 value 就地写回该控件对象本身,并重写文件。文件保持其结构:没有任何内容移动,只有 value 键发生变化。不存在从 name 到值的单独扁平映射 -- 值就存在于控件内部,就在它们被定义的地方。

这就是为什么布局对你的脚本很重要。controlstabs 数组保持你构建它们时的顺序,因此无法像字典那样按名称对文件进行索引 -- config["controls"]["threshold"] 是行不通的。这个小型辅助函数弥补了这一差距:向它提供直达你所需值的名称路径,它就会为你遍历整棵树。

import json


def find(node, *path):
    """Return the control at a path of names; read its value with
    ["value"]. The tab strip and any unnamed group are transparent,
    so you list only the tabs and controls you gave a name."""
    for key in path:
        match = _child(node, key)
        if match is None:
            raise KeyError(key)
        node = match
    return node


def _child(node, key):
    for member in node.get("controls", []) + node.get("tabs", []):
        if member.get("name") == key or member.get("title") == key:
            return member
        if "name" not in member and ("controls" in member or "tabs" in member):
            match = _child(member, key)   # see through an unnamed group / the tab strip
            if match is not None:
                return match
    return None


with open("config.json") as file:
    config = json.load(file)

threshold = find(config, "threshold")["value"]

路径中的每一步都是控件的 name,或选项卡的 title*(选项卡本身没有 *name,不过你可以添加一个,且会被保留)。选项卡条以及任何你未命名的 group 都是透明的 -- find 会穿过它们 -- 因此你只需列出选项卡和你已命名的控件,其形式接近于嵌套的 config[...][...] 查找。已命名的 group 是独立的一步,因此 find 也可以返回其复选框的值。为每个控件指定一个唯一的 name,这样路径就永远不会有歧义。完整示例 演示了从嵌套的示例布局中读取值。

由于配置是纯 JSON,你的脚本不依赖于编辑器的任何部分 -- 编辑器只是写入你的脚本本就会读取的文件。设备既可以携带其配置,又可以拥有一种友好的修改方式,而脚本始终只是一个单纯的值读取者。

有一种控件类型会在保存时强制校验其输入:带有 maskregexlineedit 在其文本无效或不完整时会拒绝编辑器保存,并指出需要修正的字段。其他所有控件都在你编辑时约束输入 -- 滑块无法超出其范围,spinbox 会被限制在其边界内 -- 因此保存的文件始终是有效的。

13.1.13.4. 配置文件参考#

各控件类型的说明见下面的页面,按其功能分组。