Skip to content

Commit

Permalink
add categorical colormaps
Browse files Browse the repository at this point in the history
  • Loading branch information
matekelemen committed Aug 5, 2024
1 parent 554a6d1 commit 6856e22
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 33 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/testrunner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ jobs:
build/bin/mtx2img .github/assets/fidap005.mtx out.png -r 10 -c binary
build/bin/mtx2img .github/assets/fidap005.mtx out.png -r 10 -c kindlmann
build/bin/mtx2img .github/assets/fidap005.mtx out.png -r 10 -c viridis
build/bin/mtx2img .github/assets/fidap005.mtx out.png -r 10 -c glasbey256
build/bin/mtx2img .github/assets/fidap005.mtx out.png -r 10 -c glasbey64
build/bin/mtx2img .github/assets/fidap005.mtx out.png -r 10 -c glasbey8
build/bin/mtx2img --help
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Optional arguments:
- `binary`: any pixel with at least one nonzero mapping to it is black; the rest are white (default)
- [`kindlmann`](https://www.kennethmoreland.com/color-advice/#extended-kindlmann) (extended)
- [`viridis`](https://www.kennethmoreland.com/color-advice/#viridis)
- [`glasbey256`](https://strathprints.strath.ac.uk/30312/1/colorpaper_2006.pdf)
- [`glasbey64`](https://strathprints.strath.ac.uk/30312/1/colorpaper_2006.pdf)
- [`glasbey8`](https://strathprints.strath.ac.uk/30312/1/colorpaper_2006.pdf)

## Installation

Expand Down
65 changes: 33 additions & 32 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ void printHelp()
<< " -a <aggregation> : controls how sparse entries are aggregated to pixels. Options: [count, sum]\n"
<< " \"count\" ignores values and counts the number of entries referencing each pixel.\n"
<< " \"sum\" reads values and sums them up for each pixel.\n"
<< " -c <colormap> : colormap to use for aggregated pixel values. Options: [binary, kindlmann, viridis] (default: " << defaultArguments.at("-c") << ").\n"
<< " -c <colormap> : colormap to use for aggregated pixel values.\n"
<< " Options: [binary, kindlmann, viridis, glasbey256, glasbey64, glasbey8] (default: " << defaultArguments.at("-c") << ").\n"
<< "\n"
<< "The input path must point to an existing MatrixMarket file (or pass '-' to read the same format from stdin).\n"
<< "The parent directory of the output path must exist, and the output path is assumed to either not exist, or\n"
Expand All @@ -64,8 +65,8 @@ std::optional<Arguments> parseArguments(int argc, char const* const* argv)

// Parse optional argMap
auto it_argument = argMap.end();
for (int i_arg=3; i_arg<argc; ++i_arg) {
std::string arg = argv[i_arg];
for (int iArg=3; iArg<argc; ++iArg) {
std::string arg = argv[iArg];
if (!arg.empty() && arg.front() == '-') {
// Parse a key
if (it_argument == argMap.end()) {
Expand Down Expand Up @@ -105,7 +106,7 @@ std::optional<Arguments> parseArguments(int argc, char const* const* argv)
));
} // else (it_argument != argMap.end())
} // else (!arg.empty() && arg.front() == '-')
} // while (i_arg < argc)
} // while (iArg < argc)

// If the arg iterator was not reset, a value
// was not provided for the last key.
Expand Down Expand Up @@ -198,7 +199,7 @@ std::optional<Arguments> parseArguments(int argc, char const* const* argv)
// Validate colormap
arguments.colormap = argMap["-c"];
{
if (!std::set<std::string>({"binary", "kindlmann", "viridis"}).contains(arguments.colormap)) {
if (!std::set<std::string>({"binary", "kindlmann", "viridis", "glasbey256", "glasbey64", "glasbey8"}).contains(arguments.colormap)) {
throw std::invalid_argument(std::format(
"Error: invalid colormap: {}\n",
arguments.colormap
Expand All @@ -207,11 +208,11 @@ std::optional<Arguments> parseArguments(int argc, char const* const* argv)
}

// Convert and validate resolution
char* it_end = nullptr;
char* itEnd = nullptr;
const std::string& r_resolutionString = argMap["-r"];
const long long resolution = std::strtoll(r_resolutionString.data(), &it_end, 0);
if (it_end < r_resolutionString.data() ||
static_cast<std::size_t>(std::distance(r_resolutionString.data(), static_cast<const char*>(it_end))) != r_resolutionString.size()) {
const long long resolution = std::strtoll(r_resolutionString.data(), &itEnd, 0);
if (itEnd < r_resolutionString.data() ||
static_cast<std::size_t>(std::distance(r_resolutionString.data(), static_cast<const char*>(itEnd))) != r_resolutionString.size()) {
throw std::invalid_argument(std::format(
"Error: invalid output image resolution: {}\n",
r_resolutionString
Expand All @@ -230,13 +231,13 @@ std::optional<Arguments> parseArguments(int argc, char const* const* argv)


// Write function to pass to stbi_write_png_to_func
void writeImageData(void* p_context, // <== pointer to an std::ostream
void* p_data, // <== pointer to the beginning of an unsigned char array
void writeImageData(void* pContext, // <== pointer to an std::ostream
void* pData, // <== pointer to the beginning of an unsigned char array
int extent) // <== number of bytes to write
{
std::ostream& r_stream = *reinterpret_cast<std::ostream*>(p_context);
r_stream.write(
reinterpret_cast<const char*>(p_data),
std::ostream& rStream = *reinterpret_cast<std::ostream*>(pContext);
rStream.write(
reinterpret_cast<const char*>(pData),
static_cast<std::streamsize>(extent)
);
}
Expand All @@ -254,28 +255,28 @@ int main(int argc, char const* const* argv)
printHelp();
return 0;
}
} catch (std::invalid_argument& r_exception) {
std::cerr << r_exception.what();
} catch (std::invalid_argument& rException) {
std::cerr << rException.what();
printHelp();
return 1;
}

// Set up input stream
std::istream* p_inputStream = nullptr;
std::istream* pInputStream = nullptr;
std::optional<std::ifstream> maybeInputFile;

if (arguments.inputPath == "-") {
// Special case: read from the pipe.
if (!std::cin.eof()) {
p_inputStream = &std::cin;
pInputStream = &std::cin;
} else {
std::cerr << "Error: requested to read input from the pipe, but it is closed.\n";
return 2;
}
} else {
// Otherwise read from a file.
maybeInputFile.emplace(arguments.inputPath);
p_inputStream = &maybeInputFile.value();
pInputStream = &maybeInputFile.value();
if (!maybeInputFile.value().good()) {
std::cerr << "Error: failed to open input file: " << arguments.inputPath << '\n';
return 3;
Expand All @@ -284,16 +285,16 @@ int main(int argc, char const* const* argv)

// Set up output stream
const std::string outputPath = arguments.outputPath.string();
std::ostream* p_outputStream = nullptr;
std::ostream* pOutputStream = nullptr;
std::optional<std::ofstream> maybeOutputFile;

if (arguments.outputPath == "-") {
// Special case: write to stdout.
p_outputStream = &std::cout;
pOutputStream = &std::cout;
} else {
// Otherwise write to a file.
maybeOutputFile.emplace(arguments.outputPath);
p_outputStream = &maybeOutputFile.value();
pOutputStream = &maybeOutputFile.value();
}

std::vector<unsigned char> image;
Expand All @@ -309,31 +310,31 @@ int main(int argc, char const* const* argv)
// Parse the input file and fill an output image buffer
// Note: the image gets resized if the matrix dimensions
// are smaller than the requested image dimensions.
image = mtx2img::convert(*p_inputStream,
image = mtx2img::convert(*pInputStream,
imageSize.first,
imageSize.second,
arguments.aggregation,
arguments.colormap);

#ifdef NDEBUG
} catch (mtx2img::ParsingException& r_exception) {
std::cerr << r_exception.what();
} catch (mtx2img::ParsingException& rException) {
std::cerr << rException.what();
return 4;
} catch (mtx2img::InvalidFormat& r_exception) {
std::cerr << r_exception.what();
} catch (mtx2img::InvalidFormat& rException) {
std::cerr << rException.what();
return 5;
} catch (mtx2img::UnsupportedFormat& r_exception) {
std::cerr << r_exception.what();
} catch (mtx2img::UnsupportedFormat& rException) {
std::cerr << rException.what();
return 6;
} catch (std::invalid_argument& r_exception) {
std::cerr << r_exception.what();
} catch (std::invalid_argument& rException) {
std::cerr << rException.what();
return 7;
}
#endif

if (stbi_write_png_to_func(
writeImageData, // <== write functor
reinterpret_cast<void*>(p_outputStream), // <== write context (output stream)
reinterpret_cast<void*>(pOutputStream), // <== write context (output stream)
imageSize.first, // <== image width
imageSize.second, // <== image height
image.size() / imageSize.first / imageSize.second, // <== number of color channels
Expand Down
59 changes: 58 additions & 1 deletion src/mtx2img.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,62 @@ std::vector<std::array<unsigned char, CHANNELS>> makeColormap(const std::string&
{216,226, 25},{218,227, 25},{221,227, 24},{223,227, 24},{226,228, 24},{229,228, 25},{231,228, 25},{234,229, 26},
{236,229, 27},{239,229, 28},{241,229, 29},{244,230, 30},{246,230, 32},{248,230, 33},{251,231, 35},{253,231, 37}
};
} else if (r_colormapName == "glasbey256") {
// Categorical color map for 256 values.
// doi:10.1002/col.20327
return {
{130,89 ,146},{24 ,105,255},{0 ,138,0 },{243,109,255},{113,0 ,121},{170,251,0 },{0 ,190,194},{255,162,53 },
{93 ,61 ,4 },{8 ,0 ,138},{0 ,93 ,93 },{154,125,130},{162,174,255},{150,182,117},{158,40 ,255},{77 ,0 ,20 },
{255,174,190},{206,0 ,146},{0 ,255,182},{0 ,45 ,0 },{158,117,0 },{61 ,53 ,65 },{243,235,146},{101,97 ,138},
{138,61 ,77 },{89 ,4 ,186},{85 ,138,113},{178,190,194},{255,93 ,130},{28 ,198,0 },{146,247,255},{45 ,134,166},
{57 ,93 ,40 },{235,206,255},{255,93 ,0 },{166,97 ,170},{134,0 ,0 },{53 ,0 ,89 },{0 ,81 ,142},{158,73 ,16 },
{206,190,0 },{0 ,40 ,40 },{0 ,178,255},{202,166,134},{190,154,194},{45 ,32 ,12 },{117,101,69 },{130,121,223},
{0 ,194,138},{186,231,194},{134,142,166},{202,113,89 },{130,154,0 },{45 ,0 ,255},{210,4 ,247},{255,215,190},
{146,206,247},{186,93 ,125},{255,65 ,194},{190,134,255},{146,142,101},{166,4 ,170},{134,227,117},{73 ,0 ,61 },
{251,239,12 },{105,85 ,93 },{89 ,49 ,45 },{105,53 ,255},{182,4 ,77 },{93 ,109,113},{65 ,69 ,53 },{101,113,0 },
{121,0 ,73 },{28 ,49 ,81 },{121,65 ,158},{255,146,113},{255,166,243},{186,158,65 },{130,170,154},{215,121,0 },
{73 ,61 ,113},{81 ,162,85 },{231,130,182},{210,227,251},{0 ,73 ,49 },{109,219,194},{61 ,77 ,93 },{97 ,53 ,85 },
{0 ,113,81 },{93 ,24 ,0 },{154,93 ,81 },{85 ,142,219},{202,202,154},{53 ,24 ,32 },{57 ,61 ,0 },{0 ,154,150},
{235,16 ,109},{138,69 ,121},{117,170,194},{202,146,154},{210,186,198},{154,206,0 },{69 ,109,170},{117,89 ,0 },
{206,77 ,12 },{0 ,223,251},{255,61 ,65 },{255,202,73 },{45 ,49 ,146},{134,105,134},{158,130,190},{206,174,255},
{121,69 ,45 },{198,251,130},{93 ,117,73 },{182,69 ,73 },{255,223,239},{162,0 ,113},{77 ,77 ,166},{166,170,202},
{113,28 ,40 },{40 ,121,121},{8 ,73 ,0 },{0 ,105,134},{166,117,73 },{251,182,130},{85 ,24 ,125},{0 ,255,89 },
{0 ,65 ,77 },{109,142,146},{170,36 ,0 },{190,210,109},{138,97 ,186},{210,65 ,190},{73 ,97 ,81 },{206,243,239},
{97 ,194,97 },{20 ,138,77 },{0 ,255,231},{0 ,105,0 },{178,121,158},{170,178,158},{186,85 ,255},{198,121,206},
{32 ,49 ,32 },{125,4 ,219},{194,198,247},{138,198,206},{231,235,206},{40 ,28 ,57 },{158,255,174},{130,206,154},
{49 ,166,12 },{0 ,162,117},{219,146,85 },{61 ,20 ,4 },{255,138,154},{130,134,53 },{105,77 ,113},{182,97 ,0 },
{125,45 ,0 },{162,178,57 },{49 ,4 ,125},{166,61 ,202},{154,32 ,45 },{4 ,223,134},{117,125,109},{138,150,210},
{8 ,162,202},{247,109,93 },{16 ,85 ,202},{219,182,101},{146,89 ,109},{162,255,227},{89 ,85 ,40 },{113,121,170},
{215,89 ,101},{73 ,32 ,81 },{223,77 ,146},{0 ,0 ,202},{93 ,101,210},{223,166,0 },{178,73 ,146},{182,138,117},
{97 ,77 ,61 },{166,150,162},{85 ,28 ,53 },{49 ,65 ,65 },{117,117,134},{146,158,162},{117,154,113},{255,130,32 },
{134,85 ,255},{154,198,182},{223,150,243},{202,223,49 },{142,93 ,40 },{53 ,190,227},{113,166,255},{89 ,138,49 },
{255,194,235},{170,61 ,105},{73 ,97 ,125},{73 ,53 ,28 },{69 ,178,158},{28 ,36 ,49 },{247,49 ,239},{117,0 ,166},
{231,182,170},{130,105,101},{227,162,202},{32 ,36 ,0 },{121,182,16 },{158,142,255},{210,117,138},{202,182,219},
{174,154,223},{255,113,219},{210,247,178},{198,215,206},{255,210,138},{93 ,223,53 },{93 ,121,146},{162,142,0 },
{174,223,239},{113,77 ,194},{125,69 ,0 },{101,146,182},{93 ,121,255},{81 ,73 ,89 },{150,158,81 },{206,105,174},
{101,53 ,117},{219,210,227},{182,174,117},{81 ,89 ,0 },{182,89 ,57 },{85 ,4 ,235},{61 ,117,45 },{146,130,154},
{130,36 ,105},{186,134,57 },{138,178,227},{109,178,130},{150,65 ,53 },{109,65 ,73 },{138,117,61 },{178,113,117},
{146,28 ,73 },{223,109,49 },{0 ,227,223},{146,4 ,202},{49 ,40 ,89 },{0 ,125,210},{162,109,255},{255,255,255}
};
} else if (r_colormapName == "glasbey64") {
// Categorical color map for 64 values.
// doi:10.1002/col.20327
return {
{73 ,0 ,61 },{24 ,105,255},{0 ,138,0 },{243,109,255},{113,0 ,121},{170,251,0 },{0 ,190,194},{255,162,53 },
{93 ,61 ,4 },{8 ,0 ,138},{0 ,93 ,93 },{154,125,130},{162,174,255},{150,182,117},{158,40 ,255},{77 ,0 ,20 },
{255,174,190},{206,0 ,146},{0 ,255,182},{0 ,45 ,0 },{158,117,0 },{61 ,53 ,65 },{243,235,146},{101,97 ,138},
{138,61 ,77 },{89 ,4 ,186},{85 ,138,113},{178,190,194},{255,93 ,130},{28 ,198,0 },{146,247,255},{45 ,134,166},
{57 ,93 ,40 },{235,206,255},{255,93 ,0 },{166,97 ,170},{134,0 ,0 },{53 ,0 ,89 },{0 ,81 ,142},{158,73 ,16 },
{206,190,0 },{0 ,40 ,40 },{0 ,178,255},{202,166,134},{190,154,194},{45 ,32 ,12 },{117,101,69 },{130,121,223},
{0 ,194,138},{186,231,194},{134,142,166},{202,113,89 },{130,154,0 },{45 ,0 ,255},{210,4 ,247},{255,215,190},
{146,206,247},{186,93 ,125},{255,65 ,194},{190,134,255},{146,142,101},{166,4 ,170},{134,227,117},{255,255,255}
};
} else if (r_colormapName == "glasbey8") {
// Categorical color map for 8 values.
// doi:10.1002/col.20327
return {
{255,162,53 },{24 ,105,255},{0 ,138,0 },{243,109,255},{113,0 ,121},{170,251,0 },{0 ,190,194},{255,255,255}
};
} else {
throw std::invalid_argument(std::format(
"Error: invalid colormap: {}\n",
Expand Down Expand Up @@ -721,8 +777,9 @@ void fill(Parser& r_parser,
}

// Apply the colormap and fill the image buffer
const std::size_t maxColor = colormap.empty() ? 0 : colormap.size() - 1;
for (std::size_t i_pixel=0ul; i_pixel<pixelCount; ++i_pixel) {
const std::size_t intensity = std::min<std::size_t>(0xff, 0xff - 0xff * values[i_pixel] / maxValue);
const std::size_t intensity = std::min<std::size_t>(maxColor, maxColor - maxColor * values[i_pixel] / maxValue);
const auto& r_color = colormap[intensity];
const std::size_t i_imageBegin = CHANNELS * i_pixel;
for (std::size_t i_component=0; i_component<CHANNELS; ++i_component) {
Expand Down

0 comments on commit 6856e22

Please sign in to comment.