您的位置:首页 > 编程语言 > C语言/C++

PET重建技术 MLEM迭代法(C++)(一) 原理及成像

2015-11-09 18:39 441 查看


PET重建MLEM迭代法(一) 原理及成像

 一、名词解释:

1)系统矩阵:
        系统矩阵,也叫概率矩阵(通常用阵A表示),在重建中属于一个已知量或是一个固定参数。它是一个稀疏矩阵(大多数元素为零的矩阵)。在PET重建中所具有的系统矩阵为每个点所接收到电子的概率,从而判定该点应该有多少放射电子。这里A(i,j)表示第j个像素发射的光子对被第i个探测器接收的概率。
    系统矩阵的存储格式为三元(线)表:(将非零元素的行、列、值通过二维数组存储)

     2)测试数据:
     从机器上获得的每个点所接受到的电子的数量,一般为一列科学技术法,通过代码转换为十进制数后转存为向量Y;

二、成像原理:

    根据PET的工作原理:

     实际的接收到的电子的数据(Y)=电子接受(发送)的概率矩阵(A)*人体内的放射性元素数量(向量X);
  
   即:Y=A*X;

     而将X根据各个点的放射性元素多少来分配明暗程度即可得到图像;
 
     因此求得X的过程即为求得图像过程;

    由于计算量的问题按照上式进行X的求解并不现实,因此我们采用迭代的方法使一个随机的X值通过多次的迭代接近真正的X从而得到图像X的近似图像;

三、迭代公式(MLEM):

      
   我们可以通过读取矩阵A向量Y并进行计算得到X的图片的值
四、C++代码的实现:

 1)矩阵的存储:通过建立Class SparseMtx 来存储概率矩阵 
                    建立Class Vector 来存储向量 
                    建立class ELEM 来存储每个非零的点的信息(行、列、值)
为了方便,将三个存储用的class 存储在了一个.h头文件中:
#pragma once
#include "stdafx.h"

#include <string>
#include <stdio.h>
using namespace std;

#ifndef EPSILON
#define EPSILON       0.00000000001
#endif

class ELEM
{
public:
int row;
int col;
double val;
ELEM()
{
row = 0;
col = 0;
val = 0;
}
ELEM(int i, int j, double d)
{
row = i;
col = j;
val = d;
}
void operator=(ELEM t)
{
row = t.row;
col = t.col;
val = t.val;
}
bool operator<(ELEM t)
{
if (row < t.row)
return true;
else if (row == t.row && col < t.col)
{
return true;
}
else
return false;
}

bool operator >(ELEM t)
{
if (row > t.row)
return true;
else if (row == t.row && col > t.col)
{
return true;
}
else
return false;
}

bool operator == (ELEM t)
{
if (row == t.row && col == t.col)
{
return true;
}
else
{
return false;
}
}

bool operator!=(ELEM t)
{
return !(*this == t);
}

bool operator>=(ELEM t)
{
return (*this > t || *this == t);
}

bool operator<=(ELEM t)
{
return (*this < t || *this == t);
}
};
class Vector
{
public:
int iNum;
double* pdData;
// 构造函数
Vector(int iDimen)
{
iNum = iDimen;
pdData = new double[iNum];
}

// 析构函数
~Vector()
{
//delete[] pdData;
}
void getvector(int i, double val)
{
pdData[i] = val;
}
void Initilize()
{
memset(pdData, 0, iNum * sizeof(double));
}

void Initilize(double dVal)
{
int i;
for (i = 0; i < iNum; i++)
{
pdData[i] = dVal;
}
}

double Sum()
{
double d = 0.0;
for (int i = 0; i < iNum; i++)
{
d += pdData[i];
}
return d;
}

void ShowVector()
{
CString add, message;
int i;
for (i = 0; i < iNum; i++)
{
printf("%f ", pdData[i]);

add.Format("%f/t", pdData[i]);
message = message + add;
add = "";
}
MessageBox(NULL, message, "提示", MB_OK);
}
int SaveVector(CString pchFileName)
{
FILE *fp;
errno_t err;
if ( (err = fopen_s(&fp,pchFileName, "wb"))!=0)
{
printf("Fail to open writen file\n");
return (-1);
}
for (int i = 0; i < iNum; i++)
fprintf(fp,"%lf\r\n", pdData[i]);
//{
//	printf("Error data size\n ");
//	return (-1);
//}
fclose(fp);
return 0;
}
};

class SparseMtx
{
//private:
public:
int n_rows;
int n_cols;
int n_max_ELEM;
int n_actual_ELEM;
ELEM *items;

public:
// 构造函数
SparseMtx(int rows, int cols, int n_max);

// 析构函数
~SparseMtx()
{
//free(items);
}
// 稀疏矩阵初始化
void Empty();

// 矩阵转置
void Transpose();

4000

// 添加元素,添加了处理溢出的机制
bool AddElment(ELEM e);

// 对元素按行进行一次排序
void ElemRowSort();

// 缩减稀疏矩阵存储空间到最低值
void CutSparMtxMem();

// 显示函数 for debugging
void ShowElements();

// 元素交换
void MySwap(ELEM &s1, ELEM &s2);

// 递归快速排序,注意递归算法处理大的数据容易栈溢出
void QuickSort(ELEM* vec, int low, int high);
// 保存到文件
void SaveToFile(char* pchFileName);
};


 下面是.cpp:
#include "stdafx.h"
#include "SparseMtx.h"
#include<iostream>
#include<vector>
#include<stack>
#include<cstdlib>
#include<algorithm>
#include <memory.h>

static bool is_debugging = false;
#ifndef SPAR_MTX_LEN
#define SPAR_MTX_LEN 2000000
#endif

SparseMtx::SparseMtx(int rows, int cols, int n_max)
{
this->n_rows = rows;
this->n_cols = cols;
this->n_max_ELEM = n_max;
this->n_actual_ELEM = 0;
this->items = (ELEM*)malloc(n_max_ELEM * sizeof(ELEM));
if (NULL == this->items)
{
printf("稀疏矩阵构造函数分配内存空间失败\n");
}
}

void SparseMtx::MySwap(ELEM &s1, ELEM &s2)
{
ELEM t = s1;
s1 = s2;
s2 = t;
}

void SparseMtx::QuickSort(ELEM* arr, int left, int right)
{
// key位置和对应的值
int key_pos = left;
ELEM key_value = arr[key_pos];

// 循环搜索范围
int i = left;
int j = right;

// 一次快速排序
while (i < j)
{
// 从左向右,大于key就互换
while (arr[i]<key_value && i<right)
i++;
if (i<j)
{
MySwap(arr[i], arr[key_pos]);
key_pos = i;
}

// 从右向左,小于key就互换
while (arr[j]>key_value && j>left)
j--;
if (i<j)
{
MySwap(arr[j], arr[key_pos]);
key_pos = j;
}
}

// 递归调用,key的左右两个子集进行一次快速排序
if (key_pos>left)
{
QuickSort(arr, left, key_pos - 1);
}
if (key_pos<right)
{
QuickSort(arr, key_pos + 1, right);
}
}

/************************************************************************/
/*      为了提高速度,放弃了对重复元素判断
/************************************************************************/
bool SparseMtx::AddElment(ELEM e)//进行矩阵读入的函数,输入类型为为ELEM,可将三线表中行列值读入ELEM中 在将每个点放入矩阵中
{
// check,防止溢出
if (e.col <0 || e.col>n_cols
|| e.row <0 || e.row>n_rows)
{
printf("OverFlow when adding elements...\n");
return false;
}
else if (this->n_actual_ELEM >= this->n_max_ELEM)// 需要处理溢出
{
this->n_max_ELEM += SPAR_MTX_LEN;
printf("追加分配空间,新的n_max_ELEM = %d\n", n_max_ELEM);
this->items = (ELEM*)realloc(this->items, n_max_ELEM*sizeof(ELEM));
if (NULL == this->items)
{
printf("追加分配空间失败\n");
exit(-1);
}
this->items[this->n_actual_ELEM] = e;
this->n_actual_ELEM++;

return true;
}
else
{
this->items[this->n_actual_ELEM] = e;
this->n_actual_ELEM++;
return true;
}

}

void SparseMtx::ShowElements()
{
for (int i = 0; i<this->n_actual_ELEM; i++)
{
printf("(%d, %d)\t%.4f\n", this->items[i].row, this->items[i].col, this->items[i].val);
CString message;
message.Format("%d %d %lf", this->items[i].row, this->items[i].col, this->items[i].val);
MessageBox(NULL, message, "检测", MB_OK);
}
}

// 分成两部分:首先把所有点按行排列,再在每行中进行排序
void SparseMtx::ElemRowSort()
{
int i, j;
int iRowNum;
int iColNum;
int iTotElemNum;
ELEM e;
iRowNum = this->n_rows;
iColNum = this->n_cols;
iTotElemNum = this->n_actual_ELEM;

int* piRowElemNum = new int[iRowNum];
int* piStartPos = new int[iRowNum];
int* piStartPosCopy = new int[iRowNum];
double* pdMem = new double[iColNum];

ELEM* NewItem = new ELEM[iTotElemNum];

if (iTotElemNum > 0)
{
memset(piRowElemNum, 0, iRowNum * sizeof(int));
memset(piStartPos, 0, iRowNum * sizeof(int));
}
else
{
printf("数组内没有元素!\n");
return;
}

for (i = 0; i < iTotElemNum; i++)
{
piRowElemNum[this->items[i].row]++; //统计第行元素的个数
}
/**//*处理存放地址*/
piStartPos[0] = 0;
for (i = 1; i < iRowNum; i++)
{
piStartPos[i] = piStartPos[i - 1] + piRowElemNum[i - 1];
}

// 首先按行粗排序,使得同行的数据相连
memcpy(piStartPosCopy, piStartPos, iRowNum * sizeof(int));

for (i = 0; i < iTotElemNum; i++)
{
j = piStartPos[this->items[i].row];
NewItem[j] = this->items[i];
piStartPos[this->items[i].row]++;
}
// 对每一行下的所有元素进行排序

this->Empty();
for (i = 0; i < iRowNum; i++)
{
memset(pdMem, 0, iColNum * sizeof(double));
if (0 == piRowElemNum[i])
{
continue;
}
// 重排
for (j = piStartPosCopy[i]; j < piStartPosCopy[i] + piRowElemNum[i]; j++)
{
pdMem[NewItem[j].col] = NewItem[j].val;
}
for (j = 0; j < iColNum; j++)
{
if (pdMem[j] > EPSILON)
{
e.row = i;
e.col = j;
e.val = pdMem[j];
this->AddElment(e);
}
}
}

delete[] pdMem;
delete[] piRowElemNum;
delete[] piStartPos;
delete[] piStartPosCopy;
delete NewItem;

}

void SparseMtx::SaveToFile(char* pchFileName)
{
int i;
FILE* fp;
errno_t err;
err=fopen_s(&fp,pchFileName, "w+");
if (err!=0)
{
//prrintf("Cannot open file %s\n", pchFileName);
SparseMtx A(16354, 16354, 1);
CString message;
message.Format("Cannot open file %s!", pchFileName);
AfxMessageBox(message);
return;
}

for (i = 0; i< this->n_actual_ELEM; i++)
{
fprintf(fp, "%d\t%d\t%f\n", this->items[i].row, this->items[i].col, this->items[i].val);
}
fclose(fp);
}

void SparseMtx::Empty()
{
for (int i = 0; i < this->n_max_ELEM; i++)
{
this->items[i].col = -1;
this->items[i].row = -1;
this->items[i].val = -1;
}
this->n_actual_ELEM = 0;
}

void SparseMtx::CutSparMtxMem()
{
this->items = (ELEM*)realloc(items, this->n_actual_ELEM*sizeof(ELEM));
}

/**//*矩阵转置算法*/
void SparseMtx::Transpose()
{
int i, j;
int iRowNum;
int iColNum;
int iTotElemNum;
iRowNum = this->n_rows;
iColNum = this->n_cols;
iTotElemNum = this->n_actual_ELEM;

int* piColElemNum = new int[iColNum];
int* piStartPos = new int[iColNum];
ELEM* NewItem = new ELEM[iTotElemNum];

if (iTotElemNum > 0)
{
memset(piColElemNum, 0, iColNum * sizeof(int));
memset(piStartPos, 0, iColNum * sizeof(int));
}
else
{
printf("数组内没有元素!\n");
return;
}

for (i = 0; i < iTotElemNum; i++)
{
piColElemNum[this->items[i].col]++; //统计第i列的所有元素个数
}
/**//*处理存放地址*/
piStartPos[0] = 0;
for (i = 1; i < iColNum; i++)
{
piStartPos[i] = piStartPos[i - 1] + piColElemNum[i - 1];
}
/**//*存放*/
for (i = 0; i < iTotElemNum; i++)
{
j = piStartPos[this->items[i].col]; // 使得j能下一个地址
NewItem[j].row = this->items[i].col;
NewItem[j].col = this->items[i].row;
NewItem[j].val = this->items[i].val;
piStartPos[this->items[i].col]++;
}
for (i = 0; i < iTotElemNum; i++)
{
this->items[i] = NewItem[i];
}

i = this->n_rows;
this->n_rows = this->n_cols;
this->n_cols = i;

delete[] piColElemNum;
delete[] piStartPos;
delete NewItem;
}


其中Class SparseMtx 定义变量时需要输入矩阵的行列数 Vector 需要输入向量元素个数;
 
  2)文件的读入:

矩阵的读入:
 
 通过定义FILE 和使用 fopen函数 fscanf函数(加快读入速度)来进行文件的读入(读取Y时需要将科学计数法转化为十进制数字进行读入;

读入矩阵的代码:

double val;
FILE*fp;
errno_t err;
if ((err = fopen_s(&fp, a, "r")) != 0)
{
CString message;
message.Format("Can't open the file: %s", a);
AfxMessageBox(message);
}
else
{
int row,col;
while (fscanf(fp, "%d%d%lf", &row, &col, &val) != EOF)
{
//printf("%d %d %f /n");

ELEM e(row, col, val);
A.AddElment(e);
//			A.ShowElements();
}
}
//A.ElemRowSort();
fclose(fp);


读取向量的代码:
if (NULL == (fp = fopen(y, "r")))
{
CString message;
message.Format("Can't open the file: %s", y);
AfxMessageBox(message);
}
else
{
int k = 0;
while (fscanf(fp,"%s",&data)!=EOF)
{
if ((data[0]=='-'&&data[9]!='e')||(data[0]!='-'&&data[8]!='e'))
{
CString message;
message.Format("%s", data);

MessageBox(NULL, message, "测试", MB_OK);
}
//科学计数法转换:
if (data[0] == '\n')
break;
val = 0;
bool x = true;
int d;
double a=1.0;
if (data[0] == '-')
{
x = false;
for (int i = 0; i <= 13; i++)
data[i] = data[i + 1];
}
d = 100 * (data[10]-'0') + 10 * (data[11]-'0') + (data[12]-'0');
if (data[9] == '+')
{
for (int i = 0; i < d; i++)
a = a * 10;
}
else if(data[9]=='-')
{
for (int i = 0; i < d; i++)
a = a/10;
}
else MessageBox(NULL, "出错1", "测试", MB_OK);
for(int i = 0; i <= 7; i++)
{
if (i != 1)
{
double min = 1.0;
for (int l = 2; l <= i; l++)
min = min / 10;
val = val + (data[i]-'0')*min;
}
}
if (x == true)
val = val*a;
else if(x==false)
{
val = -1 * val*a;
}
else
{
MessageBox(NULL, "出错2", "测试", MB_OK);
}
//将转化完的科学计数法进行读入:
Y.getvector(k, val);
CString message;
message.Format("%lf", Y.pdData[k]);
//if (x== false)
//MessageBox(NULL, message, "测试", MB_OK);
k++;

}

}
fclose(fp);
 3)矩阵与向量计算:
 通过一个Class  SprsMtxOperator 将所有需要用到的 矩阵计算加入进去:

头文件:
#pragma once
#include "SparseMtx.h"
#include "stdafx.h"

#ifndef EPSILON
#define EPSILON       0.000000001
#endif
class SprsMtxOperator
{
public:
SprsMtxOperator();
~SprsMtxOperator();

// 矩阵与向量相乘
int MtxVecMultiple(const SparseMtx& Mtx, Vector& InVec, Vector& ResultVec);

// 向量与矩阵相乘
int VecMtxMultiple(Vector& InVec, const SparseMtx& Mtx, Vector& ResultVec);

// 向量逐项相乘
int VecMultiple(Vector& FirstVec, Vector& SecondVec, Vector& ResultVec);

// 向量逐项相除
int VecDiv(Vector& FirstVec, Vector& SecondVec, Vector& ResultVec);

// 向量的每一项乘以一个因子
int VecScale(Vector& FirstVec, double dScale, Vector& ResultVec);

// 计算稀疏矩阵乘积的列和
int MtxColSum(const SparseMtx& Mtx, Vector& ResultVec);

};


#pragma once

#include "stdafx.h"
#include <stdio.h>
#include "SprsMtxOperator.h"
using namespace std;
SprsMtxOperator::SprsMtxOperator(void)
{
}

SprsMtxOperator::~SprsMtxOperator(void)
{
}
// 矩阵乘以向量
int SprsMtxOperator::MtxVecMultiple(const SparseMtx& Mtx, Vector& InVec, Vector& ResultVec)
{
int i;
int iRowNum;
int iColNum;
int iTotElemNum;
int iVecNum;

iRowNum = Mtx.n_rows;
iColNum = Mtx.n_cols;
iTotElemNum = Mtx.n_actual_ELEM;
iVecNum = InVec.iNum;

if (iColNum != iVecNum || iRowNum != ResultVec.iNum)
{
printf("Dimensions of matrix and vector mismatch\n");
return (-1);
}

Vector NewVector(iRowNum);
NewVector.Initilize();

for (i = 0; i < iTotElemNum; i++)
{
NewVector.pdData[Mtx.items[i].row] += Mtx.items[i].val * InVec.pdData[Mtx.items[i].col];
}

for (i = 0; i < ResultVec.iNum; i++)
{
ResultVec.pdData[i] = NewVector.pdData[i];
}
return 0;
}

// 向量乘以矩阵,该乘法也可是使用矩阵转置和MtxVecMultiple函数实现,
// 但是考虑到系统矩阵太大,矩阵转置消耗内存,因此不使用
int SprsMtxOperator::VecMtxMultiple(Vector& InVec, const SparseMtx& Mtx, Vector& ResultVec)
{
int i;
int iRowNum;
int iColNum;
int iTotElemNum;
int iVecNum;

iRowNum = Mtx.n_rows;
iColNum = Mtx.n_cols;
iTotElemNum = Mtx.n_actual_ELEM;
iVecNum = InVec.iNum;

if (iRowNum != iVecNum || iColNum != ResultVec.iNum)
{
printf("Dimensions of matrix and vector mismatch\n");
return (-1);
}

Vector NewVector(iColNum);
NewVector.Initilize();

for (i = 0; i < iTotElemNum; i++)
{
NewVector.pdData[Mtx.items[i].col] += Mtx.items[i].val * InVec.pdData[Mtx.items[i].row];
}

for (i = 0; i < ResultVec.iNum; i++)
{
ResultVec.pdData[i] = NewVector.pdData[i];
}
return 0;
}

int SprsMtxOperator::VecMultiple(Vector& FirstVec, Vector& SecondVec, Vector& ResultVec)
{
int i, iFirstNum, iSecondNum;
iFirstNum = FirstVec.iNum;
iSecondNum = SecondVec.iNum;

if (ResultVec.iNum != iFirstNum || ResultVec.iNum != iSecondNum)
{
printf("Dimensions of two vectors mismatch\n");
return (-1);
}
Vector NewVec(iFirstNum);
NewVec.Initilize();
for (i = 0; i < iFirstNum; i++)
{
NewVec.pdData[i] = FirstVec.pdData[i] * SecondVec.pdData[i];
}
for (i = 0; i < ResultVec.iNum; i++)
{
ResultVec.pdData[i] = NewVec.pdData[i];
}
return 0;
}

int SprsMtxOperator::VecDiv(Vector& FirstVec, Vector& SecondVec, Vector& ResultVec)
{
int i, iFirstNum, iSecondNum;
iFirstNum = FirstVec.iNum;
iSecondNum = SecondVec.iNum;

if (iFirstNum != iSecondNum)
{
printf("Dimensions of two vectors mismatch\n");
return (-1);
}
Vector NewVec(iFirstNum);
NewVec.Initilize();
for (i = 0; i < iFirstNum; i++)
{
NewVec.pdData[i] = FirstVec.pdData[i] / (SecondVec.pdData[i] + EPSILON);
}
for (i = 0; i < ResultVec.iNum; i++)
{
ResultVec.pdData[i] = NewVec.pdData[i];
}
return 0;
}

// 向量与一个数相乘
int SprsMtxOperator::VecScale(Vector& InVec, double dScale, Vector& ResultVec)
{
int i, iNum;
iNum = InVec.iNum;
if (ResultVec.iNum != iNum)
{
printf("Dim of output does not equal to input\n");
return (-1);
}

Vector NewVec(iNum);
NewVec.Initilize();
for (i = 0; i < iNum; i++)
{
NewVec.pdData[i] = InVec.pdData[i] * dScale;
}
for (i = 0; i < iNum; i++)
{
ResultVec.pdData[i] = NewVec.pdData[i];
}
return 0;
}

// 计算稀疏矩阵乘积的列和
int SprsMtxOperator::MtxColSum(const SparseMtx& Mtx, Vector& ResultVec)
{
int i;
int iRowNum;
int iColNum;
int iTotElemNum;
iRowNum = Mtx.n_rows;
iColNum = Mtx.n_cols;
iTotElemNum = Mtx.n_actual_ELEM;
if (iColNum != ResultVec.iNum)
{
printf("Result vector has error length\n");
return (-1);
}
ResultVec.Initilize();
for (i = 0; i < iTotElemNum; i++)
{
ResultVec.pdData[Mtx.items[i].col] += Mtx.items[i].val;
}
return 0;
}


4)迭代公式的 代码编写:

通过代码将迭代公式完成  并可以进行迭代:
int i = 0;
Vector X1(16384);
Vector X2(16384);
Vector X3(16384);
Vector X4(16384);
Vector X5(16384);
Vector X6(16384);
GetBest *best;
best = new GetBest;

SparseMtx Aa(16384,16384,1);
for (int l = 0; l<A.n_actual_ELEM; l++)
Aa.AddElment(A.items[l]);
Aa.Transpose();
for (i = 0; i < t; i++)
{

SprsMtxOperator * handle;
handle = new SprsMtxOperator;
handle->MtxVecMultiple(A, X, X1);
handle->VecDiv(Y, X1, X2);
handle->MtxVecMultiple(Aa, X2, X3);
handle->VecMultiple(X, X3, X4);
handle->MtxColSum(A, X5);
handle->VecDiv(X4, X5, X6);
CString filename;
filename.Format("%d.txt", i);
X6.SaveVector(filename);
for (int l = 0; l < X6.iNum; l++)
X.pdData[l] = X6.pdData[l];
filename.Format("%d.jpg", i);
GetPic *Pic;
Pic = new GetPic;
Pic->GetSavePic(X, filename);
i
9758
f(best->getbest==false)
best->GetBestPicA(Y, X, A, i);

}


使用公式可以进行迭代  并通过循环  更改t的 数量进行迭代

5)将迭代后的X向量转换为jpg图片文本:
迭代后  X依然是一个向量 可以将其元素个数开方 得到a,将其看作一个axa的矩阵 通过CImage 和 SetPixel描点公式进行描点 将像素值换算成一个个点画到图片上
成像代码:
double  max = 0;
int point = 0;

for (int i = 0; i < X.iNum; i++)
if (X.pdData[i] >= max)
max = X.pdData[i];
COLORREF COLOR = RGB(40, 123, 90);
CImage image;
image.Create(128, 128, 24);
HDC DC;
DC = image.GetDC();

for (int i = 1; i <=128; i++)
{
for (int l = 1; l <=128; l++)
{
point = (X.pdData[(i - 1) * 128 + l - 1] / max) * 255;
SetPixel(DC, i - 1, l - 1, RGB(point, point, point));
}
}
image.ReleaseDC();
DC = NULL;

image.Save(Picname);


这样就得到了  我们迭代出来的图像!!!

那么得到的图像通过多次迭代可以得到近似图像,迭代次数越多越接近原始图像,但是我们却不能盲目的加多迭代次数,过多的迭代会导致图片质量下降,那么到底应该进行多少次迭代呢?
请关注下次更新:
 
PET重建技术 MLEM迭代法(C++)(二) 迭代次数判定算法

(未完待续)

本文版权归作者和CSDN共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  PET重建 C++ MLEM 迭代