tiledimage 0.3.1__tar.gz → 0.6.0__tar.gz
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.
- {tiledimage-0.3.1 → tiledimage-0.6.0}/PKG-INFO +7 -4
- {tiledimage-0.3.1 → tiledimage-0.6.0}/pyproject.toml +5 -2
- tiledimage-0.6.0/tiledimage/__init__.py +2 -0
- {tiledimage-0.3.1 → tiledimage-0.6.0}/tiledimage/cachedimage.py +63 -13
- {tiledimage-0.3.1 → tiledimage-0.6.0}/tiledimage/pngs2.py +0 -4
- tiledimage-0.6.0/tiledimage/simpleimage.py +175 -0
- {tiledimage-0.3.1 → tiledimage-0.6.0}/tiledimage/tilecache.py +4 -11
- tiledimage-0.6.0/tiledimage/tiledimage.py +207 -0
- tiledimage-0.3.1/tiledimage/__init__.py +0 -1
- tiledimage-0.3.1/tiledimage/tiledimage.py +0 -177
- {tiledimage-0.3.1 → tiledimage-0.6.0}/README.md +0 -0
- {tiledimage-0.3.1 → tiledimage-0.6.0}/tiledimage/2pngs.py +0 -0
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: tiledimage
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: tools for tiled image that can be cached on a filesystem.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: vitroid
|
|
7
7
|
Author-email: vitroid@gmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Requires-Dist: opencv-python-headless (>=4.11.0.86,<5.0.0.0)
|
|
15
15
|
Requires-Dist: pylru (>=1.2.1,<2.0.0)
|
|
16
|
+
Requires-Dist: pyperbox (>=0.1.1,<0.2.0)
|
|
17
|
+
Requires-Dist: rasterio (>=1.4.3,<2.0.0)
|
|
18
|
+
Requires-Dist: tifffile (>=2025.9.9,<2026.0.0)
|
|
16
19
|
Description-Content-Type: text/markdown
|
|
17
20
|
|
|
18
21
|
#TiledImage
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "tiledimage"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "tools for tiled image that can be cached on a filesystem."
|
|
5
5
|
authors = ["vitroid <vitroid@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
|
-
python = "^3.
|
|
10
|
+
python = "^3.11"
|
|
11
11
|
pylru = "^1.2.1"
|
|
12
12
|
opencv-python-headless = "^4.11.0.86"
|
|
13
|
+
tifffile = "^2025.9.9"
|
|
14
|
+
rasterio = "^1.4.3"
|
|
15
|
+
pyperbox = "^0.1.1"
|
|
13
16
|
|
|
14
17
|
[tool.poetry.scripts]
|
|
15
18
|
pngs2 = "tiledimage.pngs2:main"
|
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
|
5
5
|
|
|
6
6
|
from tiledimage.tiledimage import TiledImage
|
|
7
7
|
import tiledimage.tilecache as tilecache
|
|
8
|
+
from pyperbox import Rect, Range
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class CachedImage(TiledImage):
|
|
@@ -33,14 +34,42 @@ class CachedImage(TiledImage):
|
|
|
33
34
|
self.modified = False
|
|
34
35
|
if mode == "inherit":
|
|
35
36
|
# read the info.txt in the dir.
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
info
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
self.rect = None
|
|
38
|
+
try:
|
|
39
|
+
with open(f"{dir}/info.json", "r") as file:
|
|
40
|
+
info = json.load(file)
|
|
41
|
+
self.rect = Rect(
|
|
42
|
+
x_range=Range(
|
|
43
|
+
min_val=info["xrange"][0], max_val=info["xrange"][1]
|
|
44
|
+
),
|
|
45
|
+
y_range=Range(
|
|
46
|
+
min_val=info["yrange"][0], max_val=info["yrange"][1]
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
self.tilesize = info["tilesize"]
|
|
50
|
+
self.bgcolor = info["bgcolor"]
|
|
51
|
+
self.fileext = info["filetype"]
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
# backward compatibility
|
|
54
|
+
xrange = None
|
|
55
|
+
yrange = None
|
|
56
|
+
with open(f"{dir}/info.txt", "r") as file:
|
|
57
|
+
while True:
|
|
58
|
+
line = file.readline().strip()
|
|
59
|
+
if len(line) == 0:
|
|
60
|
+
break
|
|
61
|
+
cols = line.split(" ")
|
|
62
|
+
if cols[-1] == "xrange":
|
|
63
|
+
xrange = Range(min_val=int(cols[0]), max_val=int(cols[1]))
|
|
64
|
+
if cols[-1] == "yrange":
|
|
65
|
+
yrange = Range(min_val=int(cols[0]), max_val=int(cols[1]))
|
|
66
|
+
if cols[-1] == "tilesize":
|
|
67
|
+
self.tilesize = [int(cols[0]), int(cols[1])]
|
|
68
|
+
if cols[-1] == "bgcolor":
|
|
69
|
+
self.bgcolor = [int(cols[0]), int(cols[1]), int(cols[2])]
|
|
70
|
+
if cols[-1] == "filetype":
|
|
71
|
+
self.fileext = cols[-1]
|
|
72
|
+
self.rect = Rect(x_range=xrange, y_range=yrange)
|
|
44
73
|
defaulttile = np.zeros((self.tilesize[1], self.tilesize[0], 3), dtype=np.uint8)
|
|
45
74
|
self.bgcolor = np.array(self.bgcolor)
|
|
46
75
|
# logger.info("Color: {0}".format(self.bgcolor))
|
|
@@ -74,8 +103,8 @@ class CachedImage(TiledImage):
|
|
|
74
103
|
if isinstance(bgcolor, np.ndarray):
|
|
75
104
|
bgcolor = bgcolor.tolist()
|
|
76
105
|
info = dict(
|
|
77
|
-
xrange=self.
|
|
78
|
-
yrange=self.
|
|
106
|
+
xrange=self.rect.x_range.as_list(),
|
|
107
|
+
yrange=self.rect.y_range.as_list(),
|
|
79
108
|
tilesize=self.tilesize,
|
|
80
109
|
bgcolor=bgcolor,
|
|
81
110
|
filetype=self.fileext,
|
|
@@ -84,15 +113,36 @@ class CachedImage(TiledImage):
|
|
|
84
113
|
json.dump(info, file)
|
|
85
114
|
self.tiles.done() # タイルキャッシュの終了処理を呼び出す
|
|
86
115
|
|
|
87
|
-
def put_image(self, pos, img, linear_alpha=None):
|
|
88
|
-
super(CachedImage, self).put_image(pos, img, linear_alpha)
|
|
116
|
+
def put_image(self, pos, img, linear_alpha=None, full_alpha=None):
|
|
117
|
+
super(CachedImage, self).put_image(pos, img, linear_alpha, full_alpha)
|
|
89
118
|
self.modified = True
|
|
90
119
|
logger = getLogger()
|
|
91
120
|
nmiss, naccess, cachesize = self.tiles.cachemiss()
|
|
92
|
-
logger.
|
|
121
|
+
logger.debug(
|
|
93
122
|
"Cache miss {0}% @ {1} tiles".format(nmiss * 100 // naccess, cachesize)
|
|
94
123
|
)
|
|
95
124
|
self.tiles.adjust_cache_size()
|
|
96
125
|
|
|
97
126
|
def set_hook(self, hook):
|
|
98
127
|
self.tiles.set_hook(hook)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test():
|
|
131
|
+
import sys
|
|
132
|
+
import cv2
|
|
133
|
+
|
|
134
|
+
png = sys.argv[1]
|
|
135
|
+
tilesize = int(sys.argv[2])
|
|
136
|
+
with CachedImage("new", tilesize=tilesize) as tiled_image:
|
|
137
|
+
tiled_image[10:, 20:] = cv2.imread(png)
|
|
138
|
+
tiled_image[-10:, -20:] = cv2.imread(png)
|
|
139
|
+
image = tiled_image[:, :]
|
|
140
|
+
|
|
141
|
+
with CachedImage("inherit", dir="image.pngs") as tiled_image:
|
|
142
|
+
cv2.imshow("image", tiled_image.get_image(width=100))
|
|
143
|
+
cv2.waitKey(0)
|
|
144
|
+
cv2.destroyAllWindows()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
test()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
# external modules
|
|
4
|
+
import numpy as np
|
|
5
|
+
from pyperbox import Rect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimpleImage:
|
|
9
|
+
|
|
10
|
+
def __init__(self, bgcolor=(100, 100, 100)):
|
|
11
|
+
self.rect = None
|
|
12
|
+
self.bgcolor = np.array(bgcolor, dtype=np.uint8)
|
|
13
|
+
|
|
14
|
+
def __enter__(self):
|
|
15
|
+
return self
|
|
16
|
+
|
|
17
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
18
|
+
pass # TiledImageは特別なクリーンアップ処理は必要ありません
|
|
19
|
+
|
|
20
|
+
def _parse_slice(self, key):
|
|
21
|
+
"""スライスを解析してRectに変換する
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
key: スライスまたはタプル。例: (slice(0, 10), slice(0, 20))
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Rect: スライスに対応する領域
|
|
28
|
+
"""
|
|
29
|
+
if not isinstance(key, tuple) or len(key) != 2:
|
|
30
|
+
raise IndexError("2次元のスライスを指定してください")
|
|
31
|
+
|
|
32
|
+
y_slice, x_slice = key
|
|
33
|
+
if not (isinstance(y_slice, slice) and isinstance(x_slice, slice)):
|
|
34
|
+
raise IndexError("スライスを指定してください")
|
|
35
|
+
|
|
36
|
+
# スライスの開始と終了を取得
|
|
37
|
+
y_start = y_slice.start if y_slice.start is not None else 0
|
|
38
|
+
y_stop = y_slice.stop if y_slice.stop is not None else float("inf")
|
|
39
|
+
x_start = x_slice.start if x_slice.start is not None else 0
|
|
40
|
+
x_stop = x_slice.stop if x_slice.stop is not None else float("inf")
|
|
41
|
+
|
|
42
|
+
# ステップは未対応
|
|
43
|
+
if y_slice.step is not None or x_slice.step is not None:
|
|
44
|
+
raise NotImplementedError("ステップ付きスライスには未対応です")
|
|
45
|
+
|
|
46
|
+
return Rect.from_bounds(x_start, x_stop, y_start, y_stop)
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, key):
|
|
49
|
+
"""スライスで領域を取得する
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
image = tiled_image[10:20, 30:40] # 10:20行、30:40列の領域を取得
|
|
53
|
+
"""
|
|
54
|
+
region = self._parse_slice(key)
|
|
55
|
+
return self.get_region(region)
|
|
56
|
+
|
|
57
|
+
def __setitem__(self, key, value):
|
|
58
|
+
"""スライスで領域を設定する
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
tiled_image[10:20, 30:40] = image # 10:20行、30:40列の領域に画像を設定
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(value, np.ndarray):
|
|
64
|
+
raise TypeError("NumPy配列を指定してください")
|
|
65
|
+
|
|
66
|
+
region = self._parse_slice(key)
|
|
67
|
+
self.put_image((region.x_range.min_val, region.y_range.min_val), value)
|
|
68
|
+
|
|
69
|
+
def get_region(self, rect: Rect | None = None):
|
|
70
|
+
logger = logging.getLogger()
|
|
71
|
+
if rect is None:
|
|
72
|
+
rect = self.rect
|
|
73
|
+
if rect is None:
|
|
74
|
+
return
|
|
75
|
+
logger.debug(f"get_region region:{rect}")
|
|
76
|
+
# # region.x_rangeが0:や:infの場合には、それぞれself.regionのx_rangeを使用する
|
|
77
|
+
if rect.left == 0:
|
|
78
|
+
rect.x_range.min_val = self.rect.left
|
|
79
|
+
if rect.right == float("inf"):
|
|
80
|
+
rect.x_range.max_val = self.rect.right
|
|
81
|
+
if rect.top == 0:
|
|
82
|
+
rect.y_range.min_val = self.rect.top
|
|
83
|
+
if rect.bottom == float("inf"):
|
|
84
|
+
rect.y_range.max_val = self.rect.bottom
|
|
85
|
+
|
|
86
|
+
dst_width = rect.width
|
|
87
|
+
dst_height = rect.height
|
|
88
|
+
|
|
89
|
+
image = np.zeros((dst_height, dst_width, 3), dtype=np.uint8) + self.bgcolor
|
|
90
|
+
crop = self.rect & rect
|
|
91
|
+
image[
|
|
92
|
+
crop.top - rect.top : crop.bottom - rect.top,
|
|
93
|
+
crop.left - rect.left : crop.right - rect.left,
|
|
94
|
+
] = self.image[
|
|
95
|
+
crop.top - self.rect.top : crop.bottom - self.rect.top,
|
|
96
|
+
crop.left - self.rect.left : crop.right - self.rect.left,
|
|
97
|
+
]
|
|
98
|
+
return image
|
|
99
|
+
|
|
100
|
+
def put_image(self, position, image, linear_alpha=None, full_alpha=None):
|
|
101
|
+
"""
|
|
102
|
+
split the existent tiles
|
|
103
|
+
and put a big single tile.
|
|
104
|
+
the image must be larger than a single tile.
|
|
105
|
+
otherwise, a different algorithm is required.
|
|
106
|
+
"""
|
|
107
|
+
h, w = image.shape[:2]
|
|
108
|
+
rect = Rect.from_bounds(
|
|
109
|
+
position[0], position[0] + w, position[1], position[1] + h
|
|
110
|
+
)
|
|
111
|
+
# expand the canvas
|
|
112
|
+
if self.rect is None:
|
|
113
|
+
self.rect = rect
|
|
114
|
+
self.image = image.copy()
|
|
115
|
+
else:
|
|
116
|
+
newrect = self.rect | rect
|
|
117
|
+
new_image = (
|
|
118
|
+
np.zeros([newrect.height, newrect.width, 3], dtype=np.uint8)
|
|
119
|
+
+ self.bgcolor
|
|
120
|
+
)
|
|
121
|
+
new_image[
|
|
122
|
+
self.rect.top - newrect.top : self.rect.bottom - newrect.top,
|
|
123
|
+
self.rect.left - newrect.left : self.rect.right - newrect.left,
|
|
124
|
+
] = self.image
|
|
125
|
+
self.image = new_image
|
|
126
|
+
self.rect = newrect
|
|
127
|
+
|
|
128
|
+
if linear_alpha is None and full_alpha is None:
|
|
129
|
+
self.image[
|
|
130
|
+
rect.top - self.rect.top : rect.bottom - self.rect.top,
|
|
131
|
+
rect.left - self.rect.left : rect.right - self.rect.left,
|
|
132
|
+
] = image
|
|
133
|
+
else:
|
|
134
|
+
if full_alpha is not None:
|
|
135
|
+
alpha = full_alpha[:, :, np.newaxis]
|
|
136
|
+
else:
|
|
137
|
+
alpha = linear_alpha[np.newaxis, :, np.newaxis]
|
|
138
|
+
self.image[
|
|
139
|
+
rect.top - self.rect.top : rect.bottom - self.rect.top,
|
|
140
|
+
rect.left - self.rect.left : rect.right - self.rect.left,
|
|
141
|
+
:,
|
|
142
|
+
] = (
|
|
143
|
+
alpha * image
|
|
144
|
+
+ (1 - alpha)
|
|
145
|
+
* self.image[
|
|
146
|
+
rect.top - self.rect.top : rect.bottom - self.rect.top,
|
|
147
|
+
rect.left - self.rect.left : rect.right - self.rect.left,
|
|
148
|
+
:,
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def get_image(self):
|
|
153
|
+
# widthを指定すると縮小する。
|
|
154
|
+
return self.get_region(self.rect)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test():
|
|
158
|
+
import sys
|
|
159
|
+
import cv2
|
|
160
|
+
import logging
|
|
161
|
+
|
|
162
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
163
|
+
|
|
164
|
+
png = sys.argv[1]
|
|
165
|
+
with SimpleImage() as canvas:
|
|
166
|
+
canvas[20:, 40:] = cv2.imread(png)
|
|
167
|
+
canvas[10:, 20:] = cv2.imread(png)
|
|
168
|
+
image = canvas[:, :]
|
|
169
|
+
cv2.imshow("image", image)
|
|
170
|
+
cv2.waitKey(0)
|
|
171
|
+
cv2.destroyAllWindows()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
test()
|
|
@@ -47,11 +47,11 @@ class TileCache:
|
|
|
47
47
|
self.hook = hook
|
|
48
48
|
|
|
49
49
|
def key_to_filename(self, key):
|
|
50
|
-
return "{
|
|
50
|
+
return f"{self.dir}/{key[0]},{key[1]}.{self.fileext}"
|
|
51
51
|
|
|
52
52
|
def __getitem__(self, key):
|
|
53
53
|
logger = logging.getLogger()
|
|
54
|
-
logger.debug("getitem key:{
|
|
54
|
+
logger.debug(f"getitem key:{key}")
|
|
55
55
|
self.nget += 1
|
|
56
56
|
try:
|
|
57
57
|
modified, value = self.cache[key]
|
|
@@ -60,17 +60,15 @@ class TileCache:
|
|
|
60
60
|
if os.path.exists(filename):
|
|
61
61
|
value = cv2.imread(filename)
|
|
62
62
|
self.nmiss += 1
|
|
63
|
-
# logger.info("cache miss key:{0}".format(key))
|
|
64
63
|
else:
|
|
65
|
-
|
|
66
|
-
logger.info("blank key:{0}".format(key))
|
|
64
|
+
logger.info(f"blank key:{key}")
|
|
67
65
|
value = self.default
|
|
68
66
|
self.cache[key] = [False, value]
|
|
69
67
|
return value
|
|
70
68
|
|
|
71
69
|
def __setitem__(self, key, value):
|
|
72
70
|
logger = logging.getLogger()
|
|
73
|
-
logger.debug("update key:{
|
|
71
|
+
logger.debug(f"update key:{key}")
|
|
74
72
|
self.cache[key] = [True, value]
|
|
75
73
|
|
|
76
74
|
def writeback(self, key, value):
|
|
@@ -79,20 +77,15 @@ class TileCache:
|
|
|
79
77
|
"""
|
|
80
78
|
logger = logging.getLogger()
|
|
81
79
|
if value[0]:
|
|
82
|
-
# logger.info("purge key:{0}".format(key))
|
|
83
80
|
filename = self.key_to_filename(key)
|
|
84
81
|
cv2.imwrite(filename, value[1])
|
|
85
82
|
if self.hook is not None:
|
|
86
83
|
self.hook(key, value[1])
|
|
87
84
|
|
|
88
85
|
def __contains__(self, key):
|
|
89
|
-
# logger = logging.getLogger()
|
|
90
|
-
# logger.debug("Query: {0}".format(key))
|
|
91
86
|
if key in self.cache:
|
|
92
|
-
# logger.debug("On cache: {0}".format(key))
|
|
93
87
|
return True
|
|
94
88
|
filename = self.key_to_filename(key)
|
|
95
|
-
# logger.debug("On file: {0}".format(filename))
|
|
96
89
|
return os.path.exists(filename)
|
|
97
90
|
|
|
98
91
|
def done(self):
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
# external modules
|
|
4
|
+
import numpy as np
|
|
5
|
+
from pyperbox import Rect, Range
|
|
6
|
+
import cv2
|
|
7
|
+
|
|
8
|
+
from tiledimage.simpleimage import SimpleImage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TiledImage(SimpleImage):
|
|
12
|
+
"""
|
|
13
|
+
it has no size.
|
|
14
|
+
size is determined by the tiles.
|
|
15
|
+
!!! it is better to fix the tile size. (128x128, for example)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, tilesize=128, bgcolor=(100, 100, 100)):
|
|
19
|
+
super(TiledImage, self).__init__(bgcolor)
|
|
20
|
+
self.tiles = dict()
|
|
21
|
+
if type(tilesize) is int:
|
|
22
|
+
self.tilesize = (tilesize, tilesize)
|
|
23
|
+
else:
|
|
24
|
+
assert type(tilesize) is tuple
|
|
25
|
+
self.tilesize = tilesize
|
|
26
|
+
|
|
27
|
+
def tiles_containing(self, rect: Rect, includeempty=False):
|
|
28
|
+
"""
|
|
29
|
+
return the tiles containing the given region
|
|
30
|
+
"""
|
|
31
|
+
logger = logging.getLogger()
|
|
32
|
+
t = []
|
|
33
|
+
xran = (
|
|
34
|
+
rect.x_range.min_val // self.tilesize[0],
|
|
35
|
+
(rect.x_range.max_val + self.tilesize[0] - 1) // self.tilesize[0],
|
|
36
|
+
)
|
|
37
|
+
yran = (
|
|
38
|
+
rect.y_range.min_val // self.tilesize[1],
|
|
39
|
+
(rect.y_range.max_val + self.tilesize[1] - 1) // self.tilesize[1],
|
|
40
|
+
)
|
|
41
|
+
for ix in range(xran[0], xran[1]):
|
|
42
|
+
for iy in range(yran[0], yran[1]):
|
|
43
|
+
tile = (ix * self.tilesize[0], iy * self.tilesize[1])
|
|
44
|
+
logger.debug("Tile: {0}".format(tile))
|
|
45
|
+
if (tile in self.tiles) or includeempty:
|
|
46
|
+
tregion = Rect.from_bounds(
|
|
47
|
+
tile[0],
|
|
48
|
+
tile[0] + self.tilesize[0],
|
|
49
|
+
tile[1],
|
|
50
|
+
tile[1] + self.tilesize[1],
|
|
51
|
+
)
|
|
52
|
+
o = tregion & rect
|
|
53
|
+
t.append((tile, o))
|
|
54
|
+
return t
|
|
55
|
+
|
|
56
|
+
def get_region(self, rect: Rect | None = None, width: int = 0):
|
|
57
|
+
logger = logging.getLogger()
|
|
58
|
+
if rect is None:
|
|
59
|
+
rect = self.rect
|
|
60
|
+
if rect is None:
|
|
61
|
+
return
|
|
62
|
+
logger.debug(f"get_region region:{rect}")
|
|
63
|
+
# # region.x_rangeが0:や:infの場合には、それぞれself.regionのx_rangeを使用する
|
|
64
|
+
if rect.left == 0:
|
|
65
|
+
rect.x_range.min_val = self.rect.left
|
|
66
|
+
if rect.right == float("inf"):
|
|
67
|
+
rect.x_range.max_val = self.rect.right
|
|
68
|
+
if rect.top == 0:
|
|
69
|
+
rect.y_range.min_val = self.rect.top
|
|
70
|
+
if rect.bottom == float("inf"):
|
|
71
|
+
rect.y_range.max_val = self.rect.bottom
|
|
72
|
+
|
|
73
|
+
if width:
|
|
74
|
+
dst_width = int(width)
|
|
75
|
+
dst_height = int(width * rect.height) // rect.width
|
|
76
|
+
scale = width / rect.width
|
|
77
|
+
else:
|
|
78
|
+
dst_width = rect.width
|
|
79
|
+
dst_height = rect.height
|
|
80
|
+
scale = 1.0
|
|
81
|
+
|
|
82
|
+
image = np.zeros((dst_height, dst_width, 3), dtype=np.uint8)
|
|
83
|
+
image[:, :] = self.bgcolor
|
|
84
|
+
for tile, overlap in self.tiles_containing(rect):
|
|
85
|
+
if overlap is None:
|
|
86
|
+
continue
|
|
87
|
+
src = self.tiles[tile]
|
|
88
|
+
if src is None:
|
|
89
|
+
continue
|
|
90
|
+
originx, originy = tile
|
|
91
|
+
src_top = overlap.top - originy
|
|
92
|
+
src_bottom = overlap.bottom - originy
|
|
93
|
+
src_left = overlap.left - originx
|
|
94
|
+
src_right = overlap.right - originx
|
|
95
|
+
src_image = src[src_top:src_bottom, src_left:src_right]
|
|
96
|
+
|
|
97
|
+
dst_top = int((overlap.top - rect.top) * scale)
|
|
98
|
+
dst_bottom = int((overlap.bottom - rect.top) * scale)
|
|
99
|
+
dst_left = int((overlap.left - rect.left) * scale)
|
|
100
|
+
dst_right = int((overlap.right - rect.left) * scale)
|
|
101
|
+
|
|
102
|
+
src_image = cv2.resize(
|
|
103
|
+
src_image, (dst_right - dst_left, dst_bottom - dst_top)
|
|
104
|
+
)
|
|
105
|
+
image[dst_top:dst_bottom, dst_left:dst_right] = src_image
|
|
106
|
+
# dst_bottom = overlap.bottom-rect.top
|
|
107
|
+
# dst_left = overlap.left-rect.left
|
|
108
|
+
# dst_right = overlap.right-rect.left
|
|
109
|
+
# image[
|
|
110
|
+
# overlap.y_range.min_val
|
|
111
|
+
# - rect.y_range.min_val : overlap.y_range.max_val
|
|
112
|
+
# - rect.y_range.min_val,
|
|
113
|
+
# overlap.x_range.min_val
|
|
114
|
+
# - rect.x_range.min_val : overlap.x_range.max_val
|
|
115
|
+
# - rect.x_range.min_val,
|
|
116
|
+
# ] = src[
|
|
117
|
+
# overlap.y_range.min_val - originy : overlap.y_range.max_val - originy,
|
|
118
|
+
# overlap.x_range.min_val - originx : overlap.x_range.max_val - originx,
|
|
119
|
+
# ]
|
|
120
|
+
return image
|
|
121
|
+
|
|
122
|
+
def put_image(self, position, image, linear_alpha=None, full_alpha=None):
|
|
123
|
+
"""
|
|
124
|
+
split the existent tiles
|
|
125
|
+
and put a big single tile.
|
|
126
|
+
the image must be larger than a single tile.
|
|
127
|
+
otherwise, a different algorithm is required.
|
|
128
|
+
"""
|
|
129
|
+
h, w = image.shape[:2]
|
|
130
|
+
rect = Rect.from_bounds(
|
|
131
|
+
position[0], position[0] + w, position[1], position[1] + h
|
|
132
|
+
)
|
|
133
|
+
# expand the canvas
|
|
134
|
+
if self.rect is None:
|
|
135
|
+
self.rect = rect
|
|
136
|
+
else:
|
|
137
|
+
self.rect |= rect
|
|
138
|
+
for tile, overlap in self.tiles_containing(rect, includeempty=True):
|
|
139
|
+
if overlap is None:
|
|
140
|
+
continue
|
|
141
|
+
if tile not in self.tiles:
|
|
142
|
+
self.tiles[tile] = np.zeros(
|
|
143
|
+
(self.tilesize[1], self.tilesize[0], 3), dtype=np.uint8
|
|
144
|
+
)
|
|
145
|
+
self.tiles[tile][:, :] = self.bgcolor
|
|
146
|
+
src = self.tiles[tile]
|
|
147
|
+
originx, originy = tile
|
|
148
|
+
|
|
149
|
+
dy0 = overlap.top - originy
|
|
150
|
+
dy1 = overlap.bottom - originy
|
|
151
|
+
dx0 = overlap.left - originx
|
|
152
|
+
dx1 = overlap.right - originx
|
|
153
|
+
sx0 = overlap.left - rect.left
|
|
154
|
+
sx1 = overlap.right - rect.left
|
|
155
|
+
sy0 = overlap.top - rect.top
|
|
156
|
+
sy1 = overlap.bottom - rect.top
|
|
157
|
+
if linear_alpha is None and full_alpha is None:
|
|
158
|
+
src[
|
|
159
|
+
dy0:dy1,
|
|
160
|
+
dx0:dx1,
|
|
161
|
+
] = image[
|
|
162
|
+
sy0:sy1,
|
|
163
|
+
sx0:sx1,
|
|
164
|
+
]
|
|
165
|
+
else:
|
|
166
|
+
if full_alpha is not None:
|
|
167
|
+
alpha = full_alpha[:, :, np.newaxis]
|
|
168
|
+
src[dy0:dy1, dx0:dx1, :] = (
|
|
169
|
+
alpha[sy0:sy1, sx0:sx1, :] * image[sy0:sy1, sx0:sx1, :]
|
|
170
|
+
+ (1 - alpha[sy0:sy1, sx0:sx1, :]) * src[dy0:dy1, dx0:dx1, :]
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
alpha = linear_alpha[np.newaxis, :, np.newaxis]
|
|
174
|
+
src[dy0:dy1, dx0:dx1, :] = (
|
|
175
|
+
alpha[:, sx0:sx1, :] * image[sy0:sy1, sx0:sx1, :]
|
|
176
|
+
+ (1 - alpha[:, sx0:sx1, :]) * src[dy0:dy1, dx0:dx1, :]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# rewrite the item explicitly (for caching)
|
|
180
|
+
self.tiles[tile] = src
|
|
181
|
+
|
|
182
|
+
def get_image(self, width: int = 0):
|
|
183
|
+
# widthを指定すると縮小する。
|
|
184
|
+
return self.get_region(self.rect, width)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test():
|
|
188
|
+
import sys
|
|
189
|
+
import cv2
|
|
190
|
+
import logging
|
|
191
|
+
|
|
192
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
193
|
+
|
|
194
|
+
png = sys.argv[1]
|
|
195
|
+
tilesize = int(sys.argv[2])
|
|
196
|
+
with TiledImage(tilesize=tilesize) as tiled_image:
|
|
197
|
+
tiled_image[20:, 40:] = cv2.imread(png)
|
|
198
|
+
tiled_image[10:, 20:] = cv2.imread(png)
|
|
199
|
+
image = tiled_image[:, :]
|
|
200
|
+
image = tiled_image.get_image(width=100)
|
|
201
|
+
cv2.imshow("image", image)
|
|
202
|
+
cv2.waitKey(0)
|
|
203
|
+
cv2.destroyAllWindows()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
test()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.1"
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
# external modules
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
# a range is always spacified with the min and max=min+width
|
|
7
|
-
# 2d region consists of two ranges.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def overlap(r1, r2):
|
|
11
|
-
"""
|
|
12
|
-
True if the give regions (1D) overlap
|
|
13
|
-
|
|
14
|
-
there are 6 possible orders
|
|
15
|
-
( ) [ ] x
|
|
16
|
-
( [ ) ] o
|
|
17
|
-
( [ ] ) o
|
|
18
|
-
[ ( ) ] o
|
|
19
|
-
[ ( ] ) o
|
|
20
|
-
[ ] ( ) x
|
|
21
|
-
! { ) [ | ] ( }
|
|
22
|
-
ie [ ) && ( ]
|
|
23
|
-
"""
|
|
24
|
-
if r1[0] < r2[1] and r2[0] < r1[1]:
|
|
25
|
-
return max(r1[0], r2[0]), min(r1[1], r2[1])
|
|
26
|
-
return None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# It should also return the overlapping region
|
|
30
|
-
def overlap2D(r1, r2):
|
|
31
|
-
x = overlap(r1[0], r2[0])
|
|
32
|
-
if x is not None:
|
|
33
|
-
y = overlap(r1[1], r2[1])
|
|
34
|
-
if y is not None:
|
|
35
|
-
return x, y
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class TiledImage:
|
|
40
|
-
"""
|
|
41
|
-
it has no size.
|
|
42
|
-
size is determined by the tiles.
|
|
43
|
-
!!! it is better to fix the tile size. (128x128, for example)
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(self, tilesize=128, bgcolor=(100, 100, 100)):
|
|
47
|
-
self.tiles = dict()
|
|
48
|
-
if type(tilesize) is int:
|
|
49
|
-
self.tilesize = (tilesize, tilesize)
|
|
50
|
-
else:
|
|
51
|
-
assert type(tilesize) is tuple
|
|
52
|
-
self.tilesize = tilesize
|
|
53
|
-
self.region = None
|
|
54
|
-
self.bgcolor = np.array(bgcolor)
|
|
55
|
-
|
|
56
|
-
def __enter__(self):
|
|
57
|
-
return self
|
|
58
|
-
|
|
59
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
60
|
-
pass # TiledImageは特別なクリーンアップ処理は必要ありません
|
|
61
|
-
|
|
62
|
-
def tiles_containing(self, region, includeempty=False):
|
|
63
|
-
"""
|
|
64
|
-
return the tiles containing the given region
|
|
65
|
-
"""
|
|
66
|
-
logger = logging.getLogger()
|
|
67
|
-
t = []
|
|
68
|
-
xran, yran = region
|
|
69
|
-
xran = (
|
|
70
|
-
xran[0] // self.tilesize[0],
|
|
71
|
-
(xran[1] + self.tilesize[0] - 1) // self.tilesize[0],
|
|
72
|
-
)
|
|
73
|
-
yran = (
|
|
74
|
-
yran[0] // self.tilesize[1],
|
|
75
|
-
(yran[1] + self.tilesize[1] - 1) // self.tilesize[1],
|
|
76
|
-
)
|
|
77
|
-
for ix in range(xran[0], xran[1]):
|
|
78
|
-
for iy in range(yran[0], yran[1]):
|
|
79
|
-
tile = (ix * self.tilesize[0], iy * self.tilesize[1])
|
|
80
|
-
logger.debug("Tile: {0}".format(tile))
|
|
81
|
-
if (tile in self.tiles) or includeempty:
|
|
82
|
-
tregion = (
|
|
83
|
-
(tile[0], tile[0] + self.tilesize[0]),
|
|
84
|
-
(tile[1], tile[1] + self.tilesize[1]),
|
|
85
|
-
)
|
|
86
|
-
o = overlap2D(tregion, region)
|
|
87
|
-
t.append((tile, o))
|
|
88
|
-
return t
|
|
89
|
-
|
|
90
|
-
def get_region(self, region=None):
|
|
91
|
-
logger = logging.getLogger()
|
|
92
|
-
# logger.debug("Get region {0} {1}".format(region,self.tiles))
|
|
93
|
-
if region is None:
|
|
94
|
-
region = self.region
|
|
95
|
-
xrange, yrange = region
|
|
96
|
-
image = np.zeros(
|
|
97
|
-
(yrange[1] - yrange[0], xrange[1] - xrange[0], 3), dtype=np.uint8
|
|
98
|
-
)
|
|
99
|
-
image[:, :] = self.bgcolor
|
|
100
|
-
for tile, overlap in self.tiles_containing(region):
|
|
101
|
-
# logger.debug("Should get a tile at {0} {1}".format(tile,self.tiles))
|
|
102
|
-
src = self.tiles[tile]
|
|
103
|
-
originx, originy = tile
|
|
104
|
-
xr, yr = overlap
|
|
105
|
-
image[
|
|
106
|
-
yr[0] - yrange[0] : yr[1] - yrange[0],
|
|
107
|
-
xr[0] - xrange[0] : xr[1] - xrange[0],
|
|
108
|
-
:,
|
|
109
|
-
] = src[
|
|
110
|
-
yr[0] - originy : yr[1] - originy, xr[0] - originx : xr[1] - originx, :
|
|
111
|
-
]
|
|
112
|
-
return image
|
|
113
|
-
|
|
114
|
-
def put_image(self, position, image, linear_alpha=None):
|
|
115
|
-
"""
|
|
116
|
-
split the existent tiles
|
|
117
|
-
and put a big single tile.
|
|
118
|
-
the image must be larger than a single tile.
|
|
119
|
-
otherwise, a different algorithm is required.
|
|
120
|
-
"""
|
|
121
|
-
h, w = image.shape[:2]
|
|
122
|
-
xrange, yrange = (position[0], position[0] + w), (position[1], position[1] + h)
|
|
123
|
-
region = (xrange, yrange)
|
|
124
|
-
for tile, overlap in self.tiles_containing(region, includeempty=True):
|
|
125
|
-
if tile not in self.tiles:
|
|
126
|
-
self.tiles[tile] = np.zeros(
|
|
127
|
-
(self.tilesize[1], self.tilesize[0], 3), dtype=np.uint8
|
|
128
|
-
)
|
|
129
|
-
self.tiles[tile][:, :] = self.bgcolor
|
|
130
|
-
src = self.tiles[tile]
|
|
131
|
-
originx, originy = tile
|
|
132
|
-
xr, yr = overlap
|
|
133
|
-
if linear_alpha is None:
|
|
134
|
-
src[
|
|
135
|
-
yr[0] - originy : yr[1] - originy,
|
|
136
|
-
xr[0] - originx : xr[1] - originx,
|
|
137
|
-
:,
|
|
138
|
-
] = image[
|
|
139
|
-
yr[0] - yrange[0] : yr[1] - yrange[0],
|
|
140
|
-
xr[0] - xrange[0] : xr[1] - xrange[0],
|
|
141
|
-
:,
|
|
142
|
-
]
|
|
143
|
-
else:
|
|
144
|
-
dy0 = yr[0] - originy
|
|
145
|
-
dy1 = yr[1] - originy
|
|
146
|
-
dx0 = xr[0] - originx
|
|
147
|
-
dx1 = xr[1] - originx
|
|
148
|
-
sx0 = xr[0] - xrange[0]
|
|
149
|
-
sx1 = xr[1] - xrange[0]
|
|
150
|
-
sy0 = yr[0] - yrange[0]
|
|
151
|
-
sy1 = yr[1] - yrange[0]
|
|
152
|
-
src[dy0:dy1, dx0:dx1, :] = (
|
|
153
|
-
linear_alpha[sx0:sx1, :] * image[sy0:sy1, sx0:sx1, :]
|
|
154
|
-
+ (1 - linear_alpha[sx0:sx1, :]) * src[dy0:dy1, dx0:dx1, :]
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# rewrite the item explicitly (for caching)
|
|
158
|
-
self.tiles[tile] = src
|
|
159
|
-
if self.region is None:
|
|
160
|
-
self.region = (
|
|
161
|
-
(position[0], position[0] + w),
|
|
162
|
-
(position[1], position[1] + h),
|
|
163
|
-
)
|
|
164
|
-
else:
|
|
165
|
-
self.region = (
|
|
166
|
-
(
|
|
167
|
-
min(self.region[0][0], position[0]),
|
|
168
|
-
max(self.region[0][1], position[0] + w),
|
|
169
|
-
),
|
|
170
|
-
(
|
|
171
|
-
min(self.region[1][0], position[1]),
|
|
172
|
-
max(self.region[1][1], position[1] + h),
|
|
173
|
-
),
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def get_image(self):
|
|
177
|
-
return self.get_region(self.region)
|
|
File without changes
|
|
File without changes
|