diff --git a/README.md b/README.md index 46f314a..dc793d7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,109 @@ # python-FloatImg -A python binding to FloatImg library available at https://git.tetalab.org/tTh/FloatImg \ No newline at end of file +A pythonesque binding to FloatImg library available at https://git.tetalab.org/tTh/FloatImg + +python-FloatImg requires a quite recent Python 3 implementation (tested with 3.6.9). + +## Installation + +python-FloatImg requires a shared object file (.so) that is not yet officialy available from FloatImg. + +The file `floatimg/settings.py` contains the path to the .so file. + +## Usage + +The FloatImg class encapsulate core and file functionnalities of FloatImg library. Import if from the main module: + +```python +from floatimg import FloatImg +``` + +### Image creation + +`FloatImg` has two class methods to create an image. + +```python +img = FloatImg.create(640, 480, FloatImg.RGB) +``` +Or use the `create_rgb` shortcut: + +```python +img = FloatImg.create_rgb(640, 480) +``` + +Note that calling the `destroy` method of the instance is not necessary as it will be called automatically at instance destruction. + +Or completely duplicate an existing image: + +```python +new_img = img.clone() +``` + +### Image manipulation + +#### Clear image + +```python +img.clear() +``` + +#### Copy pixels to another image + +```python +img.copy_data(another_img) +``` + +#### Set pixel value + +```python +img.put_rgb(x, y, (r, g, b)) +``` + +#### Get pixel value + +```python +r, g, b = img.get_rgb(x, y) +``` + +### File manipulation + +#### Dump image to file + +```python +img.dump_to_file("/tmp/image_dump") +``` + +#### Restore image data from a dump file + +```python +img.load_from_dump("/tmp/image_dump") +``` + +#### Create a new image from a dump file + +```python +img = FloatImg.create_from_dump("/tmp/image_dump") +``` +#### Get dump metadata + +```python +witdh, height, img_type = FloatImg.fileinfos("/tmp/image_dump") +``` + + +## Development + +### Run tests + +Install development requirements (you may add --user if not in a virtual environment or not in system-wide install): + +``` +pip install -r requirements-devel.txt +``` + +Ensure `floatimg` containing path is in the `PYTHONPATH`, this will run the tests and show a coverage report: + +``` +pytest --cov=floatimg tests/* +``` + diff --git a/floatimg/__init__.py b/floatimg/__init__.py new file mode 100644 index 0000000..86fe2da --- /dev/null +++ b/floatimg/__init__.py @@ -0,0 +1,5 @@ + + +from floatimg.image import FloatImg + +__version__ = "0.0.1" \ No newline at end of file diff --git a/floatimg/image.py b/floatimg/image.py new file mode 100644 index 0000000..ac9ada2 --- /dev/null +++ b/floatimg/image.py @@ -0,0 +1,241 @@ +import ctypes as ct + +from floatimg.settings import LIB + +class C_FloatImg(ct.Structure): + """mapping to the C structure of FloatImg""" + pass + + +C_FloatImg._fields_ = [ + ("magic", ct.c_ulong), + ("width", ct.c_int), + ("height", ct.c_int), + ("type", ct.c_int), + ("fval", ct.c_float), + ("count", ct.c_int), + ("R", ct.POINTER(ct.c_float)), + ("G", ct.POINTER(ct.c_float)), + ("B", ct.POINTER(ct.c_float)), + ("A", ct.POINTER(ct.c_float)), + ("reserved", ct.c_int), +] + +############################################################################################################ +# declaration of input / output types +c_fimgcreate = LIB.fimg_create +c_fimgcreate.argtypes = (ct.POINTER(C_FloatImg), ct.c_int, ct.c_int, ct.c_int) +c_fimgcreate.restype = ct.c_int + +c_fimg_destroy = LIB.fimg_destroy +c_fimg_destroy.argtypes = (ct.POINTER(C_FloatImg),) +c_fimg_destroy.restype = ct.c_int + +c_fimg_clone = LIB.fimg_clone +c_fimg_clone.argtypes = (ct.POINTER(C_FloatImg), ct.POINTER(C_FloatImg)) +c_fimg_clone.restype = ct.c_int + +c_fimg_copy_data = LIB.fimg_copy_data +c_fimg_copy_data.argtypes = (ct.POINTER(C_FloatImg), ct.POINTER(C_FloatImg)) +c_fimg_copy_data.restype = ct.c_int + +c_fimg_type_is_valid = LIB.fimg_type_is_valid +c_fimg_type_is_valid.argtypes = (ct.c_int,) +c_fimg_copy_data.restype = ct.c_int + +c_fimg_str_type = LIB.fimg_str_type +c_fimg_str_type.argtypes = (ct.c_int,) +c_fimg_str_type.restype = ct.c_char_p + +c_fimg_clear = LIB.fimg_clear +c_fimg_clear.argtypes = (ct.POINTER(C_FloatImg),) +c_fimg_clear.restype = ct.c_int + + +c_fimg_plot_rgb = LIB.fimg_plot_rgb +c_fimg_plot_rgb.argtypes = ( + ct.POINTER(C_FloatImg), + ct.c_int, + ct.c_int, + ct.c_float, + ct.c_float, + ct.c_float, +) +c_fimg_plot_rgb.restype = ct.c_int + + +# int fimg_get_rgb(FloatImg *head, int x, int y, float *rgb); +c_fimg_get_rgb = LIB.fimg_get_rgb +c_fimg_get_rgb.argtypes = (ct.POINTER(C_FloatImg), ct.c_int, ct.c_int, ct.POINTER(ct.c_float * 3)) +c_fimg_get_rgb.restype = ct.c_int + +# int fimg_put_rgb(FloatImg *head, int x, int y, float *rgb); +c_fimg_put_rgb = LIB.fimg_put_rgb +c_fimg_put_rgb.argtypes = (ct.POINTER(C_FloatImg), ct.c_int, ct.c_int, ct.POINTER(ct.c_float * 3)) +c_fimg_put_rgb.restype = ct.c_int + +c_fimg_rgb_constant = LIB.fimg_rgb_constant +c_fimg_rgb_constant.argtypes = (ct.POINTER(C_FloatImg), ct.c_float, ct.c_float, ct.c_float) +c_fimg_rgb_constant.restype = ct.c_int + +c_fimg_dump_to_file = LIB.fimg_dump_to_file +c_fimg_dump_to_file.argtypes = (ct.POINTER(C_FloatImg), ct.c_char_p, ct.c_int) +c_fimg_dump_to_file.restype = ct.c_int + +c_fimg_load_from_dump = LIB.fimg_load_from_dump +c_fimg_load_from_dump.argtypes = (ct.c_char_p, ct.POINTER(C_FloatImg)) +c_fimg_load_from_dump.restype = ct.c_int + +c_fimg_fileinfos = LIB.fimg_fileinfos +c_fimg_fileinfos.argtypes = (ct.c_char_p, ct.POINTER(ct.c_int * 3)) +c_fimg_fileinfos.restype = ct.c_int + +############################################################################################################ +class FloatImg: + """ + + Pythonic Object-Oriented encapsulation of floatimg library + + :attr c_img: an instance of C_FloatImg structure + :attr c_img: an pointer to the c_img structure + """ + + # type constants + GRAY = 1 + RGB = 3 + RGBA = 4 + RGBZ = 99 + + # proxy attributes to the C_FloatImg structure + magic = property(lambda self: self.c_img.magic) + width = property(lambda self: self.c_img.width) + height = property(lambda self: self.c_img.height) + type_id = property(lambda self: self.c_img.type) + fval = property(lambda self: self.c_img.fval) + count = property(lambda self: self.c_img.count) + + # oh yeah, really really sluggish access to pixels img.R[0].contents + # however, pixel data are not designed to be accessed this way + R = property( + lambda self: pointer(self.c_img.contents.R)[: self.width * self.height] + ) + + ####################################################################################################### + def __init__(self, c_img): + self.c_img = c_img + self.c_img_p = ct.pointer(c_img) + + ####################################################################################################### + def __str__(self): + return f"<{self.__class__.__name__} instance {id(self)} width={self.width} height={self.height} type={self.str_type}>" + + ####################################################################################################### + @classmethod + def create(cls, witdh, height, type_id): + """create an new FloatImg instance""" + assert cls.type_is_valid(type_id) + img = C_FloatImg() + assert c_fimgcreate(ct.pointer(img), witdh, height, type_id) == 0 + return FloatImg(img) + + ####################################################################################################### + @classmethod + def create_rgb(cls, witdh, height): + """create a new rgb instance """ + return cls.create(witdh, height, cls.RGB) + + ####################################################################################################### + def destroy(self): + """destroy the underlying structure. automattically called at instance destruction""" + assert c_fimg_destroy(self.c_img) == 0 + + ####################################################################################################### + def __del__(self): + self.destroy() + + ####################################################################################################### + def clear(self): + """clear data""" + return c_fimg_clear(self.c_img_p) + + ####################################################################################################### + def clone(self, flags=0): + """return a clone of the current instance""" + new_pic = C_FloatImg() + assert c_fimg_clone(self.c_img_p, ct.pointer(new_pic), flags) == 0 + return FloatImg(new_pic) + + ####################################################################################################### + def copy_data(self, to_img): + assert c_fimg_copy_data(self.c_img_p, to_img.c_img_p) == 0 + + ####################################################################################################### + @staticmethod + def type_is_valid(type_id): + """return True if type_id is a valid one""" + return c_fimg_type_is_valid(type_id) == 1 + + ####################################################################################################### + @property + def str_type(self): + """return the type of the image as a string""" + return c_fimg_str_type(self.c_img.type).decode("utf-8") + + ####################################################################################################### + def rgb_constant(self, r, v, b): + assert c_fimg_rgb_constant(self.c_img_p, r, v, b) == 0 + + ####################################################################################################### + def get_rgb(self, x, y): + """get r,g,b triplet from a pixel""" + rgb = (ct.c_float * 3)() + assert c_fimg_get_rgb(self.c_img_p, x, y, ct.pointer(rgb)) == 0 + return rgb[:3] + + ####################################################################################################### + def put_rgb(self, x, y, rgb): + """put r,g,b triplet to a pixel""" + # TODO may be a better way to create the array rather than iterating + c_rgb = (ct.c_float * 3)() + for i, c in enumerate(rgb): + c_rgb[i] = c + assert c_fimg_put_rgb(self.c_img_p, x, y, ct.pointer(c_rgb)) == 0 + + ####################################################################################################### + def dump_to_file(self, fname): + """save data to a dump file""" + # TODO use system encoding instead of utf-8 + assert ( + c_fimg_dump_to_file( + self.c_img_p, ct.c_char_p(bytes(fname, encoding="utf8")), 0 + ) + == 0 + ) + + ####################################################################################################### + def load_from_dump(self, fname): + """load data from a dump. size and type have to be compatible""" + assert ( + c_fimg_load_from_dump(ct.c_char_p(bytes(fname, encoding="utf8")), self.c_img_p) + == 0 + ) + + ####################################################################################################### + @classmethod + def fileinfos(cls, fname): + """return witdh, height, img_type triplet read from a dump file""" + datas = (ct.c_int * 3)() + assert ( + c_fimg_fileinfos(ct.c_char_p(bytes(fname, encoding="utf8")), ct.pointer(datas)) + == 0 + ) + return datas[:3] + + ####################################################################################################### + @classmethod + def create_from_dump(cls, fname): + """Create a new instance from a dump file""" + witdh, height, img_type = cls.fileinfos(fname) + img = cls.create(witdh, height, img_type) + img.load_from_dump(fname) + return img diff --git a/floatimg/operators.c b/floatimg/operators.c new file mode 100644 index 0000000..e69de29 diff --git a/floatimg/settings.py b/floatimg/settings.py new file mode 100644 index 0000000..58a4e2b --- /dev/null +++ b/floatimg/settings.py @@ -0,0 +1,3 @@ +import ctypes + +LIB = ctypes.cdll.LoadLibrary("../FloatImg/libfloatimg.so") diff --git a/requirements-devel.txt b/requirements-devel.txt new file mode 100644 index 0000000..9ea3227 --- /dev/null +++ b/requirements-devel.txt @@ -0,0 +1,2 @@ +pytest +pytest-cov \ No newline at end of file diff --git a/tests/basics.py b/tests/basics.py new file mode 100644 index 0000000..1d4d322 --- /dev/null +++ b/tests/basics.py @@ -0,0 +1,37 @@ +from floatimg import FloatImg + + +def test_create(): + width = 640 + height = 480 + img = FloatImg.create(width, height, FloatImg.RGB) + assert img.width == width + assert img.height == height + assert img.type_id == FloatImg.RGB + + img = FloatImg.create_rgb(width, height) + + assert img.type_id == FloatImg.RGB + # TODO inspect RVB + + +def test_clone(): + width = 640 + height = 480 + img = FloatImg.create(width, height, FloatImg.RGB) + img2 = img.clone() + assert img.width == img2.width + assert img.height == img2.height + assert img.type_id == img2.type_id + # TODO inspect RVB and do pixel per pixel comparison + + +def test_rgb_constant(): + width = 5 + height = 5 + color = [127.0, 127.0, 127.0] + img = FloatImg.create_rgb(width, height) + img.rgb_constant(*color) + for y in range(height): + for x in range(width): + assert img.get_rgb(x, y) == color