Image

1 CFLImage

1.1 CFLImage 개요

FLImaging® 에는 기본적으로 이미지 객체(CFLImage)가 있습니다. 이 이미지 객체에 이미지 파일을 로드하거나, 알고리즘을 이 이미지에 적용하거나, 변경된 이미지를 파일로 저장하는 등 여러 기능을 사용할 수 있습니다.

CFLImage object
Fig. CFLImage object

1.2 CFLImage 제공 기능

이외에도 개발에 필요한 풍부한 기능을 제공합니다.

2 이미지 로드하기

2.1 새 이미지 로드

먼저 이미지 객체를 선언하고, 선언한 객체에 Load() 함수를 호출해서 이미지를 로드할 수 있습니다.

Load Image
Fig. CFLImage Load
// Declare an image object
CFLImage fli;
// Load an image
CResult res = fli.Load(L"D:\\ExampleImage.png");

2.2 기존 이미지의 맨 앞장에 추가로 로드

로드한 이미지의 맨 앞장에 새 이미지를 로드할 수 있습니다.

LoadFront
Fig. CFLImage LoadFront
// Insert an image file into front page
CResult res = fli.LoadFront(L"D:\\ImageToPushFront.png");

2.3 기존 이미지의 맨 뒷장에 추가로 로드

로드한 이미지의 맨 뒷장에 새 이미지를 로드할 수 있습니다.

LoadBack
Fig. CFLImage LoadBack
// Insert an image file into last page
CResult res = fli.LoadBack(L"D:\\ImageToPushBack.png");

2.4 기존 이미지의 특정 인덱스에 로드하여 삽입

로드한 이미지의 원하는 페이지에 새 이미지를 로드해서 삽입할 수 있습니다. 아래는 1번 인덱스 위치에 이미지를 삽입하는 예제입니다.

LoadInsert
Fig. CFLImage LoadInsert
// Load an image file to the desired page index
CResult res = fli.LoadInsert(L"D:\\ImageToInsert.png", 1);

2.5 Raw 이미지 로드

Raw 이미지를 로드하려면 이미지의 픽셀 정보, 이미지의 너비와 높이 정보 등이 추가로 필요합니다. 추가 정보를 전달하며 raw 이미지를 로드하는 예제입니다.

// Declare an image object
CFLImage fli;
// Set parameters
int32_t i32Width = 2660; // image width
int32_t i32Height = 2560; // image height
EPixelFormat ePixelFormat = EPixelFormat_C1_U8;
int32_t i32AlignByte = 4; // The alignment byte
int64_t i64OffsetFromBegin = 0; // The offset from the beginning of the data.
int64_t i64OffsetFromEnd = 0; // The offset from the end of the data, starting from the end of the file.

// Load a raw image
CResult res = fli.LoadRaw(L"D:\\ExampleRawImage.raw", i32Width, i32Height, ePixelFormat, i32AlignByte, i64OffsetFromBegin, i64OffsetFromEnd);

3 이미지 버퍼에 접근하기

3.1 이미지 버퍼 얻어 오기

이미지 버퍼를 얻고 싶다면 아래와 같이 버퍼의 시작 위치 포인터를 얻어 올 수 있습니다.

CFLImage fli;
uint8_t* pU8Buffer = fli.GetBuffer();

CFLImage::GetBuffer()uint8_t*를 리턴하므로, 이미지의 픽셀 값 타입에 따라 버퍼 포인터를 다른 자료형으로 캐스팅해서 사용합니다.

CFLImage fli;
uint8_t* pU8Buffer = fli.GetBuffer();
int32_t i32Depth = fli.GetDepth();

switch(fli.GetValueType())
{
case EValueType_FloatingPoint:
	{
		if(i32Depth == 64)
		{
			double* pF64Buffer = (double*)pU8Buffer;
			// do something...
		}
		else if(i32Depth == 32)
		{
			float* pF32Buffer = (float*)pU8Buffer;
			// do something...
		}
	}
	break;
case EValueType_Signed:
	{
		if(i32Depth == 64)
		{
			int64_t* pI64Buffer = (int64_t*)pU8Buffer;
			// do something...
		}
		else if(i32Depth == 32)
		{
			int32_t* pI32Buffer = (int32_t*)pU8Buffer;
			// do something...
		}
		else if(i32Depth > 8)
		{
			int16_t* pI16Buffer = (int16_t*)pU8Buffer;
			// do something...
		}
		else if(i32Depth == 8)
		{
			int8_t* pI8Buffer = (int8_t*)pU8Buffer;
			// do something...
		}
	}
	break;
case EValueType_Unsigned:
	{
		if(i32Depth == 64)
		{
			uint64_t* pU64Buffer = (uint64_t*)pU8Buffer;
			// do something...
		}
		else if(i32Depth == 32)
		{
			uint32_t* pU32Buffer = (uint32_t*)pU8Buffer;
			// do something...
		}
		else if(i32Depth > 8)
		{
			uint16_t* pU16Buffer = (uint16_t*)pU8Buffer;
			// do something...
		}
		else if(i32Depth == 8)
		{
			// do something with uint8_t* pU8Buffer
		}
	}
	break;
}

3.2 이미지 버퍼의 픽셀 값을 얻어 오거나 변경하기

아래 코드는 반복문을 통해 이미지의 모든 픽셀 값에 0.5를 곱하는 예제를 보여 줍니다. 이 예제를 통해 이미지의 버퍼에 접근해서 값을 얻어 오거나 변경하는 방법을 익혀 보세요.

CFLImage fli;
uint8_t* pU8Buffer = fli.GetBuffer();
int32_t i32Depth = fli.GetDepth();
int64_t i64W = fli.GetWidth(); // 이미지의 너비 (픽셀 단위)
int64_t i64H = fli.GetHeight(); // 이미지의 높이 (픽셀 단위)
int64_t i64WidthStepByte = fli.GetWidthStepByte(); // 한 줄(row)을 저장하는 데 필요한 바이트 수

switch(fli.GetValueType())
{
case EValueType_FloatingPoint:
    {
        if(i32Depth == 64)
        {
            for(int64_t y = 0; y < i64H; ++y)
            {
                double* pF64Buffer = (double*)pU8Buffer;

                for(int64_t x = 0; x < i64W; ++x)
                {
                    double f64PixelValue = *(pF64Buffer + x);
                    *(pF64Buffer + x) = f64PixelValue * 0.5;
                    // do something...
                }
                // 한 줄 내리기
                pU8Buffer += i64WidthStepByte;
            }
        }
        else if(i32Depth == 32)
        {
            for(int64_t y = 0; y < i64H; ++y)
            {
                float* pF32Buffer = (float*)pU8Buffer;

                for(int64_t x = 0; x < i64W; ++x)
                {
                    float f32PixelValue = *(pF32Buffer + x);
                    *(pF32Buffer + x) = f32PixelValue * 0.5f;
                    // do something...
                }
                // 한 줄 내리기
                pU8Buffer += i64WidthStepByte;
            }
        }
    }
    break;
    // and so on...

Width Step Byte 란?

이미지 처리에서 width step (또는 stride)과 byte에 대한 개념은 주로 컴퓨터 비전이나 그래픽스에서 다루는 내용입니다. 이는 이미지 데이터를 메모리에서 어떻게 저장하고 처리하는지와 관련이 있습니다.

Byte와 Width Step

이미지의 메모리 구조는 다음과 같은 방식으로 정의됩니다:

3.3 Y-Offset Table, X-Offset Table 이용하기

위에서 확인한 예제에서는 width step byte와 버퍼 casting을 이용하여 각 줄의 시작 위치와 각 픽셀의 시작 위치를 알 수 있었습니다. 그러나 y-offset table, x-offset table을 이용하면 각 줄의 시작 주소와 각 픽셀 값의 시작 위치에 바로 접근할 수 있습니다.

CFLImage fli;
// 4채널 32비트 floating point 이미지 로드
fli.Load(L"D:\\C4_F32_Image.png");

// 각 줄의 시작 주소 테이블
uint8_t** pU8YOffsetTable = fli.GetYOffsetTable();
// 각 픽셀의 시작 위치 오프셋 테이블
int64_t* pU64XOffsetTable = fli.GetXOffsetTable();
// 이미지 버퍼
uint8_t* pU8Buffer = fli.GetBuffer();

for(int64_t y = 0; y < fli.GetHeight(); ++y)
{
	// YOffsetTable을 통해 y 번째 줄의 시작 주소를 얻기
	uint8_t* pU8CurrY = pU8YOffsetTable[y];

	for(int64_t x = 0; x < fli.GetWidth(); ++x)
	{
		// XOffsetTable을 통해 x 번째 픽셀의 시작 주소를 얻기
		float* pF32CurrPixelValue = (float*)(pU8CurrY + pU64XOffsetTable[x]);

		// x 번째 픽셀의 채널 0, 1, 2 값에 0.5를 곱하여 어둡게 만들기
		pF32CurrPixelValue[0] = pF32CurrPixelValue[0] * 0.5f;
		pF32CurrPixelValue[1] = pF32CurrPixelValue[1] * 0.5f;
		pF32CurrPixelValue[2] = pF32CurrPixelValue[2] * 0.5f;
	}
}

위 예제 코드를 실행하면, 아래와 같이 원본 이미지보다 어두워진 이미지를 확인할 수 있습니다.

Original Image
Fig. Original Image
Modified Image
Fig. Modified Image

4 이미지 정리, 페이지 삭제하기

이미지를 클리어하려면 CFLImage::Clear()함수를 사용하면 간단합니다. 이 외에도, CFLImage 클래스는 사용자의 필요에 따라 세분화된 클리어 기능들을 제공합니다. 튜토리얼을 따라 여러 가지 이미지 정리 기능을 익혀보세요.

4.1 페이지 버퍼 클리어하기(ClearPage)

우선 아래와 같이 총 6장의 페이지가 있는 이미지를 CFLImage 객체에 로드했다고 가정합니다.

Original Image
Fig. Original Image

여기서 2번 페이지의 버퍼를 클리어하려면, ClearPage() 함수를 이용해 페이지 버퍼를 클리어할 수 있습니다.

CFLImage fli;
fli.ClearPage(2); // 2번 페이지를 초기화

위 예제 코드를 실행하면, 아래와 같이 2번 페이지가 빈 버퍼가 되는 결과가 나옵니다.

Original Image
Fig. Original Image
Clear Page
Fig. Clear Page

4.2 페이지 삭제하기(RemovePage)

2번 페이지를 삭제하려면, RemovePage 함수를 사용하면 됩니다.

CFLImage fli;
fli.RemovePage(2); // 2번 페이지를 제거

위 예제 코드를 실행하면, 아래와 같이 2번 페이지가 이미지 객체에서 완전히 삭제되어 총 5페이지가 됩니다.

Original Image
Fig. Original Image
Remove Page
Fig. Remove Page

4.3 여러 페이지 삭제하기(RemovePages)

여러 페이지를 삭제하려면, 삭제할 인덱스를 Base::CFLArray<int32_t> 객체에 담아서 RemovePages() 함수에 전달하면 됩니다.

CFLImage fli;
CFLArray<int32_t> flaPageIndex;
flaPageIndex.PushBack(2);
flaPageIndex.PushBack(3);
flaPageIndex.PushBack(4);
fli.RemovePages(flaPageIndex); // 2, 3, 4번 페이지를 제거

위 예제 코드를 실행하면, 아래와 같이 2, 3, 4번 페이지가 이미지 객체에서 완전히 삭제되어 총 3페이지가 됩니다.

Original Image
Fig. Original Image
Remove Pages
Fig. Remove Pages

5 이미지 생성하기

이미지 버퍼를 생성하는 방법을 익혀 봅시다.

5.1 이미지 생성하기 - 단일 값

CFLImage 객체를 선언하고, 이 객체에 원하는 크기와 포맷으로 이미지를 생성하는 방법을 익혀 봅시다. 모든 픽셀 값이 222인 이미지를 생성하는 단순한 예제입니다.

CFLImage fli;
int32_t i32W = 128;
int32_t i32H = 128;
fli.Create(i32W, i32H, CMultiVarUL(222), EPixelFormat_C1_U8);

위 예제 코드를 실행하면 아래와 같이 모든 픽셀 값이 222인 128*128 크기의 1채널 8비트 이미지가 생성됩니다.

Create Image
Fig. Create image by MultiVar

5.2 이미지 생성하기 - 버퍼

CFLImage 객체를 선언하고, 이 객체에 특정 버퍼를 전달하여 3채널 8비트 이미지를 생성하는 방법을 익혀 봅시다.

CFLImage fli;
int32_t i32W = 128;
int32_t i32H = 128;
int32_t i32Channel = 3;
uint8_t* pU8Buffer = new uint8_t[i32W * i32H * i32Channel];

for(int32_t y = 0; y < i32H; ++y)
{
    uint8_t* pU8CurrBuffer = pU8Buffer + y * i32W * i32Channel;

    for(int32_t x = 0; x < i32W; ++x)
    {
        float f32Ratio = (float)x / (float)i32W;
        *pU8CurrBuffer = (uint8_t)(f32Ratio * 255.f); // Blue
        *(pU8CurrBuffer + 1) = 200; // Green
        *(pU8CurrBuffer + 2) = 255 - (uint8_t)(f32Ratio * 255.f); // Red

        pU8CurrBuffer += 3;
    }
}

fli.Create(i32W, i32H, pU8Buffer, EPixelFormat_C3_U8);

위 예제 코드를 실행하면 아래와 같이 미리 정의된 버퍼를 이용해서 이미지가 생성된 것을 확인할 수 있습니다.

Create Image
Fig. Create image by buffer

5.3 페이지 생성하기

CFLImage 객체에 빈 페이지를 생성하는 방법을 익혀 봅시다.

CFLImage fli;
fli.CreatePage(1); // insert index = 1

위 예제 코드를 실행하면 아래와 같이 1번째 인덱스에 빈 페이지가 생성된 것을 확인할 수 있습니다.

Original Image
Fig. Original Image
Create Page
Fig. Create Page

5.4 페이지에 이미지 버퍼 생성

CFLImage 객체의 특정 페이지에 버퍼를 생성하는 방법을 익혀 봅시다.

int32_t i32SelectedPage = 1;
fli.SelectPage(i32SelectedPage); // 페이지 선택
int32_t i32W = 128;
int32_t i32H = 128;
fli.Create(i32W, i32H, CMultiVarUL(222), EPixelFormat_C1_U8);

위 예제 코드를 실행하면 아래와 같이 1번째 인덱스 페이지에 이미지 버퍼가 생성된 것을 확인할 수 있습니다. 우선 페이지를 선택하고, 5.1 이미지 생성하기 섹션에서 사용한 CFLImage::Create() 함수를 호출하면 선택한 페이지에 이미지 버퍼가 생성됩니다.

Original Image
Fig. Original Image
Create Page
Fig. Create buffer on page

6 이미지에 도형과 레이블 삽입, 삭제, 변경

6.1 이미지에 도형과 레이블 추가

이미지 안에 도형과 레이블을 추가할 수 있습니다.

CFLImage fli;
fli.Load(L"D://Image.png");

CFLRect<int32_t> flrB(169, 113, 240, 196);
flrB.SetName(L"B"); // label 설정
CFLRect<int32_t> flrC(50, 283, 121, 365);
flrC.SetName(L"C"); // label 설정

// 이미지에 도형과 label 추가
fli.PushBackFigure(CFigureUtilities::ConvertFigureObjectToString(flrB));
fli.PushBackFigure(CFigureUtilities::ConvertFigureObjectToString(flrC));

위 예제 코드와 같이 도형 객체를 선언, 정의하고 이름을 설정한 뒤(optional) CFigureUtilities::ConvertFigureObjectToString() 을 통해 문자열로 바꾸어서 이미지에 도형과 레이블을 추가할 수 있습니다.

Original Image
Fig. Original Image
Push Back Figure
Fig. Push Back Figure(B,C)

특정 인덱스에 도형을 삽입할 수 있습니다. 아래 예제 코드는 0번 인덱스에 도형과 레이블을 추가하는 방법을 보여 줍니다.

CFLRect<int32_t> flrA(72, 46, 143, 128);
flrA.SetName(L"A");
// 0번 인덱스에 도형과 label 추가
fli.InsertFigureAt(0, CFigureUtilities::ConvertFigureObjectToString(flrA));
Original Image
Fig. Original Image
Insert Figure
Fig. Insert Figure(A)

6.2 이미지에 도형과 레이블 삭제

이미지에 추가한 도형과 레이블을 아래와 같이 인덱스로 접근하여 삭제할 수 있습니다.

// 1번 인덱스 도형을 삭제
fli.DeleteFigureAt(1);

위 코드를 수행하면 아래와 같이 1번 인덱스의 도형(B 위의 사각형)이 삭제된 것을 확인할 수 있습니다.

Original Image
Fig. Original Image
Insert Figure
Fig. Delete Figure(B)

전체 도형을 삭제하려면 아래와 같이 ClearFigures 계열 함수를 이용하면 모든 도형을 삭제할 수 있습니다.

// 현재 선택된 페이지 안의 모든 도형을 삭제
fli.ClearFigures();
// 3번 페이지 안의 모든 도형을 삭제
fli.ClearFigures(3);
// 모든 페이지에서 도형을 전부 삭제
fli.ClearFiguresAllPages();

6.3 인덱스의 도형 설정

특정 인덱스의 도형을 변경하고자 하는 경우, SetFigureAt() 함수를 이용하면 해당 인덱스의 도형을 설정할 수 있습니다.

CFLRegion flrg;
flrg.PushBack(CFLPoint<double>(75.4, 123.0));
flrg.PushBack(CFLPoint<double>(106.9, 38.0));
flrg.PushBack(CFLPoint<double>(140.9, 121.7));
flrg.SetName(L"A");
// 0번 인덱스 도형을 변경
fli.SetFigureAt(0, CFigureUtilities::ConvertFigureObjectToString(flrg));

위 코드를 수행하면 0번째 인덱스의 도형(A 위의 사각형)이 삼각형으로 변경됩니다.

Original Image
Fig. Original Image
Insert Figure
Fig. Set Figure(A)

6.4 이미지에서 도형 얻어 오기

이미지에 저장된 도형 개수와 이미지에 저장된 도형을 인덱스를 통해 얻는 방법입니다. 아래 예제 코드와 같이 GetFigureCount() 를 통해 이미지에 저장된 도형 개수를 얻을 수 있고, GetFigure()를 통해 특정 인덱스의 도형을 문자열로 얻어 올 수 있습니다. 이 문자열을 CFigureUtilities::ConvertFigureStringToObject()를 통해 CFLFigure 객체로 변환할 수 있습니다. 단, CFigureUtilities::ConvertFigureStringToObject()를 사용해 CFLFigure 객체를 얻었다면, 메모리를 반드시 해제해야 합니다.

for(int32_t i = 0; i < fli.GetFigureCount(); i++)
{
    CFLFigure* pFlf = CFigureUtilities::ConvertFigureStringToObject(fli.GetFigure(i));

    if(!pFlf)
        continue;

    CFLString<wchar_t> strName = pFlf->GetName();    
    // do something...

    // 메모리 해제
    delete pFlf;
    pFlf = nullptr;
}

7 추가 정보(컬러 시퀀스, FT 이미지 정보 등) 이용

이미지에 따라 특수 목적의 정보가 필요한 경우가 있습니다. 예를 들어 컬러 이미지의 경우, RGB 순서인지 BGR 순서인지 등의 순서 정보가 필요하고 Fourier Transform 이미지의 경우 원본 이미지 크기나 픽셀 최댓값 등의 정보가 필요합니다. 이 경우 CFLImage 클래스는 Extra data 객체를 통해 추가 정보를 관리합니다.

7.1 이미지에서 추가 정보 얻어 오기

Extra data는 이미지에서 인덱스로 접근 가능합니다. 아래와 같이 GetExtraData()를 통해 이미지에 저장된 CFLImageExtraDataBase 객체의 포인터를 얻어 올 수 있습니다. 이 객체에 대해 GetExtraDataType()을 호출하면 Extra data의 타입을 알 수 있고, 이 타입을 통해 적절한 클래스로 캐스팅하여 추가 정보에 올바르게 접근할 수 있습니다.

CFLImage fli;
fli.Load(L"D://Image.png");

for(int32_t i = 0; i < fli.GetExtraDataCount(); i++)
{
    CFLImageExtraDataBase* pEd = fli.GetExtraData(i);

    if(!pEd)
        continue;

    EImageExtraDataType eExtraDataType = pEd->GetExtraDataType();

    switch(eExtraDataType)
    {
    case FLImaging::Base::EImageExtraDataType_FourierTransform:
        {
            CFLImageExtraDataFT* pExtraDataFT = dynamic_cast<CFLImageExtraDataFT*>(pEd);

            if(!pExtraDataFT)
                break;

            int64_t i64OrigW = pExtraDataFT->GetOriginalWidth();
            int64_t i64OrigH = pExtraDataFT->GetOriginalHeight();
            EPixelFormat ePFOrig = pExtraDataFT->GetOriginalPixelFormat();
            bool bShiftState = pExtraDataFT->GetShiftState();
            double f64MaxVal = pExtraDataFT->GetMaxValue();
        }
        break;
    case FLImaging::Base::EImageExtraDataType_RealValuedFourierTransform:
        {
            CFLImageExtraDataRFT* pExtraDataRFT = dynamic_cast<CFLImageExtraDataRFT*>(pEd);

            if(!pExtraDataRFT)
                break;

            int64_t i64OrigW = pExtraDataRFT->GetOriginalWidth();
            int64_t i64OrigH = pExtraDataRFT->GetOriginalHeight();
            EPixelFormat ePFOrig = pExtraDataRFT->GetOriginalPixelFormat();
            bool bShiftState = pExtraDataRFT->GetShiftState();
            double f64MaxVal = pExtraDataRFT->GetMaxValue();
        }
        break;
    case FLImaging::Base::EImageExtraDataType_Color:
        {
            CFLImageExtraDataColor* pExtraDataColor = dynamic_cast<CFLImageExtraDataColor*>(pEd);

            if(!pExtraDataColor)
                break;

            EColorSequence eCS = pExtraDataColor->GetColorSequence();
        }
        break;
    case FLImaging::Base::EImageExtraDataType_DiscreteWaveletTransform:
        {
            CFLImageExtraDataWT* pExtraDataWT = dynamic_cast<CFLImageExtraDataWT*>(pEd);

            if(!pExtraDataWT)
                break;

            int64_t i64OrigW = pExtraDataWT->GetOriginalWidth();
            int64_t i64OrigH = pExtraDataWT->GetOriginalHeight();
            EPixelFormat ePFOrig = pExtraDataWT->GetOriginalPixelFormat();
            int32_t i32AlignByte = pExtraDataWT->GetOriginalAlignByte();
            int32_t i32WSB = pExtraDataWT->GetOriginalWidthStepByte();
            int64_t i64DecompositionLevel = pExtraDataWT->GetDecompositionLevel();
            int32_t i32BasisFunction = pExtraDataWT->GetBasisFunction();
            bool bNormalized = pExtraDataWT->IsNormalized();
        }
        break;
    case FLImaging::Base::EImageExtraDataType_DiscreteCosineTransform:
        {
            CFLImageExtraDataCT* pExtraDataCT = dynamic_cast<CFLImageExtraDataCT*>(pEd);

            if(!pExtraDataCT)
                break;

            int64_t i64OrigW = pExtraDataCT->GetOriginalWidth();
            int64_t i64OrigH = pExtraDataCT->GetOriginalHeight();
            EPixelFormat ePFOrig = pExtraDataCT->GetOriginalPixelFormat();
            int32_t i32AlignByte = pExtraDataCT->GetOriginalAlignByte();
            int32_t i32WSB = pExtraDataCT->GetOriginalWidthStepByte();
            double f64MaxVal = pExtraDataCT->GetMaxValue();
        }
        break;
    }
}

7.2 이미지에 Extra data 추가하기

이미지에 Extra data를 추가할 수도 있습니다. 아래는 컬러 이미지에 color sequence 정보를 추가하는 예시입니다.

CFLImage fli;
fli.Load(L"D://Image.png");
int32_t i32Channel = fli.GetChannels();

// Extra data 선언
CFLImageExtraDataColor extDataColor;

// Extra data 설정
if(i32Channel == 4)
    extDataColor.SetColorSequence(EColorSequence_RGBA);
else if(i32Channel == 3)
    extDataColor.SetColorSequence(EColorSequence_RGB);

// Extra data를 이미지에 추가
fli.AddExtraData(extDataColor);

7.3 이미지에서 추가 정보 제거

이미지에 추가된 Extra data를 하나씩 제거하거나, 모두 클리어할 수 있습니다. 아래 함수를 통해 Extra data를 제거하면, 현재 선택된 페이지에서 제거됩니다.

// 현재 선택된 페이지에서 0번째 Extra data를 제거
fli.DeleteExtraData(0);
// 현재 선택된 페이지에서 Extra data를 모두 제거
fli.ClearExtraData();