Skip to content

Commit e812ed1

Browse files
committed
CPU compositing for exporting and watched pixels
1 parent ca897a1 commit e812ed1

File tree

5 files changed

+177
-51
lines changed

5 files changed

+177
-51
lines changed

src/app.cpp

+149-35
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ static float g_scroll_multiplier = 1.0f;
7171
struct WatchedPixel
7272
{
7373
int2 pixel;
74-
int color_mode_current = 0;
75-
int color_mode_reference = 0;
74+
int3 color_mode{0, 0, 0}; //!< Color mode for current, reference, and composite pixels
7675
};
7776

7877
static vector<WatchedPixel> g_watched_pixels;
@@ -791,6 +790,21 @@ HDRViewApp::HDRViewApp(std::optional<float> force_exposure, std::optional<float>
791790
save_as(filename);
792791
},
793792
if_img});
793+
add_action({"Export image as...", ICON_MY_SAVE_AS, ImGuiKey_None, 0,
794+
[this]()
795+
{
796+
string filename =
797+
pfd::save_file("Export image as", g_blank_icon,
798+
{
799+
"Supported image files",
800+
fmt::format("*.{}", fmt::join(Image::savable_formats(), "*.")),
801+
})
802+
.result();
803+
804+
if (!filename.empty())
805+
export_as(filename);
806+
},
807+
if_img});
794808

795809
#else
796810
add_action({"Save as...", ICON_MY_SAVE_AS, ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_S, 0,
@@ -1222,6 +1236,7 @@ void HDRViewApp::draw_menus()
12221236
ImGui::Separator();
12231237

12241238
MenuItem(action("Save as..."));
1239+
MenuItem(action("Export image as..."));
12251240

12261241
ImGui::Separator();
12271242

@@ -1359,6 +1374,55 @@ void HDRViewApp::save_as(const string &filename) const
13591374
}
13601375
}
13611376

1377+
void HDRViewApp::export_as(const string &filename) const
1378+
{
1379+
try
1380+
{
1381+
Image img(current_image()->size(), 4);
1382+
img.finalize();
1383+
int2 p;
1384+
auto bounds = current_image()->data_window;
1385+
int block_size = std::max(1, 1024 * 1024 / img.size().x);
1386+
parallel_for(blocked_range<int>(0, img.size().y, block_size),
1387+
[this, &img, bounds](int begin_y, int end_y, int, int)
1388+
{
1389+
for (int y = begin_y; y < end_y; ++y)
1390+
for (int x = 0; x < img.size().x; ++x)
1391+
{
1392+
float4 v = pixel_value(int2{x, y} + bounds.min, false, 2);
1393+
1394+
img.channels[0](x, y) = v[0];
1395+
img.channels[1](x, y) = v[1];
1396+
img.channels[2](x, y) = v[2];
1397+
img.channels[3](x, y) = v[3];
1398+
}
1399+
});
1400+
1401+
#if !defined(__EMSCRIPTEN__)
1402+
std::ofstream os{filename, std::ios_base::binary};
1403+
img.save(os, filename, 1.f, 1.f, false, m_dither);
1404+
#else
1405+
std::ostringstream os;
1406+
img.save(os, filename, 1.f, 1.f, false, m_dither);
1407+
string buffer = os.str();
1408+
emscripten_browser_file::download(
1409+
filename, // the default filename for the browser to save.
1410+
"application/octet-stream", // the MIME type of the data, treated as if it were a webserver
1411+
// serving a file
1412+
string_view(buffer.c_str(), buffer.length()) // a buffer describing the data to download
1413+
);
1414+
#endif
1415+
}
1416+
catch (const std::exception &e)
1417+
{
1418+
spdlog::error("An error occurred while exporting to '{}':\n\t{}.", filename, e.what());
1419+
}
1420+
catch (...)
1421+
{
1422+
spdlog::error("An unknown error occurred while exporting to '{}'.", filename);
1423+
}
1424+
}
1425+
13621426
void HDRViewApp::load_images(const vector<string> &filenames)
13631427
{
13641428
for (auto f : filenames)
@@ -1645,19 +1709,68 @@ void HDRViewApp::draw_info_window()
16451709
return img->draw_info();
16461710
}
16471711

1648-
static void draw_pixel_color(ConstImagePtr img, const int2 &pixel, int &color_mode, Target target,
1649-
bool allow_copy = false)
1712+
float4 HDRViewApp::pixel_value(int2 p, bool raw, int which_image) const
16501713
{
1651-
if (!img)
1652-
return;
1714+
auto img1 = current_image();
1715+
auto img2 = reference_image();
1716+
1717+
float4 value;
1718+
1719+
if (which_image == 0)
1720+
value = img1 ? (raw ? img1->raw_pixel(p, Target_Primary) : img1->rgba_pixel(p, Target_Primary)) : float4{0.f};
1721+
else if (which_image == 1)
1722+
value =
1723+
img2 ? (raw ? img2->raw_pixel(p, Target_Secondary) : img2->rgba_pixel(p, Target_Secondary)) : float4{0.f};
1724+
else if (which_image == 2)
1725+
{
1726+
auto rgba1 = img1 ? img1->rgba_pixel(p, Target_Primary) : float4{0.f};
1727+
auto rgba2 = img2 ? img2->rgba_pixel(p, Target_Secondary) : float4{0.f};
1728+
value = blend(rgba1, rgba2, m_blend_mode);
1729+
}
1730+
1731+
return raw ? value
1732+
: ::tonemap(float4{powf(2.f, m_exposure_live) * value.xyz(), value.w}, m_gamma_live, m_tonemap,
1733+
m_colormap);
1734+
}
16531735

1654-
int group_idx = target == Target_Primary ? img->selected_group : img->reference_group;
1655-
auto &group = img->groups[group_idx];
1656-
float4 color32 = img->raw_pixel(pixel, target);
1657-
float4 displayed_color = img->shaded_pixel(pixel, target, powf(2.f, hdrview()->exposure_live()),
1658-
hdrview()->gamma_live(), hdrview()->tonemap(), hdrview()->colormap());
1736+
static void pixel_color_widget(const int2 &pixel, int &color_mode, int which_image, bool allow_copy = false)
1737+
{
1738+
float4 color32 = hdrview()->pixel_value(pixel, true, which_image);
1739+
float4 displayed_color = hdrview()->pixel_value(pixel, false, which_image);
16591740
uint32_t hex = color_f128_to_u32(color_u32_to_f128(color_f128_to_u32(displayed_color)));
16601741
int4 ldr_color = int4{float4{color_u32_to_f128(hex)} * 255.f};
1742+
bool3 inside = {false, false, false};
1743+
1744+
int components = 4;
1745+
string channel_names[4] = {"R", "G", "B", "A"};
1746+
if (which_image != 2)
1747+
{
1748+
ConstImagePtr img;
1749+
ChannelGroup group;
1750+
if (which_image == 0)
1751+
{
1752+
if (!hdrview()->current_image())
1753+
return;
1754+
img = hdrview()->current_image();
1755+
components = color_mode == 0 ? img->groups[img->selected_group].num_channels : 4;
1756+
group = img->groups[img->selected_group];
1757+
inside[0] = img->contains(pixel);
1758+
}
1759+
else if (which_image == 1)
1760+
{
1761+
if (!hdrview()->reference_image())
1762+
return;
1763+
img = hdrview()->reference_image();
1764+
components = color_mode == 0 ? img->groups[img->reference_group].num_channels : 4;
1765+
group = img->groups[img->reference_group];
1766+
inside[1] = img->contains(pixel);
1767+
}
1768+
1769+
if (color_mode == 0)
1770+
for (int c = 0; c < components; ++c)
1771+
channel_names[c] = Channel::tail(img->channels[group.channels[c]].name);
1772+
}
1773+
inside[2] = inside[0] || inside[1];
16611774

16621775
ImGuiColorEditFlags color_flags = ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_AlphaPreviewHalf;
16631776
if (ImGui::ColorButton("colorbutton", displayed_color, color_flags))
@@ -1672,11 +1785,11 @@ static void draw_pixel_color(ConstImagePtr img, const int2 &pixel, int &color_mo
16721785
string buf;
16731786
if (color_mode == 0)
16741787
{
1675-
if (group.num_channels == 4)
1788+
if (components == 4)
16761789
buf = fmt::format("({:g}, {:g}, {:g}, {:g})", color32.x, color32.y, color32.z, color32.w);
1677-
else if (group.num_channels == 3)
1790+
else if (components == 3)
16781791
buf = fmt::format("({:g}, {:g}, {:g})", color32.x, color32.y, color32.z);
1679-
else if (group.num_channels == 2)
1792+
else if (components == 2)
16801793
buf = fmt::format("({:g}, {:g})", color32.x, color32.y);
16811794
else
16821795
buf = fmt::format("{:g}", color32.x);
@@ -1705,11 +1818,8 @@ static void draw_pixel_color(ConstImagePtr img, const int2 &pixel, int &color_mo
17051818

17061819
ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x);
17071820

1708-
// static const char *rgba = "R,G,B,A";
1709-
static const char *rgba_names[4] = {"R", "G", "B", "A"};
1710-
float w_full = ImGui::GetContentRegionAvail().x;
1821+
float w_full = ImGui::GetContentRegionAvail().x;
17111822
// width available to all items (without spacing)
1712-
int components = color_mode == 0 ? group.num_channels : 4;
17131823
float w_items = w_full - ImGui::GetStyle().ItemInnerSpacing.x * (components - 1);
17141824
float prev_split = w_items;
17151825
// distributes the available width without jitter during resize
@@ -1720,53 +1830,46 @@ static void draw_pixel_color(ConstImagePtr img, const int2 &pixel, int &color_mo
17201830
prev_split = next_split;
17211831
};
17221832

1723-
ImGui::BeginDisabled(!img->contains(pixel));
1833+
ImGui::BeginDisabled(!inside[which_image]);
17241834
ImGui::BeginGroup();
17251835
if (color_mode == 0)
17261836
{
1727-
// ImGui::InputScalarN(group.name.c_str(), ImGuiDataType_Float, &color32, group.num_channels, NULL, NULL, "%-g",
1728-
// ImGuiInputTextFlags_ReadOnly);
17291837
for (int c = 0; c < components; ++c)
17301838
{
17311839
if (c > 0)
17321840
ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x);
17331841

17341842
set_item_width(c);
17351843
ImGui::InputFloat(fmt::format("##component {}", c).c_str(), &color32[c], 0.f, 0.f,
1736-
fmt::format("{}: %g", Channel::tail(img->channels[group.channels[c]].name)).c_str(),
1737-
ImGuiInputTextFlags_ReadOnly);
1844+
fmt::format("{}: %g", channel_names[c]).c_str(), ImGuiInputTextFlags_ReadOnly);
17381845
}
17391846
}
17401847
else if (color_mode == 1)
17411848
{
1742-
// ImGui::InputScalarN(rgba, ImGuiDataType_Float, &displayed_color, 4, NULL, NULL, "%-g",
1743-
// ImGuiInputTextFlags_ReadOnly);
17441849
for (int c = 0; c < components; ++c)
17451850
{
17461851
if (c > 0)
17471852
ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x);
17481853

17491854
set_item_width(c);
17501855
ImGui::InputFloat(fmt::format("##component {}", c).c_str(), &displayed_color[c], 0.f, 0.f,
1751-
fmt::format("{}: %g", rgba_names[c]).c_str(), ImGuiInputTextFlags_ReadOnly);
1856+
fmt::format("{}: %g", channel_names[c]).c_str(), ImGuiInputTextFlags_ReadOnly);
17521857
}
17531858
}
17541859
else if (color_mode == 2)
17551860
{
1756-
// ImGui::InputScalarN(rgba, ImGuiDataType_S32, &ldr_color, 4, NULL, NULL, "%-d", ImGuiInputTextFlags_ReadOnly);
17571861
for (int c = 0; c < components; ++c)
17581862
{
17591863
if (c > 0)
17601864
ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x);
17611865

17621866
set_item_width(c);
17631867
ImGui::InputScalarN(fmt::format("##component {}", c).c_str(), ImGuiDataType_S32, &ldr_color[c], 1, NULL,
1764-
NULL, fmt::format("{}: %d", rgba_names[c]).c_str(), ImGuiInputTextFlags_ReadOnly);
1868+
NULL, fmt::format("{}: %d", channel_names[c]).c_str(), ImGuiInputTextFlags_ReadOnly);
17651869
}
17661870
}
17671871
else if (color_mode == 3)
17681872
{
1769-
// ImGui::InputScalar(rgba, ImGuiDataType_S32, &hex, NULL, NULL, "#%-08X", ImGuiInputTextFlags_ReadOnly);
17701873
ImGui::SetNextItemWidth(IM_TRUNC(w_full));
17711874
ImGui::InputScalar("##hex color", ImGuiDataType_S32, &hex, NULL, NULL, "#%08X", ImGuiInputTextFlags_ReadOnly);
17721875
}
@@ -1819,16 +1922,22 @@ void HDRViewApp::draw_pixel_inspector_window()
18191922
auto hovered_pixel = int2{pixel_at_app_pos(io.MousePos)};
18201923
if (PixelHeader(ICON_MY_CURSOR_ARROW "##hovered pixel", hovered_pixel))
18211924
{
1822-
static int2 color_mode = {0, 0};
1925+
static int3 color_mode = {0, 0, 0};
18231926
ImGui::PushID("Current");
1824-
draw_pixel_color(current_image(), hovered_pixel, color_mode.x, Target_Primary);
1927+
pixel_color_widget(hovered_pixel, color_mode.x, 0);
18251928
ImGui::SetItemTooltip("Hovered pixel values in current channel.");
18261929
ImGui::PopID();
1930+
18271931
ImGui::PushID("Reference");
1828-
draw_pixel_color(reference_image(), hovered_pixel, color_mode.y, Target_Secondary);
1932+
pixel_color_widget(hovered_pixel, color_mode.y, 1);
18291933
ImGui::SetItemTooltip("Hovered pixel values in reference channel.");
18301934
ImGui::PopID();
18311935

1936+
ImGui::PushID("Composite");
1937+
pixel_color_widget(hovered_pixel, color_mode.z, 2);
1938+
ImGui::SetItemTooltip("Hovered pixel values in composite.");
1939+
ImGui::PopID();
1940+
18321941
ImGui::Spacing();
18331942
}
18341943

@@ -1844,15 +1953,20 @@ void HDRViewApp::draw_pixel_inspector_window()
18441953
if (PixelHeader(fmt::format("{}{}", ICON_MY_WATCHED_PIXEL, i + 1), wp.pixel, &visible))
18451954
{
18461955
ImGui::PushID("Current");
1847-
draw_pixel_color(current_image(), wp.pixel, wp.color_mode_current, Target_Primary, true);
1956+
pixel_color_widget(wp.pixel, wp.color_mode.x, 0, true);
18481957
ImGui::SetItemTooltip("Pixel %s%d values in current channel.", ICON_MY_WATCHED_PIXEL, i + 1);
18491958
ImGui::PopID();
18501959

18511960
ImGui::PushID("Reference");
1852-
draw_pixel_color(reference_image(), wp.pixel, wp.color_mode_reference, Target_Secondary, true);
1961+
pixel_color_widget(wp.pixel, wp.color_mode.y, 1, true);
18531962
ImGui::SetItemTooltip("Pixel %s%d values in reference channel.", ICON_MY_WATCHED_PIXEL, i + 1);
18541963
ImGui::PopID();
18551964

1965+
ImGui::PushID("Composite");
1966+
pixel_color_widget(wp.pixel, wp.color_mode.z, 2, true);
1967+
ImGui::SetItemTooltip("Pixel %s%d values in composite.", ICON_MY_WATCHED_PIXEL, i + 1);
1968+
ImGui::PopID();
1969+
18561970
ImGui::Spacing();
18571971
}
18581972
ImGui::PopID();
@@ -2838,7 +2952,7 @@ void HDRViewApp::draw_background()
28382952
{
28392953
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
28402954
// add watched pixel
2841-
g_watched_pixels.emplace_back(WatchedPixel{int2{pixel_at_app_pos(io.MousePos)}, 0, 0});
2955+
g_watched_pixels.emplace_back(WatchedPixel{int2{pixel_at_app_pos(io.MousePos)}});
28422956
else if (ImGui::IsMouseDragging(ImGuiMouseButton_Left))
28432957
{
28442958
if (g_watched_pixels.size())

src/app.h

+2-7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class HDRViewApp
4747
void load_image(const string filename, const std::string_view buffer = std::string_view{});
4848
void load_url(const string_view url);
4949
void save_as(const string &filename) const;
50+
void export_as(const string &filename) const;
5051
void close_image();
5152
void close_all_images();
5253
//-----------------------------------------------------------------------------
@@ -133,13 +134,7 @@ class HDRViewApp
133134
void set_zoom_level(float l);
134135
//-----------------------------------------------------------------------------
135136

136-
float4 image_pixel(int2 pixel, Target target = Target_Primary) const
137-
{
138-
auto img = current_image();
139-
if (!img)
140-
return float4{0.f};
141-
return img->raw_pixel(pixel, target);
142-
}
137+
float4 pixel_value(int2 pixel, bool raw, int which_image) const;
143138

144139
// load font with the specified name at the specified size
145140
ImFont *font(const string &name, int size) const;

src/colorspace.h

+19
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,25 @@ inline Color4 tonemap(const Color4 color, float gamma, Tonemap tonemap_mode, Col
371371
}
372372
}
373373

374+
inline float4 blend(float4 top, float4 bottom, EBlendMode blend_mode)
375+
{
376+
float3 diff = top.xyz() - bottom.xyz();
377+
float alpha = top.w + bottom.w * (1.f - top.w);
378+
switch (blend_mode)
379+
{
380+
// case NORMAL_BLEND:
381+
default: return float4(top.xyz() + bottom.xyz() * (1.f - top.w), alpha);
382+
case MULTIPLY_BLEND: return float4(top.xyz() * bottom.xyz(), alpha);
383+
case DIVIDE_BLEND: return float4(top.xyz() / bottom.xyz(), alpha);
384+
case ADD_BLEND: return float4(top.xyz() + bottom.xyz(), alpha);
385+
case AVERAGE_BLEND: return 0.5f * (top + bottom);
386+
case SUBTRACT_BLEND: return float4(diff, alpha);
387+
case DIFFERENCE_BLEND: return float4(abs(diff), alpha);
388+
case RELATIVE_DIFFERENCE_BLEND: return float4(abs(diff) / (bottom.xyz() + float3(0.01f)), alpha);
389+
}
390+
return float4(0.f);
391+
}
392+
374393
const std::vector<std::string> &colorSpaceNames();
375394

376395
// assumes values of v are in byte range: [0, 255]

src/image.cpp

+3-4
Original file line numberDiff line numberDiff line change
@@ -881,9 +881,8 @@ float4 Image::raw_pixel(int2 p, Target target) const
881881
return value;
882882
}
883883

884-
// This implements a simplified CPU version of the glsl/metal fragment shaders used to render the image
885-
float4 Image::shaded_pixel(int2 p, Target target, float gain, float gamma, Tonemap tonemap_mode,
886-
Colormap_ colormap) const
884+
/// Reconstruct the raw pixel value into an RGBA value (like the first stage of the fragment shader)
885+
float4 Image::rgba_pixel(int2 p, Target target) const
887886
{
888887
if (!contains(p))
889888
return float4{0.f};
@@ -912,5 +911,5 @@ float4 Image::shaded_pixel(int2 p, Target target, float gain, float gamma, Tonem
912911
}
913912

914913
value.xyz() = mul(M_to_Rec709, value.xyz());
915-
return tonemap(float4{gain * value.xyz(), value.w}, gamma, tonemap_mode, colormap);
914+
return value;
916915
}

0 commit comments

Comments
 (0)