4
\$\begingroup\$

I was tired to always perform pixel manipulation one by one, so I started to create my own templated header to perform computation over pixel (RGB, or RGBA), but I also make it for ND for some cases.

There are few comments in the code for some features that Im not sure yet.

I also develop it as std::array to have operator overloading, despite the classic C array could have been better for my pipeline (data casting mostly).

Usually those methods are used over int or float and occasionally uchar. As you can see, I didn't handled the limits, or the negative. Im still questioning myself to create dedicated template for uchar.

And a last question I have, I never really timed it, or check compiler behavior, but should it be better to use dedicated function for 3D and 4D ?

#ifndef PX_ND_STD_ARRAY_UTILITY_H
#define PX_ND_STD_ARRAY_UTILITY_H

// =============================================================================
// We should prefer using std::array, since we are able to define operator 
// function on it. This lead to a better usage instead using C Array.
// 
//  using pxNDArr = std::array<T, N>; 
//               vs
//  using pxNDArr = T[N];
//
// =============================================================================

#include <iostream>
#include <array>

// =============================================================================
// Define of type alias for templated std::array. (ND, 3D and 4D). 
// =============================================================================
template <class T, std::size_t N>
using pxNDArr = std::array<T, N>;

// Special class for color pixel RGB.
template <class T>
using px3DArr = pxNDArr<T, 3>;

// Special class for color pixel RGBA.
template <class T>
using px4DArr = pxNDArr<T, 4>;

// =============================================================================
// Define of type alias for a NxN template std::array (No operator define on it).
// =============================================================================
template <class T, std::size_t N1, std::size_t N2>
using pxMatrixNM = std::array<std::array<T, N2>, N1>;


// This template allows to check a "typename" condition.
template <typename Condition>
using EnableIf = typename std::enable_if<Condition::value>::type;


// =============================================================================
// Define alias to ensure to use the right Input, Output, or InOut type, same for returns.
// =============================================================================

template<class T, std::size_t N>
using InArg = const pxNDArr<T, N>&;

template<class T, std::size_t N>
using OutArg = pxNDArr<T, N>&;

template<class T, std::size_t N>
using InOutArg = OutArg<T, N>;

template<class T, std::size_t N>
using OutRet = pxNDArr<T, N>;


// Constant<T> is longer than Constant<T>, but is safer to use to avoid to
// have constant param non-const ?
// NOTE: Not used yet, wondering if I should.
template<class T = EnableIf<std::is_arithmetic<T>>>
using Constant = const T;

// =============================================================================
// Special methods that apply pixel manipulation.
// =============================================================================

// Apply a mathematical cross product between vector arr1 and arr2. Output the result into dst.
template<class T>
void crossProduct(const px3DArr<T>& arr1, const px3DArr<T>& arr2, px3DArr<T>& dst)
{
    dst[0] = arr1[1] * arr2[2] - arr1[2] * arr2[1];
    dst[1] = arr1[2] * arr2[0] - arr1[0] * arr2[2];
    dst[2] = arr1[0] * arr2[1] - arr1[1] * arr2[0];
}


template<class T>
px3DArr<T> crossProduct(const px3DArr<T>& arr1, const px3DArr<T>& arr2)
{
    px3DArr<T> dst;
    crossProduct(arr1, arr2, dst);
    return dst;
}


// Swap color channel: RGB to BGR and vice-versa.
template<class T>
void swapChannel(const px3DArr<T>& arr, px3DArr<T>& dst)
{
    dst[0] = arr[2];
    dst[1] = arr[1];
    dst[2] = arr[0];
}


template<class T>
px3DArr<T> swapChannel(const px3DArr<T>& arr)
{
    px3DArr<T> dst;
    swapChannel(arr, dst);
    return dst;
}


template<class T>
void swapChannelInPlace(px3DArr<T>& arr)
{
    T tmp = arr[0];
    arr[0] = arr[2];
    arr[2] = tmp;
}


// =============================================================================
// Method for all types of arrays (ND, 3D, or 4D)
// =============================================================================

// Copy an array into another one.
template<class T, std::size_t N>
void copy(InArg<T, N> arr, OutArg<T, N> dst)
{
    std::copy_n(arr.begin(), N, dst.begin());
}


template<class T, std::size_t N>
OutRet<T, N> copy(InArg<T, N> arr)
{
    pxNDArr<T, N> dst;
    copy(arr, dst);
    return dst;
}


// This method clamp the value InArg between a min and a max range limit.
template<class T, std::size_t N>
void clamp(InArg<T, N> arr, OutArg<T, N> dst, Constant<T> minVal, Constant<T> maxVal)
{
    for ( int i = 0; i < N; i++ )
        dst[i] = ( ( arr[i] < minVal ) ? minVal : ( ( arr[i] > maxVal ) ? maxVal : ( arr[i] ) ) );
}


template<class T, std::size_t N>
OutRet<T, N> clamp(InArg<T, N> arr, Constant<T> minVal, Constant<T> maxVal)
{
    pxNDArr<T, N> dst;
    clamp(arr, dst, minVal, maxVal);
    return dst;
}


// Addition operator between an std::array and a constant of the same type.
template <class T, std::size_t N>
OutRet<T, N> operator+( InArg<T, N> arr, Constant<T> constant )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr[i] + constant;
    return dst;
}


// Addition operator between an std::array and another array (same type)
template <class T, std::size_t N>
OutRet<T, N> operator+( InArg<T, N> arr1, InArg<T, N> arr2 )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr1[i] + arr2[i];
    return dst;
}


// substraction operator between an std::array and a constant (same type)
template <class T, std::size_t N>
OutRet<T, N> operator-( InArg<T, N> arr, Constant<T> constant )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr[i] - constant;
    return dst;
}


// substraction operator between an std::array and another array (same type)
template <class T, std::size_t N>
OutRet<T, N> operator-( InArg<T, N> arr1, InArg<T, N> arr2 )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr1[i] - arr2[i];
    return dst;
}


// Multiplication operator between an std::array and a constant (same type)
template <class T, std::size_t N>
OutRet<T, N> operator*( InArg<T, N> arr, Constant<T> constant )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr[i] * constant;
    return dst;
}


// Multiplication operator between an std::array and another array (same type)
template <class T, std::size_t N>
OutRet<T, N> operator*( InArg<T, N> arr1, InArg<T, N> arr2 )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr1[i] * arr2[i];
    return dst;
}


// Divide operator between an std::array and a constant (same type)
template <class T, std::size_t N>
OutRet<T, N> operator/( InArg<T, N> arr, Constant<T> constant )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr[i] / constant;
    return dst;
}


// Divide operator between an std::array and another array (same type)
template <class T, std::size_t N>
OutRet<T, N> operator/( InArg<T, N> arr1, InArg<T, N> arr2 )
{
    pxNDArr<T, N> dst;
    for ( int i = 0; i < N; i++ )
        dst[i] = arr1[i] / arr2[i];
    return dst;
}


// This addition two vector elements by elements.
template<class T, std::size_t N>
void add(InArg<T, N> arr1, InArg<T, N> arr2, OutArg<T, N> dst)
{
    dst = arr1 + arr2;
}


// This subtract two vector elements by elements.
template<class T, std::size_t N>
void subtract(InArg<T, N> arr1, InArg<T, N> arr2, OutArg<T, N> dst)
{
    dst = arr1 - arr2;
}


// This multiply two vector elements by elements.
template<class T, std::size_t N>
void multiply(InArg<T, N> arr1, InArg<T, N> arr2, OutArg<T, N> dst)
{
    dst = arr1 * arr2;
}


// This divide two vector elements by elements.
template<class T, std::size_t N>
void divide(InArg<T, N> arr1, InArg<T, N> arr2, OutArg<T, N> dst)
{
    dst = arr1 / arr2;
}


// This calculates the sum of a vector. (Warning with uchar)
template<class T, std::size_t N>
T sum(InArg<T, N> arr)
{
    T sum = arr[0];
    for ( int i = 1; i < N; i++ )
        sum += arr[i];
    return sum;
}


// This applies a dot product calculation between any type and any size vector.
template<class T, std::size_t N>
T dotProduct(InArg<T, N> arr1, InArg<T, N> arr2)
{
    return sum(arr1 * arr2);
}


// This applies an exponent power over vector A and store the result into B.
template<class T, std::size_t N>
void pow(InArg<T, N> arr, OutArg<T, N> dst, Constant<T> exponent)
{
    for ( int i = 0; i < N; i++ )
        dst[i] = std::pow(arr[i], exponent);
}


// This applies an InArg place exponent power over vector A.
template<class T, std::size_t N>
void powInplace(InOutArg<T, N> arr, Constant<T> exponent)
{
    for ( auto& it : arr )
        it = std::pow(it, exponent);
}


// This applies an exponent power over vector A and return result into a new array.
template<class T, std::size_t N>
OutRet<T, N> pow(InArg<T, N> arr, Constant<T> exponent)
{
    pxNDArr<T, N> dst;
    pow(arr, dst, exponent);
    return dst;
}


// This applies a sqrt element by element.
template<class T, std::size_t N>
void sqrt(InArg<T, N> arr, OutArg<T, N> dst)
{
    for ( int i = 0; i < N; i++ )
        dst[i] = std::sqrt(arr[i]);
}


// This applies an InArg place sqrt element by element.
template<class T, std::size_t N>
void sqrtInplace(InOutArg<T, N>& arr)
{
    for ( auto& it : arr )
        it = std::sqrt(it);
}


template<class T, std::size_t N>
OutRet<T, N> sqrt(InArg<T, N> arr)
{
    pxNDArr<T, N> dst;
    sqrt(arr, dst);
    return dst;
}


// This return the length of any vector.
// NOTE: The length is always float.
template<class T, std::size_t N>
const T vectorLength(InArg<T, N> arr)
{
    return static_cast<T>(std::sqrt(static_cast<float>( dotProduct(arr, arr) )));
}


// Compute the distance between vector A and B.
// NOTE: The distance is always float.
template<class T, std::size_t N>
const float distanceBetweenVector(InArg<T, N> arr1, InArg<T, N> arr2)
{
    return vectorLength(arr1 - arr2);
}


// TODO: Should we limit T to be float ? Since vectorLength only returns float.
// InArg-place vector normalization.
template<class T, std::size_t N>
void normalizeVectorInplace(InOutArg<T, N>& arr)
{
    arr /= vectorLength(arr);
}


// TODO: Should we limit T to be float ? Since vectorLength only returns float.
// Normalized vector A and output the result into dst vector (means not InArg-place).
template<class T, std::size_t N>
void normalizeVector(InArg<T, N> arr, OutArg<T, N> dst)
{
    const float len = vectorLength(arr);
    dst = arr / static_cast<T>(len);    
}


// Normalized vector A and output the result into a new array
template<class T, std::size_t N>
OutRet<T, N> normalizeVector(InArg<T, N> arr)
{
    OutRet<T, N> dst;
    normalizeVector(arr, dst);
    return dst;
}


// This function allows to convert from type T1 to type T2 a vector. 
// Might be useful when you want to use sum of any other process on a
// unsigned char vector. 
template<class T1, class T2, std::size_t N>
void convert(InArg<T1, N> arr, InArg<T2, N> dst)
{
    for ( int i = 0; i < N; i++ )
        dst[i] = static_cast<T2>( arr[i] );
}


template<class T1, class T2, std::size_t N>
OutRet<T2, N> convert(InArg<T1, N> arr)
{
    pxNDArr<T2, N> dst;
    convert(arr, dst);
    return dst;
}


// This function prints the vector values.
template<class T, std::size_t N>
void printVector(InArg<T, N> arr)
{
    for ( auto& it : arr )
        std::cout << it << " ";
    std::cout << std::endl << std::endl;
}


// TODO: Implement rounding methods, etc.

#endif // PX_ND_STD_ARRAY_UTILITY_H

Here few unit tests, really simple.

#include "pxNDstdArray.h"

#include <cassert>
#include <string>

#define EPS 1e-6

template<class T>
void assertArr(const px3DArr<T>& arr, const T v1, const T v2, const T v3, const std::string& name)
{
    do
    {
        if ( arr[0] - v1 > EPS ) break;
        if ( arr[1] - v2 > EPS ) break;
        if ( arr[2] - v3 > EPS ) break;
        return;
    } while ( false );
    std::cout << "Assertion failed for macros: " << name << std::endl;
    std::cout << arr[0] << " " << arr[1] << " " << arr[2] << " != " << v1 << " " << v2 << " " << v3 << std::endl;
    //exit(-1);
}

template<class T>
void assertVal(const T v1, const T v2, const std::string& name)
{
    if ( v1 - v2 < EPS ) return;
    std::cout << "Assertion failed for macros: " << name << std::endl;
    std::cout << v1 << " != " << v2 << std::endl;
}


int main()
{
    // Initialization.
    px3DArr<float> a = { 1.0f, 2.0f, 3.0f };
    assertArr(a, 1.0f, 2.0f, 3.0f, "Initialization: A");

    px3DArr<float> b = { 1.0f, 2.0f, 1.0f };
    assertArr(b, 1.0f, 2.0f, 1.0f, "Initialization: B");

    // Copy
    assertArr(copy(a), 1.0f, 2.0f, 3.0f, "Copy: A");

    // Clamp
    assertArr(clamp(a, 0.0f, 2.0f), 1.0f, 2.0f, 2.0f, "Clamp: A between [0,2]");

    // Overloaded operator
    assertArr(a + b, 2.0f, 4.0f, 4.0f, "Add:  A + B");
    assertArr(a - b, 0.0f, 0.0f, 2.0f, "Sub:  A - B");
    assertArr(a * b, 1.0f, 4.0f, 3.0f, "Mult: A * B");
    assertArr(a / b, 1.0f, 1.0f, 3.0f, "Div:  A / B");

    // Sum
    assertVal(sum(b), 4.0f, "Sum: B");

    // Dot product
    assertVal(dotProduct(a, b), 8.0f, "Dot product A B");

    // Pow
    assertArr(pow(a, 2.0f), 1.0f, 4.0f, 9.0f, "Pow: a^2");

    // sqrt
    assertArr(sqrt(b), 1.0f, sqrt(2.0f), 1.0f, "Sqrt: B");

    // Vector length
    assertVal(vectorLength(a), sqrt(14.0f), "Length: A");

    // Distance
    assertVal(distanceBetweenVector(a, b), 2.0f, "Distance between A and B");

    // Normalization
    assertArr(normalizeVector(a), 0.267261f, 0.534522f, 0.801784f, "Normalize: A");

    // Convert... Failing. I will check more on taht.
    //px3DArr<int> c;
    //convert<float,int>(a, c);
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Do you have a sample program and/or unit tests that show how to exercise this library? Including them makes it easier for reviewers to understand and improve your code. \$\endgroup\$ – Toby Speight Feb 28 at 15:18
  • \$\begingroup\$ I dont have any unit tests, since I started to use that recently in an existing project where pixels manipulation was done. But I will generate one and come back with the edit (sorry first time I ask for a review here). \$\endgroup\$ – Vuwox Feb 28 at 15:27
  • \$\begingroup\$ No problem - it's not mandatory, but it helps you get better reviews. \$\endgroup\$ – Toby Speight Feb 28 at 15:28
3
\$\begingroup\$
  1. At least to me, the naming of swapChannel is confusing (and I go "how can the parameter be const if the method does a swap?"). The method is not swapping, but it is simply "reversing" the channels from arr to dst. I would consider a more informative name for the method (for a swap, I'd expect two parameters by-reference and then doing std::swaps inside).

  2. If you wanted to, there are standard algorithms available so that you can avoid explicit for-loops in places like the definition of the arithmetic operator (see e.g., std::transform). Your code is more or less full of examples where you can apply these functions.

    For example, your function sum is std::accumulate, while pow and sqrt can also be written using standard algorithms. So you can just do (and here, notice I'm also passing by reference):

    template<class T, std::size_t N>
    void pow(InArg<T, N>& arr, OutArg<T, N>& dst, const T exponent)
    {
        std::transform(arr.cbegin(), arr.cend(), dst.begin(), 
            [&](auto v) { return std::pow(v, exponent); });
    }
    
    // This applies an InArg place exponent power over vector A.
    template<class T, std::size_t N>
    void powInplace(InOutArg<T, N>& arr, const T exponent)
    {
        std::transform(arr.cbegin(), arr.cend(), arr.begin(), 
            [&](auto v) { return std::pow(v, exponent); });
    }
    

    As another example, your sum (also, notice we pass by const-ref now, just as the user would expect because a sum should never modify the elements) just becomes:

    // This calculates the sum of a vector.
    template<class T, std::size_t N>
    T sum(const InArg<T, N>& arr)
    {
        return std::accumulate(arr.cbegin(), arr.cend(), 0);
    }
    

    Finally, you asked specifically about how to sum two vectors using std::transform. There is an overload for that as well, so you can do:

    template <class T, std::size_t N>
    OutRet<T, N> operator+( InArg<T, N> arr1, InArg<T, N> arr2 )
    {
        pxNDArr<T, N> dst;
        std::transform(arr1.cbegin(), arr1.cend(), arr2.cbegin(), dst.begin(), std::plus<>());
    
        return dst;
    }
    
\$\endgroup\$
  • \$\begingroup\$ So I re-implemented most of my stuff using the above recommendation (std::transform, etc). I have a test function over an image (in the top level lib that use this header). And in Release, with -O3, the timing is really good (15ms per iteration), a bit better than the for-loop (18ms). But in Debug without any optimization, this is unusable. For-loop is 100x slower, and its 4000x slower using the STL. I like the fact is clear and easy to read. But the performance is horrible when you have to debug it. \$\endgroup\$ – Vuwox Feb 28 at 19:54
3
\$\begingroup\$

Naming

I'm not fond of your naming. For example, you have a type named px3DArr. Above it you have a comment that says:

// Special class for color pixel RGB.

If this class represents an RGB pixel, then name it that! It should just be RGBPixel or PixelRGB. And while it's possible to think of a pixel as being an array, it's not really an array. It's a data structure with 3 members. In some formats the pixels have a different number of bits, for example. So even if you back it with a std::array, I would leave the word "array" (or any abbreviations of "array") out of the name. A user of this class shouldn't have to know the implementation details to understand what it is.

For function arguments, if you have one named dst, you should probably have one named src.

Data Structures

I really dislike representing a pixel as an array. For one, it leaves you unable to know by looking at the code which color component you're working with. Most of the code here assumes that you need to do the same thing to all the channels of a pixel. While that's sometimes the case, just as often it's not the case. So if I need to treat the green channel differently from the blue channel, how do I know whether to use 0 or 2 for blue? And if it's a 4 channel pixel, how do I know whether it's ARGB or RGBA? (Or BGRA or ABGR?) A pixel is really a data structure and not an array.

When Do I Need This?

For what purpose is a cross product between 2 pixels used? I've used the dot product of 2 pixels before, but never the cross product. I've used the cross product of 2 3D or 4D vectors when dealing with 3D geometry, but not with colors.

Integral Types vs. Floating Point

Some of your functions will return unexpected results with integral types. When manipulating pixels, it's frequently the case that you want to treat an integral type as a fixed point type with a value between 0 and 1. So for an unsigned 8-bit value, 0 would represent 0.0, and 255 would represent 1.0. For a function like std::pow(), you need to cast the integral value to a floating point value, divide it by the maximum, perform the operation, multiply by the maximum and convert back. Something like this:

uint_8 x = 127;
uint_8 y = static_cast<uint_8>(pow(static_cast<double>(x) / 255.0, p) * 255.0);
\$\endgroup\$
  • \$\begingroup\$ The pipeline is mostly working on pixel. But we do a lot of planes & lines fitting along the execution. So yes, in general, we need a vector of size 3 that represent a pixel. But sometimes, we need that vector to represent a direction, or a plane as px4DArr. I understand the problem of the naming. This library is mostly to use to save time about not recoding the operator, sum, dotProduct, etc everytime we need it. I agree that a structure should be really nicer to introduce this. But its involve to change a lot the pipeline. And for type, I agree and I also point it in my question. \$\endgroup\$ – Vuwox Mar 1 at 21:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.