在C++与python间传视频帧
目录
- 引言
- 1.进程间通信
- 2.基于共享内存的视频传输
- 2.1 C++之间的通信
- 2.1.1 接口函数
- 2.1.2 创建数据格式和共享内存信息
- 2.1.3 C++之间共享内存通信
- 2.1.4 C++之间共享内存通信视频测试结果
- 2.2 C++和python间视频通信
- 2.2.1 接口函数
- 2.2.1 C++与python之间共享内存通信
- 3.基于Socket的视频传输
- 3.1 cpp端socket
- 3.2 python端
- 3.3 CMakeList
- 3.4 测试结果
- 4 结论
引言
本案例旨在实现跨语言(C++和python间)视频的实时通信。这一工作内容在实际工程中很常见。由于python语言支持很多第三方库,对于开发深度学习项目很方便,验真算法速度快,很多开源算法也大多基于python实现。这时可能就会出现C++的代码借助python语言做一些图像处理(包括目标检测、姿态估计、目标跟踪等任务)的需求。
平台环境:
- Win10
- VS2019
- OpenCV
进程间通信方式:共享内存
1.进程间通信
进程间通信方式有很多种。工程上最常用的是共享内存和socket机制。前者效率高,基本思想就是开辟一块公共的内存空间,供两个或多个进程之间使用。为了标识这个公共空间,要给它起个名。但是共享内存的方式不支持多平台。而socket刚好就是支持多平台间进程通信的方式。当然这种方式也会慢一些。
在本案例中,分别尝试了两种方式。虽然最终共享内存的方式写内存帧率只达到15fps左右,但是要比socket快了近20倍(大概0.5-1fps左右)。下面将介绍这两种机制的具体实现过程。
2.基于共享内存的视频传输
2.1 C++之间的通信
2.1.1 接口函数
首先验证C++之间能通信。这里使用的是CreateFileMapping和MapViewOfFile进行共享内存的创建和映射。
其中CreateFileMapping的接口为,参数含义详解请点击链接。
HANDLE CreateFileMapping(HANDLE hFile,LPSECURITY_ATTRIBUTES lpFileMappingAttributes,DWORD flProtect,DWORD dwMaximumSizeHigh,DWORD dwMaximumSizeLow,LPCSTR lpName
);
MapViewOfFile的接口为
LPVOID MapViewOfFile(HANDLE hFileMappingObject,DWORD dwDesiredAccess,DWORD dwFileOffsetHigh,DWORD dwFileOffsetLow,SIZE_T dwNumberOfBytesToMap
);
2.1.2 创建数据格式和共享内存信息
首先需要一个图像的头部
typedef struct {int width;int height;int type;
}ImgInf; //图像信息
由于sizeof(int)=4,所以这里ImgInf结构体大小为12B。在进行共享内存映射时,我们需要这个大小去做偏移量,找到图像数据。
接下来要定义图像的数据信息
#define FRAME_NUMBER 1 // 图像路数
#define FRAME_W 1920
#define FRAME_H 1080
#define FRAME_W_H FRAME_W*FRAME_H
// 图像分辨率:彩色图(3通道)+图像信息结构体
#define FRAME_SIZE FRAME_W_H*sizeof(unsigned char)*3+sizeof(ImgInf)#define MEMORY_SIZE FRAME_NUMBER*FRAME_SIZE
定义共享内存类SHAREDMEMORY
class SHAREDMEMORY
{public:SHAREDMEMORY();~SHAREDMEMORY();//void SendBox(TrackBox& BOX);//void RecBox(TrackBox& BOX);//void SendVectorBox(vector<TrackBox>& VTrackBox);//void RecieveVectorBox(vector<TrackBox>& VTrackBox);void SendMat(cv::Mat img, char indexAddress);cv::Mat ReceiveMat(char indexAddress);void SendStr(const char data[]); char* ReceiveStr();public:int state;
private:HANDLE hShareMem; //共享内存句柄TCHAR sShareMemName[30] = TEXT("CppPytonSharedFrame"); // 共享内存名称LPCTSTR pBuf;
};
其中SendMat为图像数据发送,ReceiveMat为图像接收。
SendStr为字符串发送,ReceiveStr为字符串接收。
最后的ShareMemory.h文件如下:
#pragma once
// ShareMemory.h : 此文件包含共享内存数据定义、大小确定、位置分配、信息定义
// Author : Jiejing.Ma
// Update : 2020/11/27
#ifndef ShareMemory_H
#define ShareMemory_H#include <opencv2/core.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp> // cv::Canny()
#include <opencv2/opencv.hpp>#include <Windows.h>//=================================共享内存数据定义=================================
typedef struct {int width;int height;int type;
}ImgInf; //图像信息
//=================================共享内存大小确定=================================
// 为图像分配空间
#define FRAME_NUMBER 1 // 图像路数
#define FRAME_W 1920
#define FRAME_H 1080
#define FRAME_W_H FRAME_W*FRAME_H
// 图像分辨率:彩色图(3通道)+图像信息结构体
#define FRAME_SIZE FRAME_W_H*sizeof(unsigned char)*3+sizeof(ImgInf)#define MEMORY_SIZE FRAME_NUMBER*FRAME_SIZE//=================================共享内存信息定义=================================
#define INITSUCCESS 0
#define CREATEMAPFAILED 1
#define MAPVIEWFAILED 2class SHAREDMEMORY
{public:SHAREDMEMORY();~SHAREDMEMORY();void SendMat(cv::Mat img, char indexAddress);cv::Mat ReceiveMat(char indexAddress);void SendStr(const char data[]);char* ReceiveStr();public:int state;
private:HANDLE hShareMem; //共享内存句柄TCHAR sShareMemName[30] = TEXT("ShareMedia"); // 共享内存名称LPCTSTR pBuf;
};#endif // !ShareMemory_H
对应的ShareMemory.cpp文件为类的实现。
#pragma once
// ShareMemory.cpp : 此文件包含信息定义SHAREDMEMOR类的实现
// Author : MJJ
// Update : 2020/11/27
#ifndef ShareMemory_CPP
#define ShareMemory_CPP#include "ShareMemory.h"
#include <iostream>using namespace cv;
using namespace std;/*************************************************************************************
FuncName :SHAREDMEMORY::~SHAREDMEMORY()
Desc :构造函数创建共享内存
Input :None
Output :None
**************************************************************************************/
SHAREDMEMORY::SHAREDMEMORY() {hShareMem = CreateFileMapping(INVALID_HANDLE_VALUE, // use paging fileNULL, //default securityPAGE_READWRITE, //read/write access0, // maximum object size(high-order DWORD)MEMORY_SIZE, //maximum object size(low-order DWORD)sShareMemName); //name of mapping objectif (hShareMem) {// 映射对象视图,得到共享内存指针,设置数据pBuf = (LPTSTR)MapViewOfFile(hShareMem, //handle to map objectFILE_MAP_ALL_ACCESS, // read/write permission0,0,MEMORY_SIZE);cout << "memory size:" << MEMORY_SIZE<< endl;// 若映射失败退出if (pBuf == NULL){std::cout << "Could not map view of framebuffer file." << GetLastError() << std::endl;CloseHandle(hShareMem);state = MAPVIEWFAILED;}}else{std::cout << "Could not create file mapping object." << GetLastError() << std::endl;state = CREATEMAPFAILED;}state = INITSUCCESS;
}/*************************************************************************************
FuncName :SHAREDMEMORY::~SHAREDMEMORY()
Desc :析构函数释放
Input :None
Output :None
**************************************************************************************/
SHAREDMEMORY::~SHAREDMEMORY() {std::cout << "unmap shared addr." << std::endl;UnmapViewOfFile(pBuf); //释放;CloseHandle(hShareMem);
}/*************************************************************************************
FuncName :void SHAREDMEMORY::SendMat(cv::Mat img, char indexAddress)
Desc :发送Mat数据
Input :Mat img 发送图像char indexAddress 共享内存中起始位置,若只有一路视频则无偏移
Output :None
**************************************************************************************/
void SHAREDMEMORY::SendMat(cv::Mat img, char indexAddress) {ImgInf img_head;img_head.width = img.cols;img_head.height = img.rows;img_head.type = img.type();if (img_head.type == CV_64FC1) {memcpy((char*)pBuf + indexAddress, &img_head, sizeof(ImgInf));memcpy((char*)pBuf + indexAddress + sizeof(ImgInf), // Address of dstimg.data, // Src dataimg.cols * img.rows * img.channels() * sizeof(double) // size of data);}else{memcpy((char*)pBuf + indexAddress, &img_head, sizeof(ImgInf));memcpy((char*)pBuf + indexAddress + sizeof(ImgInf), // Address of dstimg.data, // Src dataimg.cols * img.rows * img.channels() // size of data); }cout << "write shared mem successful." << endl;
}/*************************************************************************************
FuncName :cv::Mat SHAREDMEMORY::ReceiveMat(char indexAddress)
Desc :接收Mat数据
Input :char indexAddress 共享内存中起始位置,若只有一路视频则无偏移
Output :Mat图像
**************************************************************************************/
cv::Mat SHAREDMEMORY::ReceiveMat(char indexAddress)
{ImgInf img_head;cv::Mat img;memcpy(&img_head, (char*)pBuf + indexAddress, sizeof(ImgInf));img.create(img_head.height, img_head.width, img_head.type);if (img_head.type == CV_64FC1){memcpy(img.data, (char*)pBuf + indexAddress + sizeof(ImgInf), img.cols * img.rows * img.channels() * sizeof(double));}else{memcpy(img.data, (char*)pBuf + indexAddress + sizeof(ImgInf), img.cols * img.rows * img.channels());}return img;
}/*************************************************************************************
FuncName :void SHAREDMEMORY::SendStr(cv::Mat img, char indexAddress)
Desc :发送str数据
Input :Mat img 发送图像char indexAddress 共享内存中起始位置,若只有一路视频则无偏移
Output :None
**************************************************************************************/
void SHAREDMEMORY::SendStr(const char data[]) {memcpy((char*)pBuf, data, sizeof(data));cout << "write shared mem successful." << endl;getchar();
}/*************************************************************************************
FuncName :void SHAREDMEMORY::ReceiveStr()
Desc :接收str数据
Input :None
Output :获取的字符串
**************************************************************************************/
char* SHAREDMEMORY::ReceiveStr(){char* str= (char*)pBuf;cout << "receive is:"<< str << endl;return str;
}
#endif // !ShareMemory_CPP
2.1.3 C++之间共享内存通信
创建一个新的工程,导入上面两个文件,并创建WriteMem.cpp文件
// WriteMem.cpp : 此文件为写共享内存
// Author : Jiejing.Ma
// Update : 2020/11/27#include <iostream>
#include "ShareMemory.h"using namespace std;
using namespace cv;// 读图片或视频
void send_img(SHAREDMEMORY sharedsend)
{int index = 0;int64 t0 = cv::getTickCount();;int64 t1 = 0;string fps = "";int nFrames = 0;cv::Mat frame;cout << "Opening video..." << endl;VideoCapture cap("test.flv");while (cap.isOpened()) {cap >> frame;if (frame.empty()){std::cerr << "ERROR: Can't grab video frame." << endl;break;}resize(frame, frame, Size(FRAME_W, FRAME_H));nFrames++;if (!frame.empty()) {if (nFrames % 10 == 0){const int N = 10;int64 t1 = cv::getTickCount();fps = " Send FPS:" + to_string((double)getTickFrequency() * N / (t1 - t0)) + "fps"; t0 = t1;}cv::putText(frame, fps, Point(100, 100), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(255, 255, 255),1);}sharedsend.SendMat(frame, index * FRAME_NUMBER);if ((waitKey(1) & 0xFF) == 'q') break;}
}int main()
{SHAREDMEMORY sharedmem;//char str[] = "hello";if (sharedmem.state == INITSUCCESS) send_img(sharedmem);//if (sharedmem.state == INITSUCCESS) sharedmem.SendStr(str);return 0;
}
创建一个新的工程,导入上面两个文件,并创建ReadMem.cpp文件
// ReadMem.cpp : 此文件为读共享内存
// Author : Jiejing.Ma
// Update : 2020/11/27#include <Windows.h>
#include <iostream>
#include "ShareMemory.h"
#include <string>using namespace std;
using namespace cv;int main(int argc, char** argv)
{int index=0;SHAREDMEMORY sharemem;if (sharemem.state == INITSUCCESS) {// read video frame from shared memory.sint64 t0 = cv::getTickCount();;int64 t1 = 0;string fps = "";int nFrames = 0;namedWindow("ReadMemShow", 0);while (true){nFrames++;Mat frame = sharemem.RecieveMat(index * FRAME_NUMBER);if (!frame.empty()) {if (nFrames % 10 == 0){const int N = 10;int64 t1 = cv::getTickCount();fps = " Average FPS:" + to_string((double)getTickFrequency() * N / (t1 - t0)) + "fps";t0 = t1;}cv::putText(frame, fps, Point(100, 200), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(100, 200, 200),1);imshow("ReadMemShow", frame); }if((waitKey(1) & 0xFF) == 'q') break;}//char* str = sharemem.RecieveStr();}destroyAllWindows();return 0;
}
同时开启两个工程,则可以接收视频了。
2.1.4 C++之间共享内存通信视频测试结果
这里看到,写共享内存速度为15fps,读共享内存速度为65fps(超实时),写速度主要的影响因素与opecv有关。如果优化,还需改视频编解码部分。
2.2 C++和python间视频通信
这里以C++作为发送端,python作为接受端。逆向过程还有待测试。网上有教程提到python不能创建共享内存作为发送端,这种说法是错的。本人已测试过,只是发送数据都是字符串型,对于图像数据还有待研究。
2.2.1 接口函数
这里主要用到的是mmap和numpy的frombuffer.
关于mmap,请参考官网的接口说明。十分详细,不再赘述。
frombuffer:
numpy.frombuffer(buffer, dtype=float, count=-1, offset=0)
Interpret a buffer as a 1-dimensional array.
Parameters
bufferbuffer_like
An object that exposes the buffer interface.
dtypedata-type, optional
Data-type of the returned array; default: float.
countint, optional
Number of items to read. -1 means all data in the buffer.
offsetint, optional
Start reading the buffer from this offset (in bytes); default: 0.
参考官网
2.2.1 C++与python之间共享内存通信
前面已经实现C++代码。不需要改动。只需启动写共享内存工程即可。
python代码如下:
import mmap
import os
import cv2
import numpy as np# -----------------Define info in ShareMemory.h-----------------
IMG_HEAD_OFFSET = 12
# typedef struct {# int width;
# int height;
# int type;
# }ImgInf; //图像信息12字节FRAME_NUMBER = 1
FRAME_W = 1920
FRAME_H = 1080
FRAME_W_H = FRAME_W * FRAME_H
FRAME_SIZE = FRAME_W_H * 3
MEMORY_SIZE = (FRAME_SIZE + IMG_HEAD_OFFSET) * FRAME_NUMBERsShareMemName = "ShareMedia"if __name__ == "__main__":fpx = mmap.mmap(-1, FRAME_SIZE+IMG_HEAD_OFFSET, sShareMemName)# Read img as numpycv2.namedWindow("python_sharedmem_show",0)t0 = cv2.getTickCount()N = 50nFrame = 0fps = 0while 1:nFrame += 1img = np.frombuffer(fpx, dtype=np.uint8)img = img[IMG_HEAD_OFFSET:FRAME_SIZE+IMG_HEAD_OFFSET]img = img.reshape((FRAME_H,FRAME_W,3))# Print Average FPSif nFrame % 50 == 0:t1 = cv2.getTickCount()fps = N*cv2.getTickFrequency() / (t1 - t0)t0 = t1cv2.putText(img, "Average FPS:" + str(fps) + "fps", (100, 200), cv2.FONT_HERSHEY_COMPLEX, 1, (100, 200, 200), 1)cv2.imshow("python_sharedmem_show", img)img = Noneif cv2.waitKey(1) & 0xFF == ord('q'):breakcv2.destroyAllWindows()
3.基于Socket的视频传输
这里是基于Linux开发的Socket通信。而共享内存是基于Windows平台。
3.1 cpp端socket
cppsocket.cpp
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>#include <errno.h>
#include <string>
#include <iostream>
#include <vector>#include <opencv2/opencv.hpp>using namespace cv;
using namespace std;int main(int argc, char **argv)
{// 定义socket信息char *servInetAddr = "192.168.113.173";int servPort = 8081;int connfd;struct sockaddr_in addr;// 创建socketconnfd = socket(AF_INET,SOCK_STREAM, 0);if (connfd == -1){cout<<"socket创建失败"<<endl;exit(-1);}// 准备通信地址addr.sin_family=AF_INET;addr.sin_port=htons(servPort);addr.sin_addr.s_addr = inet_addr(servInetAddr);// bindint res = connect(connfd,(struct sockaddr*)&addr,sizeof(addr));if(res==-1){cout<<"bind连接失败"<<endl;exit(-1);}cout<<"bind连接成功"<<endl;// 获取视频帧并发送Mat img;VideoCapture capture("./test.flv");vector<uchar> data_encode;while(capture.isOpened()){if(!capture.read(img)) break;imencode(".jpg",img,data_encode);int len_encode = data_encode.size();string len = to_string(len_encode);int length = len.length(); for (int i=0;i<16-length;i++) len=len+' ';// 发送数据send(connfd,len.c_str(),strlen(len.c_str()),0);char send_char[1];for (int i=0;i<len_encode;i++){send_char[0]=data_encode[i];send(connfd,send_char,1,0);}// 接收返回信息char recvBuf[32] = "";if(recv(connfd, recvBuf,32,0)) cout<<recvBuf<<endl;}close(connfd);return 0;}
3.2 python端
pysocket.py
import socket
import cv2
import numpy
import timedef recv_size(sock, count):buf=b''while count:newbuf = sock.recv(count)if not newbuf: return Nonebuf +=newbufcount -= len(newbuf)return bufdef recv_all(sock, count):buf = b''while count:newbuf = sock.recv(1)if not newbuf:return Nonebuf +=newbufcount -= len(newbuf)return buf# 创建socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 准备通信地址
address = ('192.168.113.173',8081)
s.bind(address)
s.listen(True)
print('Waiting for images...')
# 接受TCP链接并返回(conn, addr),其中conn是新的套接字对象,可以用来接收和发送数据,addr是链接客户端的地址。
conn, addr = s.accept()
n = 0while 1:n +=1length = recv_size(conn,16).decode()t0=time.time()if isinstance(length,str): # 若成功接收指定大小信息,进一步接收整张图string_data = recv_all(conn,int(length))data = numpy.fromstring(string_data,dtype='uint8')decimg = cv2.imdecode(data,1)cv2.namedWindow('python-recv')cv2.imshow('python-recv',decimg)if cv2.waitKey(1) & 0xFF == ord('q'):print("111111")breakt1 = time.time()print('Image recieved successfully!fps:'+str(1/(t1-t0)))conn.send('recieved messages!'.encode())t0=t1if cv2.waitKey(1) & 0xFF == ord('q'):breaks.close()
cv2.destroyAllWindows()
3.3 CMakeList
Linux 下编译CPP文件,这里使用CMakeList:
cmake_minimum_required(VERSION 3.0.0)
project(client VERSION 0.1.0)include(CTest)
enable_testing()# find opencv and link
find_package(OpenCV REQUIRED)
message(STATUS "Opencv library status:")
message(STATUS " version:${OpenCV_VERSION}")
message(STATUS " libraries:${OpenCV_LIBS}")
message(STATUS " include path:${OpenCV_INCLUDE_DIRS}")
include_directories(${OpenCV_INCLUDE_DIRS})
link_libraries(${OpenCV_LIBS})add_executable(client cppsocket.cpp)set(CMAKE_CXX_FLAGE "${CMAKE_CXX_FLAGE} -g")
3.4 测试结果
结果就是速度超级慢,大概一秒多一帧。
4 结论
C++和python之间通信,可以采用C++调python的方式。请参考之前的文章。Ubuntu下C++调python
这种方式,从架构的角度来讲,最简单。工程量和已有经验的角度,emm可能坑比较多。速度也应该最快(推测)
也可以使用进程间通信。当然这个成本就高了。有两种方式,一是共享内存机制,一是socket通信。前者更快,但只能在一个平台上。后者慢,可以支持不同电脑间通信。
最后关于基于共享内存方式,影响速度的主要是写共享内存,而这又与opencv读视频有关,与视频编解码有关。想要提高写内存速度,需要从底层修改视频编解码。可以参考UE4的相关插件解决。
在C++与python间传视频帧相关推荐
- python批量提取视频帧
python批量提取视频帧 python批量提取视频帧,两种提取方式: 按帧数提取,每个视频提取固定帧数,若所取帧数超过视频总帧数,则截取视频所有帧 按时间间隔提取,每个time提取一帧 1. 使用示 ...
- Python 抽取剔除视频帧工具
前言 正好有人问我怎么把视频某几帧去掉,正好有时间,正好写了,正好发出来. 大家想用的话可以拿去参考. 代码 下面是我使用opencv对视频中间几帧抽取的方法. 主要的思路是在读取frame的时候,顺 ...
- python快速检测视频跳过帧_使用Python实现跳帧截取视频帧
本文实例为大家分享了Python跳帧截取视频帧的具体代码,供大家参考,具体内容如下 可以自由设定时长来截取视频,经实测效果理想.期间遇到的一个麻烦是我的视频文件在D:盘,在原视频D盘目录上不能保存截取 ...
- 视频抽帧并存图 python_使用Python实现跳帧截取视频帧
本文实例为大家分享了Python跳帧截取视频帧的具体代码,供大家参考,具体内容如下 可以自由设定时长来截取视频,经实测效果理想.期间遇到的一个麻烦是我的视频文件在D:盘,在原视频D盘目录上不能保存截取 ...
- Python快速从视频中提取视频帧(多线程)
Python快速提取视频帧(多线程) 今天介绍一种从视频中抽取视频帧的方法,由于单线程抽取视频帧速度较慢,因此这里我们增加了多线程的方法. 1.抽取视频帧 抽取视频帧主要使用了 Opencv 模块. ...
- 解决Python OpenCV 读取视频并抽帧出现error while decoding的问题
解决Python OpenCV 读取视频抽帧出现error while decoding的问题 1. 问题 2. 解决 3. 源代码 参考 1. 问题 读取H264视频,抽帧视频并保存,报错如下: [ ...
- 使用OpenCV和Python高效计算视频的总帧数
使用OpenCV和Python高效计算视频的总帧数 1. 效果图 2. 源码 参考 这篇博客将介绍两种使用OpenCV和Python计算视频文件中帧数的方法. 超级快,它依靠OpenCV的视频属性功能 ...
- python使用方法视频-python读取视频流提取视频帧的两种方法
本文实例为大家分享了python读取视频流提取视频帧的具体代码,供大家参考,具体内容如下 方法一:通过imageio库和skimage库 1. 安装环境: pip install imageio pi ...
- python opencv按照一定间隔保存视频帧
python opencv按照一定间隔保存视频帧 文章目录: 一.获取视频流的相关参数 二.设置间隔保存视频帧 想实现opencv读取视频帧,按照一定的时间间隔然后保存图片下来,因为所有的帧都保存下来 ...
最新文章
- 基于 Python 的 8 种常用抽样方法
- 一个判断字符是不是10进制数的函数------isdigit()
- mysql创建数据库时候同时创建表空间_MySQL 创建InnoDB表空间_编程学问网
- ubuntu nfs
- apply()与call()的区别
- unreal4怎么设置游戏模式_ue4(虚幻4)基础 Unreal4 服务器模式详细介绍
- ES6/03/函数的定义方式和调用方式,函数内的this指向,改变函数中this指向的三个方法(call(),apply(),bind())
- 《深入理解 Spring Cloud 与微服务构建》第九章 熔断器 Hystrix
- 深入理解java的异常处理机制
- Java WebService视频教程
- java 代码行数统计工具_代码行数统计工具
- 小飞鱼通达二开 通达OA2017集成MongoDB(图文)
- ffmpeg 命令转vp9
- JSAPI微信支付java
- DDoS 报告攻击类型占比
- Java中的Constants类
- html页面字体飞入飞出特效,JS网页特效:星空飞入效果
- 正则表达式,和python re模块
- Bias-variance trade off
- 网站URL如何SEO优化
热门文章
- vertica 数据库 linux,CentOS 7下安装vertica记录
- OpenShift 4 - 使用 Debezium 捕获变化数据,实现MySQL到PostgreSQL数据库同步(附视频)
- idea选中多行的一列、一竖(不是多行的全部内容)
- 网易2018校园招聘编程题真题集合
- 算术,逻辑左移右移(转)
- 【开发工具】【perf】性能分析工具perf的编译和使用说明
- 【“到此一游”系列】(菜鸡参加“美亚杯” 电子取证大赛感受)
- python精选04集(选择语句)
- CCF关于举办CSP-J1 CSP-S1 初赛的报名通知
- Powermill汽车件模具五轴数控CNC编程视频教程