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

making a stab at generated images by bending the null image plugin #4197

Open
kfjahnke opened this issue Mar 23, 2024 · 3 comments
Open

making a stab at generated images by bending the null image plugin #4197

kfjahnke opened this issue Mar 23, 2024 · 3 comments

Comments

@kfjahnke
Copy link

Is your feature request related to a problem? Please describe.

I was looking for 'an easy way in' to introduce arbitrary generated image content with minimal coding effort. My problem was that I have such content and I did not find a way to make OIIO 'suck it in'.

Describe the solution you'd like

I want this capability to 'look like' an image, in order to make it 'palatable' for the higher OIIO functions.

Describe alternatives you've considered

I considered writing a specific plugin, but to flesh out a plugin for the purpose requires considerable coding effort. I looked at the extant plugins, and I found a candidate which can easily be bent to the task: the 'null' image format. While this provides a nice infrastructure, it's core functionality is very simple: colour pixels with some default colour. If, instead, it can be made to colour pixels by calling a user-provided function, it becomes much more versatile and can do the job I want done perfectly.

I have coded a proof of concept. First, I introduced a header 'feeder.h' into the OpenImageIO includes. It looks like this:

#include <map>

// to make generated images easily accessible, we introduce a new
// mode to 'null' images which obtains pixel data by calling a 'feeder'.
// The feeder has a member function 'feed' which receives a pointer to
// the pixel's memory it's supposed to feed into, plus the number of
// bytes it's supposed to fill, and the x, y, and z coordinate of that
// pixel. The feeder has a specific function to satisfy the request,
// resulting in the 'null' image being 'painted' by the feeder instead
// of being initialized with a given constant value. With this method,
// we can introduce arbitrary generated image content with minimal
// coding effort - we simply 'piggyback' on the extant null plugin
// end use it's infrastructure.
// feeders are initialized with a std::function like these:

bool feed_not ( void* trg , int nbytes , int x , int y , int z )
{
  return true ;
}

bool feed_grey ( void* trg , int nbytes , int x , int y , int z )
{
  memset ( trg , 127 , nbytes ) ;
  return true ;
}

bool feed_gradient ( void* trg , int nbytes , int x , int y , int z )
{
  auto * pc = (unsigned char*) trg ;
  pc[0] = x ;
  pc[1] = y ;
  pc[2] = 0 ;
  return true ;
}

// struct feeder_t contains - as of yet - only a single std::fuction
// of the type feed_f - like those above

struct feeder_t
{
  typedef std::function < bool ( void* , // target memory
                                 int ,   // bytes per pixel
                                 int ,   // x
                                 int ,   // y
                                 int     // z
                               ) > feed_f ;

  feed_f feed ;

  feeder_t ( const feed_f & _feed = feed_not )
  : feed ( _feed )
  { }
} ;

// let's set up two 'stock' feeders

feeder_t black_feeder ( feed_grey ) ;
feeder_t gradient_feeder ( feed_gradient ) ;

// finally, we set up a std::map, which provides feeders by name.
// we'll use this map from the plugin code to look up feeders which
// are passed in as part of the REST parameters. This way we can
// do two things:
// - we can add (or remove) feeders anytime
// - we can prescribe their use by passing a FEED=... REST parameter
// The use of the feeder isn't very fast, because it actually walks
// through all the pixels in a nested loop and calls a rather bulky
// function to deposit it's data, but if the image is read once and
// later on the data from the read are used, this is not an issue:
// the slow code is only executed when reading the image.

std::map < std::string , feeder_t * >
  feed_store { { "feed_grey" , & black_feeder } ,
               { "feed_gradient" , & gradient_feeder }
             } ;

The changes to the plugin are small - rather than pasting the entire code here, I just give the diff, I'm sure you'll get my drift:

~/src/OpenImageIO$ git diff
diff --git a/src/null.imageio/nullimageio.cpp b/src/null.imageio/nullimageio.cpp
index 1fce00363..5d4825e82 100644
--- a/src/null.imageio/nullimageio.cpp
+++ b/src/null.imageio/nullimageio.cpp
@@ -10,6 +10,7 @@
 #include <OpenImageIO/filesystem.h>
 #include <OpenImageIO/imageio.h>
 #include <OpenImageIO/strutil.h>
+#include <OpenImageIO/feeder.h>
 
 #include "imageio_pvt.h"
 
@@ -74,6 +75,7 @@ private:
     int m_subimage;                ///< What subimage are we looking at?
     int m_miplevel;                ///< What miplevel are we looking at?
     bool m_mip;                    ///< MIP-mapped?
+    feeder_t * m_feed;
     std::vector<uint8_t> m_value;  ///< Pixel value (if not black)
     ImageSpec m_topspec;
 
@@ -84,6 +86,7 @@ private:
         m_miplevel = -1;
         m_mip      = false;
         m_value.clear();
+        m_feed = nullptr ;
     }
 };
 
@@ -311,6 +314,15 @@ NullInput::open(const std::string& name, ImageSpec& newspec,
         } else if (a.first == "PIXEL") {
             Strutil::extract_from_list_string(fvalue, a.second);
             fvalue.resize(m_topspec.nchannels);
+        } else if (a.first == "FEED") {
+            auto feed = feed_store.find ( std::string ( a.second ) ) ;
+            if ( feed == feed_store.end() ) {
+              std::cout << "can't find feed " << a.second << " in store"
+                        << std::endl ;
+              m_feed = nullptr ;
+            } else {
+              m_feed = feed->second ;
+            }
         } else if (a.first.size() && a.second.size()) {
             parse_param(a.first, a.second, m_topspec);
         }
@@ -362,10 +374,14 @@ NullInput::seek_subimage(int subimage, int miplevel)
 
 
 bool
-NullInput::read_native_scanline(int /*subimage*/, int /*miplevel*/, int /*y*/,
-                                int /*z*/, void* data)
+NullInput::read_native_scanline(int /*subimage*/, int /*miplevel*/, int y,
+                                int z, void* data)
 {
-    if (m_value.size()) {
+    if ( m_feed ) {
+        size_t s = m_spec.pixel_bytes();
+        for (size_t x = 0, e = m_spec.width; x < e; ++x)
+            m_feed->feed ( (unsigned char*)data + s * x , s , x , y , z ) ;
+    } else if (m_value.size()) {
         size_t s = m_spec.pixel_bytes();
         for (int x = 0; x < m_spec.width; ++x)
             memcpy((char*)data + s * x, m_value.data(), s);
@@ -378,10 +394,23 @@ NullInput::read_native_scanline(int /*subimage*/, int /*miplevel*/, int /*y*/,
 
 
 bool
-NullInput::read_native_tile(int /*subimage*/, int /*miplevel*/, int /*x*/,
-                            int /*y*/, int /*z*/, void* data)
+NullInput::read_native_tile(int /*subimage*/, int /*miplevel*/, int x,
+                            int y, int z, void* data)
 {
-    if (m_value.size()) {
+    if ( m_feed ) {
+      auto w = m_topspec.tile_width ;
+      auto h = m_topspec.tile_height ;
+      auto xx = ( x / w ) * w ;
+      auto yy = ( y / h ) * h ;
+      size_t s = m_spec.pixel_bytes();
+      size_t ofs = 0 ;
+      for (size_t v = 0 ; v < h ; ++v ) {
+        for ( size_t u = 0 ; u < w ; ++u , ofs += s ) {
+          m_feed->feed ( (unsigned char*)data + ofs , s ,
+                         u + xx , v + yy , z ) ;
+        }
+      }
+    } else if (m_value.size()) {
         size_t s = m_spec.pixel_bytes();
         for (size_t x = 0, e = m_spec.tile_pixels(); x < e; ++x)
             memcpy((char*)data + s * x, m_value.data(), s);

To use the 'feeder', it can be specified in the REST parameters, passing an image 'filename' like this:
foo.null?RES=640x480&CHANNELS=3&FEED=feed_gradient&TYPE=uint8

@lgritz
Copy link
Collaborator

lgritz commented Mar 23, 2024

You can already do this, as you suspect, by writing a custom ImageInput plugin that does whatever you want to supply the pixels when it's asked for read_tile(). (I recommend making it look like it's tiled, rather then scanline.)

Here's one such implementation, found in the OSL project, the makes an ImageInput that, instead of reading files, runs OSL shaders to generate the pixel values: https://github.com/AcademySoftwareFoundation/OpenShadingLanguage/blob/main/src/osl.imageio/oslinput.cpp

I guess what you're suggesting here is that we make a procedural ImageInput like this, as part of the OIIO project, but instead of coding a specific procedural pattern, we allow it to take a configuration parameter that's a function pointer that supplies the value of one pixel. The point would be that somebody could then use it by ONLY supplying the function pointer to the "give me one pixel" (or "give me one tile") function instead of needing to bother setting up the rest of the ImageInput facade.

@lgritz
Copy link
Collaborator

lgritz commented Mar 23, 2024

🎣 Mackerel!

@kfjahnke
Copy link
Author

As I said - I wanted something with minimal coding effort, short of writing a custom plugin. I feel the null image plugin is underused - the extra functionality takes nothing away from it, but instead brings out it's potential. I suppose, though, that I might as well copy and paste from other code which already implements a similar plugin, so thanks for the link!

I wrote my slot-in code to serve both requests for scanlines and tiles - since the generator is pixel-based, this is simple, the read_native_tile and read_native_scanline both call the per-pixel code and user code can choose which interface is preferred, relying on the extant infrastructure code.

Using the std::map to introduce the std::function can serve two purposes: one might set up a set of 'stock' objects providing common generated images and at the same time user code can add functions at runtime which are found in the map via their name. Since the REST API code is already there, one might even consider passing stuff through to the plugin.

Ha! mackerel... the last mackerel you hung out to attract contributors hasn't yielded anything yet. Let's see if anyone bites now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants