ids工业相机与电控位移台同步控制及数据采集

通过VS2017和OpenCV,实现ids工业相机与电控位移台同步控制及数据采集

目录

光学数据采集系统中,经常需要采集几万个样本点进行数据分析,如果通过人工数据采集,人工成本太高,同时容易出现测量误差等因素。因此需要根据项目功能开发一套自动化采集图像和位移控制的工程项目,以下是具体内容:

项目环境配置

代码环境:

VS2017

OpenCV3.4.2

ids开发包,包括.lib、.dll、.h文件等

LBtek电控五相位移平台,.dll、.h文件

代码流程及思路

  1. 相机初始化,根据图像尺寸申请内存地址;
  2. 导出图像内存指针,设置传感器参数
  3. 初始化位移台,新建类
  4. 通过dll查找位移台函数指针,调用函数
  5. 设置边界条件及步进电机参数
  6. 通过函数返回值与延时等机制,实现相机同步
  7. 批量化图像采集与数据处理

项目架构

项目开发运行效果

开发关键

相机和位移台属于商业产品,商家不提供完整程序包,只给出dll动态链接库,需要通过函数指针调用接口。

通过算法自定义位移台运行模式;

提升图像增益及伽马校正,自定义图像采集AOI,提升采集帧率;

ids相机配置

本文是基于debug x64模式下,C++ 环境运行,需要一老OpenCV的部分库函数,进行图像展示和保存。

OpenCV环境配置:具体配置方法CSDN其他博客都有记录,是通用的,

比如我之前配置旧版本的环境,大家可以参考: link



大家要注意,将这里的语言 符合模式 设置为否,原因是ids相机开发包里面存储文件的时候,使用的是wchat_t *的格式,调试过程中很容易出问题。如下图设置


相机环境配置完成。

位移台环境配置

由于开发包里面只提供了配置文件、.dll文件、.h文件,函数实现方法是不知道的,因此调用函数的时候是将这三个文件放置到项目代码包中,通过函数指针调用函数,配置说明书摸索函数功能。

这一部分环境配置就是将这几个文件放置到工程项目文件夹中即可。

相机头文件

javascript 复制代码
#include "ueye.h"
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

class Idscam {

public:
	Idscam();
	INT InitCamera(HIDS *hCam, HWND hWnd);   //初始化相机   hWnd指向显示图像窗口的指针,若用DIB模式可以令hWnd=NULL  
	bool OpenCamera();
	void ExitCamera();
	int  InitDisplayMode();
	void GetMaxImageSize(INT *pnSizeX, INT *pnSizeY);//查询相机支持的图像格式
	void SaveImage(wchar_t* fileName);
	bool GetiplImgFormMem(); //从视频数据流中将图像数据拷贝给IplImage
	// uEye varibles
	HIDS	m_hCam;				// 相机句柄
	HWND	m_hWndDisplay;		 // window显示句柄
	INT		m_nColorMode;		// Y8/RGB16/RGB24/REG32
	INT		m_nBitsPerPixel;	// 图像位深
	INT		m_nSizeX;		// 图像宽度
	INT		m_nSizeY;		// 图像高度
	INT		m_nPosX;		// 图像左偏移
	INT		m_nPosY;		// 图像右偏移
	cv::Mat CamMat;
	IplImage *iplImg;
	char *m_pLastBuffer;

private:

	// 使用位图模式进行实时显示需要的内存  
	INT	 m_lMemoryId;	        // camera memory - buffer ID
	char*	m_pcImageMemory;	// camera memory - pointer to buffer
	SENSORINFO m_sInfo;		// sensor information struct
	INT     m_nRenderMode;		// render  mode
	INT     m_nFlipHor;		// 水平翻转标志
	INT     m_nFlipVert;		// 垂直翻转标志

}; 

相机参数设置

javascript 复制代码
int SetParasofCamera(HIDS hCam, uint nPixelClockSet, double nFramePerSecondSet, double nExposureTimeSet) {
	// 像素时钟查询与设置
	UINT nPixelClockDefault, nPixelClockCurrent;
	INT nRet;
	/*
	// 获取默认像素时钟
	nRet = is_PixelClock(hCam, IS_PIXELCLOCK_CMD_GET_DEFAULT, (void*)&nPixelClockDefault, sizeof(nPixelClockDefault));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and default pixel clock is : " << nPixelClockDefault << " MHz." << endl;
	}
	else {
		cout << "Failed to get default pixel clock" << endl;
		return IS_NO_SUCCESS;
	}
	*/
	// 设置该像素时钟
	//nPixelClockSet = 86;	//设置为80MHz
	nRet = is_PixelClock(hCam, IS_PIXELCLOCK_CMD_SET, (void*)&nPixelClockSet, sizeof(nPixelClockSet));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and user set pixel clock is : " << nPixelClockSet << " MHz." << endl;
	}
	else {
		cout << "Failed to set pixel clock" << endl;
		return IS_NO_SUCCESS;
	}
	// 获取当前像素时钟
	nRet = is_PixelClock(hCam, IS_PIXELCLOCK_CMD_GET, (void*)&nPixelClockCurrent, sizeof(nPixelClockCurrent));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and current pixel clock is : " << nPixelClockCurrent << " MHz." << endl;
	}
	else {
		cout << "Failed to get current pixel clock" << endl;
		return IS_NO_SUCCESS;
	}
	cout << "***********************************************************************************" << endl;
	/*
	// 相机帧率范围查询与设置
	double nFrameMin, nFrameMax, nFrameIntervall;
	nRet = is_GetFrameTimeRange(hCam, &nFrameMin, &nFrameMax, &nFrameIntervall);
	if (nRet == IS_SUCCESS) {
		cout << "Success, Min Frame = " << 1 / nFrameMax << "fps, Max Frame = " << 1 / nFrameMin << "fps, and Frame Intervall = " << nFrameIntervall << "fps." << endl;
	}
	else {
		cout << "Failed to get nFrameMin, nFrameMax, nFrameIntervall." << endl;
		return IS_NO_SUCCESS;
	}
	*/

	// 设置、并查询 当前相机采集帧率
	double nFramesPerSecondCurrent;
	nRet = is_SetFrameRate(hCam, nFramePerSecondSet, &nFramesPerSecondCurrent);
	if (nRet == IS_SUCCESS) {
		cout << "Success, user set frame is: " << nFramePerSecondSet << "fps, and current frame is : " << (nFramesPerSecondCurrent) << "fps" << endl;
	}
	else {
		cout << "Failed to get current frame" << endl;
		return IS_NO_SUCCESS;
	}
	cout << "***********************************************************************************" << endl;

	// 曝光时间查询与设置
	double nExposureTimeDefault, nExposureTimeCurrent;
	/*  
	// 获取曝光时间范围查询 这个通过和IDS程序对比,是对的
	//Success, and exposure time range is : min = 0.00898246 ms, max = 19.9832ms, delta = 0.0158596ms. IS_EXPOSURE_CMD_GET_EXPOSURE_RANGE
	//Success, and exposure time range is : min = 0.00898246 ms, max = 19.9832ms, delta = 0.000140351ms. IS_EXPOSURE_CMD_GET_FINE_INCREMENT_RANGE
	double nExposureTimeRange;
	double dblRange[3];
	nRet = is_Exposure(hCam, IS_EXPOSURE_CMD_GET_FINE_INCREMENT_RANGE, (void*)dblRange, sizeof(dblRange));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and exposure time range is : min = " << (dblRange[0]) << " ms, max = "
			<< (dblRange[1]) << "ms, delta = " << (dblRange[2]) << "ms." << endl;
	}
	else {
		cout << "Failed to get exposure time range" << endl;
		return IS_NO_SUCCESS;
	}
    
	// 获取默认曝光时间
	nRet = is_Exposure(hCam, IS_EXPOSURE_CMD_GET_EXPOSURE_DEFAULT, &nExposureTimeDefault, sizeof(nExposureTimeDefault));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and default exposure time is : " << nExposureTimeDefault << " ms." << endl;
	}
	else {
		cout << "Failed to get default exposure time" << endl;
		return IS_NO_SUCCESS;
	}
	*/
	// 设置曝光时间(单位:ms)
	nRet = is_Exposure(hCam, IS_EXPOSURE_CMD_SET_EXPOSURE, &nExposureTimeSet, sizeof(nExposureTimeSet));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and set exposure time is : " << nExposureTimeSet << " ms." << endl;
	}
	else {
		cout << "Failed to set exposure time" << endl;
		return IS_NO_SUCCESS;
	}
	// 再次获取曝光时间,确保设置成功
	nRet = is_Exposure(hCam, IS_EXPOSURE_CMD_GET_EXPOSURE, &nExposureTimeCurrent, sizeof(nExposureTimeCurrent));
	if (nRet == IS_SUCCESS) {
		cout << "Success, and current exposure time is : " << nExposureTimeCurrent << " ms." << endl;
	}
	else {
		cout << "Failed to get current exposure time" << endl;
		return IS_NO_SUCCESS;
	}

	cout << "***********************************************************************************" << endl;

	return IS_SUCCESS;
}

保存图像函数设置

javascript 复制代码
int SaveIdsImage(HIDS hCam, wchar_t* fileName) {
	IMAGE_FILE_PARAMS ImageFileParams;
	// 每次保存图像之前,都要先进行清空指针
	ImageFileParams.pwchFileName = NULL;
	ImageFileParams.pnImageID = NULL;
	ImageFileParams.ppcImageMem = NULL;
	ImageFileParams.nQuality = 0;
	//保存活动内存中的bmp图像,图像画质为80(不含"打开文件"对话框)
	ImageFileParams.pwchFileName = fileName; // L"test.bmp";
	ImageFileParams.nFileType = IS_IMG_BMP;
	ImageFileParams.nQuality = 80;
	int nRet = is_ImageFile(hCam, IS_IMAGE_FILE_CMD_SAVE, (void*)&ImageFileParams, sizeof(ImageFileParams));
	return nRet;
}

电控位移台头文件

javascript 复制代码
#ifndef MOVERLIBRARY_H
#define MOVERLIBRARY_H

#ifdef __cplusplus
#ifdef MOVER_LIBRARY
#define DLL_EXPORT extern "C" __declspec( dllexport )
#else
#define DLL_EXPORT extern "C" __declspec( dllimport )
#endif
#else
#define DLL_EXPORT
#endif

#define MOVE_CODE_STOP      0x01    // 停止
#define MOVE_CODE_RESTORE   0x02    // 回原点
#define MOVE_CODE_DRIVE_R   0x04    // 向右相对运动
#define MOVE_CODE_DRIVE_L   0x05    // 向左相对运动
#define MOVE_CODE_MOVE      0x06    // 运动到指定位置
#define MOVE_CODE_JOG_R     0x07    // 向右步进
#define MOVE_CODE_JOG_L     0x08    // 向左步进

DLL_EXPORT int listPorts(char *serialNo, unsigned int len); // 列举串口设备
DLL_EXPORT int open(char *serialNo);                        // 打开串口设备
DLL_EXPORT int isOpen(char *serialNo);                      // 检查串口设备是否已打开
DLL_EXPORT int close(int handle);                           // 关闭串口设备
DLL_EXPORT int read(int handle, char *data, int len);       // 读取数据
DLL_EXPORT int write(int handle, char *data, int len);      // 写入数据

// 设置D寄存器
DLL_EXPORT int move(int handle, int ID, int func);              // 运行
DLL_EXPORT int setSpeed(int handle, int ID, float speed);       // 设置速度
DLL_EXPORT int setAcceleration(int handle, int ID, float Acc);  // 设置加速度
DLL_EXPORT int setAbsoluteDisp(int handle, int ID, float disp); // 设置绝对位移量
DLL_EXPORT int setRelativeDisp(int handle, int ID, float disp); // 设置相对位移量
DLL_EXPORT int setJogTime(int handle, int ID, int time);        // 设置步进时间
DLL_EXPORT int setJogStep(int handle, int ID, float step);      // 设置步进步长
DLL_EXPORT int setJogDelay(int handle, int ID, int delay);      // 设置步进延迟

DLL_EXPORT int setInputEnable(int handle, int ID, int enable);              // 输入有效
DLL_EXPORT int setOutputEnable(int handle, int ID, int enable);             // 输出有效
DLL_EXPORT int setAxisEnable(int handle, int ID, int enable);               // 轴使能
DLL_EXPORT int setRelativePosEnable(int handle, int ID, int enable);        // 相对位置值使能

// 只读
DLL_EXPORT int getDoingState(int handle, int ID);           // 运动状态
DLL_EXPORT int getPositiveLimitEnable(int handle, int ID);  // 限位+
DLL_EXPORT int getNegativeLimitEnable(int handle, int ID);  // 限位-
DLL_EXPORT int getOriginEable(int handle, int ID);          // 原点
DLL_EXPORT int getDeviceCode(int handle);                   // 控制器设备型号(支持的轴数量)

// 获取寄存器值
DLL_EXPORT float getSpeed(int handle, int ID);          // 速度
DLL_EXPORT float getAcceleration(int handle, int ID);   // 加速度
DLL_EXPORT float getAbsoluteDisp(int handle, int ID);   // 绝对位移量
DLL_EXPORT float getRelativeDisp(int handle, int ID);   // 相对位移量
DLL_EXPORT int getJogTime(int handle, int ID);          // 步进时间
DLL_EXPORT float getJogStep(int handle, int ID);        // 步进步长
DLL_EXPORT int getJogDelay(int handle, int ID);         // 步进延迟

DLL_EXPORT int getAxisType(int handle, int ID);             // 轴类型
DLL_EXPORT float GetCurrentPos(int handle, int ID, int *ok);// 当前位置
DLL_EXPORT int getInputEnable(int handle, int ID);          // 输入有效
DLL_EXPORT int getOutputEnable(int handle, int ID);         // 输出有效
DLL_EXPORT int getAxisEnable(int handle, int ID);           // 轴使能
DLL_EXPORT int getRelativePosEnable(int handle, int ID);    // 相对位置值使能,未使能时,drive不需要设置位移量

DLL_EXPORT int getAllModels(char* modelName, int len);      // 获取所有位移台型号名称用","隔开
DLL_EXPORT int initAxis(int handle, int ID, char* model, int AxisCount);   // 初始化轴
DLL_EXPORT int getErrorCode(int handle, int ID);            // 获取错误代码

#endif // MOVERLIBRARY_H

电控位移台设置参数

javascript 复制代码
// 位移台运动函数设置:
int LBtekMoverParaSet() {
	cout << "***************** Loading moverLibrary_x64.dll, Get and Init Mover Device. *******************" << endl;
	// 运行时加载DLL库
	HMODULE dllHandle = LoadLibrary(L"moverLibrary_x64.dll");
	if (dllHandle == NULL)
	{
		printf("加载DLLTest1.dll动态库失败\n");
		return -1;
	}
	// 获取listPorts函数地址
	MyFunctionType1 GetMoverSerialList = (MyFunctionType1)GetProcAddress(dllHandle, "listPorts");
	/*if (GetMoverSerialList == NULL) {
		std::cout << "Failed to get function address." << std::endl;
		return -1;
	}*/
	// 通过GetMoverSerialList调用函数listPorts 
	
	ret = GetMoverSerialList(serialList, 1024);		// 获取当前电脑所有已连接的串口设备名称列表 我们的应该是COM5
	if (ret >= 0) {
		cout << "Success to get Serial List:" << serialList << std::endl;
	}
	else {
		cout << "Failed to get Serial List!" << std::endl;
		return -1;
	}
	// 获取位移台型号名称列表 我们的应该是 EM-LSS65-50C1
	MyFunctionType1 GetMoverModelsList = (MyFunctionType1)GetProcAddress(dllHandle, "getAllModels");
	ret = GetMoverModelsList(modelsList, 1024);
	if (ret >= 0) {
		cout << "Success to get Mover Model List:" << modelsList << std::endl;
	}
	else {
		cout << "Failed to get Mover Model List!" << std::endl;
		return -1;
	}

	// 打开串口设备             
	MyFunctionType2 OpenMoverEmcvx = (MyFunctionType2)GetProcAddress(dllHandle, "open");	// 打开串口设备int open(char *serialNo); 
	m_handle = OpenMoverEmcvx(name);
	if (ret > 0) {
		cout << "Success to Open Emcvx." << endl;
	}
	else {
		cout << "Failed to Open Emcvx." << endl;
		return -1;
	}
	/*
	// 检查串口设备是否正确打开
	MyFunctionType2 CheckMoverEmcvxState = (MyFunctionType2)GetProcAddress(dllHandle, "isOpen");	//
	if (CheckMoverEmcvxState(name) > 0) {
		cout << "Success to Open Emcvx.(Check)" << endl;
	}
	else {
		cout << "Failed to Open Emcvx.(Check)" << endl;
		return -1;
	}
	*/
	// 初始化轴设备 Axis1
	MyFunctionType7 InitMoverAxis = (MyFunctionType7)GetProcAddress(dllHandle, "initAxis");
	MyFunctionType8 GetMoverDeviceCode = (MyFunctionType8)GetProcAddress(dllHandle, "getDeviceCode");
	int axisCount = GetMoverDeviceCode(m_handle);	// 获取轴设备数量
	cout << "Axis Count = " << axisCount << endl;

	if (InitMoverAxis(m_handle, ID, modelName, axisCount) >= 0) {
		cout << "Success to Init Mover Axis 1." << endl;
	}
	else {
		cout << "Failed to Init Mover Axis 1." << endl;
		return -1;
	}
	cout << "************************************ Setting Device Paras. ******************************" << endl;

	// 设置运行速度
	MyFunctionType5 SetMoverSpeed = (MyFunctionType5)GetProcAddress(dllHandle, "setSpeed");
	if (SetMoverSpeed(m_handle, ID, speed) == 0) {
		cout << "Success to Set Mover Speed at " << speed << endl;
	}
	else {
		cout << "Failed to Set Mover Speed." << endl;
		return -1;
	}
	// 设置运行加速度
	MyFunctionType5 SetMoverAcceleration = (MyFunctionType5)GetProcAddress(dllHandle, "setAcceleration");
	if (SetMoverAcceleration(m_handle, ID, Acc) == 0) {
		cout << "Success to Set Mover Acc at " << Acc << endl;
	}
	else {
		cout << "Failed to Set Mover Acc." << endl;
		return -1;
	}
	// 设置运行相对位移量
	MyFunctionType5 SetMoverRelativeDisp = (MyFunctionType5)GetProcAddress(dllHandle, "setRelativeDisp");
	if (SetMoverRelativeDisp(m_handle, ID, disp) == 0) {
		cout << "Success to Set Mover Relative Disp at " << disp << "mm" << endl;
	}
	else {
		cout << "Failed to Set Mover Relative Disp." << endl;
		return -1;
	}
	// 设置步进次数
	MyFunctionType4 SetMoverJogTime = (MyFunctionType4)GetProcAddress(dllHandle, "setJogTime");
	if (SetMoverJogTime(m_handle, ID, times) == 0) {
		cout << "Success to Set Mover Jog Times at " << times << endl;
	}
	else {
		cout << "Failed to Set Mover Jog Times." << endl;
		return -1;
	}
	// 设置步进步长
	MyFunctionType5 SetMoverJogStep = (MyFunctionType5)GetProcAddress(dllHandle, "setJogStep");
	if (SetMoverJogStep(m_handle, ID, step) == 0) {
		cout << "Success to Set Mover Jog Step at " << speed << endl;
	}
	else {
		cout << "Failed to Set Mover Jog Step." << endl;
		return -1;
	}
	// 设置步进延时
	MyFunctionType4 SetMoverJogDelay = (MyFunctionType4)GetProcAddress(dllHandle, "setJogDelay");
	if (SetMoverJogDelay(m_handle, ID, times) == 0) {
		cout << "Success to Set Mover Jog Delay at " << delay << "ms" << endl;
	}
	else {
		cout << "Failed to Set Mover Jog Delay." << endl;
		return -1;
	}
	// 设置位移台轴使能
	MyFunctionType4 SetMoverAxisEnable = (MyFunctionType4)GetProcAddress(dllHandle, "setAxisEnable");
	if (SetMoverAxisEnable(m_handle, ID, 0x01) == 0) {	// 0x00未使能; 0x01使能
		cout << "Success to Enable Mover." << endl;
	}
	else {
		cout << "Failed to Enable Mover." << endl;
		return -1;
	}
	// 获取位移台运行状态
	MyFunctionType6 GetMoverDoingState = (MyFunctionType6)GetProcAddress(dllHandle, "getDoingState");
	ret = GetMoverDoingState(m_handle, ID);
	if (ret == 0x01) {
		cout << "Success to get the state of Mover, and it is running." << endl;
	}
	else if (ret == 0) {
		cout << "Success to get the state of Mover, but it isn't running." << endl;
	}
	else {
		cout << "Failed to get the state of Mover!" << endl;
		return -1;
	}
	// 获取位移台当前位置 float GetCurrentPos(int handle, int ID, int *ok);// 当前位置
	MyFunctionType3 GetMoverCurrentPos = (MyFunctionType3)GetProcAddress(dllHandle, "GetCurrentPos");
	int signOK = 0;
	currentPos = GetMoverCurrentPos(m_handle, ID, &signOK);
	if (currentPos >= 0) {	// 0x00未使能; 0x01使能
		cout << "Success to Get Mover Current Position at " << currentPos << " mm" << endl;
	}
	else {
		cout << "Failed to Get Mover Current Position." << endl;
		return -1;
	}
	// 获取位移台正方向限位状态
	MyFunctionType6 GetMoverPositiveLimitEnable = (MyFunctionType6)GetProcAddress(dllHandle, "getPositiveLimitEnable");
	int limitMaxState = GetMoverPositiveLimitEnable(m_handle, ID);
	if (limitMaxState == 0x01) {
		cout << "Warrning! It is Located in limitMaxState!" << endl;
	}
	else if (ret == 0) {
		cout << "Success to check, it isn't at limitMaxState." << endl;
	}
	else {
		cout << "Failed to get the state of limitMaxState!" << endl;
		return -1;
	}
	// 获取位移台负方向限位状态
	MyFunctionType6 GetMoverNegativeLimitEnable = (MyFunctionType6)GetProcAddress(dllHandle, "getNegativeLimitEnable");
	int limitMinState = GetMoverNegativeLimitEnable(m_handle, ID);
	if (limitMinState == 0x01) {
		cout << "Warrning! It is Located in limitMinState!" << endl;
	}
	else if (ret == 0) {
		cout << "Success to check, it isn't at limitMinState." << endl;
	}
	else {
		cout << "Failed to get the state of limitMinState!" << endl;
		return -1;
	}

	// 使能位移台运动
	MyFunctionType4 MoverEnable = (MyFunctionType4)GetProcAddress(dllHandle, "move");	// int move(int handle, int ID, int func);	

	/*
    // func: 0x01停止	0x02回原点	 0x04正方向推进	0x05负方向推进	0x06移动到指定位置	0x07正反向步进	0x08负方向步进
	ret = MoverEnable(m_handle, ID, 0x07);
	if (ret == 0) {
		cout << "Success, Jog move." << endl;
	}
	else {
		cout << "Failed to Jog Move!" << endl;
		return -1;
	}
	*/

	// 卸载DLL文件  
	FreeLibrary(dllHandle);
	return 0;
}

最后就是通过main函数进行调用和控制,需要注意的是,最好能加上sleep函数,这样采集图像不容易出问题

如果有更多需求,欢迎大家私信交流。

相关推荐
lu_rong_qq4 分钟前
决策树 DecisionTreeClassifier() 模型参数介绍
算法·决策树·机器学习
Elihuss1 小时前
ONVIF协议操作摄像头方法
开发语言·php
Swift社区4 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht5 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht5 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20245 小时前
Swift 数组
开发语言
南东山人5 小时前
一文说清:C和C++混合编程
c语言·c++
stm 学习ing6 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
LNTON羚通6 小时前
摄像机视频分析软件下载LiteAIServer视频智能分析平台玩手机打电话检测算法技术的实现
算法·目标检测·音视频·监控·视频监控
湫ccc7 小时前
《Python基础》之字符串格式化输出
开发语言·python