tiledimage 0.2__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.
tiledimage/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,101 @@
1
+ from logging import getLogger, basicConfig, DEBUG, INFO
2
+
3
+ import json
4
+ import numpy as np
5
+
6
+ from tiledimage.tiledimage import TiledImage
7
+ import tiledimage.tilecache as tilecache
8
+
9
+
10
+ class CachedImage(TiledImage):
11
+ def __init__(
12
+ self,
13
+ mode,
14
+ dir="image.pngs",
15
+ tilesize=128,
16
+ cachesize=10,
17
+ fileext="png",
18
+ bgcolor=(0, 0, 0),
19
+ hook=None,
20
+ disposal=False,
21
+ ):
22
+ """
23
+ if mode == "new", flush the dir.
24
+ hook is a function like put_image, that is called then a tile is rewritten.
25
+ dir will be removed when disposal is True.
26
+ """
27
+ # logger = getLogger()
28
+ super(CachedImage, self).__init__(tilesize)
29
+ self.fileext = fileext
30
+ self.bgcolor = bgcolor
31
+ self.disposal = disposal
32
+ self.dir = dir
33
+ self.modified = False
34
+ if mode == "inherit":
35
+ # read the info.txt in the dir.
36
+ self.region = [None, None]
37
+ with open(f"{dir}/info.json", "r") as file:
38
+ info = json.load(file)
39
+ self.region[0] = info["xrange"]
40
+ self.region[1] = info["yrange"]
41
+ self.tilesize = info["tilesize"]
42
+ self.bgcolor = info["bgcolor"]
43
+ self.fileext = info["filetype"]
44
+ defaulttile = np.zeros((self.tilesize[1], self.tilesize[0], 3), dtype=np.uint8)
45
+ self.bgcolor = np.array(self.bgcolor)
46
+ # logger.info("Color: {0}".format(self.bgcolor))
47
+ defaulttile[:, :, :] = self.bgcolor[:3]
48
+ # logger.info("Tile: {0}".format(defaulttile))
49
+ self.tiles = tilecache.TileCache(
50
+ mode,
51
+ dir=dir,
52
+ cachesize=cachesize,
53
+ fileext=self.fileext,
54
+ default=defaulttile,
55
+ hook=hook,
56
+ )
57
+ # just for done()
58
+ self.dir = dir
59
+
60
+ def __enter__(self):
61
+ return self
62
+
63
+ def __exit__(self, exc_type, exc_val, exc_tb):
64
+ if self.modified:
65
+ self._write_info()
66
+ if self.disposal:
67
+ rmdir(self.dir)
68
+
69
+ def _write_info(self):
70
+ """
71
+ 内部実装用:情報をJSONファイルに書き出す
72
+ """
73
+ bgcolor = self.bgcolor
74
+ if isinstance(bgcolor, np.ndarray):
75
+ bgcolor = bgcolor.tolist()
76
+ info = dict(
77
+ xrange=self.region[0],
78
+ yrange=self.region[1],
79
+ tilesize=self.tilesize,
80
+ bgcolor=bgcolor,
81
+ filetype=self.fileext,
82
+ )
83
+ with open(f"{self.dir}/info.json", "w") as file:
84
+ json.dump(info, file)
85
+ self.tiles.done() # タイルキャッシュの終了処理を呼び出す
86
+
87
+ def put_image(self, pos, img, linear_alpha=None):
88
+ super(CachedImage, self).put_image(pos, img, linear_alpha)
89
+ self.modified = True
90
+ logger = getLogger()
91
+ nmiss, naccess, cachesize = self.tiles.cachemiss()
92
+ logger.info(
93
+ "Cache miss {0}% @ {1} tiles".format(nmiss * 100 // naccess, cachesize)
94
+ )
95
+ self.tiles.adjust_cache_size()
96
+
97
+ def _set_hook(self, hook):
98
+ """
99
+ 内部実装用:タイル書き換え時のフック関数を設定
100
+ """
101
+ self.tiles.set_hook(hook)
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import os
3
+ import shutil
4
+
5
+ # external modules
6
+ import cv2
7
+ import pylru # "Least Recent Used" type cache
8
+
9
+
10
+ def remove_folder(path):
11
+ # check if folder exists
12
+ if os.path.exists(path):
13
+ # remove if exists
14
+ shutil.rmtree(path)
15
+
16
+
17
+ class TileCache:
18
+ """
19
+ A tile of images that are mostly stored in files
20
+ It does not care the integrity of the image.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ mode,
26
+ dir="tileimage",
27
+ cachesize=10,
28
+ default=None,
29
+ fileext="png",
30
+ hook=None,
31
+ ):
32
+ """
33
+ Hook is a function like put_image, that is called when a tile is rewritten
34
+ """
35
+ if mode == "new":
36
+ remove_folder(dir)
37
+ os.mkdir(dir)
38
+ self.dir = dir
39
+ self.cache = pylru.lrucache(cachesize, callback=self.writeback)
40
+ self.nget = 0
41
+ self.nmiss = 0
42
+ self.fileext = fileext
43
+ self.default = default
44
+ self.hook = hook
45
+
46
+ def set_hook(self, hook):
47
+ self.hook = hook
48
+
49
+ def key_to_filename(self, key):
50
+ return "{0}/{1},{2}.{3}".format(self.dir, *key, self.fileext)
51
+
52
+ def __getitem__(self, key):
53
+ logger = logging.getLogger()
54
+ logger.debug("getitem key:{0}".format(key))
55
+ self.nget += 1
56
+ try:
57
+ modified, value = self.cache[key]
58
+ except KeyError:
59
+ filename = self.key_to_filename(key)
60
+ if os.path.exists(filename):
61
+ value = cv2.imread(filename)
62
+ self.nmiss += 1
63
+ # logger.info("cache miss key:{0}".format(key))
64
+ else:
65
+ # first access is not a "miss"
66
+ logger.info("blank key:{0}".format(key))
67
+ value = self.default
68
+ self.cache[key] = [False, value]
69
+ return value
70
+
71
+ def __setitem__(self, key, value):
72
+ logger = logging.getLogger()
73
+ logger.debug("update key:{0}".format(key))
74
+ self.cache[key] = [True, value]
75
+
76
+ def writeback(self, key, value):
77
+ """
78
+ write back when it is purged from cache
79
+ """
80
+ logger = logging.getLogger()
81
+ if value[0]:
82
+ # logger.info("purge key:{0}".format(key))
83
+ filename = self.key_to_filename(key)
84
+ cv2.imwrite(filename, value[1])
85
+ if self.hook is not None:
86
+ self.hook(key, value[1])
87
+
88
+ def __contains__(self, key):
89
+ # logger = logging.getLogger()
90
+ # logger.debug("Query: {0}".format(key))
91
+ if key in self.cache:
92
+ # logger.debug("On cache: {0}".format(key))
93
+ return True
94
+ filename = self.key_to_filename(key)
95
+ # logger.debug("On file: {0}".format(filename))
96
+ return os.path.exists(filename)
97
+
98
+ def done(self):
99
+ # purge the cached images to disk
100
+ for k in self.cache:
101
+ self.writeback(k, self.cache.peek(k)) # peek do not affect the order
102
+
103
+ def cachemiss(self):
104
+ """
105
+ report cache miss ratio
106
+ """
107
+ return self.nmiss, self.nget, self.cache.size()
108
+
109
+ def adjust_cache_size(self):
110
+ """
111
+ Automatically optimize the cache size
112
+ Should not be adjusted in the final merging process
113
+ """
114
+ percent = self.nmiss * 100 // self.nget
115
+ if percent > 50:
116
+ self.cache.addTailNode(10)
117
+ elif percent > 20:
118
+ self.cache.addTailNode(1)
@@ -0,0 +1,177 @@
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)
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.1
2
+ Name: tiledimage
3
+ Version: 0.2
4
+ Summary: tools for tiled image that can be cached on a filesystem.
5
+ License: MIT
6
+ Author: vitroid
7
+ Author-email: vitroid@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: pylru (>=1.2.1,<2.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # TiledImage
17
+
18
+ 大きな画像を効率的に扱うための Python ライブラリです。メモリ使用量を抑えながら、大きな画像をタイル(小さな断片)に分割して管理します。
19
+
20
+ ## 特徴
21
+
22
+ - 大きな画像をタイルに分割して管理
23
+ - ファイルシステム上でのキャッシュ機能
24
+ - メモリ効率の良い画像処理
25
+ - コンテキストマネージャ(`with`文)による簡単な使用
26
+
27
+ ## インストール
28
+
29
+ ```bash
30
+ pip install tiledimage
31
+ ```
32
+
33
+ ## 基本的な使い方
34
+
35
+ ### 画像の分割(PNG → PNGs)
36
+
37
+ ```python
38
+ from tiledimage import CachedImage
39
+ import cv2
40
+
41
+ # 画像を読み込み
42
+ img = cv2.imread("large_image.png")
43
+
44
+ # タイル化して保存
45
+ with CachedImage(
46
+ mode="new",
47
+ dir="output.pngs",
48
+ tilesize=(64, 64),
49
+ cachesize=10,
50
+ bgcolor=(255, 255, 255), # 背景色(白)
51
+ fileext="jpg"
52
+ ) as tiled:
53
+ tiled.put_image((0, 0), img) # 画像を配置
54
+ ```
55
+
56
+ ### 画像の結合(PNGs → PNG)
57
+
58
+ ```python
59
+ from tiledimage import CachedImage
60
+ import cv2
61
+
62
+ # タイル化された画像を読み込み
63
+ with CachedImage(mode="inherit", dir="input.pngs") as tiled:
64
+ # 全体の画像を取得
65
+ full_image = tiled.get_image()
66
+ # 保存
67
+ cv2.imwrite("combined_image.png", full_image)
68
+ ```
69
+
70
+ ### コマンドラインツール
71
+
72
+ 画像の分割:
73
+
74
+ ```bash
75
+ pngs2 input.png output.pngs
76
+ ```
77
+
78
+ 画像の結合:
79
+
80
+ ```bash
81
+ 2pngs input.pngs output.png
82
+ ```
83
+
84
+ ## API リファレンス
85
+
86
+ ### CachedImage
87
+
88
+ メインのクラス。タイル化された画像を管理します。
89
+
90
+ ```python
91
+ CachedImage(
92
+ mode, # "new" または "inherit"
93
+ dir="image.pngs", # タイルの保存ディレクトリ
94
+ tilesize=128, # タイルのサイズ(整数またはタプル)
95
+ cachesize=10, # キャッシュサイズ
96
+ fileext="png", # タイルのファイル形式
97
+ bgcolor=(0,0,0), # 背景色
98
+ hook=None, # タイル書き換え時のフック関数
99
+ disposal=False # 終了時にディレクトリを削除するか
100
+ )
101
+ ```
102
+
103
+ #### 主要メソッド
104
+
105
+ - `put_image(pos, img, linear_alpha=None)`: 画像を配置
106
+ - `get_image()`: 全体の画像を取得
107
+ - `write_info()`: 情報を保存(通常は自動的に呼ばれる)
108
+
109
+ ### TiledImage
110
+
111
+ 基本的なタイル画像クラス。キャッシュ機能はありません。
112
+
113
+ ```python
114
+ TiledImage(
115
+ tilesize=128, # タイルのサイズ
116
+ bgcolor=(100,100,100) # 背景色
117
+ )
118
+ ```
119
+
120
+ ## 開発者向け情報
121
+
122
+ ### テスト
123
+
124
+ ```bash
125
+ make test
126
+ ```
127
+
128
+ ### ビルド
129
+
130
+ ```bash
131
+ make build
132
+ ```
133
+
134
+ ### デプロイ
135
+
136
+ ```bash
137
+ make deploy
138
+ ```
139
+
140
+ ## ライセンス
141
+
142
+ MIT License
143
+
144
+ ## 作者
145
+
146
+ Masakazu Matsumoto (vitroid@gmail.com)
147
+
@@ -0,0 +1,8 @@
1
+ tiledimage/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
2
+ tiledimage/cachedimage.py,sha256=Mbxpl56dZTYiUENnqO7F6pI-0q5wvbsyUt0Gq0Z8j94,3239
3
+ tiledimage/tilecache.py,sha256=gl1kzUJUfipS7tzfWoCxktFrvb5jsXoe2p24zYwh068,3413
4
+ tiledimage/tiledimage.py,sha256=UslrUDLJlCapQRmcVmUMsCsnqY4jbmNk0tkLCpl0_SY,5823
5
+ tiledimage-0.2.dist-info/METADATA,sha256=EQctWB4Oo0JdfxPi__K4als7zGfx0b5q-ze25lkLcoI,3178
6
+ tiledimage-0.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
7
+ tiledimage-0.2.dist-info/entry_points.txt,sha256=RiJU6UjfqclD9HizScQ4nX6PbysR7Tybr2DrKv0AXTQ,53
8
+ tiledimage-0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ 2pngs=2pngs:main
3
+ pngs2=pngs2:main
4
+