Design of image & view classes?

J

Jef Driesen

I'm implementing some image processing algorithms in C++. I created a
class called 'image' (see declaration below), that will take care of the
memory allocations and some basic (mathematical) stuff. The class will
behave like a std::vector (copy constructor and assignment create a deep
copy), but with 2D indexing.

Now I also need a 'view' class that will behave like a reference to an
'image' (can only be constructed from an existing image, copy
constructor creates a shallow copy and assignment operator a deep copy).
And it should have the additional property that it may contain only a
portion of the original image. I will show some examples to make clear
what I need:

// For images:
image<int> ima, imb(512,512), imc = imb; // Create some images
point p(3,3); // Create the point (3,3)
ima = 0; // Set all pixels to zero
ima = imb; // Copy all pixels
ima += 64; // Elementwise mathematical operators (+,-,* and /)
ima += imb; // Elementwise mathematical operators (+,-,* and /)
ima(0,0) = 255; // Indexing
ima(p) = 128; // Indexing with point

// For views:
range r(0,256,1) // Create the range [0,256[ with stride 1
view<int> va = ima(r,r), vb = imb(r,r); // Create subimages
view<int> vc = vb; // Create a shallow copy
va = 0; // Set all pixels (in the subimage) to zero
va = vb; // Copy all pixels (in the subimages)
.... // Indexing, mathematical operators

// For mixed images and views:
view<int> vd = ima; // Create a shallow copy
image<int> imd = vd; // Create a deep copy
ima = vb; vb = ima; // Copy all pixels

I created my first view class (see declaration below). The class does
not allocate or deallocate memory, it only keeps a pointer to the memory
obtained from the source image. But there are some problems:
* There are no conversions possible from image <--> view. I think I can
solve this with two additional constructors image(const view<T>& rhs)
and view(const image<T>& rhs)? And also for the other member functions?
* If the original image is destroyed or new memory is allocated
(resizing, assignment with different size), all associated views will
now point to invalid memory. It is also not possible to return a view
from a function, if it was created from a local image. This would be
usefull to prevent unnecessary copying, since copying an image object is
expensive, while copying a view is not.
* And the most important: I will have to duplicate every algorithm to
work on both images and views. Or even more code for mixed cases. Maybe
I could solve this by deriving the view class from the image class and
making all member functions virtual? But won't this approach slow down
the trivial functions, like operator(), due to the overhead of a virtual
function call? And these functions will be called very frequently in loops.

Any suggestions to solve these problems or to improve my classes are
welcome. Maybe there is already an existing framework that has this
functionality? Keep in mind that I will need 3D planar images as well
(extra template parameter N?).


template <typename T>
class image {
public: // Typedefs
typedef T value_type;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
protected: // Data members
value_type *m_data;
size_type m_datasize;
size_type m_rows, m_columns;
public:
// Constructors and destructor.
image();
image(size_type m, size_type n);
image(size_type m, size_type n, const value_type& init);
image(const image<T>& rhs);
~image();
// Assignment operators.
image<T>& operator=(const image<T>& rhs);
image<T>& operator=(const value_type& rhs);
// Indexing operators.
value_type& operator()(size_type i, size_type j);
value_type& operator()(point p);
view<T> operator()(range i, range j);
const value_type& operator()(size_type i, size_type j) const;
const value_type& operator()(point p) const;
const view<T> operator()(range i, range j) const;
// Size functions.
size_type size() const;
size_type rows() const;
size_type columns() const;
// Elementwise mathematical operators
image<T>& operator+=(const image<T>& rhs);
image<T>& operator-=(const image<T>& rhs);
image<T>& operator*=(const image<T>& rhs);
image<T>& operator/=(const image<T>& rhs);
image<T>& operator+=(const value_type& rhs);
image<T>& operator-=(const value_type& rhs);
image<T>& operator*=(const value_type& rhs);
image<T>& operator/=(const value_type& rhs);
};

template <typename T>
class view {
public: // Typedefs
typedef T value_type;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
protected: // Data members
value_type *m_origin;
size_type m_rows, m_columns;
difference_type m_rstride, m_cstride;
public:
// Constructors and destructor.
view(const view<T>& rhs);
~view();
// Assignment operators.
view<T>& operator=(const view<T>& rhs);
view<T>& operator=(const value_type& rhs);
// Indexing operators.
value_type& operator()(size_type i, size_type j);
value_type& operator()(point p);
view<T> operator()(range i, range j);
const value_type& operator()(size_type i, size_type j) const;
const value_type& operator()(point p) const;
const view<T> operator()(range i, range j) const;
// Size functions.
size_type size() const;
size_type rows() const;
size_type columns() const;
// Elementwise mathematical operators
view<T>& operator+=(const view<T>& rhs);
view<T>& operator-=(const view<T>& rhs);
view<T>& operator*=(const view<T>& rhs);
view<T>& operator/=(const view<T>& rhs);
view<T>& operator+=(const value_type& rhs);
view<T>& operator-=(const value_type& rhs);
view<T>& operator*=(const value_type& rhs);
view<T>& operator/=(const value_type& rhs);
};
 
G

Gernot Frisch

* There are no conversions possible from image said:
can solve this with two additional constructors image(const view<T>&
rhs) and view(const image<T>& rhs)? And also for the other member
functions?

class view;
class image
{
public:
view& operator(view); // implement in .cpp file
}

* If the original image is destroyed or new memory is allocated
(resizing, assignment with different size), all associated views
will now point to invalid memory. It is also not possible to return
a view from a function, if it was created from a local image. This
would be usefull to prevent unnecessary copying, since copying an
image object is expensive, while copying a view is not.

Do not store pointers to the image's memory, but provide a function
that returns a pointer to the memory - temporarily.
* And the most important: I will have to duplicate every algorithm
to work on both images and views. Or even more code for mixed cases.
Maybe I could solve this by deriving the view class from the image
class and making all member functions virtual? But won't this
approach slow down the trivial functions, like operator(), due to
the overhead of a virtual function call? And these functions will be
called very frequently in loops.

why not deriving both from a class: image_base ?
-Gernot
 
J

Jef Driesen

Gernot said:
class view;
class image
{
public:
view& operator(view); // implement in .cpp file
}

Is this approach better then providing the additional constructors? It
is probably a typo, but shouldn't this operator return by value instead
of by reference?
Do not store pointers to the image's memory, but provide a function
that returns a pointer to the memory - temporarily.

That would solve the resizing. But it still won't work if the original
image is destroyed. And to make this work, the view should contain a
reference (or pointer) to the original image. But this will prevent me
from creating a 2D view from a 3D image/view. Currently I'm thinking
about some reference counting mechanism.
why not deriving both from a class: image_base ?

I don't see how this could help. To be usefull, every common member
function (indexing, mathematical operators,...) should be implemented in
the image_base class. But how does this class know if it should access
the memory continuous (image) or with strides (view)? Or am I wrong?
Could you provide a small example?
 
G

Gernot Frisch

class view;
class image
{
public:
view& operator(view); // implement in .cpp file
void AddView(view* pView);
void DeleteView(view* pView) {pView->pImage=NULL;}
view& GetView(int index);
private:
int num_views;
view* pViews;
}

class view
{
public:
view(image* pImg) {pImage=pImg; pImg->AddView(this);}
bool IsValidView() {return pImage ? true:false;}
image* GetAttachedImage() {return pImage;}
void SetView(int x, int y, int width, int height);
image GetViewedImage(); // image in range x,y,width, height
image operator(image){return GetViewedImage();}

private:
image* pImage;
}

image img1, img2;
view vw1(img1);


You have an image, and can have attached a set of views. A view can
only exist with an image (See IsValid function of view).
An image can exist without a view.

Is this what you want? Don't I understand you correctly?
 
J

Jef Driesen

Gernot said:
class view;
class image
{
public:
view& operator(view); // implement in .cpp file
void AddView(view* pView);
void DeleteView(view* pView) {pView->pImage=NULL;}
view& GetView(int index);
private:
int num_views;
view* pViews;
}

class view
{
public:
view(image* pImg) {pImage=pImg; pImg->AddView(this);}
bool IsValidView() {return pImage ? true:false;}
image* GetAttachedImage() {return pImage;}
void SetView(int x, int y, int width, int height);
image GetViewedImage(); // image in range x,y,width, height
image operator(image){return GetViewedImage();}

private:
image* pImage;
}

image img1, img2;
view vw1(img1);


You have an image, and can have attached a set of views. A view can
only exist with an image (See IsValid function of view).
An image can exist without a view.

Is this what you want? Don't I understand you correctly?

This is more or less correct. An image contains its own (continuous)
memory and has some member functions to manipulate this memory. A view
is used to look at the memory of this image in a different way (e.g.
subimage), or the same way. The view should have the same member
functions as the image, but they should operate on the data in the view.
Only the copy constructor of the view should behave different (not the
underlying data, but the way of viewing should be copied).

An example for a small 4 x 4 image that look like this:

1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4

// Create 2 subimages of size 2x2
range ra(0,2),rb(2,4);
view va = img(ra,ra); // Upper left part
view vb = img(rb,rb); // Lower right part
view vc = vb; // Lower right part

va = 0;
// Original image will now contain:
0 0 2 2
0 0 2 2
3 3 4 4
3 3 4 4

va = vb;
// Original image will now contain:
4 4 2 2
4 4 2 2
3 3 4 4
3 3 4 4

img(0,0) = 0;
img(1,1) = 0;
vc(0,0) = 1;
vc(1,1) = 1;
// Original image will now contain:
0 4 2 2
4 0 2 2
3 3 1 4
3 3 4 1
 
G

Gernot Frisch

This is more or less correct. An image contains its own (continuous)
memory and has some member functions to manipulate this memory. A
view is used to look at the memory of this image in a different way
(e.g. subimage), or the same way. The view should have the same
member functions as the image, but they should operate on the data
in the view. Only the copy constructor of the view should behave
different (not the underlying data, but the way of viewing should be
copied).

So, implement the image class's functions. Then do:

((image)a_view).call_image_function();

That's what you want, no? You only have to code stuff once for the
image, if the view has an image cast operator.
-Gernot
 
J

Jef Driesen

Gernot said:
So, implement the image class's functions. Then do:

((image)a_view).call_image_function();

That's what you want, no? You only have to code stuff once for the
image, if the view has an image cast operator.

This won't work, because casting the view to an image should create a
new image (with its own memory). Applying the call_image_function on
this new image will not modify the memory of the original image anymore.

My small example image again:

image ima =
1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4

// Create a view to the upper left part
view v = img(range(0,2),range(0,2));
// Cast view to a new image
image imb = v;
// The content of imb should be a real copy
// of the upper left part of the original image.
// 1 1
// 1 1
// Changing imb should leave the original image intact.
imb = 0;
// Results in
imb =
0 0
0 0
ima =
1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4
 
G

Gernot Frisch

Jef Driesen said:
This won't work, because casting the view to an image should create
a new image (with its own memory). Applying the call_image_function
on this new image will not modify the memory of the original image
anymore.

OK. Then make the image be accessable through a view only. If you want
to operate on the image directly, cast the image to a view and use the
view's functions (fullsize) for manipulating the pixels.
-Gernot
 
J

Jef Driesen

Gernot said:
OK. Then make the image be accessable through a view only. If you want
to operate on the image directly, cast the image to a view and use the
view's functions (fullsize) for manipulating the pixels.

This works, but the downside is that accessing pixels in a view is
slower compared to pixels in an image. For example operator= is
implemented like this for an image and a view:

image& image::eek:perator=(const value_type& rhs)
{
std::fill(m_data, m_data + m_datasize, rhs);
return *this;
}

view& view::eek:perator=(const value_type& rhs)
{
for (size_type i = 0; i < m_rows; ++i) {
value_type *p = m_origin + i * m_rstride;
for (size_type j = 0; j < m_columns; ++j) {
*p = rhs; p += m_cstride;
}
}
return *this;
}

This way, there is almost no reason anymore for the image class to
exist. The only difference that will remain with this approach is the
copy constructor (shallow vs deep copy).
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,777
Messages
2,569,604
Members
45,230
Latest member
LifeBoostCBD

Latest Threads

Top