Compare commits

...

6 Commits

Author SHA1 Message Date
66b1c098f5 add contrast binding 2021-05-20 08:34:10 +02:00
2342a9e2a8 FloatImg sources upgraded to version 148 2021-05-16 11:45:00 +02:00
4b57a41705 maxvalue binding 2021-05-14 09:44:28 +02:00
fbea9488f0 file import/export 2021-05-14 09:11:35 +02:00
95c1e6d9c6 operator binding 2021-05-14 06:40:38 +02:00
eaf039a83f fimg-core.c binding completed 2021-05-12 19:50:27 +02:00
9 changed files with 473 additions and 79 deletions

113
README.md
View File

@@ -12,16 +12,18 @@ Color values are specified using triplets of float values, like (128.5, 34.234,
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.
The file `floatimg/settings.py` contains the path to the .so file and the encoding of path characters.
## Usage
The FloatImg class encapsulate core and file functionnalities of FloatImg library. Import if from the main module:
```python
import floatimg
```
The FloatImg class encapsulate core and file functionnalities of FloatImg library. Instances are created with functions from the module.
Colors are tuple of three elements `(r, g, b)`.
### Image creation
`FloatImg` has two class methods to create an image.
@@ -47,62 +49,98 @@ new_img = img.clone()
new_img = img.clone(True)
```
### Image manipulation
#### Clear image
Reset all pixels to zero.
### Basic pixel operations
```python
# Get pixel value
color = (r, g, b) = img.get(x, y)
# Set pixel value
img.put(x, y, (r, g, b))
# Reset all pixels to zero.
img.clear()
```
#### Copy pixels to another image
```python
# Copy pixels to another image of the same format
img.copy_data(another_img)
```
#### Set pixel value
### File operations
#### Raw dump files
```python
img.put(x, y, (r, g, b))
# Dump image to file
img.dump(path)
# Restore image data from a dump file
img.load(path)
# Create a new image from a dump file
img = floatimg.create_from_dump(path)
# Get dump metadata (TODO create a FileInfo named tuple)
witdh, height, img_type = floatimg.fileinfos(path)
```
#### Get pixel value
#### External formats
```python
color = (r, g, b) = img.get(x, y)
# export supports the following extentions : .fimg, .png, .tiff, .pnm, .fits
# save as png
img.export("test.png")
# create an image instance from png file
img = floatimg.create_from_png("test.png")
# load pixel data from png file in an existing instance. size and type have to be compatible
img.load_png("test.png")
```
### File manipulation
#### Dump image to file
## Operators
```python
img.dump("/tmp/image_dump")
# addition
res_img = img + img2
# substraction
res_img = img - img2
# multiplication
res_img = img * img2
# min value per color channel
res_img = img.min(img2)
# max value per color channel
res_img = img.max(img2)
```
#### Restore image data from a dump file
## Contrast operations
```python
img.load("/tmp/image_dump")
Contrast types are defined in the Contrast enumeration:
```
floatimg.Contrast.SQRT
floatimg.Contrast.POW2
floatimg.Contrast.COS01
floatimg.Contrast.COS010
```
#### Create a new image from a dump file
To apply contrast:
```python
img = floatimg.create_from_dump("/tmp/image_dump")
```
#### Get dump metadata
# Return a new image
res = img.contrast(value, floatimg.Contrast.SQRT)
TODO create a FileInfo named tuple.
```python
witdh, height, img_type = floatimg.fileinfos("/tmp/image_dump")
# set contrast in-place
img.contrast(value, floatimg.Contrast.SQRT, True)
```
## Development
### Run tests
@@ -119,3 +157,18 @@ Ensure `floatimg` containing path is in the `PYTHONPATH`, this will run the test
pytest --cov=floatimg tests/*
```
### Status
The goal is not to bind all the functions but essential ones too build a front-end with python-tkinter.
#### Done
- lib/fimg-core.c
- lib/fimg-file.c
- lib/operator.c
- funcs/exporter.c
- funcs/fimg-png.c
#### Partial
- lib/fimg-math.c

View File

@@ -1,6 +1,16 @@
"""Python Binding to FloatImg """
# FloatImg class and static functions
from floatimg.image import FloatImg, create, create_rgb, create_from_dump, GRAY, RGB, fileinfos
# FloatImg class, constants & static functions
from floatimg.image import (
GRAY,
RGB,
FloatImg,
create,
create_rgb,
create_from_dump,
create_from_png,
fileinfos,
Contrast
)
__version__ = "0.0.1"

View File

@@ -1,13 +1,26 @@
"""Binding bases"""
import ctypes as ct
from enum import Enum
import logging
from floatimg.settings import LIB
from floatimg.settings import LIB, PATH_ENCODING
############################################################################################################
# type constants
GRAY = 1
RGB = 3
RGBA = 4
RGBZ = 99
RGBA = 4 # may not be used
RGBZ = 99 # may not be used
############################################################################################################
# Contrast enumeration
class Contrast(Enum):
NONE = 0
SQRT = 1
POW2 = 2
COS01 = 3
COS010 = 4
############################################################################################################
# Type mapping
@@ -32,7 +45,9 @@ C_FloatImg._fields_ = [
]
############################################################################################################
# declaration of input / output types
# declaration of input / output types for C binding of core functions
# fimg-core.c
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
@@ -61,7 +76,6 @@ 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),
@@ -73,8 +87,6 @@ c_fimg_plot_rgb.argtypes = (
)
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),
@@ -84,7 +96,6 @@ c_fimg_get_rgb.argtypes = (
)
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),
@@ -103,6 +114,7 @@ c_fimg_rgb_constant.argtypes = (
)
c_fimg_rgb_constant.restype = ct.c_int
# fimg-file.c
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
@@ -115,6 +127,104 @@ 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
c_fimg_add_rgb = LIB.fimg_add_rgb
c_fimg_add_rgb.argtypes = (
ct.POINTER(C_FloatImg),
ct.c_int,
ct.c_int,
ct.c_float,
ct.c_float,
ct.c_float,
)
c_fimg_add_rgb.restype = ct.c_int
# # fimg-operators.c
c_fimg_add_3 = LIB.fimg_add_3
c_fimg_add_3.argtypes = (
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
)
c_fimg_add_3.restype = ct.c_int
c_fimg_sub_3 = LIB.fimg_sub_3
c_fimg_sub_3.argtypes = (
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
)
c_fimg_sub_3.restype = ct.c_int
c_fimg_mul_3 = LIB.fimg_mul_3
c_fimg_mul_3.argtypes = (
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
)
c_fimg_mul_3.restype = ct.c_int
c_fimg_minimum = LIB.fimg_minimum
c_fimg_minimum.argtypes = (
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
)
c_fimg_minimum.restype = ct.c_int
c_fimg_maximum = LIB.fimg_maximum
c_fimg_maximum.argtypes = (
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
)
c_fimg_maximum.restype = ct.c_int
# Files i/o
c_fimg_export_picture = LIB.fimg_export_picture
c_fimg_export_picture.argtypes = (ct.POINTER(C_FloatImg), ct.c_char_p)
c_fimg_export_picture.restype = ct.c_int
c_fimg_create_from_png = LIB.fimg_create_from_png
c_fimg_create_from_png.argtypes = (ct.c_char_p, ct.POINTER(C_FloatImg))
c_fimg_create_from_png.restype = ct.c_int
c_fimg_load_from_png = LIB.fimg_load_from_png
c_fimg_load_from_png.argtypes = (ct.c_char_p, ct.POINTER(C_FloatImg))
c_fimg_load_from_png.restype = ct.c_int
c_fimg_get_maxvalue = LIB.fimg_get_maxvalue
c_fimg_get_maxvalue.argtypes = (ct.POINTER(C_FloatImg),)
c_fimg_get_maxvalue.restype = ct.c_float
# contrast
c_fimg_square_root = LIB.fimg_square_root
c_fimg_square_root.argtypes = (
ct.POINTER(C_FloatImg),
ct.POINTER(C_FloatImg),
ct.c_double,
)
c_fimg_square_root.restype = ct.c_int
c_fimg_power_2 = LIB.fimg_power_2
c_fimg_power_2.argtypes = (ct.POINTER(C_FloatImg), ct.POINTER(C_FloatImg), ct.c_double)
c_fimg_power_2.restype = ct.c_int
c_fimg_cos_01 = LIB.fimg_cos_01
c_fimg_cos_01.argtypes = (ct.POINTER(C_FloatImg), ct.POINTER(C_FloatImg), ct.c_double)
c_fimg_cos_01.restype = ct.c_int
c_fimg_cos_010 = LIB.fimg_cos_010
c_fimg_cos_010.argtypes = (ct.POINTER(C_FloatImg), ct.POINTER(C_FloatImg), ct.c_double)
c_fimg_cos_010.restype = ct.c_int
# contrast function map
CONTRAST_FUNC_DICT = {
Contrast.SQRT: c_fimg_square_root,
Contrast.POW2: c_fimg_power_2,
Contrast.COS01: c_fimg_cos_01,
Contrast.COS010: c_fimg_cos_010,
}
############################################################################################################
class FloatImg:
"""
@@ -126,17 +236,11 @@ class FloatImg:
"""
# proxy attributes to the C_FloatImg structure
# TODO: now more about them
# TODO: now more about them ;)
magic = property(lambda self: self.c_img.magic)
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
@@ -153,6 +257,7 @@ class FloatImg:
#######################################################################################################
def destroy(self):
"""destroy the underlying structure. automattically called at instance destruction"""
logging.debug("Destroy %s", self)
assert c_fimg_destroy(self.c_img) == 0
#######################################################################################################
@@ -167,13 +272,9 @@ class FloatImg:
#######################################################################################################
def clone(self, copy_data=False):
"""return a clone of the current instance"""
if copy_data:
flags = 1
else:
flags = 0
new_pic = C_FloatImg()
assert c_fimg_clone(self.c_img_p, ct.pointer(new_pic), flags) == 0
return FloatImg(new_pic)
c_img_p = C_FloatImg()
assert c_fimg_clone(self.c_img_p, ct.pointer(c_img_p), int(copy_data)) == 0
return FloatImg(c_img_p)
#######################################################################################################
def copy_data(self, to_img):
@@ -192,45 +293,115 @@ class FloatImg:
#######################################################################################################
def get(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]
color = (ct.c_float * 3)()
assert c_fimg_get_rgb(self.c_img_p, x, y, ct.pointer(color)) == 0
return color[:3]
#######################################################################################################
def put(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
def put(self, x, y, color):
"""put color to a pixel"""
c_rgb = (ct.c_float * 3)(*color)
assert c_fimg_put_rgb(self.c_img_p, x, y, ct.pointer(c_rgb)) == 0
#######################################################################################################
def dump(self, fname):
def add(self, x, y, color):
"""add color to a pixel """
assert c_fimg_add_rgb(self.c_img_p, x, y, *color) == 0
#######################################################################################################
def dump(self, path):
"""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
self.c_img_p, ct.c_char_p(bytes(path, encoding=PATH_ENCODING)), 0
)
== 0
)
#######################################################################################################
def load(self, fname):
def load(self, path):
"""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
ct.c_char_p(bytes(path, encoding=PATH_ENCODING)), self.c_img_p
)
== 0
)
# TODO
# fimg_plot_rgb
# fimg_add_rgb
#######################################################################################################
def load_png(self, path):
"""load data from a png file. size and type have to be compatible"""
assert (
c_fimg_load_from_png(
ct.c_char_p(bytes(path, encoding=PATH_ENCODING)), self.c_img_p
)
== 0
)
# export(fname, flags) : depending file extension, save to the correct format
#######################################################################################################
def __add__(self, img):
""" + operator overload"""
res = self.clone(False)
assert c_fimg_add_3(self.c_img_p, img.c_img_p, res.c_img_p) == 0
return res
#######################################################################################################
def __sub__(self, img):
""" - operator overload"""
res = self.clone(False)
assert c_fimg_sub_3(self.c_img_p, img.c_img_p, res.c_img_p) == 0
return res
#######################################################################################################
def __mul__(self, img):
""" * operator overload"""
res = self.clone(False)
assert c_fimg_mul_3(self.c_img_p, img.c_img_p, res.c_img_p) == 0
return res
#######################################################################################################
def min(self, img):
"""return a new image with minimum pixel values per channel between current and parameter"""
res = self.clone(False)
# C code inverts comparison
assert c_fimg_minimum(self.c_img_p, img.c_img_p, res.c_img_p) == 0
return res
#######################################################################################################
def max(self, img):
"""return a new image with maximum pixel values per channel between current and parameter"""
res = self.clone(False)
# C code inverts comparison
assert c_fimg_maximum(self.c_img_p, img.c_img_p, res.c_img_p) == 0
return res
#######################################################################################################
def export(self, path):
"""export image to path. allowed extension: .fimg, .png, .tiff, .pnm, .fits"""
assert (
c_fimg_export_picture(
self.c_img_p, ct.c_char_p(bytes(path, encoding=PATH_ENCODING)), 0
)
== 0
)
#######################################################################################################
def maxvalue(self):
"""return the maximum value for any channel in the image"""
return c_fimg_get_maxvalue(self.c_img_p)
#######################################################################################################
def contrast(self, max_value, contrast_id=Contrast.SQRT, in_place=False):
""""""
func = CONTRAST_FUNC_DICT[contrast_id]
dest = None
dest_c_img = None
if not in_place:
dest = self.clone(False)
dest_c_img = dest.c_img_p
assert (func(self.c_img, dest_c_img, max_value)) == 0
return dest
###########################################################################################################
@@ -238,7 +409,9 @@ def fileinfos(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))
c_fimg_fileinfos(
ct.c_char_p(bytes(fname, encoding=PATH_ENCODING)), ct.pointer(datas)
)
== 0
)
return datas[:3]
@@ -260,14 +433,25 @@ def create_rgb(witdh, height):
###########################################################################################################
def create_from_dump(fname):
"""Create a new instance from a dump file"""
witdh, height, img_type = fileinfos(fname)
def create_from_dump(path):
"""Create a FloatImg instance from a dump file"""
witdh, height, img_type = fileinfos(path)
img = create(witdh, height, img_type)
img.load(fname)
img.load(path)
return img
###########################################################################################################
def create_from_png(path):
"""Create a FloatImg instance from a png file"""
img = C_FloatImg()
assert (
c_fimg_create_from_png(ct.c_char_p(bytes(path, encoding=PATH_ENCODING)), img)
== 0
)
return FloatImg(img)
###########################################################################################################
def type_is_valid(type_id):
"""return True if type_id is a valid one"""

View File

View File

@@ -1,5 +1,5 @@
import ctypes
# LIB = ctypes.cdll.LoadLibrary("../FloatImg/libfloatimg.so")
PATH_ENCODING = "utf8"
LIB = ctypes.cdll.LoadLibrary("../FloatImg4PythonBinding/build/lib/libfloatimg.so")

View File

@@ -54,7 +54,6 @@ def test_rgb_constant():
assert img.get(x, y) == color
def test_copy_data():
width = 5
height = 5
@@ -104,3 +103,76 @@ def test_put_rgb():
assert img.get(0, 0) == [0.0, 0.0, 0.0]
img.put(0, 0, (127.0, 127.0, 127.0))
assert img.get(0, 0) == [127.0, 127.0, 127.0]
def test_add():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
color = (32.0, 32.0, 32.0)
img.put(0, 0, color)
img.add(0, 0, color)
assert img.get(0, 0) == [64.0, 64.0, 64.0]
def test_op_add():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
color = (32.0, 32.0, 32.0)
img.fill(color)
img2 = floatimg.create_rgb(width, height)
img2.fill(color)
img3 = img + img2
for y in range(height):
for x in range(width):
assert img3.get(x, y) == [64.0, 64.0, 64.0]
def test_op_sub():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
color = (32.0, 32.0, 32.0)
img.fill(color)
img2 = floatimg.create_rgb(width, height)
img2.fill(color)
img3 = img - img2
for y in range(height):
for x in range(width):
assert img3.get(x, y) == [0.0, 0.0, 0.0]
def test_op_mul():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
color = (32.0, 32.0, 32.0)
img.fill(color)
img2 = floatimg.create_rgb(width, height)
img2.fill((2.0, 2.0, 2.0))
img3 = img * img2
for y in range(height):
for x in range(width):
assert img3.get(x, y) == [64.0, 64.0, 64.0]
def test_op_min():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
img.fill((32.0, 0.0, 32.0))
img2 = floatimg.create_rgb(width, height)
img2.fill((0.0, 32.0, 0.0))
img3 = img.min(img2)
for y in range(height):
for x in range(width):
assert img3.get(x, y) == [0.0, 0.0, 0.0]
def test_op_max():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
img.fill((32.0, 0.0, 32.0))
img2 = floatimg.create_rgb(width, height)
img2.fill((0.0, 32.0, 0.0))
img3 = img.max(img2)
for y in range(height):
for x in range(width):
assert img3.get(x, y) == [32.0, 32.0, 32.0]

35
tests/contrast.py Normal file
View File

@@ -0,0 +1,35 @@
import floatimg
import logging
def test_square():
width, height = 5, 5
color = [127.0, 127.0, 127.0]
img = floatimg.create_rgb(width, height)
img.fill(color)
img.put(2, 2, [200, 200, 200])
result_dict = {
floatimg.Contrast.SQRT: (
[225.83180236816406, 225.83180236816406, 225.83180236816406],
[179.9583282470703, 179.9583282470703, 179.9583282470703],
),
floatimg.Contrast.POW2: (
[156.8627471923828, 156.8627471923828, 156.8627471923828],
[63.250980377197266, 63.250980377197266, 63.250980377197266],
),
floatimg.Contrast.COS01: (
[226.83277893066406, 226.83277893066406, 226.83277893066406],
[126.71460723876953, 126.71460723876953, 126.71460723876953],
),
floatimg.Contrast.COS010: (
[100.2235336303711, 100.2235336303711, 100.2235336303711],
[254.99032592773438, 254.99032592773438, 254.99032592773438],
),
}
for contrast_type, (center, outer) in result_dict.items():
res = img.contrast(255, contrast_type)
assert res.get(2, 2) == center
assert res.get(2, 3) == outer

28
tests/files.py Normal file
View File

@@ -0,0 +1,28 @@
import os
import floatimg
def create_img():
width, height = 5, 5
img = floatimg.create_rgb(width, height)
img.fill((255.0, 0.0, 0.0))
return img
def test_png():
width, height = 5, 5
path = "test.png"
img = create_img()
img.export(path)
assert os.path.getsize(path) == 74
img2 = floatimg.create_from_png(path)
for y in range(height):
for x in range(width):
assert img.get(x, y) == img2.get(x, y)
img3 = img2.clone(False)
img3.load_png(path)
for y in range(height):
for x in range(width):
assert img.get(x, y) == img3.get(x, y)

12
tests/maths.py Normal file
View File

@@ -0,0 +1,12 @@
"""test of core functions """
import floatimg
def test_maxvaalue():
width = 5
height = 5
img = floatimg.create_rgb(width, height)
color = (255.0, 255.0, 255.0)
img.put(2,2, color)
assert img.maxvalue() == 255.0