上一篇介绍了EDA(Extensive Display Array)拼接屏大规模图像处理(https://blog.csdn.net/gordon3000/article/details/123232508?spm=1001.2014.3001.5501)。这里分析一下拼接屏显示图像的工作过程,先上Master机部分的代码
// 99示例代码 ——打开一个 TIFF 文件,在拼接屏幕墙上显示图像。
// 在拼接屏多个显示单元上显示图像,用于演示多屏接 EDA(Extensive Display Array,大规模显示阵列)程序编写。
//
// 拼接屏幕墙由一系列的显示屏组成,一些屏幕墙无论拼接多大都只能显示4k(3840*2160)像素,而另一种显示方式则能实现单屏4K显示,
// 假如你的屏幕墙为8*12个4K显示单元,你就可以一次同时显示10亿像素。这里介绍的EDA方式可以帮助你实现后一种显示方式。
//
// EDA设计为一台主机(Master)带若干从机(Slave)的工作模式。每台Slave机可以挂接1或者多个显示单元。Slave机以行列二维方式排列,
// m*n个Slave机拖动 M*N个显示单元工作。
//
// 要运行此演示程序,你需要有一台主机,安装好最新版支持EDA的RSD软件。然后你需要2台普通台式机做Slave机,假设你的Slave机显示器
// 就是屏幕墙上的显示单元。在Slave机上安装一下随RSD一起发布的服务程序(GGServer)。如果你手头没有2台Slave机,使用1台也可以,
// 只是无法观察拼接结果。或者只有一台主机,也可以将主机既当Master又当Slave,也能演示此示例程序。本示例程序有2个扩展名为.c的
// 代码文件,这个是“0-0-EDA拼接屏示例-Master端.c”,另一个为“0-0-EDA拼接屏示例-Slave端.c”。不要执行这个Slave端的文件,但是
// 要将这个文件放在下面代码指定可以存取的目录中。
//
// 从两台节点机的演示可以类推出多台计算机带动的屏幕墙的显示结果。EDA设计支持256台节点机的工作模式。进一步的详细介绍参阅随
// 本示例代码一起发布的说明。
//
// EDA除了屏幕墙展示功能外,其最初设计的功能是大规模集群计算。参阅99脚本语言开发说明,了解如何编程实现大规模集群计算。
//
// 李国春 2022年 2月 20日
/
int displays[2][2]; //节点机接入的显示器数目。
int dispsize[2][2][8][2]; //显示器分辨率,前两维指定一个节点机,第3维是该节点机接入的显示器个数(最多不超过8个)
//第4维是显示分辨率,[0] = 显示器像素行数, [1] = 显示器像素列数
int row,col; //节点机的行数和列数
int nHeight,nWidth; //节点机上单个显示单元的图像高度和宽度
int colorchs;
double scl[] = {1.0,1.0,1.0};
double ofs[] = {0.0,0.0,0.0};
main(){
int width,height,bands,datatype;
STRING name = OpenFileDialog(TRUE,"*.tif;*.tiff"); //OpenFileDialog 取来一个tif文件名
Print("选择文件:%s",name);
int b = TifInqDataInfo(name,height,width,bands,datatype); //读取这个 TIFF 的高度、宽度、波段数和数据类型
if(UINT16BL != datatype) //只处理双字节无符号整型数据,不是就返回
{
Print("只支持双字节无符号整型数据");
return -1;
}
//为 TIFF 文件申请内存,以BSQ格式存放。(注意,数据规模过大这样全部使用内存,申请可能会失败)
WORD buffer[bands][height][width];
// TIFF 文件数据读入上面申请的内存中(执行完此行后,可以右击 buffer,从菜单“SeeSee”可以观察数据知否正确)
int e = TifReadData(name,buffer);
if(e <= 0)
{
return -1;
Print("读取 TIFF 格式数据错误");
}
Print("读取 TIFF 格式数据成功");
colorchs = 3;//数据波段不足3个时,使用颜色通道也响应减少
if(bands < 3)
{
colorchs = bands;
}
//将TIFF各通道数据变换成用于显示的 RGB 数据///
int chs[] = {1,2,3};//合成图像使用的数据通道,
BYTE rgb[colorchs][height][width];//存放全部 RGB 图像数据
RasterToRGB(buffer,height,width,bands,datatype,rgb,chs,0.001,0.999,BSQ);
Print("双字节无符号整型数据转换为单字节数据");
//启动节点机进程/
Print("准备在节点机启动进程程序");
ProcessBegin(displays,dispsize);
//获取节点机的个数(返回值),nd的第1个元素(nd[0])是节点机行数,nd的第2个元素(nd[1])是节点机的列数
int nd[2];
int n = ProcessNodes(nd);
Print("共 %d 台节点机,%d 行 %d 列",n,nd[0],nd[1]);
//遍历所有节点机,查询节点机显示单元数目和显示分辨率
for(int i=0; i<nd[0]; i++)
{
for(int j=0; j<nd[1]; j++)
{
ProcessTo(i,j);//这里指定特定的节点机(不指定时使用99预先排列顺序)。
GetSlaveDispInfo(i,j);//这个进程函数获取各节点机的显示单元信息(函数在下面)
}
}
Sleep(2000);//节点机上有数据回收,应该查询所有节点机线程应答,这里简单等一秒代替
Alloc(BYTE bmpbits[colorchs][dispsize[0][0][0][0]][dispsize[0][0][0][1]];);
Print("第一台节点机主显示器显示分辨率,%d 行 %d 列",dispsize[0][0][0][0],dispsize[0][0][0][1]);
//申请全局变量,用于存放向每个节点机发送的图像数据(单字节 RGB),假设所有节点机显示设备分辨率一致
nHeight = dispsize[0][0][0][0];
nWidth = dispsize[0][0][0][1];
int pos[3] = {0,0,0}; //各节点机上的显示起始位置
int size[3]; //各节点机上显示块的范围(1屏)
size[0] = colorchs; //图像通道的波段数
size[1] = nHeight; //节点机单屏显示窗口的高度
size[2] = nWidth; //节点机单屏显示窗口的高度
int ypos = 500; //节点机显示图像左上角在主图像中的起始位置 y 坐标
int xpos = 500; //x 坐标
int bmpID;
//遍历所有节点机,在上面逐个启动1个过程进程程序/
for(i=0; i<nd[0]; i++)
{
for(j=0; j<nd[1]; j++)
{
pos[1] = ypos + i*nHeight;
pos[2] = xpos + i*nWidth;
bmpbits = Slab(rgb,pos,size);
FlushSlave(i,j,bmpbits,colorchs,scl,ofs);
ProcessCall("D:\\99Codes\\99-EDA拼接屏示例-Slave端.c");
Print(" 第 %d 行 %d 列节点机进程已经启动",i+1,j+1);
}
}
//通过向节点机发送消息,循环更新各个节点机上的显示内容//
Print("节点机开始显示图像");
for(int o=0; o<20; o++)//指定循环次数(或者定时)
{
//更新显示位置
ypos = ypos+200;
xpos = xpos+200;
for(i=0; i<nd[0]; i++)
{
for(j=0; j<nd[1]; j++)
{
pos[1] = ypos + i*nHeight;//计算第 i行第 j 列的节点机应该显示部分的位置
if(pos[1] + nHeight >= height)
{
pos[1] = height - nHeight;
}
pos[2] = xpos + j*nWidth;
if(pos[2] + nWidth >= width)
{
pos[2] = width - nWidth;
}
bmpbits = Slab(rgb,pos,size);//为该节点机准备图像
FlushSlave(i,j,bmpbits);//更新节点机显示数据
SendMessageSlave(i,j,1,"GGM_NEWBITMAP");//发送显示更新消息
Sleep(5);
}
}
}
ProcessEnd();//结束所有节点机上的进程
Print("节点机进程程序结束");
Print("主程序结束");
return;
}
//
//这个进程函数被分配到所有节点机上执行,获取各节点机的显示单元信息
process GetSlaveDispInfo(int i,int j)
{
// Monitors()函数获取本机显示器数目,并将该数据上传至主机标记的第i行j列的 displays[i][j]
displays[i][j] = Monitors();
for(int k=0; k<displays[i][j]; k++)
{
//GetScreenSize()函数获取本机第 k+1(不是 0 Based)显示单元的尺寸并上传,注意dispsize[i][j][k]是2个数据(第4维)
dispsize[i][j][k] = GetScreenSize(k+1);
Flush(dispsize[i][j][k]);//刷新主机数据
}
Flush(displays[i][j]);//刷新主机数据
}
第36~43行通过交互方式选择一个TIFF文件,其中第38行的TifInqDataInfo()函数获得该TIFF文件的几个基本信息,如果数据类型不是双字节无符号整型就不继续处理。
第45~55行读取该TIFF文件的数据,存入一个3维数组buffer[bands] [height][width],数组各维这样排列表示数据是BSQ格式(99数组和子变量有介绍,子变量不是打错了,就是子,不是自)。
第57~67行将双字节无符号整型变换成单字节,统一变换供各个节点机(Slave)使用,节省传输和处理时间。
第71行的 ProcessBegin(displays,dispsize);函数连接各Slave机,准备启动进程。在后面的第150行结束。注意ProcessBegin函数有两个参数,这是Master的公有变量,发送给各节点机的,个数不限,也可以没有。第1个变量int displays[2][2]表示该节点机的显示单元数(显示器个数)。例如displays[0][0]表示第1行第1列的节点机接入的显示单元数。第2个变量int dispsize[2][2][8][2]表示显示单元的分辨率。例如dispsize[0][0][1][0]的值如果是1080,表示第1行第1列的节点机的第2个显示单元的垂直分辨率为1080像元。任意共有变量都可以通过ProcessBegin函数传递以保证任何节点机能够对其存取或者复制。共有变量也可以不在这里申明,然后强制暴露给各节点机。
第74行到第89行收集各节点机显示信息。由84行GetSlaveDispInfo(i,j)函数负责收集。这个函数在下面声明和定义(159~170行)process GetSlaveDispInfo(int i,int j)。函数由process关键字定义,表示该函数是一个在节点机上执行的线程函数,而不是在Master主机上运行。不是本机(Master)的线程称为过程线程。就是说,在每台节点机上都要启动这个函数的一个线程,收集到节点机信息,填入公有变量,方便Master存取。process关键字定义过程线程,下面还有在节点机上定义的过程进程(第116行ProcessCall函数)。可能同学们会问,有了 process关键字了,为什么还要ProcessCall函数。其实ProcessCall是为了更复杂的应用,过程线程一个函数不够用了需要更多的过程线程,像Slave端事件的的收集等。打个比方,触摸屏大屏的某一个显示单元上,人为输入了一个滚屏或者缩放的手势。在Slave端出发了一个事件,但是该事件并不在本Slave端处理,因为那么多Slave机你自己处理了别人怎么知道?而是通过一个哑函数关联到Master的事件响应函数。Master分析该事件,然后通知所有的Slave响应该事件,才能使整个大屏看起来是一个整体。注意:触摸屏手势RSD只是预留的,没做呢。因为其它所有我提及的技术都是我们已经完成的,这个没有,不要引起误会。
第96和第97行定义一个三维数据块的起点和大小,从原始数据裁剪1块数据发送的各个节点机。
第107~119行,遍历各节点机,其主要目的是在每个Slave上都启动过程进程,第116行rocessCall("D:\\99Codes\\99-EDA拼接屏示例-Slave端.c");。ProcessCall函数的参数是一个文件名,也就是需要在Slave上运行的代码。
从123~148行循环向Slave机刷新20次,就是动态演示一下。第128和第130行遍历各Slave。第142行根据当前Slave位置切来一块对应的图像数据,第143行将这块数据强制刷新到Slave机(其实这里也可以将所有数据预先传送到Slave,或者直接存放在共享存储区,比这种临时切了再传效率高)。
第144行向Slave发送了一个字符串消息"GGM_NEWBITMAP"。这个消息在Slave机代码里有一个消息映射,关联到一个过程线程函数。发消息等于通知Slave执行这个线程函数。也就是Slave显示动作的主体。
第150行Master结束过程进程。151行的提示错了,不是节点机进程结束,Master结束后Slave还可能继续执行。
下面是为Slave端准备的代码(Master端动态发送到Slave端的代码)。
/
// GeoGeo示例代码 —— 刷新Slave显示
//
// 李国春 2014年 8月 23日,2022 22 23修改
extern bmpbits;
extern colorchs;
extern scl;
extern ofs;
int winID,bmpID,width,height;
main()
{
bmpID = 0;
int sz[2] = GetScreenSize(1);//这里仅使用第一个显示器
height = sz[0];
width = sz[1];
//消息映射,接收来自主机的 GGM_NEWBITMAP 消息,并由函数 OnGGNewBitmap 响应
MapMessage("GGM_NEWBITMAP","OnGGNewBitmap");
//由主机传来的数据创建一个位图(仅窗口大小)
bmpID = CreateBitmap(height,width,bmpbits,colorchs,scl,ofs);
//创建窗口
SuspendEvent("OnDraw");//创建完成前禁止显示刷新
winID = CreateWindow("RSD 99窗口",VIEW);//创建普通视图窗口
ModifyStyle(winID,0,20);
MoveWindow(winID,0,0,width,height);
SetStaticTextColor(winID,255,0,0);
SetCtrlFont(winID,48,"",0,0,1000);
ResumeEvent("OnDraw");//允许显示刷新
UpdateView(winID);//强制刷新
}
/
//缺省的窗口重绘消息响应函数
event OnDraw(int id)
{
if(id == winID)
{
DrawBitmap(winID,bmpID);
STRING str = "RSD-EDA(Extensive Display Array,大规模显示阵列)演示程序";
CreateStaticCtrl(winID,str,200,200,1520,50);
}
}
/
//响应主机消息,更新数据
event OnGGNewBitmap(int id,STRING msg)
{
//删除上次旧的位图,再用更新后数据创建一个新位图(很低效,示意代码)
SuspendEvent("OnDraw");
if( 0 != bmpID )
{
DeleteBitmap(bmpID);
}
bmpID = CreateBitmap(height,width,bmpbits,colorchs,scl,ofs);
ResumeEvent("OnDraw");
UpdateView(winID);//强制刷新
}
第6到第9行是Master的公有变量,可以在Slave端使用,记得必要时Flush刷新同步。
第19行 MapMessage("GGM_NEWBITMAP","OnGGNewBitmap"); 就是上面提到的消息映射,对应的函数是第52~64行,这是一个消息响应函数,不是事件。不过逻辑一样都使用了一个 event 关键字做函数开头。然后在第60行将传送来的数据块做成一个位图。再然后在63行强制刷新显示,这个机制和VC++是一样的。
注意,在55和61行将OnDraw()挂起和恢复,防止数据更新期间有强制显示刷新发生。
第22~35行进行一些初始化工作,也进行了第1次的显示。
第40~48行是重绘的消息响应函数。主要负责窗口的图形图像以及文字的绘制,与VC++窗口中的同名函数差不多。不同的是该函数带的一个参数不是设备上下文指针,而是一个窗口ID,当有多个窗口时在该参数指向的窗口内绘图。OnDraw()等少数常用消息响应函数不需要消息映射。
是不是有点小复杂? 熟悉了其中逻辑关系或者使用过VC++的会好一些。欢迎多提宝贵意见。
加企鹅758461012,原来的满了。