rsheet 0.1.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.
- rsheet-0.1.0/LICENSE +21 -0
- rsheet-0.1.0/PKG-INFO +12 -0
- rsheet-0.1.0/README.md +273 -0
- rsheet-0.1.0/pyproject.toml +10 -0
- rsheet-0.1.0/rsheet/__init__.py +90 -0
- rsheet-0.1.0/rsheet/_fastops_fallback.py +52 -0
- rsheet-0.1.0/rsheet/_scipy_ops.py +46 -0
- rsheet-0.1.0/rsheet/animation.py +194 -0
- rsheet-0.1.0/rsheet/baking.py +327 -0
- rsheet-0.1.0/rsheet/cache_utils.py +46 -0
- rsheet-0.1.0/rsheet/config.py +83 -0
- rsheet-0.1.0/rsheet/coords_io.py +112 -0
- rsheet-0.1.0/rsheet/default_vocab.txt +65 -0
- rsheet-0.1.0/rsheet/detection.py +229 -0
- rsheet-0.1.0/rsheet/normalizer.py +312 -0
- rsheet-0.1.0/rsheet/parallel.py +62 -0
- rsheet-0.1.0/rsheet/sprite_editor.py +105 -0
- rsheet-0.1.0/rsheet/vfx.py +75 -0
- rsheet-0.1.0/rsheet/vocab.py +98 -0
- rsheet-0.1.0/rsheet.egg-info/PKG-INFO +12 -0
- rsheet-0.1.0/rsheet.egg-info/SOURCES.txt +24 -0
- rsheet-0.1.0/rsheet.egg-info/dependency_links.txt +1 -0
- rsheet-0.1.0/rsheet.egg-info/requires.txt +4 -0
- rsheet-0.1.0/rsheet.egg-info/top_level.txt +1 -0
- rsheet-0.1.0/setup.cfg +4 -0
- rsheet-0.1.0/setup.py +23 -0
rsheet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ELiijah-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rsheet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rsheet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Boîte à outils générique pour sprite sheets et animations 2D.
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: Pillow
|
|
9
|
+
Requires-Dist: scipy
|
|
10
|
+
Requires-Dist: pygame
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
Dynamic: requires-python
|
rsheet-0.1.0/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
**Rsheet** is a generic Python toolkit for 2D animation (background
|
|
4
|
+
removal, sprite sheet splitting, size normalization, animation
|
|
5
|
+
playback). It knows nothing about your specific game: it works just
|
|
6
|
+
as well for a fighting game, a platformer, a physics simulation... any
|
|
7
|
+
2D project that needs to animate sprites.
|
|
8
|
+
|
|
9
|
+
**Why "Rsheet"?** It all starts with the sprite **sheet**: Rsheet
|
|
10
|
+
analyzes it to automatically find where the frames are, in a few
|
|
11
|
+
seconds, so you don't have to do it by hand.
|
|
12
|
+
|
|
13
|
+
This README follows the full pipeline, in the order you'll use it on
|
|
14
|
+
a real project: **1) clean the background, 2) split the frames,
|
|
15
|
+
3) normalize the sizes, 4) play the animation**. All screenshots use
|
|
16
|
+
the same example image, `penguin_walk.png`:
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install rsheet
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
No local compilation is needed (the only heavy computation,
|
|
27
|
+
background detection, is delegated to `scipy`, distributed as
|
|
28
|
+
precompiled wheels).
|
|
29
|
+
|
|
30
|
+
## Try it now
|
|
31
|
+
|
|
32
|
+
Clone the repo and run the example — it's ready to go, no setup
|
|
33
|
+
needed:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/ELiijah-dev/rsheet.git
|
|
37
|
+
cd rsheet/examples
|
|
38
|
+
python demo.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This opens a real pygame window playing a normalized, background-free
|
|
42
|
+
animation, built end to end from a raw sprite sheet.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Demo — what you'll see in your console
|
|
47
|
+
|
|
48
|
+
This is exactly the script from step 4 below, run from VS Code:
|
|
49
|
+
detection + splitting + normalization happen in a fraction of a
|
|
50
|
+
second in the terminal, then the pygame window opens with the
|
|
51
|
+
already-normalized animation ready to play.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
*(Compressed GIF for the preview — [full video with sound and detailed logs](assets/console_demo.mp4))*
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 1. `rsheet.cached_removed_bg` — isolating the character
|
|
60
|
+
|
|
61
|
+
A developer who just wants to strip a background often makes the
|
|
62
|
+
mistake of targeting *one* specific color (the corner pixel, say).
|
|
63
|
+
That breaks the moment the background has a slight gradient,
|
|
64
|
+
anti-aliasing noise, or changes from one asset to another. Rsheet
|
|
65
|
+
therefore assumes nothing about the color: it looks at the pixels
|
|
66
|
+
along the **edge** of the image, infers the two dominant colors
|
|
67
|
+
(K-means), then floods outward from those edges through everything
|
|
68
|
+
connected that resembles the background — following the actual
|
|
69
|
+
outline rather than guessing its shape ahead of time.
|
|
70
|
+
|
|
71
|
+
Concretely, `cached_removed_bg` returns a PNG with an alpha channel,
|
|
72
|
+
keeping only the character. The result is cached next to the source
|
|
73
|
+
file: the computation is only redone if the original image changes.
|
|
74
|
+
|
|
75
|
+
The `tolerance` parameter exists because "resembles" has no universal
|
|
76
|
+
answer: every sprite has its own level of noise around its edges, so
|
|
77
|
+
it's a dial to tune per project rather than a value baked into
|
|
78
|
+
Rsheet's code. The default is `40`, but **`6` is a good starting
|
|
79
|
+
point** (raise it if background residue is still visible, lower it if
|
|
80
|
+
chunks of the character disappear).
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import rsheet
|
|
84
|
+
|
|
85
|
+
png_transparent = rsheet.cached_removed_bg("penguin_walk.png", tolerance=6)
|
|
86
|
+
# -> penguin_walk._rsheet_bg_cache.png (background removed, ready to load in pygame)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Before | After (`tolerance=6`) |
|
|
90
|
+
|---|---|
|
|
91
|
+
|  |  |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 2. `rsheet.sprite_editor` — splitting and naming sprite sheets
|
|
96
|
+
|
|
97
|
+
The real problem this module solves isn't "cutting up an image" — it's
|
|
98
|
+
the time lost manually saying "this row has 6 frames, that one has 4,
|
|
99
|
+
that other one has 8". Rsheet treats this as a pure geometry problem
|
|
100
|
+
rather than a layout one: a row of non-background pixels is an
|
|
101
|
+
animation, a column of non-background pixels inside that row is a
|
|
102
|
+
frame. No grid is assumed, so it works the same on a neatly arranged
|
|
103
|
+
sheet or one full of gaps.
|
|
104
|
+
|
|
105
|
+
The random naming (`rsheet.vocab`) comes from a similar observation:
|
|
106
|
+
giving each detected animation a meaningful name is still a manual
|
|
107
|
+
task, while the code only needs a stable, unique key. Rsheet picks a
|
|
108
|
+
random name and guarantees it never collides with another already
|
|
109
|
+
used by that character — finding a free slot quickly rather than
|
|
110
|
+
choosing one yourself.
|
|
111
|
+
|
|
112
|
+
Concretely, `process_project` **automatically** detects the number of
|
|
113
|
+
rows (animations) and frames per row — you never specify a frame
|
|
114
|
+
count up front — then draws a unique name for each detected animation.
|
|
115
|
+
The result is saved to a text file (`frame_coords.txt`) which is then
|
|
116
|
+
used to build the in-game animation.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import rsheet
|
|
120
|
+
|
|
121
|
+
entries = rsheet.process_project(
|
|
122
|
+
"frame_coords.txt",
|
|
123
|
+
sheets=[
|
|
124
|
+
("penguin_walk.png", "penguin", "player"), # (file, character, role)
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
for e in entries:
|
|
129
|
+
print(e.character, e.sheet_num, list(e.animations.keys()))
|
|
130
|
+
# penguin 1 ['glide_a'] <- animation name drawn automatically
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`role` (`"player"`, `"enemy"`, anything else, or `None`) is free-form —
|
|
134
|
+
Rsheet never enforces it, it just stores it.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 3. `rsheet.normalizer` — consistent on-screen sizes
|
|
139
|
+
|
|
140
|
+
An artist never draws two frames at the exact same size — a raised
|
|
141
|
+
wing takes up a bit more space than a lowered one, there's a few
|
|
142
|
+
extra or missing pixels of empty space depending on the pose. If each
|
|
143
|
+
frame were displayed as-is, the character would seem to slightly
|
|
144
|
+
"float" or "jump" on every frame change, even while standing still.
|
|
145
|
+
|
|
146
|
+
The normalizer fixes this by computing, for each frame, its offset
|
|
147
|
+
from a common reference size — then always anchoring to the ground
|
|
148
|
+
rather than the center, so that only a character's head moves on a
|
|
149
|
+
small variation, never its feet. The result is cached and invalidated
|
|
150
|
+
by a hash of the coordinates file, because this computation only ever
|
|
151
|
+
needs redoing if the splitting changed — not on every game launch.
|
|
152
|
+
|
|
153
|
+
Concretely, `load_or_compute_norm_cache` computes this offset for
|
|
154
|
+
every frame and saves it to `frame_norm_cache.txt`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import pygame
|
|
158
|
+
import rsheet
|
|
159
|
+
|
|
160
|
+
norm = rsheet.load_or_compute_norm_cache(
|
|
161
|
+
"frame_coords.txt",
|
|
162
|
+
cache_path="frame_norm_cache.txt",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# frame_surface = the sub-image of one specific frame, cut from the
|
|
166
|
+
# sheet at the coordinates found by sprite_editor in step 2 (this is
|
|
167
|
+
# what `build_frame_cache`, in step 4, does for you automatically):
|
|
168
|
+
sheet = pygame.image.load("penguin_walk.png").convert_alpha()
|
|
169
|
+
frame_rect = entries[0].animations["glide_a"][0] # `entries` comes from step 2
|
|
170
|
+
frame_surface = sheet.subsurface(frame_rect.to_tuple()).copy()
|
|
171
|
+
|
|
172
|
+
dw, dh = norm["penguin"]["glide_a"][0]
|
|
173
|
+
surf = rsheet.apply_norm_to_surface(frame_surface, dw, dh) # ground-anchored
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> In practice, you'll almost never write this cutting logic by hand:
|
|
177
|
+
> `build_frame_cache` (step 4) does exactly this, for every frame at
|
|
178
|
+
> once.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 4. `rsheet.animation` — building and playing the animation
|
|
183
|
+
|
|
184
|
+
Steps 1 to 3 are deliberately "offline": they never touch `pygame`,
|
|
185
|
+
know nothing about a game loop, and write their result to plain text
|
|
186
|
+
files. `rsheet.animation` is the only module that bridges to the
|
|
187
|
+
runtime — it's the one that turns pixel rectangles into actual
|
|
188
|
+
`pygame.Surface` objects ready to be displayed. This separation exists
|
|
189
|
+
so the expensive computation (detection, normalization) is never
|
|
190
|
+
redone while the game is running.
|
|
191
|
+
|
|
192
|
+
`AnimationController` stays deliberately "dumb": it only knows how to
|
|
193
|
+
do one thing, advance a frame on a timer and loop — no fighting-game,
|
|
194
|
+
platformer, or other gameplay logic gets mixed in, so it fits any
|
|
195
|
+
project. `Entity` goes one step further by adding minimal physics
|
|
196
|
+
(gravity, jumping, movement) because that's such a common need it was
|
|
197
|
+
worth providing ready-made — but it stays optional: a project that
|
|
198
|
+
already has its own physics can use only `AnimationController` and
|
|
199
|
+
ignore `Entity`.
|
|
200
|
+
|
|
201
|
+
Concretely, `build_frame_cache` loads the sprite sheet as a
|
|
202
|
+
`pygame.Surface`, cuts each frame at the right spot and automatically
|
|
203
|
+
applies the normalization computed in step 3 — you get back
|
|
204
|
+
`{animation_name: [surfaces...]}` directly, ready to play with
|
|
205
|
+
`AnimationController`:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
import pygame
|
|
209
|
+
import rsheet
|
|
210
|
+
|
|
211
|
+
pygame.init()
|
|
212
|
+
screen = pygame.display.set_mode((640, 360))
|
|
213
|
+
pygame.display.set_caption("Rsheet demo")
|
|
214
|
+
clock = pygame.time.Clock()
|
|
215
|
+
|
|
216
|
+
# Full pipeline (steps 1 to 3) — run once to prepare the files
|
|
217
|
+
png_transparent = rsheet.cached_removed_bg("penguin_walk.png", tolerance=6)
|
|
218
|
+
entries = rsheet.process_project(
|
|
219
|
+
"frame_coords.txt",
|
|
220
|
+
sheets=[(png_transparent, "penguin", "player")],
|
|
221
|
+
)
|
|
222
|
+
anim_name = list(entries[-1].animations.keys())[0] # actual name generated in step 2
|
|
223
|
+
sheet_num = entries[-1].sheet_num
|
|
224
|
+
rsheet.load_or_compute_norm_cache("frame_coords.txt", cache_path="frame_norm_cache.txt")
|
|
225
|
+
|
|
226
|
+
# Step 4 — build and play the animation
|
|
227
|
+
frames = rsheet.build_frame_cache("penguin", {sheet_num: png_transparent}, "frame_coords.txt")
|
|
228
|
+
anim = rsheet.AnimationController(frames=frames)
|
|
229
|
+
anim.play(anim_name)
|
|
230
|
+
|
|
231
|
+
x, y = 270, 115
|
|
232
|
+
running = True
|
|
233
|
+
while running:
|
|
234
|
+
for event in pygame.event.get():
|
|
235
|
+
if event.type == pygame.QUIT:
|
|
236
|
+
running = False
|
|
237
|
+
|
|
238
|
+
dt = clock.tick(60) / 1000
|
|
239
|
+
anim.update(dt)
|
|
240
|
+
|
|
241
|
+
screen.fill((40, 40, 40))
|
|
242
|
+
screen.blit(anim.current_surface(), (x, y))
|
|
243
|
+
pygame.display.flip()
|
|
244
|
+
|
|
245
|
+
pygame.quit()
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This script opens a real window and plays the animation on loop until
|
|
249
|
+
closed — copy-pasteable as-is (just put `penguin_walk.png` next to the
|
|
250
|
+
script, or swap in your own sprite sheet). This exact script is also
|
|
251
|
+
available ready to run in `examples/demo.py` — see [Try it now](#try-it-now)
|
|
252
|
+
above.
|
|
253
|
+
|
|
254
|
+
For an entity with simple physics (movement, gravity, jumping),
|
|
255
|
+
`Entity` embeds an `AnimationController` directly — replace the last
|
|
256
|
+
two lines of the loop above with:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
hero = rsheet.Entity(anim=anim)
|
|
260
|
+
hero.move(1) # moves to the right
|
|
261
|
+
hero.update(dt, ground_y=300) # physics + animation updated together
|
|
262
|
+
screen.blit(anim.current_surface(), (hero.x, hero.y))
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## What else is in there?
|
|
268
|
+
|
|
269
|
+
Rsheet also includes `rsheet.vfx` (generic light glow) and
|
|
270
|
+
`rsheet.baking` (animation pre-computation, facing-direction handling,
|
|
271
|
+
surface cropping/resizing) — useful once the 4 building blocks above
|
|
272
|
+
are in place, but not essential to get started. See each module's
|
|
273
|
+
docstrings for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rsheet"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Boîte à outils générique pour sprite sheets et animations 2D."
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = ["numpy", "Pillow", "scipy", "pygame"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rsheet — boîte à outils générique pour sprite sheets et animations 2D.
|
|
3
|
+
|
|
4
|
+
Usage typique :
|
|
5
|
+
|
|
6
|
+
from rsheet.vocab import AnimVocabulary
|
|
7
|
+
from rsheet.sprite_editor import process_project
|
|
8
|
+
|
|
9
|
+
process_project(
|
|
10
|
+
"frame_coords.txt",
|
|
11
|
+
sheets=[
|
|
12
|
+
("hero_1.png", "hero", "player"),
|
|
13
|
+
("hero_2.png", "hero", "player"),
|
|
14
|
+
("golem_1.png", "golem", "enemy"),
|
|
15
|
+
],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from rsheet.animation import build_frame_cache, AnimationController, Entity
|
|
19
|
+
from rsheet.config import RsheetConfig
|
|
20
|
+
|
|
21
|
+
cfg = RsheetConfig.load("game_config.txt")
|
|
22
|
+
frames = build_frame_cache("hero", {1: "hero_1.png", 2: "hero_2.png"},
|
|
23
|
+
"frame_coords.txt", "game_config.txt")
|
|
24
|
+
|
|
25
|
+
hero = Entity(config=cfg)
|
|
26
|
+
hero.anim.frames = frames
|
|
27
|
+
hero.anim.config = cfg
|
|
28
|
+
hero.anim.play("idle")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from .vocab import AnimVocabulary, VocabExhaustedError
|
|
32
|
+
from .config import RsheetConfig
|
|
33
|
+
from .coords_io import SheetEntry, load_coords, save_coords
|
|
34
|
+
from .detection import analyze_sheet, FrameRect, DetectedRow
|
|
35
|
+
from .sprite_editor import add_sheet, process_project
|
|
36
|
+
from .normalizer import (
|
|
37
|
+
load_or_compute_norm_cache,
|
|
38
|
+
apply_norm_to_surface,
|
|
39
|
+
load_norm_cache_file,
|
|
40
|
+
save_norm_cache_file,
|
|
41
|
+
)
|
|
42
|
+
from . import vfx
|
|
43
|
+
from . import baking
|
|
44
|
+
from .animation import AnimationController, Entity, build_frame_cache
|
|
45
|
+
from .cache_utils import cached_removed_bg, cache_path_for
|
|
46
|
+
from .parallel import precompute_backgrounds_parallel
|
|
47
|
+
from .vfx import add_glow
|
|
48
|
+
from .baking import (
|
|
49
|
+
bake_frames, precompute_mirrored, BakedCycle, mirror_angle_for_facing,
|
|
50
|
+
BurstPauseCycle, resolve_facing_sprite, DepletingScale,
|
|
51
|
+
slice_single_row, trim_and_scale,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"vfx",
|
|
58
|
+
"baking",
|
|
59
|
+
"AnimVocabulary",
|
|
60
|
+
"VocabExhaustedError",
|
|
61
|
+
"RsheetConfig",
|
|
62
|
+
"SheetEntry",
|
|
63
|
+
"load_coords",
|
|
64
|
+
"save_coords",
|
|
65
|
+
"analyze_sheet",
|
|
66
|
+
"FrameRect",
|
|
67
|
+
"DetectedRow",
|
|
68
|
+
"add_sheet",
|
|
69
|
+
"process_project",
|
|
70
|
+
"load_or_compute_norm_cache",
|
|
71
|
+
"apply_norm_to_surface",
|
|
72
|
+
"load_norm_cache_file",
|
|
73
|
+
"save_norm_cache_file",
|
|
74
|
+
"AnimationController",
|
|
75
|
+
"Entity",
|
|
76
|
+
"build_frame_cache",
|
|
77
|
+
"cached_removed_bg",
|
|
78
|
+
"cache_path_for",
|
|
79
|
+
"precompute_backgrounds_parallel",
|
|
80
|
+
"add_glow",
|
|
81
|
+
"bake_frames",
|
|
82
|
+
"precompute_mirrored",
|
|
83
|
+
"BakedCycle",
|
|
84
|
+
"mirror_angle_for_facing",
|
|
85
|
+
"BurstPauseCycle",
|
|
86
|
+
"resolve_facing_sprite",
|
|
87
|
+
"DepletingScale",
|
|
88
|
+
"slice_single_row",
|
|
89
|
+
"trim_and_scale",
|
|
90
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fallback pur Python pour les opérations pixel-par-pixel de Rsheet.
|
|
3
|
+
Utilisé automatiquement quand scipy n'est pas disponible. Fonctionnellement
|
|
4
|
+
identique, juste plus lent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def flood_fill_bfs(arr_rgb: np.ndarray, bg1: np.ndarray, bg2: np.ndarray, tolerance: int) -> np.ndarray:
|
|
15
|
+
"""BFS depuis les 4 bords de l'image pour marquer les pixels de fond.
|
|
16
|
+
|
|
17
|
+
Retourne un masque booléen (True = pixel de fond) de même taille
|
|
18
|
+
que l'image.
|
|
19
|
+
"""
|
|
20
|
+
h, w = arr_rgb.shape[:2]
|
|
21
|
+
threshold = tolerance * 3
|
|
22
|
+
rgb = arr_rgb[:, :, :3].astype(np.int32)
|
|
23
|
+
d1 = np.abs(rgb - bg1).sum(axis=2)
|
|
24
|
+
d2 = np.abs(rgb - bg2).sum(axis=2)
|
|
25
|
+
maybe = (d1 < threshold) | (d2 < threshold)
|
|
26
|
+
|
|
27
|
+
visited = np.zeros((h, w), dtype=bool)
|
|
28
|
+
is_bg = np.zeros((h, w), dtype=bool)
|
|
29
|
+
q = deque()
|
|
30
|
+
|
|
31
|
+
for x in range(w):
|
|
32
|
+
for y in (0, h - 1):
|
|
33
|
+
if maybe[y, x] and not visited[y, x]:
|
|
34
|
+
visited[y, x] = is_bg[y, x] = True
|
|
35
|
+
q.append((y, x))
|
|
36
|
+
for y in range(h):
|
|
37
|
+
for x in (0, w - 1):
|
|
38
|
+
if maybe[y, x] and not visited[y, x]:
|
|
39
|
+
visited[y, x] = is_bg[y, x] = True
|
|
40
|
+
q.append((y, x))
|
|
41
|
+
|
|
42
|
+
dirs = ((-1, 0), (1, 0), (0, -1), (0, 1))
|
|
43
|
+
while q:
|
|
44
|
+
cy, cx = q.popleft()
|
|
45
|
+
for dy, dx in dirs:
|
|
46
|
+
ny, nx = cy + dy, cx + dx
|
|
47
|
+
if 0 <= ny < h and 0 <= nx < w and not visited[ny, nx]:
|
|
48
|
+
visited[ny, nx] = True
|
|
49
|
+
if maybe[ny, nx]:
|
|
50
|
+
is_bg[ny, nx] = True
|
|
51
|
+
q.append((ny, nx))
|
|
52
|
+
return is_bg
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flood-fill de détection de fond, version scipy.
|
|
3
|
+
|
|
4
|
+
Reformule le problème comme une recherche de composantes connexes,
|
|
5
|
+
déjà résolue par scipy.ndimage.label (écrit en C, distribué en wheels
|
|
6
|
+
précompilées pour toutes les plateformes — `pip install scipy` ne
|
|
7
|
+
compile jamais rien chez l'utilisateur).
|
|
8
|
+
|
|
9
|
+
Équivalence : un pixel est "fond" si (a) sa couleur est proche de l'une
|
|
10
|
+
des deux couleurs de fond détectées, ET (b) il appartient à une région
|
|
11
|
+
connexe (4-connectivité, comme le BFS) qui touche au moins un bord de
|
|
12
|
+
l'image.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from scipy.ndimage import label, generate_binary_structure
|
|
19
|
+
|
|
20
|
+
# 4-connectivité (haut/bas/gauche/droite), identique aux directions du BFS d'origine
|
|
21
|
+
_STRUCT = generate_binary_structure(2, 1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def flood_fill_bfs(arr_rgb: np.ndarray, bg1: np.ndarray, bg2: np.ndarray, tolerance: int) -> np.ndarray:
|
|
25
|
+
"""Retourne un masque booléen (True = pixel de fond), résultat
|
|
26
|
+
identique à un BFS depuis les 4 bords, calculé via composantes
|
|
27
|
+
connexes (scipy), sans aucune compilation requise."""
|
|
28
|
+
h, w = arr_rgb.shape[:2]
|
|
29
|
+
threshold = tolerance * 3
|
|
30
|
+
rgb = arr_rgb[:, :, :3].astype(np.int32)
|
|
31
|
+
d1 = np.abs(rgb - bg1).sum(axis=2)
|
|
32
|
+
d2 = np.abs(rgb - bg2).sum(axis=2)
|
|
33
|
+
maybe = (d1 < threshold) | (d2 < threshold)
|
|
34
|
+
|
|
35
|
+
labeled, n_labels = label(maybe, structure=_STRUCT)
|
|
36
|
+
if n_labels == 0:
|
|
37
|
+
return np.zeros((h, w), dtype=bool)
|
|
38
|
+
|
|
39
|
+
border_labels = set(labeled[0, :].tolist()) | set(labeled[-1, :].tolist())
|
|
40
|
+
border_labels |= set(labeled[:, 0].tolist()) | set(labeled[:, -1].tolist())
|
|
41
|
+
border_labels.discard(0) # 0 = pixels hors du masque `maybe`, pas une vraie composante
|
|
42
|
+
|
|
43
|
+
if not border_labels:
|
|
44
|
+
return np.zeros((h, w), dtype=bool)
|
|
45
|
+
|
|
46
|
+
return np.isin(labeled, list(border_labels))
|