Em continuação ao artigo anterior sobre inversão vertical de imagens, este artigo aborda a inversão horizontal (FlipX), focando primeiramente em imagens de 32 bits para preparar o terreno para algoritmos mais complexos em imagens de 24 bits.
Serão apresentados tanto algoritmos escalares quanto vetorizados. Estes algoritmos são multiplataforma, permitindo que o mesmo código-fonte seja executado em arquiteturas como x86 (com conjuntos de instruções SSE, AVX, etc.) e ARM (com conjuntos de instruções AdvSIMD, etc.), beneficiando-se da aceleração por hardware SIMD em todas elas.
Algoritmo Escalar
Ideia do Algoritmo
A inversão horizontal, também conhecida como inversão esquerda-direita, espelha a imagem através de seu eixo vertical central. Dada uma função para acessar os pixels de origem src[x, y] e destino dst[x, y], e considerando width como a largura da imagem em pixels, a fórmula para inversão horizontal é:
dst[x, y] = src[width - 1 - x, y]
É importante notar que as coordenadas de pixel começam em 0, portanto, o pixel mais à direita tem o coordenada x de width - 1.
Essencialmente, cada pixel em uma linha é copiado na ordem inversa. Como o processamento é feito pixel a pixel, o algoritmo deve ser adaptado para diferentes tamanhos de pixel.
Detalhes do Pixel de 32 Bits
Pixels de 32 bits são convenientes para processar, pois 32 bits equivalem a 4 bytes, uma potência de dois, o que facilita a manipulação. Por isso, imagens de 32 bits são as mais frequentemente utilizadas.
Existem vários formatos de pixels de 32 bits, dependendo da ordem dos canais de cor e da presença de um canal alfa:
- Bgr32 (também conhecido como BGRX8888, B8G8R8X8; em GDI+ como "Format32bppRgb")
- Bgra32 (também conhecido como BGRA8888, B8G8R8A8; em GDI+ como "Format32bppArgb")
- Pbgra32 (BGRA pré-multiplicado; em GDI+ como "Format32bppPArgb")
- Rgb32 (também conhecido como RGBX8888, R8G8B8X8)
- Rgba32 (também conhecido como RGBA8888, R8G8B8A8)
- Prgba32 (RGBA pré-multiplicado)
Como o objetivo aqui é a inversão horizontal, não é necessário processar os canais de cor individualmente. O pixel inteiro pode ser tratado como uma unidade. Portanto, o algoritmo apresentado é válido para todos os formatos de pixel de 32 bits, não apenas os mencionados acima, mas também outros como CMYK32.
Implementação do Algoritmo
Com o tamanho em bytes do pixel (cbPixel) conhecido (4 bytes para 32 bits), os pixels podem ser copiados.
O código-fonte a seguir demonstra a implementação:
public static unsafe void ScalarDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bits: Bgr32, Bgra32, Rgb32, Rgba32.
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
byte* p = pRow + (width - 1) * cbPixel; // Ponto de partida no final da linha de origem
byte* q = qRow; // Ponto de partida no início da linha de destino
for (int j = 0; j < width; j++) {
// Copia um pixel inteiro (4 bytes)
for (int k = 0; k < cbPixel; k++) {
q[k] = p[k];
}
p -= cbPixel; // Move o ponteiro de origem para o pixel anterior
q += cbPixel; // Move o ponteiro de destino para o próximo pixel
}
pRow += strideSrc; // Avança para a próxima linha de origem
qRow += strideDst; // Avança para a próxima linha de destino
}
}
Ao usar ponteiros, o cálculo de endereços é crucial. Para inversão horizontal, o foco está nos cálculos de endereço relacionados aos pixels dentro de uma linha (o loop interno j).
O loop interno utiliza uma estratégia de "leitura reversa, escrita sequencial":
- A leitura começa do último pixel e, a cada iteração, o ponteiro
pé deslocado para o pixel anterior. Inicialmente,paponta para o último pixel da linha de origem. - A escrita começa do primeiro pixel e, a cada iteração, o ponteiro
qé deslocado para o próximo pixel. Inicialmente,qaponta para o primeiro pixel da linha de destino.
Código de Teste de Referência
Utiliza-se o BenchmarkDotNet para testes de referência.
[Benchmark(Baseline = true)]
public void Scalar() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, false);
}
//[Benchmark]
public void ScalarParallel() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, true);
}
public static unsafe void ScalarDo(BitmapData src, BitmapData dst, bool useParallel = false) {
int width = src.Width;
int height = src.Height;
int strideSrc = src.Stride;
int strideDst = dst.Stride;
byte* pSrc = (byte*)src.Scan0.ToPointer();
byte* pDst = (byte*)dst.Scan0.ToPointer();
// Paralelismo é habilitado se o processamento for para mais de uma linha,
// a altura for razoável e o número de processadores for maior que 1.
bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
if (allowParallel) {
// Processa cada linha em paralelo.
Parallel.For(0, height, i => {
int start = i;
int len = 1;
// Calcula o endereço de início da linha atual para origem e destino.
byte* pSrc2 = pSrc + start * (long)strideSrc;
byte* pDst2 = pDst + start * (long)strideDst;
ScalarDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);
});
} else {
// Processa todas as linhas sequencialmente.
ScalarDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
}
}
Como a inversão horizontal processa pixels dentro de uma linha, o loop externo para cada linha pode ser processado sequencialmente. O cálculo de endereço para paralelismo é relativamente simples neste caso.
Algoritmo Vetorizado
Ideia do Algoritmo
Inversão dentro de um Vetor
Enquanto o algoritmo escalar processa um byte por vez, o algoritmo vetorizado pode processar um vetor de bytes por vez, aproveitando as instruções SIMD. O principal desafio aqui é a granularidade dos vetores.
Um vetor SIMD (como Vector128) tem um tamanho mínimo de 128 bits (16 bytes). Para pixels de 32 bits (4 bytes), um vetor pode conter 4 pixels.
O primeiro obstáculo é realizar a inversão dentro de um único vetor.
Solução com .NET 7.0: Método Shuffle
Antes do .NET 7.0, não havia uma maneira direta de realizar essa operação de reordenação de elementos dentro de um vetor. A partir do .NET 7.0, os tipos de vetor como Vector128 introduziram o método Shuffle, que permite reordenar os elementos de um vetor com base em um vetor de índices.
Existem sobrecargas para diferentes tipos de elementos, como:
public static Vector128<byte> Shuffle(Vector128<byte> vector, Vector128<byte> indices);
public static Vector128<int> Shuffle(Vector128<int> vector, Vector128<int> indices);
// ...
O parâmetro indices especifica quais elementos do vetor de origem devem ser copiados para o vetor de destino. Por exemplo, vector[indices[i]].
Uma abordagem inicial seria usar a versão de Shuffle para bytes, alinhada com a lógica do algoritmo escalar. No entanto, como estamos lidando com pixels de 32 bits, é mais eficiente tratar cada pixel como uma unidade de 32 bits (um inteiro). Assim, a versão de Shuffle para int é preferível.
Para Vector128<int>, que pode conter 4 pixels de 32 bits, a inversão pode ser realizada da seguinte forma:
// Suponha que 'src' seja um Vector128<int> carregado com os pixels de origem.
Vector128<int> indices = Vector128.Create(3, 2, 1, 0); // Índices para inverter a ordem (0,1,2,3 -> 3,2,1,0)
Vector128<int> dst = Vector128.Shuffle(src, indices);
Embora funcional, essa abordagem tem uma desvantagem significativa: desempenho. A análise de descompilação revela que, até o .NET 8.0, o método Shuffle não era acelerado por hardware; em vez disso, ele usava código de fallback escalar.
Outras desvantagens do Shuffle incluem:
- Disponível apenas para tipos de vetor de tamanho fixo (
Vector128,Vector256, etc.), não para o tipo de vetor de tamanho automático (Vector). - Introduzido apenas no .NET 7.0, o que limita a compatibilidade com versões anteriores do .NET.
Usando VectorTraits para Superar as Limitações do Shuffle
A biblioteca VectorTraits foi desenvolvida para resolver o problema da falta de aceleração por hardware no método Shuffle. Ela utiliza instruções específicas de cada arquitetura para fornecer aceleração por hardware para operações de shuffle, incluindo:
- x86: Utiliza instruções como
_mm_shuffle_epi8. - Arm: Utiliza a instrução
vqvtbl1q_u8. - Wasm: Utiliza a instrução
i8x16.swizzle.
Além de suportar tipos de vetor de tamanho fixo, o VectorTraits também oferece suporte para o tipo de vetor de tamanho automático (Vector). Ele também é compatível com versões mais antigas do .NET, incluindo .NET Framework 4.5 e superior.
Para usar a aceleração por hardware com VectorTraits, basta adicionar um 's' ao final do nome do método Shuffle, transformando-o em Vector128s.Shuffle (ou usando a classe estática apropriada para o tipo de vetor). Isso indica o uso da implementação acelerada do VectorTraits.
using Zyl.VectorTraits;
// Suponha que 'src' seja um Vector128<int> carregado com os pixels de origem.
Vector128<int> indices = Vector128.Create(3, 2, 1, 0);
// Usa a versão acelerada por hardware do VectorTraits.
Vector128<int> dst = Vector128s.Shuffle(src, indices);
Usando o Tipo de Vetor de Tamanho Automático Vector<T>
Tipos de vetor de tamanho fixo como Vector128 requerem .NET Core 3.0 ou superior. Para compatibilidade com versões anteriores do .NET (a partir do .NET Framework 4.5), pode-se usar o tipo de vetor de tamanho automático Vector<T>, disponível através do pacote NuGet System.Numerics.Vectors.
O tamanho do Vector<T> é dinâmico, geralmente correspondendo ao tamanho máximo de vetor suportado pela CPU da máquina. Ele fornece a propriedade Count para indicar o número de elemnetos que ele contém.
- Para um vetor de 128 bits,
Vector<int>.Counté 4. - Para um vetor de 256 bits,
Vector<int>.Counté 8.
A natureza dinâmica do tamanho do Vector<T> introduz um desafio para a inicialização do vetor de índices. Em vez de definir os índices explicitamente como em Vector128.Create(3, 2, 1, 0), é necessário criar um array ou Span dinamicamente e preenchê-lo com os índices corretos.
A partir do .NET Core 3.0, os construtores de Vector<T> suportam Span, permitindo o uso de alocação na pilha (stack allocation) para reduzir o overhead de memória.
// Aloca buffer na pilha para os índices.
Span<int> buf = stackalloc int[Vector<int>.Count];
// Preenche o buffer com os índices invertidos.
for (int i = 0; i < Vector<int>.Count; i++) {
buf[i] = Vector<int>.Count - 1 - i;
}
// Cria o vetor de índices usando o buffer.
// Vectors.Create é uma utilidade do VectorTraits para compatibilidade.
Vector<int> indices = Vectors.Create(buf);
O cálculo Vector<int>.Count - 1 - i determina o índice correto para a inversão.
Como o Count de Vector<T> é fixado em tempo de execução, o cálculo do vetor de índices pode ser feito uma vez no construtor estático da classe para otimizar o desempenho, evitando recálculos repetidos.
private static readonly Vector<int> _shuffleIndices;
static ImageFlipXOn32bitBenchmark() {
// Opção 1: Usando a sobrecarga de loop duplo do VectorTraits para inicialização.
_shuffleIndices = Vectors.CreateByDoubleLoop<int>(Vector<int>.Count - 1, -1);
// Opção 2: Usando Span (como mostrado anteriormente).
/*
Span<int> buf = stackalloc int[Vector<int>.Count];
for (int i = 0; i < Vector<int>.Count; i++) {
buf[i] = Vector<int>.Count - 1 - i;
}
_shuffleIndices = Vectors.Create(buf);
*/
}
O método Vectors.CreateByDoubleLoop do VectorTraits simplifica a inicialização de vetores com padrões sequenciais, como os índices de shuffle.
Uma vez que o vetor de índices _shuffleIndices é obtido, a operação de shuffle pode ser realizada no vetor de tamanho automático Vector<T>:
// Suponha que 'src' seja um Vector<int> carregado com os pixels de origem.
Vector<int> indices = _shuffleIndices;
Vector<int> dst = Vectors.Shuffle(src, indices);
Usando o Método YShuffleKernel para Otimização Adicional
O método Shuffle pode zerar elementos se os índices estiverem fora do intervalo. Para a inversão horizontal, todos os índices estão dentro do intervalo válido. Nesses casos, o método YShuffleKernel, também do VectorTraits, pode oferecer melhor desempenho, pois não realiza verificações de limite.
// Suponha que 'src' seja um Vector<int> carregado com os pixels de origem.
Vector<int> indices = _shuffleIndices;
// Usa YShuffleKernel para melhor desempenho em casos onde os índices são sempre válidos.
Vector<int> dst = Vectors.YShuffleKernel(src, indices);
O prefixo 'Y' nos métodos do VectorTraits indica que eles são implementações que visam otimizar o desempenho, diferenciando-os dos métodos padrão do BCL.
Invertendo uma Linha Inteira
Com a capacidade de inverter elementos dentro de um vetor, é possível inverter uma linha inteira de pixels. A estratégia de "leitura reversa, escrita sequencial" continua válida.
Se o número de bytes em uma linha for um múltiplo exato do tamanho do vetor, o processamento é simplificado. Os passos são:
- Inicializar o ponteiro de origem
ppara o final da linha de dados. - Inicializar o ponteiro de destino
qpara o início da linha. - Carregar dados da memória para um vetor usando
p. - Inverter o vetor usando
YShuffleKernel. - Escrever o vetor invertido na memória usando
q. - Verificar se todos os dados foram processados; se sim, terminar.
- Avançar os ponteiros
p(para trás) eq(para frente) para o próximo vetor. - Retornar ao passo 3 para continuar o loop.
- Concluir o processamento da linha.
Quando o número de bytes em uma linha não é um múltiplo do tamanho do vetor, o tratamento dos bytes restantes (o "pedaço final") requer lógica adicional, semelhante à abordagem do "ponteiro final" mencionada em artigos anteriores.
Implementação do Algoritmo
O código a seguir implementa o algoritmo vetorizado:
public static unsafe void UseVectorsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bits: Bgr32, Bgra32, Rgb32, Rgba32.
// Obtém os índices de shuffle pré-calculados.
Vector<int> indices = _shuffleIndices;
// Tamanho do vetor em número de elementos 'int'.
int vectorWidthInts = Vector<int>.Count;
// Largura da linha em bytes que é um múltiplo do tamanho do vetor.
int widthAligned = width - (width % vectorWidthInts);
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
// Ponteiros para o início e fim da linha de origem e destino, alinhados ao tamanho do vetor.
byte* pRowEnd = pRow + widthAligned * cbPixel;
byte* qRowEnd = qRow + widthAligned * cbPixel;
// Vetores para processamento em bloco.
Vector<int>* p = (Vector<int>*)pRow;
Vector<int>* q = (Vector<int>*)qRowEnd; // Começa do final para escrita reversa
// Loop principal: processa blocos de vetores.
while ((byte*)p < pRowEnd) {
// Carrega um vetor da origem (lendo do início para o fim).
Vector<int> data = *p;
// Inverte o vetor usando YShuffleKernel.
Vector<int> temp = Vectors.YShuffleKernel(data, indices);
// Decrementa o ponteiro de destino (q aponta para o bloco a ser escrito).
q -= 1;
// Escreve o vetor invertido no destino (escrevendo do fim para o início).
*q = temp;
// Avança o ponteiro de origem para o próximo bloco.
p++;
}
// Processa os bytes restantes se a largura não for múltiplo do tamanho do vetor.
// Esta parte é simplificada aqui, mas em uma implementação completa,
// seria necessário um loop para os bytes restantes.
// ... tratamento para os bytes restantes ...
pRow += strideSrc; // Avança para a próxima linha de origem
qRow += strideDst; // Avança para a próxima linha de destino
}
}
Nesta implementação, a estratégia de "leitura do início para o fim" e "escrita do fim para o início" é usada para alcançar a inversão horizontal com vetores. O ponteiro q é inicializado no final da linha e decrementado a cada escrita.
Código de Teste de Referência
O código de teste de referência para o algoritmo vetorizado:
[Benchmark]
public void UseVectors() {
UseVectorsDo(_sourceBitmapData, _destinationBitmapData, false);
}
//[Benchmark]
public void UseVectorsParallel() {
UseVectorsDo(_sourceBitmapData, _destinationBitmapData, true);
}
public static unsafe void UseVectorsDo(BitmapData src, BitmapData dst, bool useParallel = false) {
// Tamanho do vetor em bytes.
int vectorWidthBytes = Vector<byte>.Count;
int width = src.Width;
int height = src.Height;
// Se a largura da imagem for menor ou igual ao tamanho de um vetor,
// usa o algoritmo escalar, pois vetorizar não traria benefícios.
if (width * sizeof(int) <= vectorWidthBytes) { // Verifica em bytes de int (32-bit pixel)
ScalarDo(src, dst, useParallel);
return;
}
int strideSrc = src.Stride;
int strideDst = dst.Stride;
byte* pSrc = (byte*)src.Scan0.ToPointer();
byte* pDst = (byte*)dst.Scan0.ToPointer();
// Lógica para paralelismo, semelhante ao ScalarDo.
bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
if (allowParallel) {
Parallel.For(0, height, i => {
int start = i;
int len = 1;
byte* pSrc2 = pSrc + start * (long)strideSrc;
byte* pDst2 = pDst + start * (long)strideDst;
UseVectorsDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);
});
} else {
UseVectorsDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
}
}
Otimização Adicional com YShuffleKernel_Args e YShuffleKernel_Core
Para otimizar ainda mais, especialmente em loops onde parte do cálculo pode ser movida para fora, o VectorTraits oferece os métodos YShuffleKernel_Args e YShuffleKernel_Core. O método _Args prepara os parâmetros, enquanto o _Core executa a operação principal, resultando em melhor desempenho quando usado em conjunto.
public static unsafe void UseVectorsArgsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bits.
Vector<int> indices = _shuffleIndices;
Vector<int> args0, args1;
// Prepara os argumentos para YShuffleKernel_Core.
Vectors.YShuffleKernel_Args(indices, out args0, out args1);
int vectorWidthInts = Vector<int>.Count;
int widthAligned = width - (width % vectorWidthInts);
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
byte* pRowEnd = pRow + widthAligned * cbPixel;
byte* qRowEnd = qRow + widthAligned * cbPixel;
Vector<int>* p = (Vector<int>*)pRow;
Vector<int>* q = (Vector<int>*)qRowEnd;
while ((byte*)p < pRowEnd) {
Vector<int> data = *p;
// Executa a inversão usando a versão _Core otimizada.
Vector<int> temp = Vectors.YShuffleKernel_Core(data, args0, args1);
q -= 1;
*q = temp;
p++;
}
// ... tratamento para os bytes restantes ...
pRow += strideSrc;
qRow += strideDst;
}
}
Resultados de Testes de Referência
Arquitetura x86
Os resultados dos testes de referência na arquitetura x86:
BenchmarkDotNet v0.14.0, Windows 11 ...
AMD Ryzen 7 7840H ...
.NET SDK 8.0.403
[Host] : .NET 8.0.10 ..., X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.10 ..., X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD |
|--------------- |------ |------------:|----------:|----------:|------:|--------:|
| Scalar | 1024 | 784.7 us | 14.56 us | 14.30 us | 1.00 | 0.03 |
| UseVectors | 1024 | 106.4 us | 2.12 us | 4.96 us | 0.14 | 0.01 |
| UseVectorsArgs | 1024 | 101.4 us | 2.03 us | 3.85 us | 0.13 | 0.01 |
| ... (outros tamanhos) ...
Comparando Scalar com UseVectorsArgs para uma largura de 1024 pixels, o algoritmo vetorizado (UseVectorsArgs) é aproximadamente 7.74 vezes mais rápido que o algoritmo escalar.
Arquitetura ARM
Os mesmos testes na arquitetura ARM:
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 ...
Apple M2 ...
.NET SDK 8.0.204
[Host] : .NET 8.0.4 ..., Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 8.0.4 ..., Arm64 RyuJIT AdvSIMD
| Method | Width | Mean | Error | StdDev | Ratio |
|--------------- |------ |------------:|---------:|---------:|------:|
| Scalar | 1024 | 625.8 us | 0.81 us | 0.68 us | 1.00 |
| UseVectors | 1024 | 151.9 us | 0.32 us | 0.27 us | 0.24 |
| UseVectorsArgs | 1024 | 151.2 us | 0.13 us | 0.12 us | 0.24 |
| ... (outros tamanhos) ...
Na arquitetura ARM, UseVectorsArgs é aproximadamente 4.14 vezes mais rápido que o algoritmo escalar.
É notável que em .NET 8.0, a diferença de performance entre UseVectors e UseVectorsArgs é mínima. Isso se deve às otimizações automáticas do compilador JIT do .NET 7.0+, que já movem parte dos cálculos para fora do loop. Em versões anteriores do .NET, a diferença seria mais pronunciada.
Resultados .NET 6.0 na Arquitetura ARM
Testes com .NET 6.0 na arquitetura ARM mostram uma diferença maior:
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 ...
Apple M2 ...
.NET SDK 8.0.204
[Host] : .NET 6.0.33 ..., Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 6.0.33 ..., Arm64 RyuJIT AdvSIMD
| Method | Width | Mean | Error | StdDev | Ratio |
|--------------- |------ |------------:|---------:|---------:|------:|
| Scalar | 1024 | 1,805.2 us | 0.72 us | 0.60 us | 1.00 |
| UseVectors | 1024 | 454.5 us | 5.45 us | 5.10 us | 0.25 |
| UseVectorsArgs | 1024 | 158.4 us | 0.05 us | 0.04 us | 0.09 |
| ... (outros tamanhos) ...
Neste cenário, UseVectors é ~3.97x mais rápido que Scalar, e UseVectorsArgs é ~11.40x mais rápido, demonstrando o benefício da otimização _Args/_Core em versões mais antigas do .NET.
.NET Framework
Testes no .NET Framework:
BenchmarkDotNet v0.14.0, Windows 11 ...
AMD Ryzen 7 7840H ...
[Host] : .NET Framework 4.8.1 ..., X64 RyuJIT VectorSize=256
DefaultJob : .NET Framework 4.8.1 ..., X64 RyuJIT VectorSize=256
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD | Code Size |
|--------------- |------ |------------:|----------:|----------:|------:|--------:|----------:|
| Scalar | 1024 | 1,315.2 us | 26.06 us | 25.59 us | 1.00 | 0.03 | 2,718 B |
| UseVectors | 1024 | 968.2 us | 17.55 us | 16.42 us | 0.74 | 0.02 | 3,507 B |
| UseVectorsArgs | 1024 | 887.0 us | 9.91 us | 8.78 us | 0.67 | 0.01 | 3,507 B |
| ... (outros tamanhos) ...
No .NET Framework, UseVectorsArgs é aproximadamente 1.48 vezes mais rápido que o Scalar. Isso ocorre porque o .NET Framework não tem aceleração SIMD nativa para operações como Shuffle; em vez disso, ele usa código de fallback escalar. No entanto, o algoritmo escalar subjacente do VectorTraits é altamente otimizado e, ao operar em inteiros (pixels), ainda supera o algoritmo escalar baseado em bytes.