Use Oklab when auto-selecting filament colors

This commit is contained in:
sentientstardust
2026-05-15 21:20:30 +01:00
parent 8a550bb513
commit e8c305e4cd
6 changed files with 163 additions and 11 deletions

View File

@@ -362,6 +362,11 @@ std::array<float, 3> mix_color_solver_components(const std::vector<std::array<fl
return pigment_painter::mix_srgb(component_colors, safe_weights);
}
std::array<float, 3> color_solver_oklab_from_srgb(const std::array<float, 3> &rgb)
{
return oklab_from_srgb(rgb);
}
std::string color_solver_candidate_cache_key(const std::vector<std::array<float, 3>> &component_colors,
ColorSolverMixModel mix_model,
int total_units)

View File

@@ -82,6 +82,7 @@ std::array<float, 3> mix_color_solver_components(const std::vector<std::array<fl
std::array<float, 3> mix_color_solver_components(const std::vector<std::array<float, 3>> &component_colors,
const std::vector<float> &weights,
ColorSolverMixModel mix_model);
std::array<float, 3> color_solver_oklab_from_srgb(const std::array<float, 3> &rgb);
std::string color_solver_candidate_cache_key(const std::vector<std::array<float, 3>> &component_colors,
ColorSolverMixModel mix_model,

View File

@@ -7314,11 +7314,13 @@ static const GCodeUVTextureTriangleCache &uv_texture_triangle_cache_for_gcode(
return it->second;
}
static float color_distance_sq_for_gcode(const std::array<float, 3> &lhs, const std::array<float, 3> &rhs)
static float perceptual_color_distance_sq_for_gcode(const std::array<float, 3> &lhs, const std::array<float, 3> &rhs)
{
const float dr = lhs[0] - rhs[0];
const float dg = lhs[1] - rhs[1];
const float db = lhs[2] - rhs[2];
const std::array<float, 3> lhs_oklab = oklab_from_srgb_for_gcode(lhs);
const std::array<float, 3> rhs_oklab = oklab_from_srgb_for_gcode(rhs);
const float dr = lhs_oklab[0] - rhs_oklab[0];
const float dg = lhs_oklab[1] - rhs_oklab[1];
const float db = lhs_oklab[2] - rhs_oklab[2];
return dr * dr + dg * dg + db * db;
}
@@ -7337,7 +7339,7 @@ static std::vector<size_t> best_matching_component_indices_for_semantic_colors_f
do {
float error = 0.f;
for (size_t role_idx = 0; role_idx < semantic_colors.size(); ++role_idx)
error += color_distance_sq_for_gcode(component_colors[permutation[role_idx]], semantic_colors[role_idx]);
error += perceptual_color_distance_sq_for_gcode(component_colors[permutation[role_idx]], semantic_colors[role_idx]);
if (error < best_error) {
best_error = error;

View File

@@ -248,6 +248,71 @@ static std::vector<std::string> collect_texture_mapping_vertex_color_match_warni
};
}
static std::vector<std::string> collect_texture_mapping_filament_color_match_warnings(const PrintObject &print_object)
{
const Print *print = print_object.print();
if (print == nullptr)
return {};
const ModelObject *model_object = print_object.model_object();
if (model_object == nullptr)
return {};
const size_t num_physical = print->config().filament_colour.size();
if (num_physical == 0)
return {};
std::vector<std::string> warnings;
std::set<unsigned int> seen_zone_ids;
for (const ModelVolume *volume : model_object->volumes) {
if (volume == nullptr)
continue;
const std::vector<int> used_extruders = volume->get_extruders();
for (const int filament_id : used_extruders) {
if (filament_id <= 0)
continue;
const unsigned int filament_id_u = unsigned(filament_id);
if (seen_zone_ids.find(filament_id_u) != seen_zone_ids.end())
continue;
seen_zone_ids.insert(filament_id_u);
const TextureMappingZone *zone = print->texture_mapping_manager().zone_from_id(filament_id_u);
if (zone == nullptr || !zone->enabled || zone->deleted || !zone->is_image_texture())
continue;
const std::vector<TextureMappingColorMatch> matches =
TextureMappingManager::texture_component_color_matches(*zone, num_physical, print->config().filament_colour.values);
if (matches.empty())
continue;
std::vector<std::string> poor_matches;
for (const TextureMappingColorMatch &match : matches) {
if (match.perceptual_distance <= TextureMappingManager::poor_color_match_distance())
continue;
poor_matches.emplace_back("F" + std::to_string(match.filament_id) + " for " + match.expected_color_name);
}
if (poor_matches.empty())
continue;
std::string detail;
for (size_t idx = 0; idx < poor_matches.size(); ++idx) {
if (idx > 0)
detail += ", ";
detail += poor_matches[idx];
}
warnings.emplace_back(
L("Filaments used in Texture Mapping zone ") +
std::to_string(filament_id_u) + L(" do not appear to match the expected colors: ") + detail +
L("."));
}
}
return warnings;
}
static const char *vertex_color_mode_name_for_error(int filament_color_mode)
{
switch (filament_color_mode) {
@@ -1404,6 +1469,8 @@ void PrintObject::slice_volumes()
this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg);
for (const std::string &warning_msg : collect_texture_mapping_vertex_color_match_warnings(*this))
this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg);
for (const std::string &warning_msg : collect_texture_mapping_filament_color_match_warnings(*this))
this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg);
for (const std::string &error_msg : collect_texture_mapping_vertex_color_mode_mismatch_errors(*this))
this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, error_msg);

View File

@@ -22,6 +22,7 @@ namespace {
constexpr unsigned int TextureMappingZoneIdBase = 99;
constexpr unsigned int MaxTextureMappingZoneId = 255;
constexpr const char *TextureMappingGapDisplayColor = "#8C8C8C";
constexpr float TextureMappingPoorColorMatchDistance = 0.22f;
struct RGB {
int r = 0;
@@ -379,11 +380,22 @@ static RGB filament_color(unsigned int id, const std::vector<std::string> &filam
return {};
}
static float color_distance_sq(const RGB &lhs, const RGB &rhs)
static std::array<float, 3> rgb_to_srgb01(const RGB &color)
{
const float dr = float(lhs.r - rhs.r);
const float dg = float(lhs.g - rhs.g);
const float db = float(lhs.b - rhs.b);
return {
float(std::clamp(color.r, 0, 255)) / 255.f,
float(std::clamp(color.g, 0, 255)) / 255.f,
float(std::clamp(color.b, 0, 255)) / 255.f
};
}
static float perceptual_color_distance_sq(const RGB &lhs, const RGB &rhs)
{
const std::array<float, 3> lhs_oklab = color_solver_oklab_from_srgb(rgb_to_srgb01(lhs));
const std::array<float, 3> rhs_oklab = color_solver_oklab_from_srgb(rgb_to_srgb01(rhs));
const float dr = lhs_oklab[0] - rhs_oklab[0];
const float dg = lhs_oklab[1] - rhs_oklab[1];
const float db = lhs_oklab[2] - rhs_oklab[2];
return dr * dr + dg * dg + db * db;
}
@@ -405,6 +417,24 @@ static std::vector<RGB> semantic_colors(int filament_color_mode)
}
}
static std::vector<std::string> semantic_color_names(int filament_color_mode)
{
switch (clamp_int(filament_color_mode,
int(TextureMappingZone::FilamentColorAny),
int(TextureMappingZone::FilamentColorRGBKW))) {
case int(TextureMappingZone::FilamentColorRGB): return {"Red", "Green", "Blue"};
case int(TextureMappingZone::FilamentColorCMY): return {"Cyan", "Magenta", "Yellow"};
case int(TextureMappingZone::FilamentColorCMYK): return {"Cyan", "Magenta", "Yellow", "Black"};
case int(TextureMappingZone::FilamentColorCMYW): return {"Cyan", "Magenta", "Yellow", "White"};
case int(TextureMappingZone::FilamentColorRGBK): return {"Red", "Green", "Blue", "Black"};
case int(TextureMappingZone::FilamentColorRGBW): return {"Red", "Green", "Blue", "White"};
case int(TextureMappingZone::FilamentColorBW): return {"Black", "White"};
case int(TextureMappingZone::FilamentColorCMYKW): return {"Cyan", "Magenta", "Yellow", "Black", "White"};
case int(TextureMappingZone::FilamentColorRGBKW): return {"Red", "Green", "Blue", "Black", "White"};
default: return {};
}
}
static std::vector<unsigned int> ids_from_json(const nlohmann::json &value, size_t num_physical)
{
std::vector<unsigned int> ids;
@@ -1412,7 +1442,7 @@ std::vector<unsigned int> TextureMappingManager::effective_texture_component_ids
for (unsigned int id = 1; id <= num_physical; ++id) {
if (id < used.size() && used[id])
continue;
const float distance = color_distance_sq(filament_color(id, filament_colours), target);
const float distance = perceptual_color_distance_sq(filament_color(id, filament_colours), target);
if (distance < best_distance) {
best_distance = distance;
best_id = id;
@@ -1448,7 +1478,7 @@ std::vector<unsigned int> TextureMappingManager::effective_texture_component_ids
const unsigned int id = selected[i];
if (selected_used[i] || id < 1 || id > num_physical)
continue;
const float distance = color_distance_sq(filament_color(id, filament_colours), role);
const float distance = perceptual_color_distance_sq(filament_color(id, filament_colours), role);
if (distance < best_distance) {
best_distance = distance;
best_selected = i;
@@ -1502,6 +1532,42 @@ bool TextureMappingManager::auto_adjust_texture_component_ids(TextureMappingZone
return true;
}
std::vector<TextureMappingColorMatch> TextureMappingManager::texture_component_color_matches(
const TextureMappingZone &zone,
size_t num_physical,
const std::vector<std::string> &filament_colours)
{
if (!zone.enabled || zone.deleted || !zone.is_image_texture() || num_physical == 0)
return {};
const size_t expected = expected_component_count(zone.texture_mapping_mode, zone.filament_color_mode);
if (expected == 0)
return {};
const std::vector<RGB> roles = semantic_colors(zone.filament_color_mode);
const std::vector<std::string> role_names = semantic_color_names(zone.filament_color_mode);
if (roles.size() != expected || role_names.size() != expected)
return {};
const std::vector<unsigned int> component_ids = effective_texture_component_ids(zone, num_physical, filament_colours);
const size_t count = std::min(expected, component_ids.size());
std::vector<TextureMappingColorMatch> matches;
matches.reserve(count);
for (size_t idx = 0; idx < count; ++idx) {
const unsigned int id = component_ids[idx];
if (id < 1 || id > num_physical)
continue;
const float distance = std::sqrt(perceptual_color_distance_sq(filament_color(id, filament_colours), roles[idx]));
matches.push_back({id, role_names[idx], distance});
}
return matches;
}
float TextureMappingManager::poor_color_match_distance()
{
return TextureMappingPoorColorMatchDistance;
}
float TextureMappingManager::max_component_surface_offset_mm(float reference_width_mm)
{
const float safe_reference = std::max(0.05f, std::abs(reference_width_mm));

View File

@@ -272,6 +272,13 @@ struct TextureMappingGlobalSettings
static bool is_generic_solver_color_mode(const std::string &mode);
};
struct TextureMappingColorMatch
{
unsigned int filament_id = 0;
std::string expected_color_name;
float perceptual_distance = 0.f;
};
class TextureMappingManager
{
public:
@@ -325,6 +332,10 @@ public:
static bool auto_adjust_texture_component_ids(TextureMappingZone &zone,
size_t num_physical,
const std::vector<std::string> &filament_colours);
static std::vector<TextureMappingColorMatch> texture_component_color_matches(const TextureMappingZone &zone,
size_t num_physical,
const std::vector<std::string> &filament_colours);
static float poor_color_match_distance();
static std::vector<float> default_offset_distances(size_t component_count, float reference_width_mm = 0.4f);
static std::vector<float> default_offset_angles(size_t component_count);