Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Grid resampling. Changes default loadpng image ordering #212

Merged
merged 8 commits into from
Jul 19, 2024
6 changes: 6 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ if(ARMADILLO_FOUND)
target_link_libraries(grid_image GLEW::GLEW)
endif()

add_executable(grid_resampled_image grid_resampled_image.cpp)
target_link_libraries(grid_resampled_image OpenGL::GL glfw Freetype::Freetype)
if(USE_GLEW)
target_link_libraries(grid_resampled_image GLEW::GLEW)
endif()

add_executable(grid_layout grid_layout.cpp)

add_executable(hsvwheel hsvwheel.cpp)
Expand Down
Binary file added examples/bike256_64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions examples/grid_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ int main()
// Load an image
std::string fn = "../examples/bike256_65.png";
morph::vvec<float> image_data_tlbr;
morph::vec<unsigned int, 2> dims = morph::loadpng (fn, image_data_tlbr);
morph::vec<unsigned int, 2> dims = morph::loadpng (fn, image_data_tlbr, morph::vec<bool, 2>{false,false});
morph::vvec<float> image_data_bltr;
dims = morph::loadpng (fn, image_data_bltr, morph::vec<bool, 2>({false,true}));
dims = morph::loadpng (fn, image_data_bltr);
std::cout << "Image dims: " << dims << std::endl;

// Now visualise with a GridVisual
Expand Down
102 changes: 102 additions & 0 deletions examples/grid_resampled_image.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include <iostream>
#include <vector>
#include <cmath>

#include <morph/Scale.h>
#include <morph/vec.h>
#include <morph/vvec.h>
#include <morph/loadpng.h>
#include <morph/Visual.h>
#include <morph/VisualDataModel.h>
#include <morph/Grid.h>
#include <morph/GridVisual.h>
#include <morph/HexGrid.h>
#include <morph/HexGridVisual.h>

int main()
{
morph::Visual v(1400, 1300, "Demo of Grid showing a resampled image");
v.setSceneTrans (morph::vec<float,3>({-2.60691f, 1.39885f, -11.1f}));

unsigned int scaledown = 2U;

morph::vec<float, 2> dx = { 0.02f, 0.02f };
morph::vec<float, 2> dx2 = dx * scaledown;
// Top left to bottom right order matches image loaded by loadpng and avoids the need for a
// vec<bool, 2> flip arg to morph::loadpng.
morph::Grid g1(256U, 64U, dx);
std::cout << "g1 extents: (xmin,xmax,ymin,ymax): " << g1.extents() << std::endl;

// Resample onto a lower res grid
morph::Grid g2(256U/scaledown, 64U/scaledown, dx2);
std::cout << "g2 extents: (xmin,xmax,ymin,ymax): " << g2.extents() << std::endl;

// Load an image
std::string fn = "../examples/bike256_64.png";

morph::vvec<float> image_data; // image loaded BLTR by default
morph::vec<unsigned int, 2> dims = morph::loadpng (fn, image_data);

std::cout << "Image dims: " << dims << std::endl;

if (dims[0] != 256) {
std::cerr << "Wrong image width!\n";
return -1;
}

// Resample
// This allows you to expand or contract the image on the Grid.
morph::vec<float,2> image_scale = {1.0f, 1.0f};
// You can shift the image with an offset if necessary. Use the units of the target Grid/HexGrid
morph::vec<float,2> image_offset = {0.0f, 0.0f};

morph::vvec<float> img_resampled = g2.resample_image (image_data, dims[0], image_scale, image_offset);

// Resample to HexGrid too, for good measure
morph::vec<float, 2> g2_dx = g2.get_dx();
morph::HexGrid hg(g2_dx[0], g2.width()*2.0f, 0.0f);
hg.setRectangularBoundary (g2.width(), g2.height()); // Make rectangular boundary same size as g2

// Here's the HexGrid method that will resample the square pixel grid onto the hex
// grid Because HexGrids are not naturally rectangular, a scaling of {1,1} does not
// expand the image to fit the HexGrid. Instead, we scale by the width of g2, which
// scales the interpreted image (which is interpreted as having width 1.0) to the
// correct dimensions.
morph::vec<float,2> hex_image_scale = { g2.width(), g2.width() };
morph::vvec<float> hex_image_data = hg.resampleImage (image_data, dims[0], hex_image_scale, image_offset);

// Visualise original with a GridVisual
auto gv1 = std::make_unique<morph::GridVisual<float>>(&g1, morph::vec<float>({0,0,0}));
v.bindmodel (gv1);
gv1->gridVisMode = morph::GridVisMode::RectInterp;
gv1->setScalarData (&image_data);
gv1->cm.setType (morph::ColourMapType::GreyscaleInv); // inverse greyscale is good for a monochrome image
gv1->zScale.setParams (0, 0); // As it's an image, we don't want relief, so set the zScale to have a zero gradient
gv1->addLabel ("Original", {0, -0.2, 0}, morph::TextFeatures(0.1f));
gv1->finalize();
v.addVisualModel (gv1);

// Visualize resampled
auto gv2 = std::make_unique<morph::GridVisual<float>>(&g2, morph::vec<float>{ 0.0f, -2.0f, 0.0f });
v.bindmodel (gv2);
gv2->gridVisMode = morph::GridVisMode::RectInterp;
gv2->setScalarData (&img_resampled);
gv2->cm.setType (morph::ColourMapType::GreyscaleInv);
gv2->zScale.setParams (0, 0);
gv2->addLabel ("Resampled to coarser Grid", {0, -0.2, 0}, morph::TextFeatures(0.1f));
gv2->finalize();
v.addVisualModel (gv2);

auto hgv = std::make_unique<morph::HexGridVisual<float>>(&hg, morph::vec<float>{ g2.width() / 2.0f, g2.height() / 2.0f - 4.0f , 0.0f });
v.bindmodel (hgv);
hgv->setScalarData (&hex_image_data);
hgv->cm.setType (morph::ColourMapType::GreyscaleInv);
hgv->zScale.setParams (0, 0);
hgv->addLabel ("Resampled to HexGrid", {-g2.width() / 2.0f, -0.2f - g2.height() / 2.0f, 0}, morph::TextFeatures(0.1f));
hgv->finalize();
v.addVisualModel (hgv);

v.keepOpen();

return 0;
}
4 changes: 2 additions & 2 deletions examples/gridct_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ int main()
// Load an image
std::string fn = "../examples/bike256_65.png";
morph::vvec<float> image_data_tlbr;
morph::vec<unsigned int, 2> dims = morph::loadpng (fn, image_data_tlbr);
morph::vec<unsigned int, 2> dims = morph::loadpng (fn, image_data_tlbr, morph::vec<bool, 2>{false,false});
morph::vvec<float> image_data_bltr;
dims = morph::loadpng (fn, image_data_bltr, morph::vec<bool, 2>({false,true}));
dims = morph::loadpng (fn, image_data_bltr);
std::cout << "Image dims: " << dims << std::endl;

// Now visualise with a GridVisual
Expand Down
2 changes: 1 addition & 1 deletion examples/hexgrid_image_rect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ int main()
morph::Visual v(1600, 1000, "Demo of HexGrid::resampleImage");

morph::HexGrid hg(0.01f, 3.0f, 0.0f);
hg.setCircularBoundary (1.2f);
hg.setRectangularBoundary (2.0f, 0.5f);

// Load a rectangular image with the help of morph::loadpng().
std::string fn = "../examples/bike256_65.png";
Expand Down
77 changes: 73 additions & 4 deletions morph/Grid.h
Original file line number Diff line number Diff line change
Expand Up @@ -501,18 +501,18 @@ namespace morph {
* left-most pixel to the right edge of the right-most pixel? It could be either, so I
* provide width() and width_of_pixels() as well as height() and height_of_pixels().
*/
C width() const { return dx[I{0}] * w; }
C width() const { return dx[I{0}] * (w - I{1}); }

//! Return the width of the grid if drawn as pixels
C width_of_pixels() const { return dx[I{0}] * w + dx[I{0}]; }
C width_of_pixels() const { return dx[I{0}] * w; }

//! Return the distance from the centre of the bottom row to the centre of the top row
C height() const { return dx[I{1}] * h; }
C height() const { return dx[I{1}] * (h - I{1}); }

C area() const { return this->width() * this->height(); }

//! Return the height of the grid if drawn as pixels
C height_of_pixels() const { return dx[I{1}] * h + dx[I{1}]; }
C height_of_pixels() const { return dx[I{1}] * h; }

//! Return the area of the grid, if drawn as pixels
C area_of_pixels() const { return this->width_of_pixels() * this->height_of_pixels(); }
Expand Down Expand Up @@ -587,6 +587,75 @@ namespace morph {
return index < n ? index / h : std::numeric_limits<I>::max();
}

/*!
* Resampling function (monochrome).
*
* \param image_data (input) The monochrome image as a vvec of floats. The image
* is interpreted as running from bottom left to top right (matching the default
* value of Grid::order). Thus, the very first float in the vvec is at x=0,
* y=0. The image width is normalized to 1.0. The height of the image is
* computed from this assumption and on the assumption that pixels are square.
*
* \param image_pixelwidth (input) The number of pixels that the image is wide
* \param image_scale (input) The size that the image should be resampled to (same units as Grid)
* \param image_offset (input) An offset in Grid units to shift the image wrt to the Grid's origin
* \param sigma (input) The sigma for the 2D resampling Gaussian
*
* \return A new data vvec containing the resampled (and renormalised) hex pixel values
*/
morph::vvec<float> resample_image (const morph::vvec<float>& image_data,
const unsigned int image_pixelwidth,
const morph::vec<float, 2>& image_scale,
const morph::vec<float, 2>& image_offset) const
{
if (this->order != morph::GridOrder::bottomleft_to_topright) {
throw std::runtime_error ("Grid::resample_image: resampling assumes image has morph::GridOrder::bottomleft_to_topright, so your Grid should, too.");
}

unsigned int csz = image_data.size();
morph::vec<unsigned int, 2> image_pixelsz = {image_pixelwidth, csz / image_pixelwidth};
std::cout << "image_pixelsz = " << image_pixelsz << " csz: " << csz << std::endl;

// Before scaling, image assumed to have width 1, height whatever
morph::vec<float, 2> image_dims = { 1.0f, 0.0f };
image_dims[1] = 1.0f / (image_pixelsz[0] - 1u) * (image_pixelsz[1] - 1u);
// Now scale the image dims to have the same width as *this:
image_dims *= this->width();
// Then apply any manual scaling requested:
image_dims *= image_scale;

// Distance per pixel in the image. This defines the Gaussian width (sigma) for the
// resample. Compute this from the image dimensions, assuming pixels are square
morph::vec<float, 2> dist_per_pix = image_dims / (image_pixelsz - 1u);

// Parameters for the Gaussian computation
morph::vec<float, 2> params = 1.0f / (2.0f * dist_per_pix * dist_per_pix);
morph::vec<float, 2> threesig = 3.0f * dist_per_pix;

morph::vvec<float> expr_resampled(this->w * this->h, 0.0f);
#pragma omp parallel for // parallel on this outer loop gives best result (5.8 s vs 7 s)
for (typename std::vector<float>::size_type xi = 0u; xi < this->v_x.size(); ++xi) {
float expr = 0.0f;
for (unsigned int i = 0; i < csz; ++i) {
// Get x/y pixel coords:
morph::vec<unsigned int, 2> idx = {(i % image_pixelsz[0]), (i / image_pixelsz[0])};
// Get the coordinates of the input pixel at index idx (in target units):
morph::vec<float, 2> posn = (dist_per_pix * idx) + image_offset;
// Distance from input pixel to output pixel:
float _v_x = this->v_x[xi] - posn[0];
float _v_y = this->v_y[xi] - posn[1];
// Compute contributions to each Grid pixel, using 2D (elliptical) Gaussian
if (_v_x < threesig[0] && _v_y < threesig[1]) { // Testing for distance gives slight speedup
expr += std::exp ( - ( (params[0] * _v_x * _v_x) + (params[1] * _v_y * _v_y) ) ) * image_data[i];
}
}
expr_resampled[xi] = expr;
}

expr_resampled /= expr_resampled.max(); // renormalise result
return expr_resampled;
}

//! Two vector structures that contains the coords for this grid. v_x is a vector of the x coordinates
morph::vvec<C> v_x;
//! v_y is a vector of the y coordinates
Expand Down
8 changes: 4 additions & 4 deletions morph/Gridct.h
Original file line number Diff line number Diff line change
Expand Up @@ -327,18 +327,18 @@ namespace morph {
* left-most pixel to the right edge of the right-most pixel? It could be either, so I
* provide width() and width_of_pixels() as well as height() and height_of_pixels().
*/
constexpr C width() const { return dx[0] * w; }
constexpr C width() const { return dx[0] * (w - I{1}); }

//! Return the width of the grid if drawn as pixels
constexpr C width_of_pixels() const { return dx[0] * w + dx[0]; }
constexpr C width_of_pixels() const { return dx[0] * w; }

//! Return the distance from the centre of the bottom row to the centre of the top row
constexpr C height() const { return dx[1] * h; }
constexpr C height() const { return dx[1] * (h - I{1}); }

constexpr C area() const { return this->width() * this->height(); }

//! Return the height of the grid if drawn as pixels
constexpr C height_of_pixels() const { return dx[1] * h + dx[1]; }
constexpr C height_of_pixels() const { return dx[1] * h; }

//! Return the area of the grid, if drawn as pixels
constexpr C area_of_pixels() const { return this->width_of_pixels() * this->height_of_pixels(); }
Expand Down
18 changes: 10 additions & 8 deletions morph/HexGrid.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ namespace morph {
* bottom? Set this to a larger number if the boundary is expected to grow
* during a simulation.
*/
unsigned int d_growthbuffer_horz = 5;
unsigned int d_growthbuffer_horz = 0;
unsigned int d_growthbuffer_vert = 0;

//! Add entries to all the d_ vectors for the Hex pointed to by hi.
Expand Down Expand Up @@ -1444,11 +1444,13 @@ namespace morph {
/*!
* Resampling function (monochrome).
*
* \param image_data (input) The monochrome image as a vvec of floats.
* \param image_data (input) The monochrome image as a vvec of floats. The
* image is interpreted as running from bottom left to top right. Thus, the very
* first float in the vvec is at x=0, y=0.
*
* \param image_pixelwidth (input) The number of pixels that the image is wide
* \param image_scale (input) The size that the image should be resampled to (same units as HexGrid)
* \param image_offset (input) An offset in HexGrid units to shift the image wrt to the HexGrid's origin
* \param sigma (input) The sigma for the 2D resampling Gaussian
*
* \return A new data vvec containing the resampled (and renormalised) hex pixel values
*/
Expand All @@ -1463,7 +1465,7 @@ namespace morph {
// Distance per pixel in the image. This defines the Gaussian width (sigma) for the
// resample. Assume that the unscaled image pixels are square. Use the image width to
// set the distance per pixel (hence divide by image_scale by image_pixelsz[*0*]).
morph::vec<float, 2> dist_per_pix = image_scale / (image_pixelsz[0]-1u);
morph::vec<float, 2> dist_per_pix = image_scale / (image_pixelsz[0] - 1u);
// This is an offset to centre the image wrt to the HexGrid
morph::vec<float, 2> input_centering_offset = dist_per_pix * image_pixelsz * 0.5f;
// Parameters for the Gaussian computation
Expand All @@ -1474,10 +1476,9 @@ namespace morph {
#pragma omp parallel for // parallel on this outer loop gives best result (5.8 s vs 7 s)
for (typename std::vector<float>::size_type xi = 0u; xi < this->d_x.size(); ++xi) {
float expr = 0.0f;
//#pragma omp parallel for reduction(+:expr)
for (unsigned int i = 0; i < csz; ++i) {
// Get x/y pixel coords:
morph::vec<unsigned int, 2> idx = {(i % image_pixelsz[0]), (image_pixelsz[1] - (i / image_pixelsz[0]))};
morph::vec<unsigned int, 2> idx = {(i % image_pixelsz[0]), (i / image_pixelsz[0])};
// Get the coordinates of the pixel at index i (in HexGrid units):
morph::vec<float, 2> posn = (dist_per_pix * idx) - input_centering_offset + image_offset;
// Distance from input pixel to output hex:
Expand Down Expand Up @@ -3938,12 +3939,13 @@ namespace morph {
rtn[2] = (int)(limits[2] / d_gi);
rtn[3] = (int)(limits[3] / d_gi);

// Add 'growth buffer'
#if 0
// Add any 'growth buffer', in case boundary expected to change? I think this is old thinking and maybe not relevant?
rtn[0] -= this->d_growthbuffer_horz;
rtn[1] += this->d_growthbuffer_horz;
rtn[2] -= this->d_growthbuffer_vert;
rtn[3] += this->d_growthbuffer_vert;

#endif
return rtn;
}

Expand Down
13 changes: 8 additions & 5 deletions morph/loadpng.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ namespace morph {
*
* If flip[1] is true, then flip the order of the rows to do an up/down flip of the
* image during loading.
*
* Note: The default for flip is {false, true}, which means that by default,
* image_data will be filled in a bottom-left to top-right order.
*/
template <typename T>
static morph::vec<unsigned int, 2> loadpng (const std::string& filename, morph::vvec<T>& image_data,
const morph::vec<bool,2> flip = {false, false})
const morph::vec<bool,2> flip = {false, true})
{
std::vector<unsigned char> png;
unsigned int w = 0;
Expand Down Expand Up @@ -114,7 +117,7 @@ namespace morph {
template <typename T, std::size_t N>
static morph::vec<unsigned int, 2> loadpng (const std::string& filename,
morph::vvec<morph::vec<T, N>>& image_data,
const morph::vec<bool,2> flip = {false, false})
const morph::vec<bool,2> flip = {false, true})
{
std::vector<unsigned char> png;
unsigned int w = 0;
Expand Down Expand Up @@ -196,7 +199,7 @@ namespace morph {
// Load a colour PNG and return a vector of type T with elements ordered as RGBRGBRGB...
template <typename T>
static morph::vec<unsigned int, 2> loadpng_rgb (const std::string& filename, morph::vvec<T>& image_data,
const morph::vec<bool,2> flip = {false, false})
const morph::vec<bool,2> flip = {false, true})
{
std::vector<unsigned char> png;
unsigned int w = 0;
Expand Down Expand Up @@ -261,7 +264,7 @@ namespace morph {
// Load a colour PNG and return a vector of type T with elements ordered as RGBARGBARGBA...
template <typename T>
static morph::vec<unsigned int, 2> loadpng_rgba (const std::string& filename, morph::vvec<T>& image_data,
const morph::vec<bool,2> flip = {false, false})
const morph::vec<bool,2> flip = {false, true})
{
std::vector<unsigned char> png;
unsigned int w = 0;
Expand Down Expand Up @@ -329,7 +332,7 @@ namespace morph {
// Load a colour PNG and return a vector of type T with elements ordered as RGBARGBARGBA...
template <typename T, unsigned int im_w, unsigned int im_h>
static morph::vec<unsigned int, 2> loadpng_rgba (const std::string& filename, morph::vec<T, 4*im_w*im_h>& image_data,
const morph::vec<bool,2> flip = {false, false})
const morph::vec<bool,2> flip = {false, true})
{
std::vector<unsigned char> png;
unsigned int w = 0;
Expand Down
Loading