Attention pavé.
Ca fait quelques temps que je bute sur des histoires de filtres codés en templates C++ d'un côté, et impératifs de dispatch dynamique de l'autre. Typiquement, pour un programme qui lit un fichier d'image de type variable, et de fait uniquement connu à l'exécution, bien qu'appartenant à une liste bien délimitée.
Récemment, j'ai trouvé SITK, un wrapper C++ et/ou python qui permet d'utiliser ITK sans avoir à expliciter tous les types et dimensions à manipuler comme des paramètres template. Pour mon taf, je dois l'utiliser et c'est pas mal compte-tenu du très grand nombre de filtres ITK immédiatement accessibles avec une syntaxe minimaliste (celle de python, rien de plus). Fin de l'apparté sur SITK, je vous expose mon souci.
J'ai un programme simple qui lit une image (volume 3D) passée en argument et affiche une image SITK en troisdé avec un slicer. On peut consulter la taille de l'image allouée et le type de données allouée (enum class du genre sitk::sitkFloat64 pour double). Je veux effectuer un traitement avec un filtre custom qui soit le plus simple possible. Pour gérer la multiplicité de types possibles à gérer pour le volume, et éventuellement les arguments du filtre, j'ai opté pour écrire une fonction template.
Voici le code que j'obtiens du côté du programme principal:
Code:
int
main(int argc, char *argv[])
{
progArgs args; // structure unique où on range les arguments du prog.
usage(argc, argv, args);
sitk::Image imgRead;
imgRead = sitk_util::sitk_read_series(args . input_volume_path); // fonction à moi qui lit des images 3D DICOM
cout << endl << << imgRead.GetPixelIDTypeAsString(); // 64-bit float, 32-bit float, etc selon fichier lu
ExecutorSITK exe_filter;
exe_filter . execute_dispatch(imgRead, {"-32768", "0"}); // remplace les occurences erratiques de "-32768" par la valeur "0"
sitk_util::showme_blocking_function(imgRead, "image apres lecture et filtrage viteuf");
return EXIT_SUCCESS;
}
Voici la classe ExecutorSITK dans laquelle on définit le filtre personnalisé. C'est en gros la partie publique du mécanisme, celle où l'utilisateur écrit son filtre sans limite de typage ni contraintes de signature, et doit juste écrire une fonction qui dispatche les arguments du filtre à partir d'un vecteur de strings.
Code:
/*!
* @class ExecutorSITK
* @brief user-created class for dispatching a user template filter on a SITK instance
*/
class ExecutorSITK : public motherVolume
{
public:
ExecutorSITK() = default;
/*!
* @brief replaces all voxel occurences of a value by another for any numeric type T
* @tparam T : instance of numerical C-type handled by SITK, compile error if not scalar
* @param src_value : value to replace
* @param target_value : target value
* @return void
*/
template < typename T >
std::enable_if<std::is_scalar<T>::value, void>
filter_function(T src_value, T target_value)
{
T *data = static_cast<T *>(motherVolume::data_ptr);
for (size_t i = 0; i < motherVolume::nb_voxels; i ++)
{
if ( data[i] == src_value )
data[i] = target_value;
}
}
protected:
/*!
* @brief user-defined function dispatching string arguments to the user-defined filter above
* @tparam T
* @param img
* @param args : vector of strings containing arguments to dispatch
*/
template < typename T >
void filter_execute_typed(sitk::Image &img, std::vector<string> args)
{
T a = boost::lexical_cast<T>(args[0]);
T b = boost::lexical_cast<T>(args[1]);
filter_function<T>(a, b);
}
// macro-call linking the filter to the override system
OVERRIDE_INVOKE(filter_execute_typed)
};
La macro s'expande comme suit:
Code:
/*
void override_execute_double(sitk::Image &img, std::vector<string> args)
{
filter_execute_typed<double>(img, args);
}
void override_execute_float(sitk::Image &img, std::vector<string> args)
{
filter_execute_typed<float>(img, args);
}
void override_execute_int(sitk::Image &img, std::vector<string> args)
{
filter_execute_typed<int>(img, args);
}
*/
Et enfin, voilà la classe de base qui permet de faire fonctionner le mécanisme. L'idée est de pousser le plus de choses possible dans elle afin que l'utilisateur n'ait presque rien à faire en définissant ses classes dérivées pour gérer les filtres qu'il écrit:
Code:
class motherVolume
{
protected:
size_t x_dim, y_dim, z_dim;
double x_size, y_size, z_size;
void *data_ptr;
size_t nb_voxels;
public:
motherVolume()
{}
protected:
void set_volume(sitk::Image &img)
{
data_ptr = sitk_util::get_ptr_from_sitk(img); // extrait le pointeur void* du volume sitk
nb_voxels = img . GetNumberOfPixels();
vector<unsigned int> v_dim = img . GetSize();
x_dim = v_dim[0];
y_dim = v_dim[1];
z_dim = v_dim[2];
// TODO size
}
virtual void override_execute_double(sitk::Image &img, std::vector<string> args)
{
MSG_ERROR("nope double");
}
virtual void override_execute_float(sitk::Image &img, std::vector<string> args)
{
MSG_ERROR("nope float");
}
virtual void override_execute_int(sitk::Image &img, std::vector<string> args)
{
MSG_ERROR("nope int");
}
public:
void execute_dispatch(sitk::Image &img, std::vector<string> args)
{
motherVolume::set_volume(img);
switch (img . GetPixelID())
{
case sitk::sitkFloat64:
{
override_execute_double(img, args);
break;
}
case sitk::sitkFloat32:
{
override_execute_float(img, args);
break;
}
case sitk::sitkInt32:
{
override_execute_int(img, args);
break;
}
default:
MSG_ERROR("yet unhandled type");
}
}
};
La bonne nouvelle, c'est que c'est du vrai code qui fonctionne, et ce après diverses versions toutes plus foirées les unes que les autres.
Je pense que je ne suis pas loin d'avoir atteint mes limites dans la compréhension du langage : si vous avez des idées pour templatizer ça mieux, je suis prendeur.
Et oui, il y a de la redondance de l'instance de l'image sitk dans les différentes classes et signatures de fonction; mais c'est pas encore trop grave vu que c'est un smart pointer à la base (je l'ai fixé dans une autre version)
Voilà, n'hésitez pas à tout défoncer; c'est fait pour ça