diff --git a/robots-at-runes/research/dataset/__init__.py b/robots-at-runes/research/dataset/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/robots-at-runes/research/dataset/blend/__init__.py b/robots-at-runes/research/dataset/blend/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/robots-at-runes/research/dataset/blend/blend.py b/robots-at-runes/research/dataset/blend/blend.py
deleted file mode 100644
index dc164893b69f52e54d5a445b57a37e72264f9bfa..0000000000000000000000000000000000000000
--- a/robots-at-runes/research/dataset/blend/blend.py
+++ /dev/null
@@ -1,148 +0,0 @@
-import random
-import xml.etree.ElementTree as ET
-from pathlib import Path
-from xml.dom.minidom import parseString
-
-import cv2
-import matplotlib.pyplot as plt
-import numpy as np
-from imutils import rotate_bound
-
-EXAMPLES_DIR = Path(__file__).parent / "examples"
-
-# Facteur de redimensionnement. La taille de l'image sera divisée ou multipliée par ce chiffre au maximum
-FACT_RESIZE = 1
-
-# Max angle in degrees for random rotation
-MAX_ANGLE = 20
-
-
-def preprocess_background(path_back):
-    background = cv2.imread(path_back)
-    return cv2.cvtColor(background, cv2.COLOR_BGR2RGB)
-
-
-def preprocess_sticker(path_item):
-    item = cv2.imread(path_item, cv2.IMREAD_UNCHANGED)
-    return cv2.cvtColor(item, cv2.COLOR_BGRA2RGBA)
-
-
-def generate_one(
-    background, item, rotate=True, scale=True, custom_rotate=None, custom_scale=None, to_print=False, save_name=None
-):
-    if rotate:
-        if custom_rotate is None:
-            percent_rotate = np.round((2 * random.random()) - 1, 3)  # entre -1 et 1
-        else:
-            percent_rotate = custom_rotate
-        item = rotate_percentage(item, percent_rotate)
-    if scale:
-        if custom_scale is None:
-            percent_scale = np.round((2 * random.random()) - 1, 3)  # entre -1 et 1
-        else:
-            percent_scale = custom_scale
-        item = reshape_percentage(item, percent_scale)
-    mask_alpha = item[:, :, 3]
-    item = cv2.cvtColor(item, cv2.COLOR_RGBA2RGB)
-    hs, ws, _ = item.shape
-
-    background_subset, h_start, w_start = get_subset_shapes(background, hs, ws)
-    composition_subset = superimpose(background_subset, item, mask_alpha)
-
-    composition = background.copy()
-    composition[h_start : h_start + hs, w_start : w_start + ws, :] = composition_subset
-
-    labels = [h_start, w_start, h_start + hs, w_start + ws]
-
-    if not (save_name is None):
-        cv2.imwrite(save_name, cv2.cvtColor(composition, cv2.COLOR_RGB2BGR))
-
-    if to_print:
-        plt.imshow(composition)
-        plt.show()
-
-    return composition, labels
-
-
-def superimpose(img1, img2, mask):
-    """
-    Superimpose two pictures based on a mask. The two pictures
-    must have the same shape.
-    :param img1: first picture
-    :param img2: second picture
-    :param mask: mask applied on both pictures. Values between 0 and 255
-    :return: Composition
-    """
-    dst_shape = img1.shape
-    img1f = img1.astype(np.float)
-    img2f = img2.astype(np.float)
-    mix = np.zeros(dst_shape, dtype=np.uint8)
-    for i in range(dst_shape[2]):
-        mix[:, :, i] = ((~mask * img1f[:, :, i] + mask * img2f[:, :, i]) / 255).astype(np.uint8)
-    return mix
-
-
-def get_subset_shapes(img_extract, hs, ws):
-    he, we, _ = img_extract.shape
-    delta_h = he - hs
-    delta_w = we - ws
-    h_start = int(random.random() * delta_h)
-    w_start = int(random.random() * delta_w)
-    return img_extract[h_start : h_start + hs, w_start : w_start + ws, :], h_start, w_start
-
-
-def reshape_percentage(img_base, percent):
-    intensity = 1 + abs(percent) * FACT_RESIZE
-    h, w, _ = img_base.shape
-    if percent < 0:
-        h_dest, w_dest = int(h / intensity), int(w / intensity)
-    else:
-        h_dest, w_dest = h * int(intensity), w * int(intensity)
-
-    return cv2.resize(img_base, (w_dest, h_dest), interpolation=cv2.INTER_AREA)
-
-
-def rotate_percentage(img_base, percent):
-    angle = MAX_ANGLE * percent
-    return rotate_bound(img_base, angle)
-
-
-path_background = EXAMPLES_DIR / "back1.jpg"
-path_sticker = EXAMPLES_DIR / "logo.png"
-
-background = preprocess_background(str(path_background))
-sticker = preprocess_sticker(str(path_sticker))
-
-labels = []
-filenames = []
-for i in range(10):
-    folder = EXAMPLES_DIR
-    filename = "image_" + str(i) + ".jpg"
-    filenames.append(filename)
-    _, label = generate_one(background, sticker, to_print=False, save_name=str(folder / filename))
-    labels.append(label)
-
-
-data = ET.Element("annotations")
-
-for i, [xmin, ymin, xmax, ymax] in enumerate(labels):
-    object = ET.SubElement(data, "object")
-    sub_name = ET.SubElement(object, "filename")
-    sub_xmin = ET.SubElement(object, "xmin")
-    sub_ymin = ET.SubElement(object, "ymin")
-    sub_xmax = ET.SubElement(object, "xmax")
-    sub_ymax = ET.SubElement(object, "ymax")
-    sub_name.text = filenames[i]
-    sub_xmin.text = str(xmin)
-    sub_ymin.text = str(ymin)
-    sub_xmax.text = str(xmax)
-    sub_ymax.text = str(ymax)
-
-dom = parseString(ET.tostring(data).decode("utf-8"))
-pretty_xml = dom.toprettyxml()
-myfile = EXAMPLES_DIR / "labels.xml"
-myfile.write_text(pretty_xml)
-
-# with open('dataset/labels.txt', 'w') as f:
-#     for label in labels:
-#         f.write("%s\n" % label)
diff --git a/robots-at-runes/research/dataset/blend/image_blender.py b/robots-at-runes/research/dataset/blend/image_blender.py
new file mode 100644
index 0000000000000000000000000000000000000000..57452daf1976d85f59402ac55b763449633566a9
--- /dev/null
+++ b/robots-at-runes/research/dataset/blend/image_blender.py
@@ -0,0 +1,82 @@
+from dataclasses import dataclass
+from random import random
+from typing import List, Tuple
+
+import cv2
+import numpy as np
+
+from polystar.common.models.image import Image
+from research.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import LabeledImageModifierABC
+from research.dataset.labeled_image import LabeledImage, PointOfInterest
+
+
+@dataclass
+class ImageBlender:
+    modifiers: List[LabeledImageModifierABC]
+
+    def blend(self, background: Image, obj: LabeledImage) -> LabeledImage:
+        obj = self._modify_object(obj)
+        x, y = self._generate_position_of_object(background.shape, obj.image.shape)
+        return LabeledImage(
+            image=self._blend_obj_on_background(background, obj.image, x, y),
+            point_of_interests=[self._translate_poi(poi, x, y) for poi in obj.point_of_interests],
+        )
+
+    def _modify_object(self, obj: LabeledImage) -> LabeledImage:
+        for modifier in self.modifiers:
+            obj = modifier.randomly_modify(obj)
+        return obj
+
+    def _generate_position_of_object(
+        self, background_shape: Tuple[int, int, int], obj_shape: Tuple[int, int, int]
+    ) -> Tuple[int, int]:
+        return (
+            self._generate_position_of_object_amoung_axis(background_shape[1], obj_shape[1]),
+            self._generate_position_of_object_amoung_axis(background_shape[0], obj_shape[0]),
+        )
+
+    @staticmethod
+    def _generate_position_of_object_amoung_axis(background_size: int, obj_size: int) -> int:
+        return int(random() * (background_size - obj_size))
+
+    def _blend_obj_on_background(self, background: Image, obj_img: Image, x: int, y: int) -> Image:
+        background_roi = background[y : y + obj_img.shape[0], x : x + obj_img.shape[1], :]
+        mask = obj_img[:, :, 3]
+        obj_img = cv2.cvtColor(obj_img, cv2.COLOR_RGBA2RGB)
+        background_roi = background_roi.astype(np.float)
+        obj_img = obj_img.astype(np.float)
+        rv = background.copy()
+        for i in range(3):
+            rv[y : y + obj_img.shape[0], x : x + obj_img.shape[1], i] = (
+                (~mask * background_roi[:, :, i] + mask * obj_img[:, :, i]) / 255
+            ).astype(np.uint8)
+        return rv
+
+    @staticmethod
+    def _translate_poi(poi: PointOfInterest, x: int, y: int) -> PointOfInterest:
+        return PointOfInterest(poi.x + x, poi.y + y)
+
+
+if __name__ == "__main__":
+    from pathlib import Path
+
+    import matplotlib.pyplot as plt
+
+    EXAMPLES_DIR = Path(__file__).parent / "examples"
+
+    _obj = LabeledImage(
+        cv2.cvtColor(cv2.imread(str(EXAMPLES_DIR / "logo.png"), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGRA2RGBA),
+        [PointOfInterest(432, 76), PointOfInterest(432, 13)],
+    )
+    _bg = cv2.cvtColor(cv2.imread(str(EXAMPLES_DIR / "back1.jpg")), cv2.COLOR_BGR2RGB)
+
+    _blender = ImageBlender([])
+    for i in range(10):
+        res = _blender.blend(_bg, _obj)
+
+        plt.imshow(res.image)
+        plt.plot([poi.x for poi in res.point_of_interests], [poi.y for poi in res.point_of_interests], "r.")
+        plt.axis("off")
+        plt.tight_layout()
+        plt.savefig(str(EXAMPLES_DIR / f"image_{i}.jpg"))
+        plt.show()
diff --git a/robots-at-runes/research/dataset/blend/labeled_image_modifiers/__init__.py b/robots-at-runes/research/dataset/blend/labeled_image_modifiers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/robots-at-runes/research/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py b/robots-at-runes/research/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3ab2e7f4c7bd4bec333f24a3267e48b02b639c6
--- /dev/null
+++ b/robots-at-runes/research/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py
@@ -0,0 +1,40 @@
+from abc import ABC, abstractmethod
+from random import random
+
+from polystar.common.models.image import Image
+from research.dataset.labeled_image import LabeledImage, PointOfInterest
+
+
+class LabeledImageModifierABC(ABC):
+    def randomly_modify(self, labeled_image: LabeledImage) -> LabeledImage:
+        return self.modify_from_factor(labeled_image, random() * 2 - 1)
+
+    def modify_from_factor(self, labeled_image: LabeledImage, factor: float) -> LabeledImage:
+        return self.modify_from_value(labeled_image, self._get_value_from_factor(factor))
+
+    def modify_from_value(self, labeled_image: LabeledImage, value: float) -> LabeledImage:
+        new_image = self._generate_modified_image(labeled_image.image, value)
+        return LabeledImage(
+            image=new_image,
+            point_of_interests=[
+                self._generate_modified_poi(poi, labeled_image.image, new_image, value)
+                for poi in labeled_image.point_of_interests
+            ],
+        )
+
+    @abstractmethod
+    def _get_value_from_factor(self, factor: float) -> float:
+        """
+        :param factor: a factor of modification, in range [-1, 1]
+        :return: the value of modification used by other function of this class
+        """
+
+    @abstractmethod
+    def _generate_modified_image(self, image: Image, value: float) -> Image:
+        pass
+
+    @abstractmethod
+    def _generate_modified_poi(
+        self, poi: PointOfInterest, original_image: Image, new_image: Image, value: float
+    ) -> PointOfInterest:
+        pass
diff --git a/robots-at-runes/research/dataset/labeled_image.py b/robots-at-runes/research/dataset/labeled_image.py
new file mode 100644
index 0000000000000000000000000000000000000000..fec391238b4fc7089a37575ca144777be49dc378
--- /dev/null
+++ b/robots-at-runes/research/dataset/labeled_image.py
@@ -0,0 +1,16 @@
+from dataclasses import dataclass, field
+from typing import List
+
+from polystar.common.models.image import Image
+
+
+@dataclass
+class PointOfInterest:
+    x: int
+    y: int
+
+
+@dataclass
+class LabeledImage:
+    image: Image
+    point_of_interests: List[PointOfInterest] = field(default_factory=list)