컬러 양자화는 이미지의 색상 수(예: 24-bit True Color)를 지정된 수(예: 256색 이하)로 줄이면서도 시각적 손실을 최소화하는 기법이다. Octree 자료구조를 활용하면 색상 공간을 효율적으로 분할하고 축소할 수 있다. Octree는 양자화 과정에서 메모리를 효율적으로 사용하고, 탐색 속도를 향상시켜준다.

  • RGB 색상은 Red, Green, Blue의 3개 축으로 이루어진 3차원 공간(Cuboid)의 각 점에 대응한다. 8개의 자식 노드를 갖는 Octree는 3차원 공간을 8등분씩 균일하게 분할하는 데 최적화된 자료구조이므로 직관적으로 RGB 공간을 구조화를 할 수 있다.
  • 메모리 및 계산 효율성 이미지에 존재하지 않는 색상 영역은 트리의 노드로 생성되지 않아서 256×256×256(=약 1,670만 개) 전체 색상 공간을 배열로 생성할 때 발생하는 메모리 낭비를  줄여준다.
  • 빠른 탐색과 대표색 통합 비트 연산을 통해 픽셀의 RGB 값을 노드 인덱스로 빠르게 변환할 수 있다. 또한, 지정된 최대 색상 수를 초과할 경우 가장 깊은 단계의 자식 노드들을 부모 노드로 병합하는 과정이 간편해 색상 공간상 가까운 색상들을 효율적으로 병합할 수 있다.

1. 핵심 자료구조 (COctreeQuantizer::NODE)

Octree의 각 노드는 8개의 자식 노드(pChild[8])를 가질 수 있으며, 이는 RGB 각각의 Bit값 조합(0~7)에 대응한다.

  • IsLeaf: 리프 노드 여부를 나타내며, 리프 노드에 최종 대표 색상을 저장
  • PixelCount: 해당 노드(또는 하위 영역)에 속한 픽셀의 총개수
  • RedSum, GreenSum, BlueSum: 픽셀들의 RGB 채널별 누적 합산값으로, 최종 팔레트 색상 계산(평균값)에 사용
  • pNext: 축소 가능한 동일 깊이의 노드들을 단방향 연결 리스트로 관리하기 위한 포인터

2. 주요 처리 과정

  • 트리 생성 및 색상 추가 (AddColor) 이미지의 각 픽셀 RGB 값을 탐색하며 트리를 내려갑니다. 비트 연산(rBit << 2 | gBit << 1 | bBit)으로 자식 노드의 인덱스를 결정하며, 리프 노드에 도달하면 픽셀 카운트와 RGB 누적값을 갱신
  • 트리 축소 (ReduceTree) 생성된 리프 노드의 수(m_nLeafCount)가 목표 색상 수(m_MaxColors)를 초과하면, 가장 깊은 레벨의 노드부터 자식 노드들을 병합(Delete & Clear)하여 하나의 리프 노드로 통합
  • 팔레트 생성 (GetPalette) 완성된 Octree의 리프 노드들을 재귀적으로 순회하며, 누적된 RGB 합을 픽셀 수로 나누어 평균 색상을 구한 뒤 최종 팔레트 Vector에 저장
  • 픽셀 재매핑 (QuantizeColor) 원본 이미지의 각 픽셀을 Octree 탐색을 통해 가장 적절하게 양자화된 팔레트 인덱스로 변환
class COctreeQuantizer {
    struct NODE {
        bool IsLeaf;                    // TRUE if node has no children
        uint64_t  PixelCount;           // Number of pixels represented by this leaf
        uint64_t  RedSum;               // Sum of red components
        uint64_t  GreenSum;             // Sum of green components
        uint64_t  BlueSum;              // Sum of blue components
        int  Index;                     // Index of current node;
        struct NODE* pChild[8];    
        struct NODE* pNext;        
    };
    
protected:
    NODE	*m_pTree;
    int		m_nLeafCount;
    NODE	*m_pReducibleNodes[9];
    int		m_MaxColors;
    int		m_ColorBits;
public:
    COctreeQuantizer(int nMaxColors, int nColorBits);
    virtual ~COctreeQuantizer();
    std::vector<RGBQUAD> ProcessImage(const CRaster& raster);
    int QuantizeColor(BYTE r, BYTE g, BYTE b);
protected:
    std::vector<RGBQUAD> GetPalette();
    void AddColor(BYTE r, BYTE g, BYTE b);
    void* CreateNode(int nLevel);
    void ReduceTree();
private:
    void Reset();
    void DeleteTree();
    void DeleteSubtree(NODE* &pNode);
};
COctreeQuantizer::COctreeQuantizer(int nMaxColors, int nColorBits) {
    m_ColorBits  = nColorBits < 8 ? max(1, nColorBits) : 8;
    m_pTree      = NULL;
    m_nLeafCount = 0;
    for	(int i = 0; i <= m_ColorBits; i++) 
        m_pReducibleNodes[i] = NULL;

    m_MaxColors = max(1, nMaxColors);
}
COctreeQuantizer::~COctreeQuantizer() {
    DeleteTree();
}
void COctreeQuantizer::AddColor(BYTE r, BYTE g, BYTE b) {
    // 1. 루트 노드가 없으면 생성
    if (m_pTree == NULL)
        m_pTree = (NODE *)CreateNode(0);

    NODE* pNode = m_pTree;
    // 2. 루프를 통해 트리를 아래로 탐색
    for (int nLevel = 0; nLevel <= m_ColorBits; ++nLevel) {
        // 리프 노드에 도달했으면 RGB값 및 픽셀 수 갱신 후 종료
        if (pNode->IsLeaf) {
            pNode->PixelCount++;
            pNode->RedSum   += r;
            pNode->GreenSum += g;
            pNode->BlueSum  += b;
            return;
        }
        // 비트 연산으로 이동할 자식 노드 인덱스 계산
        int shift  = 7 - nLevel;
        BYTE currentMask = (0x80 >> nLevel);
        int nIndex = (((r & currentMask) >> shift) << 2) |
                     (((g & currentMask) >> shift) << 1) |
                      ((b & currentMask) >> shift);

        // 자식 노드가 없으면 다음 레벨(nLevel + 1) 노드로 새로 생성
        if (pNode->pChild[nIndex] == NULL)
            pNode->pChild[nIndex] = (NODE*)CreateNode(nLevel + 1);
        // 다음 레벨 자식 노드로 이동
        pNode = pNode->pChild[nIndex];
    }
}
void* COctreeQuantizer::CreateNode(int nLevel) {
    NODE* pNode = new NODE (); // 0으로 초기화;

    pNode->IsLeaf = (nLevel ==	m_ColorBits) ? true : false;
    if (pNode->IsLeaf) 
        m_nLeafCount++;
    else {
        pNode->pNext = m_pReducibleNodes[nLevel];
        m_pReducibleNodes[nLevel] = pNode;
    }
    return pNode;
}
void COctreeQuantizer::ReduceTree() {
    // 축소 가능한 노드가 존재하는 가장 깊은 레벨(i)을 탐색;
    int i;
    for	(i = m_ColorBits - 1; (i > 0) && (m_pReducibleNodes[i] == NULL); i--);
    
    // 해당 레벨의 연결 리스트에서 가장 최근에 추가된 노드 1개를 꺼냄;
    NODE* pNode	= m_pReducibleNodes[i];
    m_pReducibleNodes[i] = pNode->pNext;
    
    uint64_t RedSum = 0;
    uint64_t GreenSum = 0;
    uint64_t BlueSum = 0;
    int nChildren = 0;

    for	(int k = 0; k < 8; ++k)	{
        NODE* pChildNode = pNode->pChild[k];
        if (pChildNode) {
            RedSum            += pChildNode->RedSum;
            GreenSum          += pChildNode->GreenSum;
            BlueSum           += pChildNode->BlueSum;
            pNode->PixelCount += pChildNode->PixelCount;
            nChildren++;
            
            delete pChildNode; 
            pNode->pChild[k] = NULL;
        }
    }
    pNode->IsLeaf   = true;
    pNode->RedSum   = RedSum;
    pNode->GreenSum = GreenSum;
    pNode->BlueSum  = BlueSum;
    m_nLeafCount   -= (nChildren - 1);
}
void COctreeQuantizer::DeleteTree() {
    DeleteSubtree(m_pTree);
    m_pTree = NULL;
    m_nLeafCount = 0;
}
void COctreeQuantizer::DeleteSubtree(NODE* &pNode) {
    if (pNode == NULL) return;
    for (int i = 0; i < 8; ++i)
        if (pNode->pChild[i])
            DeleteSubtree(pNode->pChild[i]);

    delete pNode;
    pNode = NULL;
}
void COctreeQuantizer::Reset() {
    DeleteTree();
    for (int i = 0; i <= m_ColorBits; ++i)
        m_pReducibleNodes[i] = NULL;
}
// 최종적으로 leaf node에 대응하는 팔레트 컬러와 인덱스를 생성;
std::vector<RGBQUAD> COctreeQuantizer::GetPalette() {
    if (m_pTree == NULL) return std::vector<RGBQUAD> ();
    
    std::vector<RGBQUAD> Palette;
    Palette.reserve(m_MaxColors);
    
    int nIndex = 0;
    std::stack<NODE*> nodeStack;
    nodeStack.push(m_pTree);
    while (!nodeStack.empty()) {
        NODE* pCurrent = nodeStack.top();
        nodeStack.pop();

        // Leaf 노드인 경우 대표 색상 계산 후 팔레트에 추가;
        if (pCurrent->IsLeaf) {
            RGBQUAD rgba;
            rgba.rgbRed      = BYTE(pCurrent->RedSum   / pCurrent->PixelCount);
            rgba.rgbGreen    = BYTE(pCurrent->GreenSum / pCurrent->PixelCount);
            rgba.rgbBlue     = BYTE(pCurrent->BlueSum  / pCurrent->PixelCount);
            rgba.rgbReserved = 0;

            Palette.push_back(rgba);
            pCurrent->Index = nIndex++;
        } 
        // Leaf 노드가 아닌 경우 자식 노드들을 스택에 푸시
        else {
            // 역순(7->0)으로 푸시해야 순차 탐색(0->7) 순서와 일치;
            for (int i = 7; i >= 0; --i)
                if (pCurrent->pChild[i])
                    nodeStack.push(pCurrent->pChild[i]);
        }
    }
    return Palette;
}
int COctreeQuantizer::QuantizeColor(BYTE r, BYTE g, BYTE b) {
    // 주어진 (r, g, b) 컬러에 대응하는 팔레트 인덱스를 반환; 
    NODE* pNode = m_pTree;
    for (int nLevel = 0; nLevel <= m_ColorBits; ++nLevel) {
        if (pNode->IsLeaf) return pNode->Index;
        int shift = 7 - nLevel;
        BYTE currentMask = (0x80 >> nLevel);
        int nIndex = (((r & currentMask) >> shift) << 2) |
                     (((g & currentMask) >> shift) << 1) |
                      ((b & currentMask) >> shift);
        pNode = pNode->pChild[nIndex];
        if (!pNode) break;
    }
    return 0;
}
std::vector<RGBQUAD> COctreeQuantizer::ProcessImage(const CRaster& raster) {
    // 같은 객체를 재사용하는 경우 대비 reset;
    Reset();
    if (raster.IsEmpty()) return std::vector<RGBQUAD>();
    
    CSize sz = raster.GetSize();
    switch (raster.GetBPP()) {
    case 24: //	24-bit DIB
        for (int y = 0; y < sz.cy; y++) {
            BYTE *p = (BYTE *)raster.GetLinePtr(y) ;
            for	(int x = 0; x < sz.cx; x++, p += 3)	{
                AddColor(p[2], p[1], p[0]);
                while (m_nLeafCount > m_MaxColors) 
                    ReduceTree();
            }
        }
        return GetPalette();       
    default: //	Unrecognized color format
        return std::vector<RGBQUAD>();
    }
    return std::vector<RGBQUAD>();
}
CRaster OctreeQuantizerTest(CRaster& raster) {
    const int nColors = 128;
    COctreeQuantizer oq(nColors, 8) ;
    std::vector<RGBQUAD> Palette = oq.ProcessImage(raster);
    CSize sz = raster.GetSize();
    // 양자화된 컬러를 가지는 24비트 이미지 생성;
    CRaster out;
    out.SetDimensions(sz, 24);
    for (int y = 0; y < sz.cy; y++) {
        BYTE *p = (BYTE*)raster.GetLinePtr(y);
        BYTE *q = (BYTE*)out.GetLinePtr(y);
        for (int x = 0 ; x < sz.cx; x++, p += 3) {
            int idx = oq.QuantizeColor(p[2], p[1], p[0]);
            *q++ = Palette[idx].rgbBlue ;
            *q++ = Palette[idx].rgbGreen ;
            *q++ = Palette[idx].rgbRed ;
        }
    }
    return out;
}

'Image Recognition' 카테고리의 다른 글

등고선을 그리는 Subroutine  (2) 2025.02.04
Image Matting: Knockout method  (1) 2024.07.16
Anisotropic Diffusion Filter (2)  (0) 2024.02.23
Edge Preserving Smoothing  (0) 2024.02.14
Watershed Segmentation  (0) 2021.02.27
,