yta-image-base 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,128 @@
1
+ from yta_image_base.converter import ImageConverter
2
+ from yta_file.handler import FileHandler
3
+ from yta_file.filename.handler import FilenameHandler, FileType
4
+ from yta_validation import PythonValidator
5
+ from PIL import Image
6
+ from typing import Union
7
+
8
+ import numpy as np
9
+
10
+
11
+ class ImageParser:
12
+ """
13
+ Class to simplify the way we handle the image parameters
14
+ so we can parse them as Pillow images, as numpy arrays,
15
+ etc.
16
+ """
17
+
18
+ @staticmethod
19
+ def to_pillow(
20
+ image: Union[str, Image.Image, np.ndarray],
21
+ mode: str = 'RGB'
22
+ ) -> Image.Image:
23
+ """
24
+ Returns an instance of a Pillow Image.Image of the given
25
+ 'image' if it is a valid image and no error found.
26
+
27
+ Result is a Pillow image which is in RGB (or RGBA) format.
28
+ """
29
+ # TODO: By now we are only accepting string filenames,
30
+ # Pillow Image.Image instances and numpy arrays.
31
+ if image is None:
32
+ raise Exception('No "image" parameter provided.')
33
+
34
+ if (
35
+ not PythonValidator.is_string(image) and
36
+ not PythonValidator.is_numpy_array(image) and
37
+ not PythonValidator.is_instance(image, Image.Image)
38
+ ):
39
+ raise Exception('The "image" parameter provided is not a string nor a Image.Image nor a np.ndarray.')
40
+
41
+ mode = (
42
+ 'RGB'
43
+ if not mode else
44
+ mode
45
+ )
46
+
47
+ if mode not in ['RGB', 'RGBA']:
48
+ raise Exception('The provided "mode" parameters is not a valid mode: RGB or RGBA.')
49
+
50
+ # We can have problems with np.ndarray
51
+ if PythonValidator.is_numpy_array(image):
52
+ image = ImageConverter.numpy_image_to_pil(image)
53
+ elif PythonValidator.is_string(image):
54
+ if not FilenameHandler.is_of_type(image, FileType.IMAGE):
55
+ raise Exception('The "image" parameter provided is not a valid image filename.')
56
+
57
+ if not FileHandler.is_image_file(image):
58
+ raise Exception('The "image" parameter provided is not a valid image.')
59
+
60
+ image = Image.open(image)
61
+
62
+ return image.convert(mode)
63
+
64
+ @staticmethod
65
+ def to_numpy(
66
+ image: Union[str, Image.Image, np.ndarray],
67
+ mode: str = 'RGB'
68
+ ) -> np.ndarray:
69
+ """
70
+ Returns a numpy array representing the given 'image'
71
+ if it is a valid image and no error found.
72
+
73
+ The 'mode' parameter will be used to open the image
74
+ with Pillow library and then turning into numpy when
75
+ necessary. Must be 'RGB' or 'RGBA'.
76
+
77
+ Result is in RGB or RGBA format.
78
+ """
79
+ # TODO: By now we are only accepting string filenames,
80
+ # Pillow Image.Image instances and numpy arrays.
81
+ if image is None:
82
+ raise Exception('No "image" parameter provided.')
83
+
84
+ if not PythonValidator.is_instance(image, [str, Image.Image, np.ndarray]):
85
+ raise Exception('The "image" parameter provided is not a string nor a Image.Image nor a np.ndarray.')
86
+
87
+ mode = 'RGB' if not mode else mode
88
+
89
+ if mode not in ['RGB', 'RGBA']:
90
+ raise Exception('The provided "mode" parameters is not a valid mode: RGB or RGBA.')
91
+
92
+ # We can have problems with np.ndarray
93
+ if PythonValidator.is_instance(image, Image.Image):
94
+ image = ImageConverter.pil_image_to_numpy(image.convert(mode))
95
+ elif PythonValidator.is_string(image):
96
+ if not FilenameHandler.is_of_type(image, FileType.IMAGE):
97
+ raise Exception('The "image" parameter provided is not a valid image filename.')
98
+
99
+ if not FileHandler.is_image_file(image):
100
+ raise Exception('The "image" parameter provided is not a valid image.')
101
+
102
+ image = ImageConverter.pil_image_to_numpy(Image.open(image).convert(mode))
103
+ elif PythonValidator.is_numpy_array(image):
104
+ if image.shape[2] == 3 and mode == 'RGBA':
105
+ # numpy but RGB to RGBA
106
+ image = np.dstack((image, np.ones((image.shape[0], image.shape[1]), dtype = np.uint8) * 255))
107
+ # Moviepy uses the alpha channel as 0 or 1, not as 255
108
+ # but this is only an image parser not a moviepy parser
109
+ #image = np.dstack((image, np.ones((image.shape[0], image.shape[1]), dtype = np.uint8)))
110
+ elif image.shape[2] == 4 and mode == 'RGB':
111
+ # numpy but RGBA to RGB
112
+ image = image[:, :, :3]
113
+
114
+ return image
115
+
116
+ @staticmethod
117
+ def to_opencv(
118
+ image: Union[str, Image.Image, np.ndarray],
119
+ mode: str = 'RGB'
120
+ ) -> np.ndarray:
121
+ """
122
+ The 'image' is read as a RGB numpy array and then
123
+ transformed into an BGR numpy array because Opencv
124
+ uses BGR format.
125
+
126
+ Result is in BGR format.
127
+ """
128
+ return ImageParser.to_numpy(image, mode)[:, :, ::-1]
File without changes
@@ -0,0 +1,248 @@
1
+ from yta_image_base.parser import ImageParser
2
+ from yta_image_base.color.picker import PixelFilterFunction
3
+ from yta_validation.parameter import ParameterValidator
4
+ from yta_general_utils.region import Region
5
+ from typing import Union
6
+ from PIL import Image
7
+
8
+ import numpy as np
9
+ import cv2
10
+
11
+
12
+ MIN_REGION_SIZE = (1920 / 20, 1080 / 20)
13
+ """
14
+ The minimum size a region must have to be accepted
15
+ as a valid region.
16
+ """
17
+
18
+ class ImageRegionFinder:
19
+
20
+ DIRECTIONS = [
21
+ (-1, 0),
22
+ (1, 0),
23
+ (0, -1),
24
+ (0, 1),
25
+ (-1, -1),
26
+ (-1, 1),
27
+ (1, -1),
28
+ (1, 1)
29
+ ]
30
+ """
31
+ The directions to move while searching, that are
32
+ the 8 possible options: up, up-right, right, etc.,
33
+ expressed as (x, y) tuples.
34
+ """
35
+
36
+ @staticmethod
37
+ def is_valid(
38
+ position: tuple[int, int],
39
+ image,
40
+ visited,
41
+ filter_func: callable
42
+ ) -> bool:
43
+ """
44
+ This method verifies if the pixel is between the limits
45
+ and fits the filter and is unvisited.
46
+ """
47
+ rows, cols, _ = image.shape
48
+
49
+ return (
50
+ 0 <= position[0] < rows and
51
+ 0 <= position[1] < cols and
52
+ not visited[position[0], position[1]] and
53
+ filter_func(image[position[0], position[1]])
54
+ )
55
+
56
+ @staticmethod
57
+ def _dfs(
58
+ image: np.ndarray,
59
+ visited,
60
+ x,
61
+ y,
62
+ region,
63
+ filter_func: callable
64
+ ):
65
+ """
66
+ A Deep First Search algorithm applied to the
67
+ image to obtain all the pixels connected within
68
+ a region.
69
+ """
70
+ if not isinstance(image, np.ndarray):
71
+ raise Exception('The provided "image" parameter is not a valid np.ndarray.')
72
+
73
+ stack = [(x, y)]
74
+ visited[x, y] = True
75
+ region.append((x, y))
76
+
77
+ while stack:
78
+ current_x, current_y = stack.pop()
79
+ for direction_x, direction_y in ImageRegionFinder.DIRECTIONS:
80
+ new_x, new_y = current_x + direction_x, current_y + direction_y
81
+ if ImageRegionFinder.is_valid((new_x, new_y), image, visited, filter_func):
82
+ visited[new_x, new_y] = True
83
+ region.append((new_x, new_y))
84
+ stack.append((new_x, new_y))
85
+
86
+ @staticmethod
87
+ def is_inside(
88
+ small_bounds,
89
+ large_bounds
90
+ ):
91
+ """
92
+ This method verifies if the bounds of a found region are
93
+ inside another bounds to discard the smaller regions.
94
+ """
95
+ min_x_small, max_x_small, min_y_small, max_y_small = small_bounds
96
+ min_x_large, max_x_large, min_y_large, max_y_large = large_bounds
97
+
98
+ return (
99
+ min_x_small >= min_x_large and max_x_small <= max_x_large and
100
+ min_y_small >= min_y_large and max_y_small <= max_y_large
101
+ )
102
+
103
+ @staticmethod
104
+ def find_regions(
105
+ image: np.ndarray,
106
+ filter_func: PixelFilterFunction
107
+ ) -> list[Region]:
108
+ """
109
+ This method looks for all the existing regions of transparent
110
+ pixels that are connected ones to the others (neighbours). The
111
+ 'filter_func' parameter is the one that will classify the pixels
112
+ as, for example, transparent or green. That 'filter_func' must
113
+ be a method contained in the PixelFilterFunction class.
114
+
115
+ This method returns the found regions as objects with 'top_left'
116
+ and 'bottom_right' fields that are arrays of [x, y] positions
117
+ corresponding to the corners of the found regions.
118
+ """
119
+ ParameterValidator.validate_mandatory_numpy_array('image', image)
120
+
121
+ rows, cols, _ = image.shape
122
+ visited = np.zeros((rows, cols), dtype=bool)
123
+ regions = []
124
+
125
+ for row in range(rows):
126
+ for col in range(cols):
127
+ # If we find a transparent pixel, we search
128
+ if filter_func(image[row, col]) and not visited[row, col]:
129
+ region = []
130
+ ImageRegionFinder._dfs(image, visited, row, col, region, filter_func)
131
+
132
+ # TODO: What is this for? Maybe just a break
133
+ # if len(region) = 10 (?)
134
+ for i, _ in enumerate(region):
135
+ if i == 10:
136
+ break
137
+
138
+ if region:
139
+ min_x = min(px[0] for px in region)
140
+ max_x = max(px[0] for px in region)
141
+ min_y = min(px[1] for px in region)
142
+ max_y = max(px[1] for px in region)
143
+
144
+ # These are the limits of the region, calculated as a
145
+ # rectangle that wraps the whole region, but not the
146
+ # real region and limits
147
+ bounds = (min_x, max_x, min_y, max_y)
148
+
149
+ # We need to avoid small regions contained in others and
150
+ # also use a minimum size to accept a region as valid
151
+ if (
152
+ max_x - min_x >= MIN_REGION_SIZE[0] and
153
+ max_y - min_y >= MIN_REGION_SIZE[1] and
154
+ not any(
155
+ ImageRegionFinder.is_inside(bounds, r['bounds'])
156
+ for r in regions
157
+ )
158
+ ):
159
+ regions.append({
160
+ 'bounds': bounds,
161
+ 'coordinates': region
162
+ })
163
+
164
+ # I want another format, so:
165
+ for index, region in enumerate(regions):
166
+ regions[index] = Region(region['bounds'][2], region['bounds'][0], region['bounds'][3], region['bounds'][1], region['coordinates'])
167
+
168
+ return regions
169
+
170
+ @staticmethod
171
+ def find_green_regions(
172
+ image: Union[str, Image.Image, np.ndarray]
173
+ ) -> list[Region]:
174
+ """
175
+ Get the green regions that exist in the provided
176
+ 'image' (if existing), ignoring the ones whose
177
+ dimensions are below the MIN_REGION_SIZE constant.
178
+ """
179
+ image = ImageParser.to_numpy(image, 'RGB')
180
+
181
+ return ImageRegionFinder.find_regions(image, PixelFilterFunction.is_green)
182
+
183
+ @staticmethod
184
+ def find_transparent_regions(
185
+ image: Union[str, Image.Image, np.ndarray]
186
+ ) -> list[Region]:
187
+ """
188
+ Get the transparent (alpha) regions that exist in
189
+ the provided 'image' (if existing), ignoring the
190
+ ones whose dimensions are below the MIN_REGION_SIZE
191
+ constant.
192
+ """
193
+ image = ImageParser.to_numpy(image, 'RGBA')
194
+
195
+ return ImageRegionFinder.find_regions(image, PixelFilterFunction.is_transparent)
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
204
+
205
+ # TODO: This below need work, is using a HSV mask to
206
+ # recognize regions, but this is actually a new class
207
+ # to bring functionality from 'yta_general_utils'
208
+
209
+ class RegionFinderTest:
210
+
211
+ @staticmethod
212
+ def detect_regions(image, low_range, high_range):
213
+ # Convertir la imagen de BGR a HSV
214
+ imagen_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
215
+
216
+ # Crear una máscara con los píxeles que están dentro del rango de color
217
+ mascara = cv2.inRange(imagen_hsv, low_range, high_range)
218
+
219
+ # Encontrar los contornos de las regiones detectadas
220
+ contornos, _ = cv2.findContours(mascara, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
221
+
222
+ # Dibujar los contornos sobre la imagen original
223
+ imagen_contornos = image.copy()
224
+ for contorno in contornos:
225
+ if cv2.contourArea(contorno) > 100: # Filtrar contornos pequeños
226
+ cv2.drawContours(imagen_contornos, [contorno], -1, (0, 255, 0), 2)
227
+
228
+ return imagen_contornos, mascara
229
+
230
+ @staticmethod
231
+ def test():
232
+ imagen = cv2.imread('imagen.jpg')
233
+
234
+ # Definir el rango de color en HSV (por ejemplo, un verde brillante)
235
+ rango_bajo = np.array([35, 50, 50]) # El valor mínimo del verde en HSV
236
+ rango_alto = np.array([85, 255, 255]) # El valor máximo del verde en HSV
237
+
238
+ # Llamar a la función para detectar las regiones del color
239
+ imagen_contornos, mascara = RegionFinderTest.detect_regions(imagen, rango_bajo, rango_alto)
240
+
241
+ # Mostrar los resultados
242
+ cv2.imshow('Regiones Detectadas', imagen_contornos)
243
+ cv2.imshow('Máscara', mascara)
244
+
245
+ # Esperar a que se presione una tecla para cerrar las ventanas
246
+ cv2.waitKey(0)
247
+ cv2.destroyAllWindows()
248
+
yta_image_base/size.py ADDED
@@ -0,0 +1,135 @@
1
+ from yta_image_base.parser import ImageParser
2
+ # TODO: Get this method from other library
3
+ from yta_multimedia.utils.resize import get_cropping_points_to_keep_aspect_ratio
4
+ from yta_constants.file import FileExtension
5
+ from yta_programming.output import Output
6
+ # TODO: I should avoid using this FileReturn
7
+ from yta_general_utils.dataclasses import FileReturn
8
+ from PIL import Image
9
+ from typing import Union
10
+
11
+ import cv2
12
+
13
+
14
+ class ImageResizer:
15
+ """
16
+ Class to resize images.
17
+ """
18
+
19
+ @staticmethod
20
+ def resize(
21
+ image: Union[str, any],
22
+ size: tuple[int, int],
23
+ output_filename: Union[str, None] = None
24
+ ) -> FileReturn:
25
+ """
26
+ Resizes the image to the provided 'size' by cropping a
27
+ region of the given 'image' that fits the 'size' aspect
28
+ ratio and resizing that region to the 'size'.
29
+
30
+ This method is using the whole image and then resizing,
31
+ so the quality of the image is preserved and no small
32
+ regions are used. The most part of the image is
33
+ preserved.
34
+
35
+ This method returns the image modified.
36
+
37
+ This method will write the image if 'output_filename' is
38
+ provided.
39
+ """
40
+ image = ImageParser.to_pillow(image)
41
+
42
+ # TODO: Maybe move the method to another library (?)
43
+ top_left, bottom_right = get_cropping_points_to_keep_aspect_ratio(image.size, size)
44
+ image = image.crop((top_left[0], top_left[1], bottom_right[0], bottom_right[1]))
45
+ image = image.resize(size)
46
+
47
+ if output_filename is not None:
48
+ output_filename = Output.get_filename(output_filename, FileExtension.PNG)
49
+ image.save(output_filename)
50
+
51
+ return FileReturn(
52
+ image,
53
+ output_filename
54
+ )
55
+
56
+
57
+
58
+
59
+ # TODO: This below is so raw... remove if
60
+ # no longer used and replaceable with others
61
+ def resize_scaling(image_filename, width, height, output_filename = None):
62
+ """
63
+ Resizes the provided 'image_filename' to the provided 'width' and 'height' keeping the
64
+ aspect ratio. This method enlarges the image to fit the desired size and then makes a
65
+ crop to obtain that size from the center of the resized image. If 'output_filename' is
66
+ provided, the image is saved locally with that name.
67
+ """
68
+ image = Image.open(image_filename)
69
+ image_width, image_height = image.size
70
+
71
+ if image_width == width and image_height == height:
72
+ return image.save(output_filename)
73
+
74
+ aspect_ratio = image_width / image_height
75
+ if aspect_ratio > (width / height):
76
+ # Image is very horizontal, so width changes faster, we need to focus on height
77
+ factor = (height * 100 / image_height) / 100
78
+ image_width = int(image_width * factor)
79
+ image_height = height
80
+ else:
81
+ # Image is very vertical, so height changes faster, we need to focus on width
82
+ factor = (width * 100 / image_width) / 100
83
+ image_width = 1920
84
+ image_height = int(image_height * factor)
85
+ image = image.resize((image_width, image_height))
86
+
87
+ # We will crop form the center to edges
88
+ left = 0
89
+ right = width
90
+ top = 0
91
+ bottom = height
92
+ if image_width > width:
93
+ # If it is 1960 => leave [0, 20], get [20, 1940], leave [1940, 1960]
94
+ margin = int((image_width - width) / 2)
95
+ left = 0 + margin
96
+ right = image_width - margin
97
+ # We make and adjustment if some pixel left
98
+ while (right - left) > width:
99
+ right -= 1
100
+ while (right - left) < width:
101
+ if left > 0:
102
+ left -= 1
103
+ else:
104
+ right += 1
105
+ if image_height > height:
106
+ # If it is 1140 => leave [0, 30], get [30, 1110], leave [1110, 1140]
107
+ margin = int((image_height - height) / 2)
108
+ top = 0 + margin
109
+ bottom = image_height - margin
110
+ # We make and adjustment if some pixel left
111
+ while (bottom - top) > height:
112
+ bottom -= 1
113
+ while (bottom - top) < height:
114
+ if top > 0:
115
+ top -= 1
116
+ else:
117
+ bottom += 1
118
+
119
+ image = image.crop((left, top, right, bottom))
120
+ # Image that is 1920x1080 and is the center of the original image
121
+ if output_filename:
122
+ image.save(output_filename)
123
+
124
+ return image
125
+
126
+ def resize_without_scaling(image_filename, width = 1920, height = 1080):
127
+ """
128
+ This method gets an image, resizes it and overwrites the original one.
129
+
130
+ TODO: This method need work.
131
+ """
132
+ # TODO: We resize it simply, we don't care about scale
133
+ image = cv2.imread(image_filename)
134
+ resized_image = cv2.resize(image, dsize = (width, height), interpolation = cv2.INTER_CUBIC)
135
+ cv2.imwrite(image_filename, resized_image)
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: yta-image-base
3
+ Version: 0.0.1
4
+ Summary: Youtube Autonomous Image Base Module.
5
+ Author: danialcala94
6
+ Author-email: danielalcalavalera@gmail.com
7
+ Requires-Python: ==3.9
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Requires-Dist: numpy (>=1.23.5,<2.0.0)
11
+ Requires-Dist: opencv-python (>=4.11.0.86,<5.0.0.0)
12
+ Requires-Dist: pillow (>=9.5.0,<10.0.0)
13
+ Requires-Dist: yta_constants (>=0.0.1,<1.0.0)
14
+ Requires-Dist: yta_file (>=0.0.1,<1.0.0)
15
+ Requires-Dist: yta_programming (>=0.0.1,<1.0.0)
16
+ Requires-Dist: yta_temp (>=0.0.1,<1.0.0)
17
+ Requires-Dist: yta_validation (>=0.0.1,<1.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Youtube Autonomous Audio Editor Module
21
+
22
+ The Audio editor module.
23
+
24
+ Please, check the 'pyproject.toml' file to see the dependencies.
@@ -0,0 +1,16 @@
1
+ yta_image_base/__init__.py,sha256=adJ0wH4CVSHCXwlvbcl7BMV9Xy8JuEsLgYjBr3uIGug,8715
2
+ yta_image_base/background.py,sha256=8Vbe_fcaKrW2E5w3zf4URN5iUlU4VEjxzh3Gp2bDpSY,3136
3
+ yta_image_base/color/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ yta_image_base/color/picker.py,sha256=uPgFQVS4dE36EF_bxJsFxhqe8Z9M1xR_YpDDtU_1mHA,5712
5
+ yta_image_base/converter.py,sha256=83fK4yMarkQ4hEkBMzzvKrozyFfmlUpl6yzspB3YAuY,9222
6
+ yta_image_base/edition/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ yta_image_base/edition/editor.py,sha256=fRd3qZInOUfPyMbzktc877iqeSVrTZw43f4EhfBwdzE,13477
8
+ yta_image_base/edition/settings.py,sha256=N3NSxlc7NBZbB9R-Uv8GkgJU_TvjNTSgpw5bXF11YDc,577
9
+ yta_image_base/parser.py,sha256=5GGDBxpqyjzM95sGtxaQ259LnCmYxxs1BJtvkhDuMj0,5090
10
+ yta_image_base/region/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ yta_image_base/region/finder.py,sha256=-R2EV0sw-74ltEeAbO3VLIjzfWKGWNQdvsVb_ucSI8U,8859
12
+ yta_image_base/size.py,sha256=q_Q26CV_iVsb1yWDfCnDANlJ-zTNR-b1Ac-j8207WGE,4858
13
+ yta_image_base-0.0.1.dist-info/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
14
+ yta_image_base-0.0.1.dist-info/METADATA,sha256=wpac1WKMTa6_Jkz3MOQrWq66QX2CDYISDjgj0h9f0W0,818
15
+ yta_image_base-0.0.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
16
+ yta_image_base-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any