Visualizando imagens médicas com C++ Carlos Eduardo Gesser Sobre esta apresentação ● Apresentação de desafios técnicos na visualização de imagens médicas ● Demonstração de como C++ nos permite resolver estes problemas, utilizando abstrações de alto nível com nenhum (ou pouco) impacto no desempenho ● Técnicas aqui apresentadas podem ser aplicadas em outros domínios ● Disclaimer: ○ Os trechos de código a seguir são simplificações do código original, com o intuito de facilitar a apresentação. ○ 2 Aspectos como tratamento de erros e tratamento de concorrência foram abstraídos. Sobre a Pixeon 3 ● Uma das maiores empresas brasileiras de tecnologia para a saúde. Surgiu em 2012, a partir da fusão das empresas Pixeon, de Florianópolis e Medical Systems, de São Bernardo do Campo. ● Os sistemas da empresa estão presentes em mais de 2000 clientes, compreendendo hospitais, clínicas e centros de diagnóstico de todos os Estados Brasileiros e alguns países da América Latina. ● No portfólio destacam-se soluções de gestão clínica e hospitalar, processamento e distribuição de exames e telediagnóstico, aplicados aos segmentos de Radiologia, Oftalmologia, Análises Clínicas, Gestão de Impressão Médica e Captura de Imagens Médicas, etc. Sobre Mim ● 1999-2003 ○ 4 Bacharelado em Ciências da Computação - UFSC GALS - Gerador de Analisadores Léxicos e Sintáticos ● 2000-2007 Laboratório G-Sigma (Eng. Mec./Elétrica/Automação) ● 2004-2006 Mestrado em Engenharia Elétrica - UFSC ● 2007- Pixeon Sobre o Visualizador de Imagens Médicas Arya 5 ● Em desenvolvimento desde 2009 ○ Primeira versão liberada em 2011 ● Desenvolvido em C++ (~11) para Windows, Linux e Mac ○ Visual Studio, GCC e Clang ● GUI em Qt ● Boost (smart pointers, threads, function, bind, array, optional, variant, filesystem, asio, lexical_cast, string algorithms, iostream) Sobre o Visualizador de Imagens Médicas 6 Visualização de Imagens Médicas 7 ● Requisito primordial: Fidelidade ○ Médico precisa confiar nas informações e nas imagens que está vendo ● Requisito inescapável: Desempenho ○ Exames possuem grandes quantidades de imagens ○ Manipulação de imagens é computacionalmente intensiva ○ Médicos ganham por produtividade… ● C++ permite que se atenda ambos, com abstrações de alto nível e ainda assim garantindo o desempenho Imagens médicas DICOM ● DICOM (Digital Imaging and Communications in Medicine): Padrão para manipular, armazenar, imprimir e transmitir informações em medicina de imagem (http://dicom.nema.org). ● São definidos: ○ Serviços ○ Protocolo de comunicação ○ Formato de arquivo 8 Fluxo das imagens médicas PACS Laudo Picture Archiving and Communication System Equipamento 9 Visualizador Imagens médicas DICOM 10 ● Arquivos de imagem contém: ○ Metadados (cabeçalhos) ○ Dados (imagem) ● Metadados: ○ Informações do paciente ○ Informações do exame ○ Informações da imagem ○ Etc. ● Pixel Data em diversos formatos possíveis: ○ Grayscale 8 a 16 bits, RGB, etc… ○ Com ou sem compressão (JPEG, etc.) DICOM Data Set 11 ● Metadados são agrupados em Data Sets ● Um Data Set representa uma instância de um objeto de informação do mundo real. Ex.: imagem. ● Cada arquivo DICOM contém um (e apenas um) Data Set ● Data Set é um conjunto de Data Elements (Cabeçalhos) ● Data Element é composto por Tag de identificação e conteúdo ● Dados possuem codificação variada Cabeçalhos DICOM Data Set DICOM Data Element Tag Data Element VR Data Element Value Length Data Element Value 2 ou 4 bytes Tipo / Formato do valor, 2 bytes. Ausente em alguns casos Grupo + Elemento, 4 bytes 12 ... Data Element Pixel Data Element Conteúdo de um Data Element 13 ● Tag ○ Identifica unicamente um elemento ○ Composto por dois números de 16 bits cada: Group e Element ○ Elementos são dispostos no arquivo dicom em ordem crescente ● Value Representation (VR) ○ Identifica o tipo de dado do elemento ○ Definido com código de 2 caracteres ○ Não presente em alguns casos (uso de dicionário) ● Length ○ Tamanho do conteúdo do elemento ○ Tamanho sempre é divisível por 2 ● Value ○ Conteúdo propriamente dito Cabeçalhos DICOM - Estrutura de Dados class DataElement { uint16_t m_group; uint16_t m_element; char m_vr[2]; uint32_t m_length; unsigned char* m_data; // Funções de manipulação/acesso }; 14 Cabeçalhos DICOM - Estrutura de Dados class DicomFile { using DataElementList = std::vector<std::unique_ptr<DataElement>>; DataElementList m_elements; // Cabeçalhos ManagedPointer m_pixel_data; // Imagem public: const DataElement *find_data_element(uint16_t group, uint16_t element) const; const DataElement *find_data_element(const char *name) const; // Demais funções de manipulação/acesso }; 15 Manipulação de Cabeçalhos DICOM DicomFile file = … const DataElement *el = file.find_data_element("StudyDescription"); std::string descr(el->data(), el->length()); const DataElement *el = file.find_data_element("Rows" ); unsigned short rows = *reinterpret_cast<unsigned short*>(el->data()); const DataElement *el = file.find_data_element(0x0020, 0x1041); // Posição da fatia float slice_loc = std::stof( std::string(el->data(), el->length()) ); 16 Manipulação de Cabeçalhos DICOM DicomFile file = … const DataElement *el = file.find_data_element("StudyDescription"); std::string descr(el->data(), el->length()); // E se “el” for nulo? // E o padding? const DataElement *el = file.find_data_element("Rows" ); unsigned short rows = *reinterpret_cast<unsigned short*>(el->data()); // O endianess está correto? // unsigned short é o tipo certo? const DataElement *el = file.find_data_element(0x0020, 0x1041); // Posição da fatia float slice_loc = std::stof( std::string(el->data(), el->length()) ); // Neste caso era um número codificado como string 17 Manipulação de Cabeçalhos DICOM DicomFile file = … const DataElement *el = file.find_data_element("StudyDescription"); std::string descr(el->data(), el->length()); // E se “el” for nulo? // E o padding? const DataElement *el = file.find_data_element("Rows" ); unsigned short rows = *reinterpret_cast<unsigned short*>(el->data()); // O endianess está correto? // unsigned short é o tipo certo? const DataElement *el = file.find_data_element(0x0020, 0x1041); // Posição da fatia float slice_loc = std::stof( std::string(el->data(), el->length()) ); // Neste caso era um número codificado como string 18 Value Representation 19 AS Age String Idade no formato NNNU 018M, 036Y DA Date Data no formato YYYYMMDD 19851026 DS Decimal String Número decimal, como string 42, 3.1415 FL Float Single Ponto flutuante 32 bits, binário 0x40490fdb IS Integer String Número inteiro, como string 42 LO Long String String de até 64 caracteres ... SS Signed Short Inteiro de 16 bits com sinal, binário 0x2A00 PN Person Name Nome de pessoa, componentes separados por ^ Doe^John SQ Sequence Data Set Recursivo TM Time Hora no formato HHMMSS 012200 US Unsigneg Short Inteiro de 16 bits sem sinal, binário 0x2A00 Melhorando a Manipulação de Cabeçalhos DICOM //Value Representations template<typename Internal, typename External> struct ValRep{ using raw_type = Internal; using type = External; }; //Long String using LO = ValRep<char*, std::string>; //Decimal String using DS = ValRep<char*, float>; //Unsigned Short using US = ValRep<uint16_t, int>; //... 20 Dicionário DICOM (0002,0010) (0008,0020) (0008,0030) (0008,1030) (0010,0010) (0010,0020) (0010,1005) (0010,1010) (0018,0015) (0020,0032) (0020, 1041) (0028,0002) (0028,0030) (0028,0100) (0028,0010) (0028,0011) ... 21 UI DA TM LO PN LO PN AS CS DS DS US DS US US US TransferSyntaxUID StudyDate StudyTime StudyDescription PatientName PatientID PatientBirthName PatientAge BodyPartExamined ImagePositionPatient SliceLocation; SamplesPerPixel PixelSpacing BitsAllocated Rows Columns Melhorando a Manipulação de Cabeçalhos DICOM template<uint16_t G,uint16_t E, typename VR> struct DicomHeader : GenericDicomHeader<VR> { ... }; //Dicionário using StudyDescription = DicomHeader<0x0008, 0x1030, LO>; using SliceLocation = DicomHeader<0x0020, 0x1041, DS>; using Rows = DicomHeader<0x0028, 0x0010, US>; ... 22 Melhorando a Manipulação de Cabeçalhos DICOM template<typename VR> struct GenericDicomHeader { using internal_type = typename VR::raw_type; using value_type = typename VR::type; const DataElement *element; explicit operator bool() const { return element != nullptr; } value_type value_or(const value_type v) const { return !element ? v : *(*this); } value_type operator *() const { return Converter<value_type, internal_type>()(element); } }; 23 Melhorando a Manipulação de Cabeçalhos DICOM template<typename Out, typename In> struct Converter; template<> struct Converter<char*, std::string>{ std::string operator()(const DataElement *el) { return std::string(el->data(), el->length()); } }; template<> struct Converter<int, uint16_t>{ int operator()(const DataElement *el) { return *reinterpret_cast<uint16_t*>(el->data()); } }; ... 24 Melhorando a Manipulação de Cabeçalhos DICOM template<typename HEADER> HEADER get(const DicomFile &file) { return { file.find_data_element(HEADER::group, HEADER::element) }; } 25 Melhorando a Manipulação de Cabeçalhos DICOM DicomFile file = … std::string descr = get<StudyDescription>(file).value_or(""); int rows = *get<Rows>(file); if (auto pos = get<SliceLocation>(file)) { float x = *pos; ... } else { ... } 26 Melhorando a Manipulação de Cabeçalhos DICOM ● Prós ○ Garantia no acesso aos cabeçalhos ○ Agilidade no desenvolvimento ● Contras ○ Tempo de compilação ○ Arquivos malformados ainda são problema 27 Gerenciamento de Memória 28 ● Exame normal de Tomografia: ○ Imagem = 512 x 512 x 16bits = 500k + Cabeçalhos ○ 500 imagens = ~ 250 mega ● Usuário necessita manter diversos exames abertos ● Memória física se esgota com poucos exames ● Necessária solução para disco (swap) de offload de blocos de memória Gerenciamento de Memória unsigned char* ptr = new unsigned char[size]; save_to_disk(ptr); //memória apontada por ptr é deletada? //... ptr = load_from_disk(ptr); //ptr aponta para novo bloco de memória //e outros ponteiros que apontavam para o mesmo lugar? delete[] ptr; //posso ou devo? 29 Gerenciamento de Memória ● Vamos criar um Smart Pointer para isso ● Gerenciamento transparente da alocação, swap e desalocação ● Wrapper para std::shared_ptr 30 Gerenciamento de Memória //Aloca um bloco de memória “swapável” ManagedPointer ptr = SwapManager::alloc(size); //Obtem o tamanho ptr.size() //Cópia com contagem de referência auto ptr2 = ptr; //Checa se é um ponteiro válido if (ptr2) { ... } //Como acessar a memória em alocada? 31 Gerenciamento de Memória //Aloca um bloco de memória “swapável” ManagedPointer ptr = SwapManager::alloc(size); //Obtem o tamanho ptr.size() //Cópia com contagem de referência auto ptr2 = ptr; //Checa se é um ponteiro válido if (ptr2) { ... } //Como acessar a memória em alocada? ptr.get() // Erro, operações *ptr // não disponíveis 32 Gerenciamento de Memória 33 ● ManagedPointer possui operações de ○ lock para garantir que dados estejam na memória ○ unlock para liberar dados para serem salvos em disco ● E se alguém tentar acessar memória antes/depois do lock? ● RAII - Resource Acquisition is Initialization ● ManagedPointerLock: estrutura auxiliar para acesso à memória dentro de um escopo ○ Construtor garante que memória esteja acessível ○ Destrutor devolve “controle” ao gerenciador Gerenciamento de Memória { ManagedPointerLock lock(ptr); //A partir daqui, é garantido que o block está na memória unsigned char *data = lock.get(); unsigned short *data2 = lock.get_as<unsigned short>(); //disponível até o fim do escopo } //A partir daqui a memória de ptr pode //ser movida para o arquivo, se necessário 34 Gerenciamento de Memória - Ilustração Managed Pointer Managed Pointer Managed Pointer Managed Pointer 35 Managed Pointer Heap Gerenciamento de Memória - Ilustração Managed Pointer Lock Managed Pointer Managed Pointer Managed Pointer Managed Pointer 36 Managed Pointer Heap Gerenciamento de Memória - Ilustração Managed Pointer Lock Managed Pointer Managed Pointer Managed Pointer Lock Managed Pointer 37 Managed Pointer Managed Pointer Heap Gerenciamento de Memória - Ilustração Managed Pointer Lock Managed Pointer Managed Pointer Managed Pointer Lock Managed Pointer 38 Managed Pointer Managed Pointer Heap Gerenciamento de Memória - Ilustração Managed Pointer Lock Managed Pointer Managed Pointer Managed Pointer Lock Managed Pointer Lock 39 Managed Pointer Managed Pointer Managed Pointer Heap Gerenciamento de Memória - Ilustração Managed Pointer Lock Managed Pointer Managed Pointer Managed Pointer Lock Managed Pointer Lock 40 Managed Pointer Managed Pointer Managed Pointer Heap Gerenciamento de Memória - Ilustração Managed Pointer Lock Managed Pointer Managed Pointer Managed Pointer Lock Managed Pointer Lock 41 Managed Pointer Managed Pointer Managed Pointer Heap Gerenciamento de Memória - Implementação class ManagedPointer { friend class ManagedPointerLock; std::shared_ptr<ManagedHandle> m_handle; }; class ManagedPointerLock { ManagedPointer & m_ptr; public: ManagedPointerLock(ManagedPointer& ptr) : m_ptr(ptr) { m_ptr->m_handle->lock(); } ~ManagedPointerLock() { m_ptr->m_handle->unlock(); } unsigned char * get() { return m_ptr->m_handle->get(); } }; 42 Gerenciamento de Memória - Implementação class ManagedHandle { friend class SwapManager; bool m_locked; bool m_in_memory; unsigned char *m_data; //Dados, caso esteja em memória std::size_t m_file_offset; //Offset, caso esteja em arquivo void lock(){ SwapManager::ensure_memory(this); //Garante que dados estarão em m_data m_locked = true; } void unlock(){ m_locked = false; } }; 43 Gerenciamento de Memória - Implementação class SwapManager { friend class ManagedHandle; //Lista dos handles que estão em memória, ordenada por acesso std::vector<std::shared_ptr<ManagedHandle>> m_mem_handles, //Lista dos handles que estão em disco std::vector<std::shared_ptr<ManagedHandle>> m_swp_handles; std::fstream m_file; size_t m_free_mem; //... void ensure_memory(ManagedHandle *handle); //... }; 44 Gerenciamento de Memória - Implementação void SwapManager::ensure_memory(ManagedHandle *handle) { if (handle->m_in_memory) return; for (auto &h : m_mem_handles) { if (m_free_mem < handle->size()) break; if (h->m_locked) continue; move_to_file(h.get()); } move_to_memory(handle); } 45 Gerenciamento de Memória ● Prós ○ Muito mais memória disponível para imagens ○ Sistema seguro e elegante de acesso ● Contras ○ Forma não trivial de acesso à memória 46 Manipulação de Imagens 47 ● Arquivo DICOM pode conter imagens em diversos formatos ● Cabeçalhos relevantes para imagens ○ Transfer Syntax UID (0002,0010) ■ Compressão. Ex: JPEG ○ Rows (0028,0010) e Columns (0028,0011): ■ Dimensões da imagem ○ Bits Allocated (0028,0100): ■ Tamanho de cada canal (8 ou 16 bits) ○ Samples Per Pixel (0028,0002): ■ Número de canais da imagem (1 ou 3) Manipulação de Imagens ● 48 Cabeçalhos que inficam como interpretar dados ○ Bits Stored (0028,0101): ■ Quantos bits são, de fato, utilizados ○ High Bit (0028,0102): ■ Alinhamento dos Bits Stored dentro dos Bits Allocated ○ Pixel Representation (0028,0103): ■ Indica se pixels podem ter valor negativo (1) ou não (0) ○ Photometric Interpretation (0028,0004): ■ Interpretação dos canais da imagem ● MONOCHROME1 (branco → preto) ● MONOCHROME2 (preto → branco) ● RGB ● … Manipulação de Imagens Exemplo: Pixel Data = Array de bytes Bits Allocated = 16 Bits Stored = 12, High Bit = 11 49 Manipulação de Imagens Após processamento inicial, tudo é normalizado para 8, 16 ou 24 bits Imagem 8 bits Imagem 16 bits Imagem 24 bits (RGB) 50 Manipulação de Imagens class ImageRaw { ManagedPointer m_data; std::size_t m_width; std::size_t m_height; Depth m_depth; }; enum Depth { depth8 = 8, depth16 = 16, depth24 = 24 }; using pixel8 = unsigned char; using pixel16 = unsigned short; using pixel24 = std::array<pixel8, 3>; 51 Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { } } 52 Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { auto offset = x + y * image.witdh(); } } 53 Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { auto offset = x + y * image.witdh(); if (image.depth() == depth8) { } else if (image.depth() == depth16) { } else /* if (image.depth() == depth24) */ { } } } 54 Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { auto offset = x + y * image.witdh(); if (image.depth() == depth8) { auto pixel = lock.get_as<pixel8>()[offset]; ... } else if (image.depth() == depth16) { auto pixel = lock.get_as<pixel16>()[offset]; ... } else /* if (image.depth() == depth24) */ { auto pixel= lock.get_as<pixel24>()[offset]; ... } ... } } 55 Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { auto offset = x + y * image.witdh(); if (image.depth() == depth8) { auto pixel = lock.get_as<pixel8>()[offset]; ... } else if (image.depth() == depth16) { auto pixel = lock.get_as<pixel16>()[offset]; ... } else /* if (image.depth() == depth24) */ { auto pixel= lock.get_as<pixel24>()[offset]; ... } ... } } 56 Preferível evitar branching em um loop em uma imagem de 20M pixels Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); if (image.depth() == depth8) { for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { auto offset = x + y * image.witdh(); auto pixel = lock.get_as<pixel8>()[offset]; ... } } } else if (image.depth() == depth16) { for (... } else /* if (image.depth() == depth24) */ { for (... } 57 Manipulação de Imagens pixeon::ManagedLock lock(image.getData()); if (image.depth() == depth8) { for (std::size_t x=0; x<image.witdh(); ++x) { for (std::size_t y=0; y<image.height(); ++y) { auto offset = x + y * image.witdh(); auto pixel = lock.get_as<pixel8>()[offset]; ... } } } else if (image.depth() == depth16) { for (... } else /* if (image.depth() == depth24) */ { for (... } 58 Agora o código está replicado em três lugares Manipulação de Imagens template<typename PixelType> class ImageView { const PixelType *m_data; std::size_t m_width, m_height; public: ImageView(const PixelType *data, std::size_t width, std::size_t height); inline const PixelType &operator[](unsigned index) const { return m_data[index]; } inline const PixelType *data() const { return m_data; } ... }; 60 Manipulação de Imagens template<typename Func> inline void execute_over_image(const ImageRaw &image, Func &&func) { ManagedPointer img_ptr = image->data(); ManagedPointerLock lock(img_ptr); switch(depth) { case depth8: func(ImageView<pixel8>(reinterpret_cast<const pixel8*>(lock.get()), image->width(), image->height())); break; case depth16: func(ImageView<pixel16>(reinterpret_cast<const pixel16*>(lock.get()), image->width(), image->height())); break; case depth24: func(ImageView<pixel24>(reinterpret_cast<const pixel24*>(lock.get()), image->width(), image->height())); break; } } 59 Manipulação de Imagens - Exemplo struct DumpToFile { std::ofstream file; DumpToFile(const char *filename) : file(filename, std::ios::binary) {} template <typename Pixel> void operator()(ImageView<Pixel> view) const { const char *fdata = reinterpret_cast<const char*>(view.data()); file.write( fdata, sizeof(Pixel) * view.size() ); } }; execute_over_image(image, DumpToFile("image.bin")); 61 Manipulação de Imagens - Exemplo int min = 999999, max = 0; execute_over_image(image, [](const auto &view) { for ( unsigned i = 0; i < image.size(); ++i ) { const int pix = get_lum( view[i] ); min = std::min(pix, min); max = std::max(pix, max); } }); template<typename PixelType> inline typename int get_lum(PixelType p) { return p; } template<> inline int get_lum(pixel24 p) { return (306 * p[0] + 601 * p[1] + 116 * p[2]) / 1024; } 62 Manipulação de Imagens - Exemplo struct BlurAlgorithm { template <typename Pixel> void operator()(ImageView<Pixel> in, WritableImageView<Pixel> out) const { //algoritmo para imagens de 1 canal } void operator()(ImageView<pixel24> in, WritableImageView<pixel24> out) const { //algoritmo para imagens de 3 canais } }; transform_image(input, output, BlurAlgorithm()); 63 Manipulação de Imagens ● Prós ○ Branching exteriorizado ○ Tratamento uniforme para os 3 formatos ● Contras ○ Fazer operações sobre imagens fica um pouco mais complicado do que um simples loop 64 Obrigado. Bonus Slides! Reconstrução Multiplanar - MPR Geração de novas imagem a partir de uma pilha de imagens originais 2 Reconstrução Multiplanar ● ● Geração de imagem parametrizada por coordenadas do plano de corte e espessura Cada pixel da imagem resultante calculado como: ○ Média dos pixels (AVG) ○ Maior valor de pixel (MIP) ○ Menor valor de pixel (MinIP) Reconstrução Multiplanar void render(...) { ... for (int x = 0; x<output_witdh; ++x) { for (int y = 0; y<output_height; ++y) { int value = (mpr_type == MinIP) ? 99999 : 0; for (int z = -thickness/2; z<thickness/2; ++z) { int pixel = volume[ transform(x, y, z) ]; if (mpr_type == AVG) value += pixel; else if (mpr_type == MIP) value = std::max(value, pixel); else value = std::min(value, pixel); } if (mpr_type == AVG) value /= thickness; result[ {x, y} ] = value; } } ... } Reconstrução Multiplanar void render(...) { ... for (int x = 0; x<output_witdh; ++x) { for (int y = 0; y<output_height; ++y) { int value = (mpr_type == MinIP) ? 99999 : 0; for (int z = -thickness/2; z<thickness/2; ++z) { int pixel = volume[ transform(x, y, z) ]; if (mpr_type == AVG) value += pixel; else if (mpr_type == MIP) value = std::max(value, pixel); else value = std::min(value, pixel); } if (mpr_type == AVG) value /= thickness; result[ {x, y} ] = value; } } ... } Reconstrução Multiplanar template<typename Acc > void render(...) { ... for (int x = 0; x<output_witdh; ++x) { for (int y = 0; y<output_height; ++y) { int value = Acc::initialize(); for (int z = -thickness/2; z<thickness/2; ++z) { int pixel = volume[ transform(x, y, z) ]; value = Acc::accumulate(value, pixel); } result[ {x, y} ] = Acc::finalize(value, thickness); } } ... } Reconstrução Multiplanar struct AvgAcc { static int initialize() { return 0; } static int accumulate(int value, int pixel) { return value + pixel; } static int finalize(int value, int thickness) { return value / thickness; } }; struct MipAcc { static int initialize() { return 0; } static int accumulate(int value, int pixel) { return std::max(value, pixel); } static int finalize(int value, int thickness) { return value; } }; struct MinipAcc { static int initialize() { return 99999; } static int accumulate(int value, int pixel) { return std::min(value, pixel); } static int finalize(int value, int thickness) { return value; } }; Reconstrução Multiplanar if (mpr_type == AVG) { render<AvgAcc>(...); } else if (mpr_type == MIP) { render<MipAcc>(...); } else { render<MinipAcc>(...); }