From 6856e2231a642fe41b224e98341199ce85eac7f0 Mon Sep 17 00:00:00 2001 From: matekelemen Date: Tue, 6 Aug 2024 01:10:40 +0200 Subject: [PATCH] add categorical colormaps --- .github/workflows/testrunner.yml | 3 ++ readme.md | 3 ++ src/main.cpp | 65 ++++++++++++++++---------------- src/mtx2img.cpp | 59 ++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/.github/workflows/testrunner.yml b/.github/workflows/testrunner.yml index aed2c5b..3d12757 100644 --- a/.github/workflows/testrunner.yml +++ b/.github/workflows/testrunner.yml @@ -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 diff --git a/readme.md b/readme.md index 221f6c9..321198f 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 8c7e4d0..f6cf340 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,7 +48,8 @@ void printHelp() << " -a : 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 to use for aggregated pixel values. Options: [binary, kindlmann, viridis] (default: " << defaultArguments.at("-c") << ").\n" + << " -c : 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" @@ -64,8 +65,8 @@ std::optional parseArguments(int argc, char const* const* argv) // Parse optional argMap auto it_argument = argMap.end(); - for (int i_arg=3; i_arg 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. @@ -198,7 +199,7 @@ std::optional parseArguments(int argc, char const* const* argv) // Validate colormap arguments.colormap = argMap["-c"]; { - if (!std::set({"binary", "kindlmann", "viridis"}).contains(arguments.colormap)) { + if (!std::set({"binary", "kindlmann", "viridis", "glasbey256", "glasbey64", "glasbey8"}).contains(arguments.colormap)) { throw std::invalid_argument(std::format( "Error: invalid colormap: {}\n", arguments.colormap @@ -207,11 +208,11 @@ std::optional 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::distance(r_resolutionString.data(), static_cast(it_end))) != r_resolutionString.size()) { + const long long resolution = std::strtoll(r_resolutionString.data(), &itEnd, 0); + if (itEnd < r_resolutionString.data() || + static_cast(std::distance(r_resolutionString.data(), static_cast(itEnd))) != r_resolutionString.size()) { throw std::invalid_argument(std::format( "Error: invalid output image resolution: {}\n", r_resolutionString @@ -230,13 +231,13 @@ std::optional 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(p_context); - r_stream.write( - reinterpret_cast(p_data), + std::ostream& rStream = *reinterpret_cast(pContext); + rStream.write( + reinterpret_cast(pData), static_cast(extent) ); } @@ -254,20 +255,20 @@ 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 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; @@ -275,7 +276,7 @@ int main(int argc, char const* const* argv) } 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; @@ -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 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 image; @@ -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(p_outputStream), // <== write context (output stream) + reinterpret_cast(pOutputStream), // <== write context (output stream) imageSize.first, // <== image width imageSize.second, // <== image height image.size() / imageSize.first / imageSize.second, // <== number of color channels diff --git a/src/mtx2img.cpp b/src/mtx2img.cpp index e2b00b9..bf727f1 100644 --- a/src/mtx2img.cpp +++ b/src/mtx2img.cpp @@ -169,6 +169,62 @@ std::vector> 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", @@ -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(0xff, 0xff - 0xff * values[i_pixel] / maxValue); + const std::size_t intensity = std::min(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