una tipos tabla rangos pixeles leer imagen datos c++ templates c++11 design

c++ - rangos - tabla de tipos de datos en c



Describir la información de formato de píxeles en C++ de una manera que se puede usar tanto en tiempo de compilación como en tiempo de ejecución (5)

Tengo una biblioteca que realiza operaciones en píxeles. Los píxeles pueden en muchos formatos diferentes. Estoy buscando una forma efectiva de describir los formatos en la API de la biblioteca (interna y externamente).

Para algunas clases, el formato de píxeles es un argumento de plantilla , para otros es un argumento de tiempo de ejecución . Por lo tanto, los formatos de píxel deben poder utilizarse tanto en tiempo de ejecución (como constructor o argumento de función) como en tiempo de compilación (como argumento de plantilla). Quiero describir los formatos de píxel solo una vez .

Lo que tengo ahora es algo como esto:

enum class color_space : uint8_t { rgb, cmyk /* , etc... */ }; struct pixel_layout { color_space space; uint8_t channels; /* etc... */ }; template <color_space ColorSpace, uint8_t Channels /* etc.. */> struct pixel_type { static constexpr color_space space = ColorSpace; static constexpr uint8_t channels = Channels; /* etc... */ static constexpr pixel_layout layout() { return {space, channels /* , etc... */ }; } }; struct rgb : public pixel_type<color_space::rgb, 3 /* , etc... */ > {}; struct rgba : public pixel_type<color_space::rgb, 4 /* , etc... */ > {};

Esto funciona bastante bien. Puedo usar estos como argumentos de tiempo de ejecución y compilación:

template <class PixelType> class image { }; struct transform { transform(const pixel_layout from, const pixel_layout to) : from(from), to(to) { /* ... */ } pixel_layout from; pixel_layout to; };

También convierta del tipo de tiempo de compilación al tipo de tiempo de ejecución:

transform(rgb::layout(), rgba::layout());

Sin embargo , la duplicación y el almacenamiento de los detalles de pixel_layout de los tipos de píxeles cada vez que se utilizan en tiempo de ejecución me parecen tontos. Conceptualmente, todo lo que el programa debería necesitar es una ID / dirección / referencia a un pixel_type específico y una forma de recuperar las propiedades asociadas (espacio de color, canales, etc.) tanto en tiempo de compilación como en tiempo de ejecución.

Además, si quiero obtener una propiedad derivada de un tipo de píxel, necesito implementarlo en pixel_layout si quiero evitar la duplicación de la lógica. Luego, para usarlo en tiempo de compilación, necesito pasar de la pixel_type<...> a la instancia pixel_layout a la propiedad derivada. Eso también parece un poco tonto.

¿Puedo evitar pasar los detalles de pixel_layout y, en su lugar, utilizar algún tipo de referencia a las pixel_type<...> (sub)?

Intenté usar enum s , porque las enumeraciones funcionan como argumento de plantilla y argumento de función. Pero tuve problemas para obtener desde el valor enum (por ejemplo, rgba ) a la propiedad de tipo de píxel (por ejemplo, 4 canales) en el tiempo de ejecución y el tiempo de compilación en una forma idiomática de C ++.

Además, las enumeraciones como argumentos de plantilla proporcionan diagnósticos mucho menos útiles durante el error de compilación. Por ejemplo, obtengo image<(pixel_type)2> lugar de image<rgba> en compilar mensajes de error con clang. Entonces esto no parece ser un enfoque útil.


Accoriding a su pregunta:

¿Puedo evitar pasar los detalles de pixel_layout ?, y en su lugar utilizar algún tipo de referencia a las clases de tipo pixel_type<...> (sub)?

Sí, puedes usar la herencia. Desea expresar una interfaz común en tiempo de ejecución, por lo que necesita al menos un miembro de datos que señale la diferencia, como ya ha mencionado. La interfaz se vería así:

struct pixel_layout { virtual ~pixel_layout() = default; virtual color_space colorSpace() const = 0; virtual uint8_t channelCount() const = 0; };

Hasta ahora todo bien. Ahora podríamos pasar un puntero o referencia a la implementación pixel_layout y usar su información al costo de dos punteros. Uno apuntando al objeto real y el otro al vtable.

Vamos a hacer una implementación.

template<typename _PixelT> struct pixel_layout_implementation : pixel_layout { virtual color_space colorSpace() const override { return _PixelT::colorSpace(); } virtual uint8_t channelCount() const override { return _PixelT::channelCount(); } };

Ok, tenemos esto. Observe que mi declaración pixel_type difiere ligeramente.

template<color_space _SpaceT, uint8_t _ChannelC> struct pixel_type { static constexpr color_space colorSpace() { return _SpaceT; } static constexpr uint8_t channelCount() { return _ChannelC; } };

Ahora podemos actualizar tu clase de transformación.

struct transform { transfrom(const pixel_layout& from, const pixel_layout& to); };

Finalmente nos metimos en problemas. Tenemos referencias a nuestro pixel_layout y mientras permanezcan en la memoria todo está bien, pero si queremos copiarlos necesitamos implementar una copia profunda y tener varias instancias. Antes de abordar este problema, intentemos ocultar cierta complejidad como detalle de implementación de pixel_layout .

struct pixel_layout { private: struct concept_t { virtual ~concept_t () = default; virtual color_space colorSpace() const = 0; virtual uint8_t channelCount() const = 0; }; template<typename _PixelT> struct concept_implementation_t : concept_t { virtual color_space colorSpace() const override { return _PixelT::colorSpace(); } virtual uint8_t channelCount() const override { return _PixelT::channelCount(); } }; std::unique_ptr<const conept_t> pm_conceptImpl; public: template<typename _PixelT> pixel_layout(_PixelT) : pm_conceptImpl{new concept_implementation_t<_PixelT>} {/* */} virtual color_space colorSpace() const { return pm_conceptImpl->colorSpace(); } virtual uint8_t channelCount() const { return pm_conceptImpl->channelCount(); } };

Esto hace que la herencia sea invisible para el usuario de esta clase y unique_ptr implícitamente elimina la construcción y la asignación de copias. Además de que pixel_type es una clase vacía, el constructor nos permite escribir algo como pixel_layout rgb_layout = rgb(); sin la fuga de rendimiento. Vamos a abordar el problema anterior que aún no hemos resuelto. Estamos creando nuevos objetos de concept_implementation para cada pixel_layout cada uno apuntando al mismo vtable. Podemos hacerlo mejor si compartimos un objeto por cada color_space en varias instancias.

template<typename _PixelT> struct concept_implementation_t : concept_t { static const std::unique_ptr<const concept_t> shared_instance; virtual color_space colorSpace() const override { return _PixelT::colorSpace(); } virtual uint8_t channelCount() const override { return _PixelT::channelCount(); } };

Ahora solo tenemos que apuntar a la shared_instance en nuestra pixel_layout .

const concept_t& pm_conceptImpl; public: template<typename _PixelT> pixel_layout() : pm_conceptImpl{*concept_implementation_t<_PixelT>::shared_instance.get()} {/* */}

Al juntar todo, obtenemos el tamaño de un puntero por cada instancia de pixel_layout más un puntero por cada pixel_type compatible. Aquí está la clase final y no te olvides de poner la inicialización del miembro estático después de la definición de la clase.

struct pixel_layout { private: struct concept_t { virtual ~concept_t () = default; virtual color_space colorSpace() const = 0; virtual uint8_t channelCount() const = 0; }; template<typename _PixelT> struct concept_implementation_t : concept_t { static const std::unique_ptr<const concept_t> shared_instance; virtual color_space colorSpace() const override { return _PixelT::colorSpace(); } virtual uint8_t channelCount() const override { return _PixelT::channelCount(); } }; const concept_t& pm_conceptImpl; public: template<typename _PixelT> pixel_layout(_PixelT) : pm_conceptImpl{*concept_implementation_t<_PixelT>::shared_instance.get()} {/* */} virtual color_space colorSpace() const { return pm_conceptImpl.colorSpace(); } virtual uint8_t channelCount() const { return pm_conceptImpl.channelCount(); } }; template<typename _PixelT> const std::unique_ptr<const pixel_layout::concept_t> pixel_layout::concept_implementation_t<_PixelT>::shared_instance (new pixel_layout::concept_implementation_t<_PixelT>);


Aquí hay un intento. Tal vez me falta algo, pero parece que puedes hacer la mayoría de lo que quieres sin la estructura pixel_layout .

Utilizaría la misma implementación para pixel_type , así que:

enum class color_space : uint8_t { rgb, rgba }; template <color_space ColorSpace, uint8_t Channels> struct pixel_type { static constexpr color_space space = ColorSpace; static constexpr uint8_t channels = Channels; };

Pero usaría alias para definir los diferentes tipos, tales como:

using rgb = pixel_type<color_space::rgb, 3>; using rgba = pixel_type<color_space::rgba, 4>;

He descubierto que usar alias da fácil (er) descifrar los mensajes del compilador (especialmente con clang), por lo que elijo usarlos aquí.

Ahora todo lo que puedo ver que la función de layout está haciendo es envolver la información que ya tiene como parámetros de plantilla ( color_space y uint8_t ) en una estructura. Sin embargo, puede acceder a esta información mediante el uso de plantillas.

Voy a suponer que la clase de transform hace algo como esto (corrígeme si me equivoco):

class transform { transform(const pixel_layout from, const pixel_layout to) : from(from), to(to) {} // Here *to* and *from* are stored so that to.space, from.space etc // can be used for the transformation };

Pero puede hacer esto (muy bien) haciendo que esta clase de transformación sea una clase de plantilla, y pase los tipos de transformación A y De como los parámetros de la plantilla. Esto tiene la ventaja de permitir el acceso tanto a los parámetros de plantilla de tiempo de compilación como a los parámetros de tiempo de ejecución, por ejemplo:

template <typename From, typename To> class transform { transform(const From& from, const To& to) { // Access with From::channels, To::space etc, as the other transform class would have // Access any run-time parameters with to.rtime_param etc... } }

O bien, lo que creo que es una mejor solución (pero no conozco el uso específico, tal vez no), para usar una función de plantilla y devolver el tipo convertido, como por ejemplo:

template <typename From, typename To> To transform(const From& from, const To& to) { // Same access, From::channels // to.rtime_param }

Además, consideré que algunas transformaciones podrían ser específicas de los diferentes formatos de píxeles (rgb -> rgba podría ser diferente de rgb -> cmyk), entonces solo proporcionaría especializaciones de la clase o función de transformación para manejar estos casos específicos.

Aquí hay un ejemplo de uso posible con mi solución:

rgb rgb_format; rgba rgba_format; // Using the transform struct transform rgb_to_rgba(rgb_format, rgba_format); // Or similarly using the function rgba transformed_rgb_format = transform(rgb, rgba);

Además, con respecto a la adición de propiedades, dado que este método tiene acceso tanto a las propiedades de tiempo de compilación como de tiempo de ejecución, se puede acceder fácilmente a cualquier adición a la estructura tipo_de_píxel en tiempo de compilación o tiempo de ejecución.

Un posible problema es el acceso a los componentes estáticos fuera de las funciones y clases de las plantillas, ya que deben accederse a través del tipo y no de la instancia.

rgb _rgb; // Need to do this rgb::channels // Rather than this _rgb::channels

Pero creo que es más un problema de legibilidad que uno que afecta la funcionalidad.

Demo simple


Comenzaría haciendo clases vacías de rgb y rgba etc.

struct rgb{}; struct rgba{}; struct cmyk{}; //...

Use estos en lugar de enumeraciones para plantillas para obtener mejores diagnósticos.

Luego puede definir un conjunto de funciones constexpr gratuitas que producen datos específicos, por ejemplo

constexpr uint8_t channels(rgb) { return 3; } constexpr uint8_t channels(rgba) { return 4; }

De esta manera puede ofrecer los valores que tienen sentido para cualquier formato dado. Si una característica no está disponible para un formato determinado, simplemente no ofrezca la sobrecarga.

Puedes construir pixel_layout usando tales funciones donde sea necesario, pero me imagino que clases como transform obtendrán un constructor de plantilla y recopilarán la información que necesitan sin un intermediario pixel_layout .

Es posible que me falten algunos casos de uso, por lo que es posible que tenga que ajustar esto un poco, pero espero que ayude.


El uso de parámetros de plantilla de referencia sin tipo puede ser una solución. Consulte http://en.cppreference.com/w/cpp/language/template_parameters . Por ejemplo, así:

#include <iostream> #include <cstdint> #include <array> enum class color_space : std::uint8_t { rgb, cymk, other }; // PIXEL LAYOUT // Can be created/modified at runtime, but a predefined set of pixel_layouts // exists for compile-time use. struct pixel_layout { color_space space; std::uint8_t channels; }; constexpr bool operator==(const pixel_layout& a, const pixel_layout& b) { return (a.space == b.space) && (a.channels == b.channels); } constexpr bool operator!=(const pixel_layout& a, const pixel_layout& b) { return (a.space != b.space) || (a.channels != b.channels); } // Predefined pixel_layout instances, for use as template arguments // As static constexpr members of class, to make sure they have external linkage, // required for use as reference template arguments. struct default_pixel_layouts { static constexpr pixel_layout rgb{ color_space::rgb, 3 }; static constexpr pixel_layout cymk{ color_space::cymk, 4 }; }; // Definitions for the pixel_layouts constexpr pixel_layout default_pixel_layouts::rgb; constexpr pixel_layout default_pixel_layouts::cymk; // PIXEL TYPE // Takes pixel_layout reference as non-type template argument. template<const pixel_layout& Layout> struct pixel { static constexpr const pixel_layout& layout = Layout; // Because layout is constexpr, can use its members (e.g. channels), // for example as template argument. // Here size of pixel depends on number of channels in pixel_layout std::array<std::uint32_t, layout.channels> data; }; // RGB and CYMK pixel_types as type aliases using rgb = pixel<default_pixel_layouts::rgb>; using cymk = pixel<default_pixel_layouts::cymk>; // IMAGE // Takes pixel type as template argument. template<class PixelType> class image { public: using pixel_type = PixelType; }; // TRANSFORM // Takes pixel_layouts to transform from/to at runtime. Can for with the predefined // ones, but also with new ones creates at runtime. class transform { private: const pixel_layout& from_; const pixel_layout& to_; public: transform(const pixel_layout& from, const pixel_layout& to) : from_(from), to_(to) { } // Example: function working on an image template<class Image> void run(Image& img) { // Need to make sure that Image''s pixel_layout (compile-time) matches // pixel_layout of the transform (runtime). if(Image::pixel_type::layout != from_) std::cout << "Wrong pixel type on input image" << std::endl; else std::cout << "transforming..." << std::endl; } }; int main() { image<rgb> rgb_img; image<cymk> cymk_img; // Transform from rgb to cymk transform tr(default_pixel_layouts::rgb, default_pixel_layouts::cymk); tr.run(rgb_img); // ok tr.run(cymk_img); // error: input to run() must have rgb pixel_layout // Creating a new pixel_layout at runtime pixel_layout custom_layout = { color_space::other, 10 }; transform tr2(custom_layout, default_pixel_layouts::cymk); return 0; }

http://coliru.stacked-crooked.com/a/981e1b03b3b815c5

Para los casos de uso en los que se usa pixel_layout en tiempo de compilación, las diferentes instancias de pixel_layout disponibles deben crearse como objetos constexpr estáticos globales.

pixel_type luego instancia a diferentes clases, dependiendo del pixel_layout& dado como argumento de la plantilla.

Pero todavía se pueden usar en tiempo de ejecución también.


La solución que surgió involucra tres estructuras de datos:

  • Una enumeración de anotación de tipo : enum class pixel_type : char { };
  • Un tipo de unión que contiene los datos de píxeles : union pixel_data { };
  • Una estructura para contener ambos, con un parámetro de plantilla para especificar la anotación de tipo predeterminada : struct pixel{ };

Tiempo de compilación / estático:

  • Use el argumento de plantilla para establecer el tipo de formato si ya lo conoce.
  • Como unión, pixel_data será algo "sin unidades". Si ya conoce el tipo, puede escribir funciones que supongan que los datos están en la representación correcta. Si necesita una cantidad variable de memoria para contener diferentes formatos de datos de píxeles, este es claramente un problema que vale la pena abordar de forma explícita. Si esto es así, modifique la clase de pixel para parametrizar en una implementación interna que se desvíe en función del tamaño de la representación de la memoria de píxeles.
  • Utilice la clase de pixel en tiempo de ejecución para los datos que necesitan ser activados. Una vez hecho esto, puede desenvolver la anotación de tipo utilizando solo su miembro pixel_data y luego realizar transformaciones sobre eso. Al separar el tipo de datos subyacente real y los metadatos, debería ser más fácil realizar el cálculo de datos con una gran cantidad de datos sin tener que saltar a través de funciones miembro que pertenecen solo a la clase, lo que puede ser problemático si decide usar múltiples subprocesos. En realidad, diría que también podría incluir un bloqueo en la clase de píxeles si le preocupa eso, o incluso aplicarlo a los datos subyacentes. Tu decides.
  • Use pixel_data para el cálculo dentro de campos de código donde se puede suponer el tipo; de lo contrario, trate la clase de pixel como un tipo de tipo de datos variante u opcional.

Tiempo de ejecución / dinámico

  • Utilice pixel_data::unknown para seguridad en tiempo de ejecución. Si lo desea, puede crear una especie de función de apply que actuará como guardia monádica para evitar el paso de formatos tipo NULL .
  • Establezca el formato en la estructura de pixel una vez que descubra el formato.
  • Una vez más, aún puede eliminar el contenedor de datos de pixel y descartar la anotación de tipo una vez que lo desee durante el cálculo.

Almacenamiento y computación / transformación de píxeles masivos

  • Cree un tipo de clase de contenedor con la anotación de tipo; esto es para mantener muchos tipos de datos que tienen el mismo tipo de datos; desperdiciar un byte por píxel para mantener el formato de píxeles es un desperdicio; si desea almacenar una matriz, simplemente almacene un byte para toda la matriz y vuelva a parametrizar en el tipo de datos subyacente real. Esto debería ser algún tipo de pixel_matrix , y la estructura de memoria subyacente debería ser suficiente para poder aplicar fácilmente SSE u otras instrucciones vectorizadas sin tener que modificar sus estructuras de datos en gran medida en este punto.

  • Las transformaciones se pueden hacer solo con pixel_data . La lógica de conmutación debe establecerse en el tipo de datos real pixel o pixel_matrix , y el tipo eliminado y reaplicado antes y después de la transformación.

Resumen

  • Los datos dinámicos encapsularán el diseño de datos estáticos, con una anotación de tipo adjunta, y el tipo de datos estáticos es el parámetro de plantilla para permitir diferentes uniones subyacentes o tipos de datos.
  • Use el diseño de datos estáticos para las transformaciones.
  • La separación de diferentes diseños de datos estáticos subyacentes es lo suficientemente importante como para garantizar diferentes tipos.
  • Una matriz de datos dinámicos y datos estáticos es lo suficientemente importante como para ahorrar espacio de memoria y permitir la optimización vectorizada de la instrucción.

Este es un código muy escaso que se me ocurrió: http://coliru.stacked-crooked.com/a/76f31a9dd669a2fa