forked from mypaint/mypaint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmorphology.py
127 lines (102 loc) · 4.35 KB
/
morphology.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# This file is part of MyPaint.
# Copyright (C) 2018-2019 by the MyPaint Development Team.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""This module implements tile-based morphological operations;
dilation, erosion and blur
"""
import logging
import lib.mypaintlib as myplib
import lib.fill_common as fc
from lib.fill_common import _FULL_TILE, _EMPTY_TILE
N = myplib.TILE_SIZE
logger = logging.getLogger(__name__)
def adjacent_tiles(tile_coord, filled):
""" Return a tuple of tiles adjacent to the input tile coordinate.
Adjacent tiles that are not in the tileset are replaced by the empty tile.
"""
return tuple([filled.get(c, _EMPTY_TILE) for c in fc.adjacent(tile_coord)])
def complement_adjacent(tiles):
""" Ensure that each tile in the input tileset has a full neighbourhood
of eight tiles, setting missing tiles to the empty tile.
The new set should only be used as input to tile operations, as the empty
tile is readonly.
"""
new = {}
for tile_coord in tiles.keys():
for adj_coord in fc.adjacent(tile_coord):
if adj_coord not in tiles and adj_coord not in new:
new[adj_coord] = _EMPTY_TILE
tiles.update(new)
def directly_below(coord1, coord2):
""" Return true if the first coordinate is directly below the second"""
return coord1[0] == coord2[0] and coord1[1] == coord2[1] + 1
def strand_partition(tiles, dilating=False):
"""Partition input tiles for easier processing
This function partitions a tile dictionary into
two parts: one dictionary containing tiles that
do not need to be processed further (see note),
and list of coordinate lists, where each list
contains vertically contiguous coordinates,
ordered from low to high.
note: Tiles that never need further processing are
those that are fully opaque and with a full neighbourhood
of identical tiles. If the "dilating" parameter is set
to true, just being fully opaque is enough.
:return: (final_dict, strands_list)
"""
# Dict of coord->tile for tiles that need no further processing
final_tiles = {}
# Groups of contiguous tile coordinates
strands = []
strand = []
previous = None
coords = tiles.keys()
for tile_coord in sorted(coords):
is_full_tile = tiles[tile_coord] is _FULL_TILE
if is_full_tile and (dilating or adj_full(tile_coord, tiles)):
# Tile needs no processing
final_tiles[tile_coord] = _FULL_TILE
previous = None
if strand:
strands.append(strand)
strand = []
elif previous is None or directly_below(tile_coord, previous):
# Either beginning of new strand, or adds to existing one
strand.append(tile_coord)
else:
# Neither final, nor contiguous, begin new strand
strands.append(strand)
strand = [tile_coord]
previous = tile_coord
if strand:
strands.append(strand)
return final_tiles, strands
def morph(handler, offset, tiles):
""" Either dilate or erode the given set of alpha tiles, depending
on the sign of the offset, returning the set of morphed tiles.
"""
# When dilating, create new tiles to account for edge overflow
# (without checking if they are actually needed)
if offset > 0:
complement_adjacent(tiles)
handler.set_stage(handler.MORPH, len(tiles))
# Split up the coordinates of the tiles to morph, into vertically
# contiguous strands, which can be processed more efficiently
morphed, strands = strand_partition(tiles, offset > 0)
# Run the morph operation (C++, conditionally threaded)
myplib.morph(offset, morphed, tiles, strands, handler.controller)
return morphed
def blur(handler, radius, tiles):
""" Return the set of blurred tiles based on the input tiles.
"""
complement_adjacent(tiles)
handler.set_stage(handler.BLUR, len(tiles))
blurred, strands = strand_partition(tiles, dilating=False)
myplib.blur(radius, blurred, tiles, strands, handler.controller)
return blurred
def adj_full(coord, tiles):
return all(t is _FULL_TILE for t in adjacent_tiles(coord, tiles))