预热

让相机预热并达到热平衡状态可以提高应用的整体精度和成功率。本文将解释 Zivid 相机预热的原因和方法。

为什么需要预热

Zivid 相机内部含有主动电子元件(例如投影仪),在使用过程中会产生热量。内部升温会导致这些元件在温度变化时发生机械形变。

这些机械变化会导致基本光学组件发生微小的位移,从而在计算点云中点的位置时影响三角测量精度。由于每个内部组件可能处于许多未知的可能温度状态,因此相机的3D标定在加热或冷却阶段会不太准确。我们称这种现象为*预热漂移*。

建议控制相机温度并留出足够时间让所有组件达到稳定温度(热平衡)。因此,Zivid 3D标定可以识别并准确预测相机的物理状态并生成正确的点云。

备注

Zivid 相机使用 Thermal Stabilization(热稳定功能) 来保持稳定的温度,并减少预热的需要。

温度变化 - 相机 vs. 环境

预热测试表明,在最快的捕获周期(实时模式)下,Zivid 相机的内部温度可能会临时以每分钟 1.5 °C 的速度变化(10 分钟内升高 15 °C)。对于更典型的捕获周期,例如每 5 秒捕获一次,内部温度变化速度会慢两到三倍(每分钟 0.5 - 1 °C)。

另一方面,气温变化最快可达0.03 °C/min(例如6小时内增加10 °C)。这比预热期间相机中的临时温度变化慢两个数量级。

Zivid 相机采用了浮动温度标定技术,能够补偿环境温度的变化,从而在推荐的温度范围内确保性能稳定。经过充分预热并正确使用的相机,在整个规定的环境温度范围内均能保持稳定的性能表现。

预热

何时进行预热

建议在以下情况下预热相机:

  • 对误差要求很严格的应用,例如:

    • 对于小于5毫米/距相机1米距离的抓取应用。

    • 准确度误差要求 < 0.5% 的检测应用。

  • 达到中等捕获周期(快于每10秒捕获一次)的应用。

  • 在现场标定之前。

  • 在手眼标定之前。

  • 在运行生产之前。

  • 相机还未连接到电源时,例如安装相机后。

  • 当相机已开机但在过去20分钟内未捕获点云时,例如早上启动生产单元时。

如何进行预热

对相机进行预热时,请以固定的捕获周期连续捕获点云 10 分钟。

连接

第一步是连接相机。

跳转到源码

源码

Zivid::Application zivid;

std::cout << "Connecting to camera" << std::endl;
auto camera = zivid.connectCamera();
跳转到源码

源码

var zivid = new Zivid.NET.Application();

Console.WriteLine("Connecting to camera");
var camera = zivid.ConnectCamera();
跳转到源码

源码

app = zivid.Application()

print("Connecting to camera")
camera = app.connect_camera()

设定捕获周期和设置

在预热期间,捕获周期应尽可能与实际应用中的捕获周期相似(料箱拾取、拆垛等)。

如果部署的相机每5秒进行一次捕获,它应该在预热阶段以相同的间隔进行捕获。如果生产中的相机没有稳定的捕获周期,那么预热阶段的捕获周期应该在极值之间,例如,如果它在实际应用中每5到11秒捕获一次,则应该在预热时每8秒捕获一次。如果实际应用中捕获之间的间隔超过10秒,则无需预热相机。

预热期间使用的相机设置也应尽可能与实际应用中的设置相似(预先配置,从 YML 文件或捕获助手加载)

跳转到源码

源码

const auto warmupTime = std::chrono::minutes(10);
const auto captureCycle = std::chrono::seconds{ captureCycleSeconds };
const auto settings = loadOrDefaultSettings(settingsPath);
跳转到源码

源码

var warmupTime = TimeSpan.FromMinutes(10);
var captureCycle = opts.CaptureCycle.HasValue ? TimeSpan.FromSeconds(opts.CaptureCycle.Value) : TimeSpan.FromSeconds(5);
var settings = LoadOrDefaultSettings(opts.SettingsPath);
跳转到源码

源码

warmup_time = timedelta(minutes=10)
capture_cycle = timedelta(seconds=user_options.capture_cycle)
settings = _load_or_default_settings(user_options.settings_path)

加载或建议设置

跳转到源码

源码

Zivid::Settings loadOrDefaultSettings(const std::string &settingsPath)
{
    if(!settingsPath.empty())
    {
        std::cout << "Loading settings from file" << std::endl;
        return Zivid::Settings(settingsPath);
    }

    std::cout << "Using default 3D settings" << std::endl;
    auto settings = Zivid::Settings{ Zivid::Settings::Acquisitions{ Zivid::Settings::Acquisition{} } };

    return settings;
跳转到源码

源码

static Zivid.NET.Settings LoadOrDefaultSettings(string settingsPath)
{
    if (settingsPath != null)
    {
        Console.WriteLine("Loading settings from file");
        return new Zivid.NET.Settings(settingsPath);
    }

    Console.WriteLine("Using default 3D settings");
    var settings = new Zivid.NET.Settings
    {
        Acquisitions = { new Zivid.NET.Settings.Acquisition { } }
    };

    return settings;
}
跳转到源码

源码

def _load_or_default_settings(settings_path: str) -> zivid.Settings:
    """Load settings from YML file or use default settings.

    Args:
        settings_path: Path to the YML file that contains camera settings

    Returns:
        Camera Settings

    """

    if settings_path:
        print("Loading settings from file")
        return zivid.Settings.load(Path(settings_path))

    print("Using default 3D settings")
    settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()])

    return settings

备注

这种预热方法利用投影仪作为加热元件,在稳定的捕获周期中对相机进行预热。投影仪开启时间与整个周期时间的比值称为占空比(duty cycle)。占空比不仅影响相机升温的速度,也决定了预热阶段结束后相机最终达到的稳态温度。

预热相机

要预热相机,请循环捕获3D点云10分钟,直到温度稳定。

跳转到源码

源码

std::cout << "Starting warm up for: " << warmupTime.count() << " minutes" << std::endl;

const auto beforeWarmup = SteadyClock::now();

while(SteadyClock::now() - beforeWarmup < warmupTime)
{
    const auto beforeCapture = SteadyClock::now();

    // Use the same capture method as you would use in production
    // to get the most accurate results from warmup
    camera.capture3D(settings);

    const auto afterCapture = SteadyClock::now();
    const auto captureTime = afterCapture - beforeCapture;
    if(captureTime < captureCycle)
    {
        std::this_thread::sleep_for(captureCycle - captureTime);
    }
    else
    {
        std::cout << "Your capture time is longer than your desired capture cycle."
                  << "Please increase the desired capture cycle." << std::endl;
    }

    const auto remainingTime = warmupTime - (SteadyClock::now() - beforeWarmup);

    const auto remainingTimeMinutes = std::chrono::duration_cast<std::chrono::minutes>(remainingTime);
    const auto remainingTimeSeconds =
        std::chrono::duration_cast<std::chrono::seconds>(remainingTime - remainingTimeMinutes);
    std::cout << "Remaining time: " << remainingTimeMinutes.count() << " minutes, "
              << remainingTimeSeconds.count() << " seconds." << std::endl;
}
std::cout << "Warm up completed" << std::endl;
跳转到源码

源码

DateTime beforeWarmup = DateTime.Now;

Console.WriteLine("Starting warm up for: {0} minutes", warmupTime.Minutes);

while (DateTime.Now.Subtract(beforeWarmup) < warmupTime)
{
    var beforeCapture = DateTime.Now;

    // Use the same capture method as you would use in production
    // to get the most accurate results from warmup
    using (camera.Capture3D(settings)) { }

    var afterCapture = DateTime.Now;

    var captureTime = afterCapture.Subtract(beforeCapture);

    if (captureTime < captureCycle)
    {
        Thread.Sleep(captureCycle.Subtract(captureTime));
    }
    else
    {
        Console.WriteLine(
            "Your capture time is longer than your desired capture cycle. Please increase the desired capture cycle.");
    }
    var remainingTime = warmupTime.Subtract(DateTime.Now.Subtract(beforeWarmup));
    var remainingMinutes = Math.Floor(remainingTime.TotalMinutes);
    var remainingSeconds = Math.Floor(remainingTime.TotalSeconds) % 60;

    Console.WriteLine("Remaining time: {0} minutes, {1} seconds.", remainingMinutes, remainingSeconds);
}

Console.WriteLine("Warm up completed");
跳转到源码

源码

before_warmup = datetime.now()

print(f"Starting warmup for {warmup_time} minutes")
while (datetime.now() - before_warmup) < warmup_time:
    before_capture = datetime.now()

    # Use the same capture method as you would use in production
    # to get the most accurate results from warmup
    camera.capture_3d(settings)

    after_capture = datetime.now()

    duration = after_capture - before_capture

    if duration.seconds <= capture_cycle.seconds:
        sleep(capture_cycle.seconds - duration.seconds)
    else:
        print(
            "Your capture time is longer than your desired capture cycle. \
             Please increase the desired capture cycle."
        )

    remaining_time = warmup_time - (datetime.now() - before_warmup)
    remaining_time_minutes = remaining_time.seconds // 60
    remaining_time_seconds = remaining_time.seconds % 60
    print(f"Remaining time: {remaining_time_minutes} minutes, {remaining_time_seconds} seconds.")

print("Warmup completed")

有关实现的示例,请参阅下面的完整预热代码示例。

跳转到源码

源码

/*
Short example of a basic way to warm up the camera with specified time and capture cycle.
*/

#include <Zivid/Zivid.h>

#include <clipp.h>

#include <chrono>
#include <iostream>
#include <thread>

using SteadyClock = std::chrono::steady_clock;
using Duration = std::chrono::nanoseconds;

namespace
{
    Zivid::Settings loadOrDefaultSettings(const std::string &settingsPath)
    {
        if(!settingsPath.empty())
        {
            std::cout << "Loading settings from file" << std::endl;
            return Zivid::Settings(settingsPath);
        }

        std::cout << "Using default 3D settings" << std::endl;
        auto settings = Zivid::Settings{ Zivid::Settings::Acquisitions{ Zivid::Settings::Acquisition{} } };

        return settings;
    }
} // namespace

int main(int argc, char **argv)
{
    try
    {
        std::string settingsPath;
        size_t captureCycleSeconds = 5;

        auto settingsOption = clipp::option("--settings-path")
                              & clipp::value("Path to the YML file that contains camera settings", settingsPath);
        auto captureCycleOption =
            clipp::option("--capture-cycle") & clipp::value("Capture cycle in seconds", captureCycleSeconds);

        auto cli = (settingsOption, captureCycleOption);

        if(!parse(argc, argv, cli))
        {
            auto fmt = clipp::doc_formatting{}.alternatives_min_split_size(1).surround_labels("\"", "\"");
            std::cout << clipp::usage_lines(cli, "Warmup", fmt) << std::endl;
            throw std::runtime_error{ "Invalid usage" };
        }
        Zivid::Application zivid;

        std::cout << "Connecting to camera" << std::endl;
        auto camera = zivid.connectCamera();
        const auto warmupTime = std::chrono::minutes(10);
        const auto captureCycle = std::chrono::seconds{ captureCycleSeconds };
        const auto settings = loadOrDefaultSettings(settingsPath);

        std::cout << "Starting warm up for: " << warmupTime.count() << " minutes" << std::endl;

        const auto beforeWarmup = SteadyClock::now();

        while(SteadyClock::now() - beforeWarmup < warmupTime)
        {
            const auto beforeCapture = SteadyClock::now();

            // Use the same capture method as you would use in production
            // to get the most accurate results from warmup
            camera.capture3D(settings);

            const auto afterCapture = SteadyClock::now();
            const auto captureTime = afterCapture - beforeCapture;
            if(captureTime < captureCycle)
            {
                std::this_thread::sleep_for(captureCycle - captureTime);
            }
            else
            {
                std::cout << "Your capture time is longer than your desired capture cycle."
                          << "Please increase the desired capture cycle." << std::endl;
            }

            const auto remainingTime = warmupTime - (SteadyClock::now() - beforeWarmup);

            const auto remainingTimeMinutes = std::chrono::duration_cast<std::chrono::minutes>(remainingTime);
            const auto remainingTimeSeconds =
                std::chrono::duration_cast<std::chrono::seconds>(remainingTime - remainingTimeMinutes);
            std::cout << "Remaining time: " << remainingTimeMinutes.count() << " minutes, "
                      << remainingTimeSeconds.count() << " seconds." << std::endl;
        }
        std::cout << "Warm up completed" << std::endl;
    }
    catch(const std::exception &e)
    {
        std::cerr << "Error: " << Zivid::toString(e) << std::endl;
        std::cout << "Press enter to exit." << std::endl;
        std::cin.get();
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
跳转到源码

源码

        }
        catch (Exception ex)
        {
            Console.WriteLine("Error: " + ex.ToString());
            return 1;
        }
        return 0;
    }
    static Zivid.NET.Settings LoadOrDefaultSettings(string settingsPath)
    {
        if (settingsPath != null)
        {
            Console.WriteLine("Loading settings from file");
            return new Zivid.NET.Settings(settingsPath);
        }

        Console.WriteLine("Using default 3D settings");
        var settings = new Zivid.NET.Settings
        {
            Acquisitions = { new Zivid.NET.Settings.Acquisition { } }
        };

        return settings;
    }
/*
Short example of a basic way to warm up the camera with specified time and capture cycle.
*/

using CommandLine;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Duration = Zivid.NET.Duration;

class Program
{
    public class Options
    {
        [Option('s', "settings-path", Required = false, HelpText = "Path to the YML file that contains camera settings.")]
        public string SettingsPath { get; set; }

        [Option('c', "capture-cycle", Required = false, HelpText = "Capture cycle in seconds.")]
        public int? CaptureCycle { get; set; }
    }

    static int Main(string[] args)
    {
        return Parser.Default.ParseArguments<Options>(args)
        .MapResult(
            (Options opts) => RunWarmupWithOptionsAndReturnExitCode(opts),
            errs => 1);
    }

    static int RunWarmupWithOptionsAndReturnExitCode(Options opts)
    {
        try
        {
            var zivid = new Zivid.NET.Application();

            Console.WriteLine("Connecting to camera");
            var camera = zivid.ConnectCamera();
            var warmupTime = TimeSpan.FromMinutes(10);
            var captureCycle = opts.CaptureCycle.HasValue ? TimeSpan.FromSeconds(opts.CaptureCycle.Value) : TimeSpan.FromSeconds(5);
            var settings = LoadOrDefaultSettings(opts.SettingsPath);

            DateTime beforeWarmup = DateTime.Now;

            Console.WriteLine("Starting warm up for: {0} minutes", warmupTime.Minutes);

            while (DateTime.Now.Subtract(beforeWarmup) < warmupTime)
            {
                var beforeCapture = DateTime.Now;

                // Use the same capture method as you would use in production
                // to get the most accurate results from warmup
                using (camera.Capture3D(settings)) { }

                var afterCapture = DateTime.Now;

                var captureTime = afterCapture.Subtract(beforeCapture);

                if (captureTime < captureCycle)
                {
                    Thread.Sleep(captureCycle.Subtract(captureTime));
                }
                else
                {
                    Console.WriteLine(
                        "Your capture time is longer than your desired capture cycle. Please increase the desired capture cycle.");
                }
                var remainingTime = warmupTime.Subtract(DateTime.Now.Subtract(beforeWarmup));
                var remainingMinutes = Math.Floor(remainingTime.TotalMinutes);
                var remainingSeconds = Math.Floor(remainingTime.TotalSeconds) % 60;

                Console.WriteLine("Remaining time: {0} minutes, {1} seconds.", remainingMinutes, remainingSeconds);
            }

            Console.WriteLine("Warm up completed");

}
跳转到源码

源码

"""
Short example of a basic way to warm up the camera with specified time and capture cycle.

"""

import argparse
from datetime import datetime, timedelta
from pathlib import Path
from time import sleep

import zivid


def _options() -> argparse.Namespace:
    """Function to read user arguments.

    Returns:
        Arguments from user

    """
    parser = argparse.ArgumentParser(description=__doc__)

    parser.add_argument(
        "--settings-path",
        required=False,
        help="Path to the YML file that contains camera settings",
    )

    parser.add_argument(
        "--capture-cycle",
        required=False,
        type=float,
        default=5.0,
        help="Capture cycle in seconds",
    )

    return parser.parse_args()


def _load_or_default_settings(settings_path: str) -> zivid.Settings:
    """Load settings from YML file or use default settings.

    Args:
        settings_path: Path to the YML file that contains camera settings

    Returns:
        Camera Settings

    """

    if settings_path:
        print("Loading settings from file")
        return zivid.Settings.load(Path(settings_path))

    print("Using default 3D settings")
    settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()])

    return settings


def _main() -> None:
    user_options = _options()
    app = zivid.Application()

    print("Connecting to camera")
    camera = app.connect_camera()
    warmup_time = timedelta(minutes=10)
    capture_cycle = timedelta(seconds=user_options.capture_cycle)
    settings = _load_or_default_settings(user_options.settings_path)

    before_warmup = datetime.now()

    print(f"Starting warmup for {warmup_time} minutes")
    while (datetime.now() - before_warmup) < warmup_time:
        before_capture = datetime.now()

        # Use the same capture method as you would use in production
        # to get the most accurate results from warmup
        camera.capture_3d(settings)

        after_capture = datetime.now()

        duration = after_capture - before_capture

        if duration.seconds <= capture_cycle.seconds:
            sleep(capture_cycle.seconds - duration.seconds)
        else:
            print(
                "Your capture time is longer than your desired capture cycle. \
                 Please increase the desired capture cycle."
            )

        remaining_time = warmup_time - (datetime.now() - before_warmup)
        remaining_time_minutes = remaining_time.seconds // 60
        remaining_time_seconds = remaining_time.seconds % 60
        print(f"Remaining time: {remaining_time_minutes} minutes, {remaining_time_seconds} seconds.")

    print("Warmup completed")


if __name__ == "__main__":
    _main()

如需预热相机,您可以运行我们的代码示例,同时需要提供循环时间和相机参数设置文件的路径。

示例: warmup.py

python /path/to/warmup.py --settings-path /path/to/settings.yml --capture-cycle 6.0