openpnp - SlotSchultzFeeder source code bugfix

news2024/11/18 9:20:11

文章目录

    • openpnp - SlotSchultzFeeder source code bugfix
    • 概述
    • 笔记
    • openpnp源码调试环境
    • 排查思路
    • 开git分支
    • 查到的问题 - 1
    • 查到的问题 - 2
    • 查到的问题 - 3
    • 针对以上问题进行的逻辑修正
    • D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\wizards\GcodeDriverConsole.java
    • D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\GcodeDriver.java
    • D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\GcodeAsyncDriver.java
    • D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\feeder\wizards\SlotSchultzFeederConfigurationWizard.java
    • D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\GcodeDriver.java
    • D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\Main.java
    • 备注
    • END

openpnp - SlotSchultzFeeder source code bugfix

概述

我的openpnp设备接入的飞达是西门子二手飞达, 用openpnp提供的SlotSchultzFeeder.

发现原版openpnp有个问题(bug):
接入多个西门子飞达时, 因为要调整飞达参数(或仅仅就想确认一下参数), 切换到不同飞达时, 大概率会弹框报错.
报错的项目有多种(最多4种: 飞达ID取不到, 飞达送料数取不到, 步长取不到, 飞达状态取不到).

如果不使用openpnp, 而使用串口助手, 怎么发指令给mage2560控制底板, 飞达都可以正常控制, 回包都正常.

其实这个问题早发现了, 因为当时没到要大规模使用西门子二手飞达这步, 只要能通讯, 就说明飞达正常. 也没想去解决.
现在要将西门子二手飞达都挂上了, 要是时不时弹框报错, 那手工干预不起啊(这种骚扰顶不住, 这不成了被openpnp和飞达设备耍了么?)
现在必须要解决这个问题.

开始怀疑是mega2560控制板通讯处理有问题, 查了一下. 确实有问题.
但是不是编程逻辑的问题, 而是arduino库的问题.
mega2560的arduino库, 无法做到能快速正确的处理多个连续(100ms之内)的串口命令.
这可能也不是arduino库的问题, 随便哪个MCU, 串口发送的包间隔速度小于MCU的处理单条串口命令的总时间, 都不能保证每一个命令都处理正确.

观察了一下openpnp的日志, 发现openpnp会在同一时间(10ms~20ms之内), 连续发送2条串口命令给飞达控制板. 这哪个MCU也扛不住啊.
用串口助手试了一下, 循环发送命令, 只要发送间隔>200ms, 没有一次会让飞达控制板出错. 这就对了, 不能狂发命令给MCU啊.

这就需要改openpnp源码了, 将在同一时间连续不停的发送2条命令的地方找到, 简单处理一下, 在每条命令发送之前, 都至少需要间隔250ms以上才行. 间隔就用java自带的sleep处理一下.

对java不熟, 不想没事找事. 这次被openpnp逼的实在没招了. 非得自己动手才有吃的.
还好是维护性质的, 查引起bug的原因, 改点逻辑, 这个咱能搞.

笔记

openpnp源码调试环境

这个环境早就做了实验+笔记(openpnp - 软件调试环境搭建), 在本地的环境还没动, 可以直接照着笔记实验, 开心. 真机智, 早就预料到了会有改openpnp源码那一天.

排查思路

根据日志和报错提示, 在工程中用字符串搜索大法 + 断点法. 让报错时, 停在断点上, 那就说明找对了地方.
用的IDE是在这里插入图片描述, 挺好用的.

开git分支

我现在用的openpnp发布版本是 openpnp-dev-2022-0801
openpnp dev 代码的git url 为 : https://github.com/openpnp/openpnp.git, 先迁出到本地.
最新的代码日期为2023/3/15
openpnp-dev-2022-0801 对应的安装包为 OpenPnP-windows-x64-develop_2022-08-01_18-07-09.2a36a8d.exe
在git记录中查到2022-8-1上午, 是2022-8-1最后的实现代码. 在2022-8-1最后提交的代码处做了本地分支, 命名为openpnp_dev_2022_0801
在这个分支上做自己的修改.
在这里插入图片描述

查到的问题 - 1

openpnp的sendcommand函数, 参数只有回包超时时间, 而没有发送前需要sleep的时间. 这就导致发送者只要发送命令, 立刻就会被执行. 如果连续执行多条命令, 就会引起下位机处理不过来, 上位机也会处理错(因为接收是异步处理, 回包格式也不能区分出是哪个包, 就会错将不是自己的命令回包, 当作自己的, 这样从回包中取到的值的目标就错了, e.g. 飞达参数填错了位置).

查到的问题 - 2

确有连续狂发命令的地方.
在切换飞达条目时, 飞达界面上的4个数据(飞达ID, 飞达送料数, 步长, 飞达状态), 都不是存起来显示的, 而是切到新飞达条目, 就立刻取数据.
这就导致在飞达列表中切换到不同飞达条码时, 大概率会是mega2560处理不过来, 导致没回包, 或回包出错(e.g. 连发送的命令都没识别完整, 估计是被覆盖了, 因为缓冲区就64个字节, 有4个命令输入的缓冲区, 其实就一个缓冲区就够了, 主要是上位机发送的命令间隔太短了, 超出了下位机的通讯处理速度(下位机mage2560要将上位机命令转换为西门子飞达的实际通讯指令, 等飞达回包, 再转换为上位机能理解的回包, 这些都需要时间啊), 总是会出错, 这是跑不了的, 只是概率大小问题.

查到的问题 - 3

使用不同家做的冰沙主板时, 明明主板是好的, 串口也没问题, 但是有时要2~3次或者更多次才能和主板连接上. 最坏的情况是一直连不上主板(遇到过, 什么时候能连接上主板通讯, 都是靠运气)
准备给冰沙主板发连接命令时, 也要sleep之后, 才发给主板.

针对以上问题进行的逻辑修正

就按照git提交记录来, 不分先后.

D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\wizards\GcodeDriverConsole.java

driver.sendCommand(cmd, 5000, 300); // 参数3原来是没有的, 现在为发送前的sleep时间

D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\GcodeDriver.java

sendGcode(command, timeout, 0); // 如果确认这里只发一条, 且时机前后不会再发送其他串口指令, 发送前sleep时间就可以设置为0, 这个需要改完来验证.
   // 相关发送命令的函数都加上发送前sleep的参数
    protected void sendGcode(String gCode, long timeout, long time_sleep_before_send) throws Exception {
        if (gCode == null) {
            return;
        }
        for (String command : gCode.split("\n")) {
            command = command.trim();
            if (command.length() == 0) {
                continue;
            }
            sendCommand(command, timeout, time_sleep_before_send);
        }
    }

    public void sendCommand(String command) throws Exception {
        sendCommand(command, timeoutMilliseconds, 0);
    }

    public void sendCommand(String command, long timeout, long time_sleep_before_send) throws Exception {
        // An error may have popped up in the meantime. Check and bail on it, before sending the next command. 
        bailOnError();
        if (command == null) {
            return;
        }

        Logger.debug("[{}] >> {}, {}, {}", getCommunications().getConnectionName(), command, timeout, time_sleep_before_send);

		// 发送前sleep的实现, 就用Thread.sleep简单处理一下.
        if (time_sleep_before_send > 0)
        {
            Thread.sleep(time_sleep_before_send);
        }

        command = preProcessCommand(command);

D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\GcodeAsyncDriver.java

    @Override
    public void sendCommand(String command, long timeout, long time_sleep_before_send) throws Exception {
        if (waitedForCommands) {
            // We had a wait for commands and caller had the last chance to receive responses.
            waitedForCommands = false;
            // If the caller did not get them, clear them now.
            responseQueue.clear();
        }
        bailOnError();
        if (command == null) {
            return;
        }

        Logger.debug("{} commandQueue.offer({}, {})...", getCommunications().getConnectionName(), command, timeout);
        if (time_sleep_before_send > 0)
        {
            Thread.sleep(time_sleep_before_send);
        }

D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\feeder\wizards\SlotSchultzFeederConfigurationWizard.java

这个实现中, 有连发4条命令的地方, 封装一个私有函数, 用来ms延时

    private void my_delay_ms(long ms)
    {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException e)
        {
            // nothing, only catch
            // java: unreported exception java.lang.InterruptedException; must be caught or declared to be thrown
        }
    }


public SlotSchultzFeederConfigurationWizard(SlotSchultzFeeder feeder) {
// 这里是在飞达列表中切换会来的函数, 主要是填界面参数, 如果是在飞达设备中的参数, 从飞达中取出来, 再填入界面.
// ...
        statusText = new JTextField();
        statusText.setColumns(50);
        panelActuator.add(statusText, "8, 20");

        if(Configuration.get().getMachine().isEnabled()){
            // 命令不能并发, 下位机处理不过来.

			// openpnp原始实现连发了4条指令给飞达, 导致飞达处理不过来
			// 修正后, 在执行命令之前, 都sleep 300ms, 看日志, 249ms估计也可以.
            my_delay_ms(300); // add by ls
            getIdActuatorAction.actionPerformed(null);

            my_delay_ms(300); // add by ls
            getFeedCountActuatorAction.actionPerformed(null);

            my_delay_ms(300); // add by ls
            pitchActuatorAction.actionPerformed(null);

            my_delay_ms(300); // add by ls
            statusActuatorAction.actionPerformed(null);
        }

        for (Bank bank : SlotSchultzFeeder.getBanks()) {
            bankCb.addItem(bank);
        }

// ...

D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\machine\reference\driver\GcodeDriver.java

    public synchronized void connect() throws Exception {
        disconnectRequested = false;
        getCommunications().connect();
        connected = false;

        connectThreads();

        // Wait a bit while the controller starts up
        Thread.sleep(connectWaitTimeMilliseconds);

        // Consume any startup messages
        try {
            while (!receiveResponses().isEmpty()) {

            }
        }
        catch (Exception e) {

        }

        // Disable the machine
        setEnabled(false);

        // Send startup Gcode
        // 加了参数3(发送前sleep的时间), 可能是connectThreads()和这里的发送命令有冲突, e.g. connectThreads()还没有关掉串口之类的
        // 要不就没法解释清楚为啥有时openpnp抛出串口被占用, 或者链接不上的情况.
        sendGcode_Ex(getCommand(null, CommandType.CONNECT_COMMAND), 200);

        connected = true;
    }
    @Override
    public void setEnabled(boolean enabled) throws Exception {
        if (enabled && !connected) {
            connect();
        }
        if (connected) {
            if (enabled) {
                // Assume a freshly re-enabled machine has no pending moves anymore.
                motionPending = false;
                sendGcode_Ex(getCommand(null, CommandType.ENABLE_COMMAND), 200); // 在可能会在连续发送命令的时机, 加上发送前的sleep
            }
            else {
                try {
                    sendGcode_Ex(getCommand(null, CommandType.DISABLE_COMMAND), 200);// 在可能会在连续发送命令的时机, 加上发送前的sleep
                    drainCommandQueue(getTimeoutAtMachineSpeed());
                }
                catch (Exception e) {
                    // When the connection is lost, we have IO errors. We should still be able to go on
                    // disabling the machine.
                    Logger.warn(e);
                }
            }
        }

        if (connected && !enabled) {
            if (isInSimulationMode() || !connectionKeepAlive) {
                disconnect();
            }
        }
        super.setEnabled(enabled);
    }
// actutor执行的地方, 都有可能是连续发送命令的组合, 都加上睡完发送参数值
    @Override
    public void actuate(Actuator actuator, boolean on) throws Exception {
        String command = getCommand(actuator, CommandType.ACTUATE_BOOLEAN_COMMAND);
        command = substituteVariable(command, "Id", actuator.getId());
        command = substituteVariable(command, "Name", actuator.getName());
        if (actuator instanceof ReferenceActuator) {
            command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
        }
        command = substituteVariable(command, "BooleanValue", on);
        command = substituteVariable(command, "True", on ? on : null);
        command = substituteVariable(command, "False", on ? null : on);
        sendGcode_Ex(command, 200); // param2, sleep then send
        SimulationModeMachine.simulateActuate(actuator, on, true);
    }

    @Override
    public void actuate(Actuator actuator, double value) throws Exception {
        String command = getCommand(actuator, CommandType.ACTUATE_DOUBLE_COMMAND);
        command = substituteVariable(command, "Id", actuator.getId());
        command = substituteVariable(command, "Name", actuator.getName());
        if (actuator instanceof ReferenceActuator) {
            command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
        }
        command = substituteVariable(command, "DoubleValue", value);
        command = substituteVariable(command, "IntegerValue", (int) value);
        sendGcode_Ex(command, 200); // param2, sleep then send
        SimulationModeMachine.simulateActuate(actuator, value, true);
    }

    @Override
    public void actuate(Actuator actuator, String value) throws Exception {
        String command = getCommand(actuator, CommandType.ACTUATE_STRING_COMMAND);
        command = substituteVariable(command, "Id", actuator.getId());
        command = substituteVariable(command, "Name", actuator.getName());
        if (actuator instanceof ReferenceActuator) {
            command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
        }
        command = substituteVariable(command, "StringValue", value);
        sendGcode_Ex(command, 200); // param2, sleep then send
    }
    @Override
    public String actuatorRead(Actuator actuator, Object parameter) throws Exception {
        /*
         * The logic here is a little complicated. This is the only driver method that is
         * not fire and forget. In this case, we need to know if the command was serviced or not
         * and throw an Exception if not.
         */
        String command = getCommand(actuator, CommandType.ACTUATOR_READ_COMMAND);
        String regex = getCommand(actuator, CommandType.ACTUATOR_READ_REGEX);
        if (command != null && regex != null) {
            command = substituteVariable(command, "Id", actuator.getId());
            command = substituteVariable(command, "Name", actuator.getName());
            if (actuator instanceof ReferenceActuator) {
                command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
            }
            if (parameter != null) {
                if (parameter instanceof Double) { // Backwards compatibility
                    Double doubleParameter = (Double) parameter;
                    command = substituteVariable(command, "DoubleValue", doubleParameter);
                    command = substituteVariable(command, "IntegerValue", (int) doubleParameter.doubleValue());
                }

                command = substituteVariable(command, "Value", parameter);
            }
            sendGcode_Ex(command, 200); // actor相关的命令, 都加上睡后发送
	// 原始的实现为了方便调用, 只加了回包超时参数, 现在加入一个新参数time_sleep_before_send
    protected void sendGcode_Ex(String gCode, long time_sleep_before_send) throws Exception {
        sendGcode_Ex(gCode, timeoutMilliseconds, time_sleep_before_send);
    }

    protected void sendGcode_Ex(String gCode, long timeout, long time_sleep_before_send) throws Exception {
        if (gCode == null) {
            return;
        }
        for (String command : gCode.split("\n")) {
            command = command.trim();
            if (command.length() == 0) {
                continue;
            }
            sendCommand(command, timeout, time_sleep_before_send);
        }
    }

    public void sendCommand(String command) throws Exception {
        sendCommand(command, timeoutMilliseconds, 0);
    }

    public void sendCommand(String command, long timeout, long time_sleep_before_send) throws Exception {
        // An error may have popped up in the meantime. Check and bail on it, before sending the next command. 
        bailOnError();
        if (command == null) {
            return;
        }

        Logger.debug("[{}] >> {}, {}, {}", getCommunications().getConnectionName(), command, timeout, time_sleep_before_send);
        if (command == "M610N3")
        {
            Logger.debug("bp");
        }
        if (time_sleep_before_send > 0)
        {
            Thread.sleep(time_sleep_before_send);
        }
        // ...

D:\my_openpnp\openpnp_github\src\main\java\org\openpnp\Main.java

改过的程序, 给个新版本号, 和官方实现区分开.

public class Main {
    public static String getVersion() {
    	
        String version = Main.class.getPackage().getImplementationVersion();
        if (version == null) {
            // 没看清getImplementationVersion()从哪取的版本信息, 先硬写一个临时版本号
            version = "INTERNAL BUILD - base 2022-8-1 last, ls 2023_1026_0608PM";
        }
        return version;
    }

备注

用IEDA带着程序跑起来(调试状态, run状态), openpnp控制设备都好使.
此时, 在添加好的飞达列表中的飞达之间切换, 会卡大概1秒钟, 然后就会正常显示飞达信息.
这个1秒多时间的卡, 如果是自动贴片的时候(e.g. 自动取料时, 如果换了一个飞达供料, 也可能会重新取飞达参数, 遇到过在自动贴片过程中, 弹出取不到飞达ID的情况), 根本感觉不到.
问题已经解决了, 暂时未发现不良影响 😛

剩下的是事情就是将修改完的程序发布给自己用, 脱离IDE的环境. 这个也做完实验了, 确定可以简易发布给自己其他计算机用.
这个在下一篇笔记中记录, 和修改源码没关系. 也是为了自己以后好按照关键字来找笔记, e.g. 在oepnpnp栏目搜索包含"打包"关键字的笔记, 就能找到如何发布openpnp程序给自己用.

END

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1138624.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis快速上手篇(六)主从复制

主从复制 主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主 读写分离,性能扩展(主 写 从 读) 容灾快速恢复 一主二仆 拷贝多个redis.conf文件include(写绝…

动态规划太难了?是你没有找对方法,四题带你搞懂动态规划!

💯 博客内容:动态规划刷题 😀 作  者:陈大大陈 🚀 个人简介:一个正在努力学技术的准前端,专注基础和实战分享 ,欢迎私信! 💖 欢迎大家:这里是CS…

【RV1106/RV1103】RV1103增加RTL8723BS

文章目录 Kernel 部分支持配置 menuconfigWiFi 功能支持Bluetooth 功能支持 原SDK的Busybox配置BT 部分文件系统部分蓝牙测试dbus 安装测试遇到的问题让hci0出来 使用hcitool来操作 Kernel 部分支持 配置 menuconfig WiFi 功能支持 Bluetooth 功能支持 正基和海华的模块都使用…

pip 更换源

方案1 在C盘用户名录下新建pip文件夹,里面包含pip.ini文件 方案2 在C盘用户名目录的AppData的Roaming下新建pip文件夹,里面包含pip.ini文件。 内容为 [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple

题目 1056: 二级C语言-温度转换(python详解)——练气四层初期

✨博主:命运之光 🦄专栏:算法修炼之练气篇(C\C版) 🍓专栏:算法修炼之筑基篇(C\C版) 🍒专栏:算法修炼之练气篇(Python版) ✨…

CVE-2022-22965 Spring Framework远程命令执行

0x01 影响版本 Spring Framework < 5.3.18 Spring Framework < 5.2.20 JDK>9 0x02 复现环境 vulhub/spring/cve-2022-22965 0x03 漏洞复现 首先docker-compose up -d开启靶场 输入payload <%if("j".equals(request.getParameter("pwd")…

Transformer在计算机视觉领域的研究综述

论文地址&#xff1a;https://kns.cnki.net/kcms/detail/11.2127.TP.20221009.1217.003.html 目录 摘 要 1. Transformer 基本结构 1.1 位置编码 (1) 绝对位置编码 (2) 相对位置编码 1.2 自注意力机制 (1) 多头注意力 (2) 局部注意力 (3) 稀疏注意力机制 1.3 前馈神…

通信仿真软件SystemView安装教程(超详细)

介绍 system view是一种电子仿真工具。它是一个信号级的系统仿真软件&#xff0c;主要用于电路与通信系统的设计和仿真&#xff0c;是一个强有力的动态系统分析工具&#xff0c;能满足从数字信号处理&#xff0c;滤波器设计&#xff0c;直到复杂的通信系统等不同层次的设计&am…

SQL注入类型(详细讲解)

SQL注入方法(函数原理讲解) 前言 在进行SQL注入测试的时候&#xff0c;确实很重要要知道目标SQL语句是什么类型&#xff0c;因为不同类型的SQL语句对注入的有效载荷&#xff08;payload&#xff09;有不同的语法要求。下面列举了几个原因&#xff1a; 1. 语法闭合 你提到了…

“可编程网络”的基础概念介绍

什么是可编程网络&#xff1f; 可编程网络是指网络设备的行为和流量控制由独立于网络硬件运行的软件处理的网络。可编程网络的基本性质是将底层物理硬件与设备的控制软件分开。 网络可编程的历史 当可编程网络的概念首次出现时&#xff0c;它是计算机网络发展史上革命性的一…

Specializing Smaller Language Models towards Multi-Step Reasoning论文精读

0 Abstract 普遍认为&#xff0c;LLM涌现出来的few-shot learning能力是超大参数模型独有的&#xff08;>100B&#xff09;【emergent abilities】&#xff1b;作者认为&#xff0c;小模型&#xff08;<10B&#xff09;可以将这些能力从大模型&#xff08;>100B&…

【HTML/CSS学习】margin和padding的区别

1. margin margin&#xff08;外边距&#xff09;属性定义元素周围的空间。 margin主要在元素的外部创建空白区域&#xff0c;用于分隔元素与其相邻元素之间的距离。 用处&#xff1a;可以用于调整两个元素之间的间隔。 2. padding padding&#xff08;填充&#xff09;定义…

dcrcms 文件上传漏洞

dcrcms 文件上传漏洞 启动环境 启动靶场得到ip和映射端口 测试漏洞 输入http://10.9.47.6:14153/dcr进入后台 账号密码为admin:123456 寻找文件上传漏洞 看到添加新闻里有文件上传我们尝试进行上传 写一个php文件里面放入一句话木马 <?php eval($_REQUEST[666]); ?…

C++前缀和算法的应用:用地毯覆盖后的最少白色砖块 原理源码测试用例

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 题目 给你一个下标从 0 开始的 二进制 字符串 floor &#xff0c;它表示地板上砖块的颜色。 floor[i] ‘0’ 表示地板上第 i 块砖块的颜色是 黑色 。 floor[i] ‘1…

警务可视化 玫瑰图和柱折混合图

什么时候用&#xff0c;什么时候导入echarts 完整代码&#xff1a; 搭建页面结构的代码 html <template><h2>智慧公安数字服务系统</h2><div><div class"container1"> </div> <div class"container2"></di…

【golang】Windows环境下Gin框架安装和配置

Windows环境下Gin框架安装和配置 我终于搞定了Gin框架的安装&#xff0c;花了两三个小时&#xff0c;只能说道阻且长&#xff0c;所以写下这篇记录文章 先需要修改一些变量&#xff0c;这就需要打开终端&#xff0c;为了一次奏效&#xff0c;我们直接设置全局的&#xff1a; …

vue3后台管理系统

项目创建及代码规范化开发 vue脚手架创建项目 安装vue脚手架 npm install-g vue/cli npm update -g vue/cli终端输入vue create 项目名称 即可进入模板选择 //利用vue-cli创建项目 进入模板选择 Vue CLI v5.0.8 ? Please pick a preset:Default ([Vue 3] babel, eslint)De…

LabVIEW开发基于图像处理的车牌检测系统

LabVIEW开发基于图像处理的车牌检测系统 自动车牌识别的一般步骤是图像采集、去除噪声的预处理、车牌定位、字符分割和字符识别。结果主要取决于所采集图像的质量。在不同照明条件下获得的图像具有不同的结果。在要使用的预处理技术中&#xff0c;必须将彩色图像转换为灰度&am…

【每日一题】切割后面积最大的蛋糕

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;排序 其他语言python3 写在最后 Tag 【排序】【数组】【2023-10-27】 题目来源 1465. 切割后面积最大的蛋糕 题目解读 切割后面积最大的蛋糕。 解题思路 方法一&#xff1a;排序 本题较为简单&#xff0c;找出最大…

Figma是什么软件?有哪些优势和劣势?

Figma作为目前最受欢迎的设计软件&#xff0c;在国外受到UI设计师的广泛好评。如果你还对Figma感到困惑&#xff0c;不知道它在做什么&#xff0c;那么今天将为您详细介绍Figma软件&#xff0c;以帮助您快速理解和更好地使用它。 Figma软件基本介绍 简单来说&#xff0c;Figm…