【数字图像处理】水平翻转、垂直翻转

news2025/2/23 3:50:23

图像翻转是常见的数字图像处理方式,分为水平翻转和垂直翻转。本文主要介绍 FPGA 实现图像翻转的基本思路,以及使用紫光同创 PGL22G 开发板实现数字图像水平翻转、垂直翻转的过程。

目录

1 水平翻转与垂直翻转

2 FPGA 布署与实现

2.1 功能与指标定义

2.2 逻辑设计

2.3 上板调试


1 水平翻转与垂直翻转

        在数字图像处理中,图像翻转是指将图像进行水平或者垂直方向的翻转,使其呈现不同的效果,分为水平翻转和垂直翻转。

        水平翻转是将图像沿着水平轴线进行翻转,将左半部分和右半部分进行交换。垂直翻转则是将图像沿着垂直轴线进行翻转,将上部分和下部分进行交换。

        使用 FPGA 实现水平翻转时,由于每次向 DDR3 写入一行数据,因此可以借助写 DDR3 之前的 Dual-port RAM,将一行数据进行翻转,实现水平翻转的功能。FPGA 逻辑设计的大致思路是:写 Dual-port RAM 时,写地址从 0 增加;读 Dual-port RAM 写 DDR3 时,读地址从最大值逐步减小到 0 结束。 

        对于垂直翻转功能,则需要借助 DDR3 来实现。像素数据第 1 行写到第 N 行的位置,第 2 行写到第 N-1 行的位置,以此类推。FPGA 逻辑设计的大致思路是:每一行数据写入 DDR3 时,用图像高度减去原来的行地址,作为新的行地址。

2 FPGA 布署与实现

2.1 功能与指标定义

        使用紫光同创 FPGA 平台实现图像翻转功能,FPGA 需要实现的功能与指标如下:

(1)与电脑的串口通信,用于接收上位机下发的图像数据,波特率为 256000 Bd/s;

(2)水平翻转与图像翻转:借助 Dual-port RAM 与 DDR3,分别实现水平翻转与垂直翻转功能;

(3)DDR3 读写控制,将处理前后的图像数据分别写入 DDR3 的不同区域,实现图像的拼接;

(4)HDMI 输出,输出一路 HDMI 信号源,用于将拼接后的图像显示在外接显示器上,分辨率为 1024×768。

2.2 逻辑设计

        图像翻转工程主要的设计模块层次与功能说明如下:

模块名称功能说明
top_uartuart_rx_slice串口接收驱动模块
uart_rx_parse串口数据解析模块,从上位机接收 8bit 原始图像
top_vidinvidin_pipelinepipeline 单元模块,缓存两行图像数据,并将数据提交到 ddr3 数据调度模块
merge_outdvi_timing_genHDMI 视频时序产生模块
dvi_ddr_rd根据 HDMI 控制信号,提交读指令到 ddr3 数据调度模块
dvi_encoderHDMI 输出编码(8b10b 编码)与输出驱动模块

其中,vidin_pipeline 模块实现图像翻转功能,代码如下:

`timescale 1 ns/ 1 ps

`include "../ddr_scheduler/ddr_parameter.vh"

module vidin_pipeline (
   // System level
   sys_rst,
   sys_clk,
   ddr_init_done,
   flip_lr,
   flip_ud,

   // pipeline input ports
   pipeline_in_info,
   pipeline_in_data,
   pipeline_in_wren,
   pipeline_in_end,

   // pipeline output ports
   pipeline_out_info,
   pipeline_out_data,
   pipeline_out_vld,
   pipeline_out_end,

   // User defined ports for ddr_scheduler
   ddr_wr_baseaddr,
   ddr_wr_addr,
   ddr_wr_priority,
   ddr_wr_burstsize,
   ddr_wr_req,
   ddr_wr_ack,
   ddr_wr_end,
   ddr_wr_rden,
   ddr_wr_q,
   ddr_wr_mask
);

// IO direction/register definitions
input                     sys_rst;
input                     sys_clk;
input                     ddr_init_done;
input                     flip_lr;
input                     flip_ud;

input  [31:0]             pipeline_in_info;
input  [31:0]             pipeline_in_data;
input                     pipeline_in_wren;
input                     pipeline_in_end;

output [31:0]             pipeline_out_info;
output [31:0]             pipeline_out_data;
output                    pipeline_out_vld;
output                    pipeline_out_end;

input  [`DDR_A_W-1:0]     ddr_wr_baseaddr;
output [`DDR_A_W-1:0]     ddr_wr_addr;
output                    ddr_wr_priority;
output [`DDR_BURST_W-1:0] ddr_wr_burstsize;
output                    ddr_wr_req;
input                     ddr_wr_ack;
input                     ddr_wr_end;
input                     ddr_wr_rden;
output [`DDR_D_W-1:0]     ddr_wr_q;
output [`DDR_D_W/8-1:0]   ddr_wr_mask;

reg    [31:0]             pipeline_out_info;
reg    [31:0]             pipeline_out_data;
reg                       pipeline_out_vld;
reg                       pipeline_out_end;

// internal signal declarations
reg    [`DDR_CMD_W-1:0]   ddr_cmd_data;
reg                       ddr_cmd_vld;

reg    [9:0]              blk_mem_waddr;
reg    [31:0]             blk_mem_wdata;
reg                       blk_mem_wren;
reg    [9:0]              blk_mem_raddr;
wire   [31:0]             blk_mem_rdata;
reg                       blk_mem_rden;
reg                       blk_mem_rd_busy;
reg                       blk_mem_rd_end;
reg                       blk_mem_rd_vld;

// line_buffer_inst: Dual-port ram for line pixel data buffer
blk_mem_1024x32b line_buffer_inst (
   .wr_data         (blk_mem_wdata   ), // input 32-bit
   .wr_addr         (blk_mem_waddr   ), // input 10-bit
   .wr_en           (blk_mem_wren    ), // input 1-bit
   .wr_clk          (sys_clk         ), // input 1-bit
   .wr_rst          (sys_rst         ), // input 1-bit
   .rd_addr         (blk_mem_raddr   ), // input 10-bit
   .rd_data         (blk_mem_rdata   ), // output 32-bit
   .rd_clk          (sys_clk         ), // input 1-bit
   .rd_rst          (sys_rst         )  // input 1-bit
);
// End of line_buffer_inst instantiation

always @(posedge sys_rst or posedge sys_clk) begin
   if (sys_rst) begin
      blk_mem_waddr <= {10{1'b0}};
      blk_mem_wdata <= {32{1'b0}};
      blk_mem_wren  <= 1'b0;
   end
   else begin
      blk_mem_wdata <= pipeline_in_data;
      blk_mem_wren  <= pipeline_in_wren;

      // Use ping-pong storage here
      if (pipeline_in_end) 
         blk_mem_waddr <= {~blk_mem_waddr[9], {9{1'b0}}};
      else if (pipeline_in_wren) 
         blk_mem_waddr <= {blk_mem_waddr[9], blk_mem_waddr[0+:9]+1'b1};
   end
end

always @(posedge sys_rst or posedge sys_clk) begin
   if (sys_rst) begin
      blk_mem_raddr   <= {10{1'b0}};
      blk_mem_rden    <= 1'b0;
      blk_mem_rd_busy <= 1'b0;
      blk_mem_rd_end  <= 1'b0;
      blk_mem_rd_vld  <= 1'b0;
   end
   else begin
      if (~blk_mem_rd_busy && pipeline_in_end) begin
         blk_mem_rd_busy <= 1'b1;
         if (flip_lr == 1'b0) 
            blk_mem_raddr <= {blk_mem_raddr[9], {9{1'b0}}};
         else
            blk_mem_raddr <= {blk_mem_raddr[9], {9{1'b1}}};
      end
      else if (blk_mem_rd_busy) begin
         // Use ping-pong storage here
         if (flip_lr == 1'b0) begin
            if (& blk_mem_raddr[0+:9]) 
               blk_mem_raddr <= {~blk_mem_raddr[9], {9{1'b0}}};
            else 
               blk_mem_raddr <= {blk_mem_raddr[9], blk_mem_raddr[0+:9]+1'b1};
         end
         else begin
            if (blk_mem_raddr[0+:9] == 0)
               blk_mem_raddr <= {~blk_mem_raddr[9], {9{1'b1}}};
            else
               blk_mem_raddr <= {blk_mem_raddr[9], blk_mem_raddr[0+:9]-1'b1};
         end

         // Pull down read busy flag
         if (flip_lr == 1'b0) begin
            if (& blk_mem_raddr[0+:9]) 
               blk_mem_rd_busy <= 1'b0;
         end
         else begin
            if (blk_mem_raddr[0+:9] == 0)
               blk_mem_rd_busy <= 1'b0;
         end
      end

      blk_mem_rden <= blk_mem_rd_busy;
      blk_mem_rd_vld <= blk_mem_rden;

      if (blk_mem_rd_busy) begin
         if (flip_lr == 1'b0) begin
            if (& blk_mem_raddr[0+:9]) 
               blk_mem_rd_end <= 1'b1;
            else 
               blk_mem_rd_end <= 1'b0;
         end
         else begin
            if (blk_mem_raddr[0+:9] == 0)
               blk_mem_rd_end <= 1'b1;
            else 
               blk_mem_rd_end <= 1'b0;
         end
      end
      else
         blk_mem_rd_end <= 1'b0;
   end
end

always @(posedge sys_rst or posedge sys_clk) begin
   if (sys_rst) begin
      pipeline_out_info <= {32{1'b0}};
      pipeline_out_data <= {32{1'b0}};
      pipeline_out_vld  <= 1'b0;
      pipeline_out_end  <= 1'b0;
   end
   else begin
      if (pipeline_in_end) 
         pipeline_out_info <= pipeline_in_info;

      pipeline_out_data <= blk_mem_rdata;
      pipeline_out_vld  <= blk_mem_rd_vld;
      pipeline_out_end  <= blk_mem_rd_end;
   end
end

/
always @(posedge sys_rst or posedge sys_clk) begin
   if (sys_rst) begin
      ddr_cmd_data <= {`DDR_CMD_W{1'b0}};
      ddr_cmd_vld <= 1'b0;
   end
   else begin
      if (pipeline_in_end) begin
         ddr_cmd_data[32+:`DDR_BURST_W] <= 8'h7F; // used fixed size here, 512 /4 -1 = 127
         if (flip_ud == 1'b0)
            ddr_cmd_data[0+:28] <= {pipeline_in_info[0+:16], 12'd0};
         else
            ddr_cmd_data[0+:28] <= {16'd383-pipeline_in_info[0+:16], 12'd0};
      end

      if (blk_mem_rd_end) 
         ddr_cmd_vld <= 1'b1;
      else 
         ddr_cmd_vld <= 1'b0;
   end
end

// vid_ddr_wr_inst: ddr write control module
vid_ddr_wr vid_ddr_wr_inst (
   .sys_rst          (sys_rst           ), // input 1-bit
   .sys_clk          (sys_clk           ), // input 1-bit
   .ddr_init_done    (ddr_init_done     ), // input 1-bit
   .vid_cmd_data     (ddr_cmd_data      ), // input 40-bit
   .vid_cmd_vld      (ddr_cmd_vld       ), // input 1-bit
   .vid_img_data     (blk_mem_rdata     ), // input 32-bit
   .vid_img_data_vld (blk_mem_rd_vld    ), // input 1-bit
   .ddr_wr_baseaddr  (ddr_wr_baseaddr   ), // input 27-bit
   .ddr_wr_addr      (ddr_wr_addr       ), // output 27-bit
   .ddr_wr_priority  (ddr_wr_priority   ), // output 1-bit
   .ddr_wr_burstsize (ddr_wr_burstsize  ), // output 8-bit
   .ddr_wr_req       (ddr_wr_req        ), // output 1-bit
   .ddr_wr_ack       (ddr_wr_ack        ), // input 1-bit
   .ddr_wr_end       (ddr_wr_end        ), // input 1-bit
   .ddr_wr_rden      (ddr_wr_rden       ), // input 1-bit
   .ddr_wr_q         (ddr_wr_q          ), // output 128-bit
   .ddr_wr_mask      (ddr_wr_mask       )  // output 16-bit
);
// End of vid_ddr_wr_inst instantiation

endmodule

2.3 上板调试

        使用 PyQt5 和 OpenCV 库编写上位机程序,通过串口发送原始图像数据,以及水平翻转、垂直翻转参数。

# -*- Coding: UTF-8 -*-
import cv2
import sys
import struct
import numpy as np
from PyQt5 import Qt, QtGui, QtCore, QtWidgets, QtSerialPort


class mainWindow(Qt.QWidget):
   def __init__(self, com_port, parent=None):
      super(mainWindow, self).__init__(parent)
      self.setFixedSize(530, 384)
      self.setWindowTitle("PGL OpenCV Tool")

      self.flip_horizontal = False
      self.flip_vertical = False

      # 创建标签与按钮
      self.img_widget = QtWidgets.QLabel()
      self.btn1 = QtWidgets.QPushButton("打开")
      self.btn1.clicked.connect(self.getfile)
      self.btn2 = QtWidgets.QPushButton("关闭")
      self.btn2.clicked.connect(self.close)
      self.btn3 = QtWidgets.QPushButton("水平翻转")
      self.btn3.clicked.connect(self.flip_lr)
      self.btn4 = QtWidgets.QPushButton("垂直翻转")
      self.btn4.clicked.connect(self.flip_ud)

      # 创建布局
      centralLayout = QtWidgets.QVBoxLayout()
      centralLayout.addWidget(self.img_widget)
      bottomLayout = QtWidgets.QHBoxLayout()
      bottomLayout.addWidget(self.btn1)
      bottomLayout.addWidget(self.btn2)
      bottomLayout.addWidget(self.btn3)
      bottomLayout.addWidget(self.btn4)
      centralLayout.addLayout(bottomLayout)
      self.setLayout(centralLayout)

      # 串口对象
      self.COM = QtSerialPort.QSerialPort()
      self.COM.setPortName(com_port)
      self.COM.setBaudRate(256000)
      self.open_status = False
      self.row_cnt = 0
      self.img = None
      self.timer = QtCore.QTimer()
      self.timer.timeout.connect(self.sendImage)
      self.startup()

   def startup(self):
      """Write code here to run once"""
      for com_port in QtSerialPort.QSerialPortInfo.availablePorts():
         print(com_port.portName())

      # Try open serial port
      if not self.COM.open(QtSerialPort.QSerialPort.ReadWrite):
         self.open_status = False
         print("Open Serial Port failed.")
      else:
         self.open_status = True

   def flip_lr(self):
      """水平翻转回调函数"""
      if self.flip_horizontal == False:
         self.flip_horizontal = True
         self.btn3.setStyleSheet("QPushButton{color:rgb(128,128,255)}")
      else:
         self.flip_horizontal = False
         self.btn3.setStyleSheet("QPushButton{color:rgb(0,0,0)}")

   def flip_ud(self):
      """垂直翻转回调函数"""
      if self.flip_vertical == False:
         self.flip_vertical = True
         self.btn4.setStyleSheet("QPushButton{color:rgb(128,128,255)}")
      else:
         self.flip_vertical = False
         self.btn4.setStyleSheet("QPushButton{color:rgb(0,0,0)}")

   def getfile(self):
      """获取图像路径"""
      fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file',
         'C:\\Users\\Administrator\\Pictures', "Image files(*.jpg *.png)")
      self.clipImage(fname[0])
      self.updateImage()
      self.sendImage()

   def clipImage(self, fname):
      """读取并裁剪图片至512x384大小"""
      if fname:
         img = cv2.imread(fname, cv2.IMREAD_COLOR)
         img_roi = img[:384,:512,:]
         print(img_roi.shape)
         cv2.imwrite('./img_roi.png', img_roi)

   def updateImage(self):
      """显示裁剪后的图像"""
      self.img_widget.setPixmap(QtGui.QPixmap('./img_roi.png'))
      self.img = cv2.imread('./img_roi.png')

      if self.open_status:
         self.timer.start(100)

   def sendImage(self):
      """通过串口发送图片"""
      pattern = ">2BH{:d}B".format(512*3)

      # 获取图像翻转信息
      flip_flag = 0x00
      if self.flip_horizontal:
         flip_flag = flip_flag + 0x10
      
      if self.flip_vertical:
         flip_flag = flip_flag + 0x01

      # 发送图像数据
      if self.open_status:
         if self.row_cnt == 384+3:
            self.row_cnt = 0
            self.timer.stop()
         else:
            args1 = [0x55, flip_flag, self.row_cnt]
            args2 = [rgb for rgb in self.img[(self.row_cnt % 384),:].reshape(-1)]
            send_data = struct.pack(pattern, *(args1+args2))
            self.row_cnt += 1
            self.COM.write(send_data)

   def closeEvent(self, event):
      super().closeEvent(event)
      #self.slider_window.close()

      # 定时器停止
      self.timer.stop()
      if self.open_status:
         self.COM.close() # 关闭串口

def main():
   app = QtWidgets.QApplication(sys.argv)
   window = mainWindow('COM21')
   window.show()
   #for win in (window, window.slider_window):
   #   win.show()
   sys.exit(app.exec_())

if __name__ == "__main__":
   main()

        连接 HDMI 线和串口线,选择与发送图像,就可以看到 FPGA 的处理效果了。以下是水平翻转效果。

以下是垂直翻转效果。

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

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

相关文章

SSM 基础知识点

1. IoC IoC—Inversion of Control&#xff0c;即“控制反转”&#xff0c;不是什么技术&#xff0c;而是一种设计思想。在 Java 开发中&#xff0c;IoC 意味着将你设计好的对象交给容器控制&#xff0c;而不是传统的在你的对象内部直接控制。 谁控制谁&#xff0c;控制什么&…

Linux网络配置

一、查看网络配置 1、查看网络接口信息ifconfig 1.查看所有活动的网络接口信息 2.查看指定网络接口信息 ifconfig 网络接口 ifconfig -a #显示所有活动及非活动的连接 ifconfig网络接口 ifconfig -a #显示所有活动及非活动的连接 主机的网络接口卡(网卡)通常称为网络接口…

QT上位机开发(动态添加控件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 开发界面的时候&#xff0c;大多数情况下&#xff0c;我们都是推荐优先使用designer来进行界面开发。但凡事总有例外&#xff0c;如果控件本身数量…

C#编程-实现在文本文件中的读和写

实现在文本文件中的读和写 Stream类用于从文本文件读取数据和向文本文件写入数据。它是一个抽象类,支持向流读写字节。如果文件的数据仅是文本,那么您可以使用StreamReader类和StreamWriter类来完成相应的读和写任务。 StreamReader类 StreamReader类继承自从抽象类TextRea…

扩展边界opencv

扩展图像的边缘&#xff08;如上边增加50像素&#xff09;通常是通过添加额外的像素行来实现的 使用cv2.copyMakeBorder函数 valueborder_color指定了边框的颜色 import cv2 import numpy as np# 读取图像 image cv2.imread(th.jpg)# 设置边框宽度 top_border_width 50 # …

三、nginx代理功能

目录 SQUID代理服务器配置安装squid编辑squid配置文件 定义拒绝访问启动squid服务 linux客户端配置linux客户端配置正向代理测试http代理服务器上查看日志/var/log/squid/access.log windows 也可以配置网页代理 SQUID代理服务器配置 安装squid yum install squid -y 编辑squ…

Hive基础知识(六):Hive 配置运行日志信息、打印当前库和表头、参数配置方式

1. Hive 运行日志信息配置 1&#xff09;Hive 的 log 默认存放在/tmp/atguigu/hive.log 目录下&#xff08;当前用户名下&#xff09; 2&#xff09;修改 hive 的 log 存放日志到/opt/module/hive/logs &#xff08;1&#xff09;修改/opt/module/hive/conf/hive-log4j2.prop…

【GNN 1】PyG实现图神经网络,完成节点分类任务,人话、保姆级教程

我们来做一个节点分类的任务&#xff0c;选择的数据集是Karate Club&#xff0c;Karate是空手道的意思&#xff0c;所以这就是一个空手道俱乐部的数据。 简而言之&#xff0c;这个数据集&#xff0c;包含34个节点&#xff0c;156条无向无权边&#xff0c;结点总共分为4类&…

AI绘画:Midjournety的使用体验

今天的时间少&#xff0c;没有给大家做一些教程&#xff0c;就单纯分享使用体验&#xff0c;还不错&#xff0c;体验感很好。 后需如果有需要&#xff0c;我可以出一些教程类的视频。 下面是一组复刻fated的saber的一组提示词&#xff0c;效果相当不错。我后续会分享一些学习经…

uniapp自定义顶部导航并解决打包成apk后getMenuButtonBoundingClientRect方法失效问题

需求&#xff1a;要在app上的顶部导航提示哪里添加一些东西进去&#xff0c;用uniapp自带的肯定不行啊&#xff0c;所以自定义了所有的页面的顶部导航&#xff0c;之后自定义后用手机调试发现 uni.getMenuButtonBoundingClientRect()这个方法的top获取不到....网上找了很多种方…

基于 InternLM 和 LangChain 搭建你的知识库

基于 InternLM 和 LangChain 搭建你的知识库 大模型开发范式LLM的局限性&#xff1a;RAG 检索增强生成 LangChain简介构建向量数据库搭建知识库助手Web Demo部署环境配置下载 NLTK 相关资源下载本项目代码 大模型开发范式 LLM的局限性&#xff1a; 知识实效性受限&#xff1a…

模型\视图一般步骤:为什么经常要用“选择模型”QItemSelectionModel?

一、“使用视图”一般的步骤&#xff1a; //1.创建 模型(这里是数据模型&#xff01;) tabModelnew QSqlTableModel(this,DB);//数据表 //2.设置 视图的模型(这里是数据模型&#xff01;) ui->tableView->setModel(tabModel); 模型种类&#xff1a; QStringListModel…

vue element plus Typography 排版

我们对字体进行统一规范&#xff0c;力求在各个操作系统下都有最佳展示效果。 字体# 字号# LevelFont SizeDemoSupplementary text12px Extra SmallBuild with ElementBody (small)13px SmallBuild with ElementBody14px BaseBuild with ElementSmall Title16px MediumBuild w…

了解不同方式导入导出的速度之快

目录 一、用工具导出导入 Navicat&#xff08;速度慢&#xff09; 1.1、导入&#xff1a; 共耗时&#xff1a; 1.2、导出表 共耗时&#xff1a; 二、用命令语句导出导入 2.1、mysqldump速度快 导出表数据和表结构 共耗时&#xff1a; 只导出表结构 导入 共耗时&…

Linux 网络层收发包流程及 Netfilter 框架浅析

1. 前言 本文主要对 Linux 系统内核协议栈中网络层接收&#xff0c;发送以及转发数据包的流程进行简要介绍&#xff0c;同时对 Netfilter 数据包过滤框架的基本原理以及使用方式进行简单阐述。 内容如有理解错误而导致说明错误的地方&#xff0c;还请指正。如存在引用而没有添…

怎样的摆渡系统,能实现安全可管控的跨网数据传输?

大数据时代&#xff0c;数据在流通与传输的过程中&#xff0c;更需要注意到数据的安全防护&#xff0c;护航数据价值。“让数据主宰一切的隐忧”&#xff0c;数字战争的时代&#xff0c;各国早已认识到网络安全愈发重要&#xff0c;数据也成为各国发展的重要武器。 出于安全性和…

vmware虚拟机内存异常占用问题一例

关键词 vmware esxi、hypervisor虚拟化平台内存模式 Guest virtual memory 一、问题现象 业务一台vmware虚拟机出现内存使用率告警&#xff0c;运维人员登录系统检查内存确实高水位状态 检查各进程使用内存不高&#xff0c;合计内存总数与使用率占用情况明显不匹配&#xf…

面试题:什么是雪花算法?啥原理?

SnowFlake 算法&#xff0c;是 Twitter 开源的分布式 ID 生成算法。 其核心思想就是&#xff1a;使用一个 64 bit 的 long 型的数字作为全局唯一 ID。在分布式系统中的应用十分广泛&#xff0c;且 ID 引入了时间戳&#xff0c;基本上保持自增的&#xff0c;后面的代码中有详细…

Bert-vits2最终版Bert-vits2-2.3云端训练和推理(Colab免费GPU算力平台)

对于深度学习初学者来说&#xff0c;JupyterNoteBook的脚本运行形式显然更加友好&#xff0c;依托Python语言的跨平台特性&#xff0c;JupyterNoteBook既可以在本地线下环境运行&#xff0c;也可以在线上服务器上运行。GoogleColab作为免费GPU算力平台的执牛耳者&#xff0c;更…