LM模式简单来说是:用亮度的左边和(或)上边建立一个Y-C的线性模型,再把这样预测的pu中的每个像素都套入这个模型,根据已预测好的Y值和这个线性模型得到C值。
分类:分为CCLM(色度模式67,用左边和上边)、MDLM-T(色度模式68,只用上边)、MDLM-L(色度模式69,只用左边)三种模式。
入口函数:xIntraCodingTUBlock。详见:H.266/VVC代码学习9:xIntraCodingTUBlock函数
下面我们分阶段进行学习,请结合注释查阅VTM4.0中的代码。
进入LM模式的代码如下:
//===== get prediction signal 计算预测值 =====
if( compID != COMPONENT_Y && PU::isLMCMode( uiChFinalMode ) )//2.LM模式预测,得到预测角度
{
{
xGetLumaRecPixels( pu, area );//下采样,获取重建像素
}
predIntraChromaLM( compID, piPred, pu, area, uiChFinalMode );//LM模式预测
}
void IntraPrediction::predIntraChromaLM(const ComponentID compID, PelBuf &piPred, const PredictionUnit &pu, const CompArea& chromaArea, int intraDir)
{
/**********************************设置参数***********************************************/
int iLumaStride = 0; //下一列的值
PelBuf Temp; //存宽高stride三个值
if ((intraDir == MDLM_L_IDX) || (intraDir == MDLM_T_IDX))//MDLM用的是两个行的长度
{
iLumaStride = 2 * MAX_CU_SIZE + 1;
Temp = PelBuf(m_pMdlmTemp + iLumaStride + 1, iLumaStride, Size(chromaArea));
}
else//CCLM用的是两个行的长度
{
iLumaStride = MAX_CU_SIZE + 1;
Temp = PelBuf(m_piTemp + iLumaStride + 1, iLumaStride, Size(chromaArea));
}
/**************************** 下采样已结束,这里可以得到a和b ******************************/
int a, b, iShift;
xGetLMParameters(pu, compID, chromaArea, a, b, iShift);//后续重点
/********** 最终预测,用a、b做的线性模型,计算得这个CU中全部像素的色度值,与模式无关 **********/
piPred.copyFrom(Temp);
piPred.linearTransform(a, iShift, b, true, pu.cs->slice->clpRng(compID));//计算:pred(C) = a * rec(L) + b
}
1 下采样:xGetLumaRecPixels
主要工作:将亮度2Nx2N的块,用1:2:1下采样变成亮度NxN的亮度块。
目的:为了能和其对应的NxN的色度块一一对应。
void IntraPrediction::xGetLumaRecPixels(const PredictionUnit &pu, CompArea chromaArea)//这个函数是做下采样,用以重建亮度值
{
/********************************************** 初始化 ******************************************************/
int iDstStride = 0;// ==》下采样后的一行
Pel* pDst0 = 0;//=========》下采样后的缓存
int curChromaMode = pu.intraDir[1];
if ((curChromaMode == MDLM_L_IDX) || (curChromaMode == MDLM_T_IDX))//MMLM的操作
{
iDstStride = 2 * MAX_CU_SIZE + 1;
pDst0 = m_pMdlmTemp + iDstStride + 1;
}
else//CCLM的操作
{
iDstStride = MAX_CU_SIZE + 1;
pDst0 = m_piTemp + iDstStride + 1; //MMLM_SAMPLE_NEIGHBOR_LINES;
}
//assert 420 chroma subsampling
CompArea lumaArea = CompArea( COMPONENT_Y, pu.chromaFormat, chromaArea.lumaPos(), recalcSize( pu.chromaFormat, CHANNEL_TYPE_CHROMA, CHANNEL_TYPE_LUMA, chromaArea.size() ) );// 亮度块 needed for correct pos/size (4x4 Tus)
CHECK( lumaArea.width == chromaArea.width, "" );
CHECK( lumaArea.height == chromaArea.height, "" );
const SizeType uiCWidth = chromaArea.width;
const SizeType uiCHeight = chromaArea.height;
const CPelBuf Src = pu.cs->picture->getRecoBuf( lumaArea );//原始
Pel const* pRecSrc0 = Src.bufAt( 0, 0 );//======》下采样前的缓存
int iRecStride = Src.stride;
int iRecStride2 = iRecStride << 1;// ==》下采样前的一行
const CodingUnit& lumaCU = isChroma( pu.chType ) ? *pu.cs->picture->cs->getCU( lumaArea.pos(), CH_L ) : *pu.cu;//亮度CU
const CodingUnit& cu = *pu.cu;//CU
const CompArea& area = isChroma( pu.chType ) ? chromaArea : lumaArea;
const uint32_t uiTuWidth = area.width;//TU宽
const uint32_t uiTuHeight = area.height;//TU高
int iBaseUnitSize = ( 1 << MIN_CU_LOG2 );
const int iUnitWidth = iBaseUnitSize >> getComponentScaleX( area.compID, area.chromaFormat );
const int iUnitHeight = iBaseUnitSize >> getComponentScaleX( area.compID, area.chromaFormat );
const int iTUWidthInUnits = uiTuWidth / iUnitWidth;
const int iTUHeightInUnits = uiTuHeight / iUnitHeight;
const int iAboveUnits = iTUWidthInUnits;//上边用几个
const int iLeftUnits = iTUHeightInUnits;
const int chromaUnitWidth = iBaseUnitSize >> getComponentScaleX(COMPONENT_Cb, area.chromaFormat);
const int chromaUnitHeight = iBaseUnitSize >> getComponentScaleX(COMPONENT_Cb, area.chromaFormat);
/* 下面是对MDLM的定义 */
const int topTemplateSampNum = 2 * uiCWidth; // for MDLM, the number of template samples is 2W or 2H.
const int leftTemplateSampNum = 2 * uiCHeight;
assert(m_topRefLength >= topTemplateSampNum);
assert(m_leftRefLength >= leftTemplateSampNum);
const int totalAboveUnits = (topTemplateSampNum + (chromaUnitWidth - 1)) / chromaUnitWidth;//右上用几个
const int totalLeftUnits = (leftTemplateSampNum + (chromaUnitHeight - 1)) / chromaUnitHeight;
const int totalUnits = totalLeftUnits + totalAboveUnits + 1;
const int aboveRightUnits = totalAboveUnits - iAboveUnits;//右上用几个
const int leftBelowUnits = totalLeftUnits - iLeftUnits;//左下用 几个
/**/
int avaiAboveRightUnits = 0;
int avaiLeftBelowUnits = 0;
bool bNeighborFlags[4 * MAX_NUM_PART_IDXS_IN_CTU_WIDTH + 1];
memset(bNeighborFlags, 0, totalUnits);
bool bAboveAvaillable, bLeftAvaillable;
/**************************** 确定上边和左边是否可用,1:2:1下采样上边和左边(就是特殊处理下边边角角) ****************************/
int availlableUnit = isLeftAvailable( isChroma( pu.chType ) ? cu : lumaCU, toChannelType( area.compID ), area.pos(), iLeftUnits, iUnitHeight,
( bNeighborFlags + iLeftUnits + leftBelowUnits - 1 ) );
bLeftAvaillable = availlableUnit == iTUHeightInUnits;
availlableUnit = isAboveAvailable( isChroma( pu.chType ) ? cu : lumaCU, toChannelType( area.compID ), area.pos(), iAboveUnits, iUnitWidth,
( bNeighborFlags + iLeftUnits + leftBelowUnits + 1 ) );
bAboveAvaillable = availlableUnit == iTUWidthInUnits;
if (bLeftAvaillable) // if left is not available, then the below left is not available
{
avaiLeftBelowUnits = isBelowLeftAvailable(isChroma(pu.chType) ? cu : lumaCU, toChannelType(area.compID), area.bottomLeftComp(area.compID), leftBelowUnits, iUnitHeight, (bNeighborFlags + leftBelowUnits - 1));
}
if (bAboveAvaillable) // if above is not available, then the above right is not available.
{
avaiAboveRightUnits = isAboveRightAvailable(isChroma(pu.chType) ? cu : lumaCU, toChannelType(area.compID), area.topRightComp(area.compID), aboveRightUnits, iUnitWidth, (bNeighborFlags + iLeftUnits + leftBelowUnits + iAboveUnits + 1));
}
Pel* pDst = nullptr;
Pel const* piSrc = nullptr;
bool isFirstRowOfCtu = ((pu.block(COMPONENT_Cb).y)&(((pu.cs->sps)->getMaxCUWidth() >> 1) - 1)) == 0;
/*** 如果不是第一行,那么上可用,可以处理第一行重建的 ***/
if( bAboveAvaillable )
{
pDst = pDst0 - iDstStride;
int addedAboveRight = 0;
if ((curChromaMode == MDLM_L_IDX) || (curChromaMode == MDLM_T_IDX))
{
addedAboveRight = avaiAboveRightUnits*chromaUnitWidth;
}
for (int i = 0; i < uiCWidth + addedAboveRight; i++)
{
if (isFirstRowOfCtu)
{
piSrc = pRecSrc0 - iRecStride;
if (i == 0 && !bLeftAvaillable)
{
pDst[i] = piSrc[2 * i];
}
else
{
pDst[i] = ( piSrc[2 * i] * 2 + piSrc[2 * i - 1] + piSrc[2 * i + 1] + 2 ) >> 2;
}
}
#if JVET_M0142_CCLM_COLLOCATED_CHROMA
else if( pu.cs->sps->getCclmCollocatedChromaFlag() )
{
piSrc = pRecSrc0 - iRecStride2;
if( i == 0 && !bLeftAvaillable )
{
pDst[i] = ( piSrc[2 * i] * 2 + piSrc[2 * i - iRecStride] + piSrc[2 * i + iRecStride] + 2 ) >> 2;
}
else
{
pDst[i] = ( piSrc[2 * i - iRecStride]
+ piSrc[2 * i ] * 4 + piSrc[2 * i - 1] + piSrc[2 * i + 1]
+ piSrc[2 * i + iRecStride]
+ 4 ) >> 3;
}
}
#endif
else
{
piSrc = pRecSrc0 - iRecStride2;
if (i == 0 && !bLeftAvaillable)
{
pDst[i] = ( piSrc[2 * i] + piSrc[2 * i + iRecStride] + 1 ) >> 1;
}
else
{
pDst[i] = ( ( ( piSrc[2 * i ] * 2 ) + piSrc[2 * i - 1 ] + piSrc[2 * i + 1 ] )
+ ( ( piSrc[2 * i + iRecStride] * 2 ) + piSrc[2 * i - 1 + iRecStride] + piSrc[2 * i + 1 + iRecStride] )
+ 4 ) >> 3;
}
}
}
}
/*** 如果不是第一列,那么左可用,可以处理第一列重建的 ***/
if( bLeftAvaillable )
{
pDst = pDst0 - 1;
piSrc = pRecSrc0 - 3;
int addedLeftBelow = 0;
if ((curChromaMode == MDLM_L_IDX) || (curChromaMode == MDLM_T_IDX))
{
addedLeftBelow = avaiLeftBelowUnits*chromaUnitHeight;
}
for (int j = 0; j < uiCHeight + addedLeftBelow; j++)
{
#if JVET_M0142_CCLM_COLLOCATED_CHROMA
if( pu.cs->sps->getCclmCollocatedChromaFlag() )
{
if( j == 0 && !bAboveAvaillable )
{
pDst[0] = ( piSrc[1] * 2 + piSrc[0] + piSrc[2] + 2 ) >> 2;
}
else
{
pDst[0] = ( piSrc[1 - iRecStride]
+ piSrc[1 ] * 4 + piSrc[0] + piSrc[2]
+ piSrc[1 + iRecStride]
+ 4 ) >> 3;
}
}
else
{
#endif
pDst[0] = ( ( piSrc[1 ] * 2 + piSrc[0 ] + piSrc[2 ] )
+ ( piSrc[1 + iRecStride] * 2 + piSrc[iRecStride] + piSrc[2 + iRecStride] )
+ 4 ) >> 3;
#if JVET_M0142_CCLM_COLLOCATED_CHROMA
}
#endif
piSrc += iRecStride2;
pDst += iDstStride;
}
}
/*********************************************** 1:2:1下采样除了边缘的内部部分 *****************************************/
// inner part from reconstructed picture buffer 重建图片缓冲区的内部部分
for( int j = 0; j < uiCHeight; j++ )
{
for( int i = 0; i < uiCWidth; i++ )
{
#if JVET_M0142_CCLM_COLLOCATED_CHROMA
if( pu.cs->sps->getCclmCollocatedChromaFlag() )
{
if( i == 0 && !bLeftAvaillable )//处理
{
if( j == 0 && !bAboveAvaillable )
{
pDst0[i] = pRecSrc0[2 * i];
}
else
{
pDst0[i] = ( pRecSrc0[2 * i] * 2 + pRecSrc0[2 * i - iRecStride] + pRecSrc0[2 * i + iRecStride] + 2 ) >> 2;
}
}
else if( j == 0 && !bAboveAvaillable )
{
pDst0[i] = ( pRecSrc0[2 * i] * 2 + pRecSrc0[2 * i - 1] + pRecSrc0[2 * i + 1] + 2 ) >> 2;
}
else
{
pDst0[i] = ( pRecSrc0[2 * i - iRecStride]
+ pRecSrc0[2 * i ] * 4 + pRecSrc0[2 * i - 1] + pRecSrc0[2 * i + 1]
+ pRecSrc0[2 * i + iRecStride]
+ 4 ) >> 3;
}
}
else
{
#endif
if( i == 0 && !bLeftAvaillable )
{
pDst0[i] = ( pRecSrc0[2 * i] + pRecSrc0[2 * i + iRecStride] + 1 ) >> 1;
}
else
{
pDst0[i] = ( pRecSrc0[2 * i ] * 2 + pRecSrc0[2 * i + 1 ] + pRecSrc0[2 * i - 1 ]
+ pRecSrc0[2 * i + iRecStride] * 2 + pRecSrc0[2 * i + 1 + iRecStride] + pRecSrc0[2 * i - 1 + iRecStride]
+ 4 ) >> 3;
}
#if JVET_M0142_CCLM_COLLOCATED_CHROMA
}
#endif
}
pDst0 += iDstStride; //Dst处理一行或一列
pRecSrc0 += iRecStride2; //Src要跳过两行或两列
}
}
2 构建模型:xGetLMParameters
主要工作:根据NxN的亮度块的左边和上边像素,取点,找到Y的最大值和最小值点,根据他们的C值求得线性模型参数a和b。
目的:求得a、b为了后面使用该线性模型进行预测像素的色度值。
void IntraPrediction::xGetLMParameters(const PredictionUnit &pu, const ComponentID compID,
const CompArea &chromaArea,
int &a, int &b, int &iShift)//这里面是第一步:建立模型得到a和b
{
/************************************************ 初始化 ******************************************/
CHECK(compID == COMPONENT_Y, "");
const SizeType cWidth = chromaArea.width;
const SizeType cHeight = chromaArea.height;
const Position posLT = chromaArea;
CodingStructure & cs = *(pu.cs);
const CodingUnit &cu = *(pu.cu);
const SPS & sps = *cs.sps;
const uint32_t tuWidth = chromaArea.width;
const uint32_t tuHeight = chromaArea.height;
const ChromaFormat nChromaFormat = sps.getChromaFormatIdc();
const int baseUnitSize = 1 << MIN_CU_LOG2;
const int unitWidth = baseUnitSize >> getComponentScaleX(chromaArea.compID, nChromaFormat);
const int unitHeight = baseUnitSize >> getComponentScaleX(chromaArea.compID, nChromaFormat);
const int tuWidthInUnits = tuWidth / unitWidth;
const int tuHeightInUnits = tuHeight / unitHeight;
const int aboveUnits = tuWidthInUnits;//上面可以用几个采样点。宽若是8则这个值一般为4。后续左边同理
const int leftUnits = tuHeightInUnits;
int topTemplateSampNum = 2 * cWidth; // for MDLM, the template sample number is 2W or 2H;
int leftTemplateSampNum = 2 * cHeight;
assert(m_topRefLength >= topTemplateSampNum);
assert(m_leftRefLength >= leftTemplateSampNum);
int totalAboveUnits = (topTemplateSampNum + (unitWidth - 1)) / unitWidth;//上面最多可用几个采样点。宽若是8则这个值一般为4*2=8
int totalLeftUnits = (leftTemplateSampNum + (unitHeight - 1)) / unitHeight;
int totalUnits = totalLeftUnits + totalAboveUnits + 1;
int aboveRightUnits = totalAboveUnits - aboveUnits;//右上可以用几个采样点。宽若是8则这个值一般为 4*2-4 = 4
int leftBelowUnits = totalLeftUnits - leftUnits;
/*下面四个变量在后面赋值并参与运算*/
int avaiAboveRightUnits = 0;
int avaiLeftBelowUnits = 0;
int avaiAboveUnits = 0;
int avaiLeftUnits = 0;
int curChromaMode = pu.intraDir[1];//看当前模式是67,68还是69
bool neighborFlags[4 * MAX_NUM_PART_IDXS_IN_CTU_WIDTH + 1];
memset(neighborFlags, 0, totalUnits);
/*********************************************** 看左和上都是否可用 ******************************************/
bool aboveAvailable, leftAvailable;
int availableUnit =//临时定义的,为了看后面上和左是否可用
isAboveAvailable(cu, CHANNEL_TYPE_CHROMA, posLT, aboveUnits, unitWidth,
(neighborFlags + leftUnits + leftBelowUnits + 1));
aboveAvailable = availableUnit == tuWidthInUnits;//上是否可用(上面是不是边缘)
availableUnit =
isLeftAvailable(cu, CHANNEL_TYPE_CHROMA, posLT, leftUnits, unitHeight,
(neighborFlags + leftUnits + leftBelowUnits - 1));
leftAvailable = availableUnit == tuHeightInUnits;//左是否可用(左面是不是边缘)
if (leftAvailable) // if left is not available, then the below left is not available,左不可用,左下一定不可用
{
avaiLeftUnits = tuHeightInUnits;
avaiLeftBelowUnits = isBelowLeftAvailable(cu, CHANNEL_TYPE_CHROMA, chromaArea.bottomLeftComp(chromaArea.compID), leftBelowUnits, unitHeight, (neighborFlags + leftBelowUnits - 1));
}
if (aboveAvailable) // if above is not available, then the above right is not available.上不可用,上右一定不可用
{
avaiAboveUnits = tuWidthInUnits;
avaiAboveRightUnits = isAboveRightAvailable(cu, CHANNEL_TYPE_CHROMA, chromaArea.topRightComp(chromaArea.compID), aboveRightUnits, unitWidth, (neighborFlags + leftUnits + leftBelowUnits + aboveUnits + 1));
}
Pel *srcColor0, *curChroma0;
int srcStride, curStride;//一行的跨度
PelBuf temp;
if ((curChromaMode == MDLM_L_IDX) || (curChromaMode == MDLM_T_IDX))
{
srcStride = 2 * MAX_CU_SIZE + 1;
temp = PelBuf(m_pMdlmTemp + srcStride + 1, srcStride, Size(chromaArea));
}
else
{
srcStride = MAX_CU_SIZE + 1;
temp = PelBuf(m_piTemp + srcStride + 1, srcStride, Size(chromaArea));
}
srcColor0 = temp.bufAt(0, 0);
curChroma0 = getPredictorPtr(compID);
curStride = m_topRefLength + 1;
curChroma0 += curStride + 1;
unsigned internalBitDepth = sps.getBitDepth(CHANNEL_TYPE_CHROMA);
/********** Y是横坐标,C是纵坐标。这里找到最左和最右两个点,即亮度值最小和最大的两个点,拿这个做线性模型 ********/
int minLuma[2] = { MAX_INT, 0 };//最左边的点:亮度值最小的点,数组中的两个值分别是{Y,C}
int maxLuma[2] = { -MAX_INT, 0 };//最右边的点:亮度值最大的点,数组中的两个值分别是{Y,C}
Pel *src = srcColor0 - srcStride;//这里对应亮度下采样后得到的buffer(NxN),减去是因为应指向往前的一行
Pel *cur = curChroma0 - curStride;//这里色度CCLM预测后得到的buffer(NxN),减去是因为应指向往前的一行
int minDim = 1;
int actualTopTemplateSampNum = 0;
int actualLeftTemplateSampNum = 0;
if (curChromaMode == MDLM_T_IDX)//MDLM_T的情况:只用上*2
{
leftAvailable = 0;
actualTopTemplateSampNum = unitWidth*(avaiAboveUnits + avaiAboveRightUnits);
minDim = actualTopTemplateSampNum;
}
else if (curChromaMode == MDLM_L_IDX)//MDLM_L的情况:只用左*2
{
aboveAvailable = 0;
actualLeftTemplateSampNum = unitHeight*(avaiLeftUnits + avaiLeftBelowUnits);
minDim = actualLeftTemplateSampNum;
}
else if (curChromaMode == LM_CHROMA_IDX)//CCLM的情况:用左和上
{
actualTopTemplateSampNum = cWidth;
actualLeftTemplateSampNum = cHeight;
minDim = leftAvailable && aboveAvailable ? 1 << g_aucPrevLog2[std::min(actualLeftTemplateSampNum, actualTopTemplateSampNum)]
: 1 << g_aucPrevLog2[leftAvailable ? actualLeftTemplateSampNum : actualTopTemplateSampNum];
}
/****************************** 取点,根据一一对应的亮度和色度找到最大最小 *************************************/
int numSteps = minDim;//短边长:为了保证长边和短边取点数一致。如8*2,2上取2点,8上也得取2点
if (aboveAvailable)//比较亮度大小,取出最大和最小亮度值及其对应色度值。
{
for (int j = 0; j < numSteps; j++)
{
int idx = (j * actualTopTemplateSampNum) / minDim;//步长:为了保证长边和短边取点数一致
if (minLuma[0] > src[idx])
{
minLuma[0] = src[idx];//最小亮度对应的亮度值
minLuma[1] = cur[idx];//最小亮度对应的色度值
}
if (maxLuma[0] < src[idx])
{
maxLuma[0] = src[idx];//最大亮度对应的亮度值
maxLuma[1] = cur[idx];//最大亮度对应的色度值
}
}
}
if (leftAvailable)//代码和上很相似,操作也同上
{
src = srcColor0 - 1;
cur = curChroma0 - 1;
for (int i = 0; i < numSteps; i++)
{
int idx = (i * actualLeftTemplateSampNum) / minDim;
if (minLuma[0] > src[srcStride * idx])
{
minLuma[0] = src[srcStride * idx];
minLuma[1] = cur[curStride * idx];
}
if (maxLuma[0] < src[srcStride * idx])
{
maxLuma[0] = src[srcStride * idx];
maxLuma[1] = cur[curStride * idx];
}
}
}
/**************************** 进行计算a b ,这里极为麻烦,暂时跳过 ************************************/
if (leftAvailable || aboveAvailable)
{
#if JVET_M0064_CCLM_SIMPLIFICATION
int diff = maxLuma[0] - minLuma[0];
if (diff > 0)
{
int diffC = maxLuma[1] - minLuma[1];
int x = floorLog2( diff );
static const uint8_t DivSigTable[1 << 4] = {
// 4bit significands - 8 ( MSB is omitted )
0, 7, 6, 5, 5, 4, 4, 3, 3, 2, 2, 1, 1, 1, 1, 0
};
int normDiff = (diff << 4 >> x) & 15;
int v = DivSigTable[normDiff] | 8;
x += normDiff != 0;
int y = floorLog2( abs( diffC ) ) + 1;
int add = 1 << y >> 1;
a = (diffC * v + add) >> y;
iShift = 3 + x - y;
if ( iShift < 1 ) {
iShift = 1;
a = ( (a == 0)? 0: (a < 0)? -15 : 15 ); // a=Sign(a)*15
}
b = minLuma[1] - ((a * minLuma[0]) >> iShift);
}
else
{
a = 0;
b = minLuma[1];
iShift = 0;
}
#else // original
a = 0;
iShift = 16;
int shift = (internalBitDepth > 8) ? internalBitDepth - 9 : 0;
int add = shift ? 1 << (shift - 1) : 0;
int diff = (maxLuma[0] - minLuma[0] + add) >> shift;
if (diff > 0)
{
int div = ((maxLuma[1] - minLuma[1]) * g_aiLMDivTableLow[diff - 1] + 32768) >> 16;
a = (((maxLuma[1] - minLuma[1]) * g_aiLMDivTableHigh[diff - 1] + div + add) >> shift);
}
b = minLuma[1] - ((a * minLuma[0]) >> iShift);
#endif
}
else
{
a = 0;
b = 1 << (internalBitDepth - 1);
iShift = 0;
}
}
3 色度预测:piPred.linearTransform
主要工作:建立线性模型,通过该模型确定每个pu内所有像素的色度值。
目的:这个函数就是对数字的一系列操作。暂且开个坑,以后填(其实,嘿嘿,这个坑不敢保证后面会不会填了。。。。。)