scrollweave 0.9.0__py3-none-win_amd64.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.
- scrollweave/__init__.py +52 -0
- scrollweave/__main__.py +128 -0
- scrollweave/_binding.py +593 -0
- scrollweave/_native/scrollweave_ffi.dll +0 -0
- scrollweave/api.py +441 -0
- scrollweave/py.typed +0 -0
- scrollweave-0.9.0.dist-info/METADATA +194 -0
- scrollweave-0.9.0.dist-info/RECORD +11 -0
- scrollweave-0.9.0.dist-info/WHEEL +4 -0
- scrollweave-0.9.0.dist-info/entry_points.txt +2 -0
- scrollweave-0.9.0.dist-info/licenses/LICENSE +17 -0
scrollweave/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""ScrollWeave: zero third-party dependencies Python API + Rust native core."""
|
|
2
|
+
|
|
3
|
+
from ._binding import NativeUnavailable
|
|
4
|
+
from .api import (
|
|
5
|
+
AlignOptions,
|
|
6
|
+
BasicResult,
|
|
7
|
+
Diagnostics,
|
|
8
|
+
Evidence,
|
|
9
|
+
GeometryMode,
|
|
10
|
+
Goal,
|
|
11
|
+
GraphStats,
|
|
12
|
+
Image,
|
|
13
|
+
LayeredOptions,
|
|
14
|
+
LayeredResult,
|
|
15
|
+
PlanOptions,
|
|
16
|
+
PriorMasks,
|
|
17
|
+
Segmentation,
|
|
18
|
+
Viewport,
|
|
19
|
+
ViewSuggestion,
|
|
20
|
+
compose,
|
|
21
|
+
plan_views,
|
|
22
|
+
read_image,
|
|
23
|
+
stitch_basic,
|
|
24
|
+
stitch_layered,
|
|
25
|
+
write_image,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = '0.9.0'
|
|
29
|
+
__all__ = [
|
|
30
|
+
'AlignOptions',
|
|
31
|
+
'BasicResult',
|
|
32
|
+
'Diagnostics',
|
|
33
|
+
'Evidence',
|
|
34
|
+
'GeometryMode',
|
|
35
|
+
'Goal',
|
|
36
|
+
'GraphStats',
|
|
37
|
+
'Image',
|
|
38
|
+
'LayeredOptions',
|
|
39
|
+
'LayeredResult',
|
|
40
|
+
'PlanOptions',
|
|
41
|
+
'PriorMasks',
|
|
42
|
+
'Segmentation',
|
|
43
|
+
'ViewSuggestion',
|
|
44
|
+
'Viewport',
|
|
45
|
+
'compose',
|
|
46
|
+
'plan_views',
|
|
47
|
+
'read_image',
|
|
48
|
+
'stitch_basic',
|
|
49
|
+
'stitch_layered',
|
|
50
|
+
'write_image',
|
|
51
|
+
'NativeUnavailable',
|
|
52
|
+
]
|
scrollweave/__main__.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""python -m scrollweave stitch/compose ..."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from . import (
|
|
6
|
+
AlignOptions,
|
|
7
|
+
Image,
|
|
8
|
+
LayeredOptions,
|
|
9
|
+
PlanOptions,
|
|
10
|
+
Segmentation,
|
|
11
|
+
Viewport,
|
|
12
|
+
__version__,
|
|
13
|
+
compose,
|
|
14
|
+
plan_views,
|
|
15
|
+
stitch_basic,
|
|
16
|
+
stitch_layered,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv=None):
|
|
21
|
+
parser = argparse.ArgumentParser(prog='scrollweave')
|
|
22
|
+
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
|
23
|
+
sub = parser.add_subparsers(dest='operation', required=True)
|
|
24
|
+
|
|
25
|
+
st = sub.add_parser('stitch', help='reconstruct scrolling screenshots')
|
|
26
|
+
st.add_argument('images', nargs='+')
|
|
27
|
+
st.add_argument('-o', '--output', required=True)
|
|
28
|
+
st.add_argument('--algorithm', choices=('layered', 'basic'), default='layered')
|
|
29
|
+
st.add_argument('--positions', help='comma-separated vertical scroll positions')
|
|
30
|
+
st.add_argument('--viewport', help='top:bottom content band')
|
|
31
|
+
st.add_argument('--no-trim', action='store_true')
|
|
32
|
+
st.add_argument('--no-appearance', action='store_true')
|
|
33
|
+
st.add_argument('--segmentation', choices=('temporal', 'graphcut'), default='temporal')
|
|
34
|
+
st.add_argument('--debug-dir')
|
|
35
|
+
st.add_argument('--recommend-views', type=int, default=0)
|
|
36
|
+
st.add_argument('--recommend-repair-views', type=int, default=0)
|
|
37
|
+
st.add_argument('--candidate-positions', help='comma-separated candidate scroll offsets')
|
|
38
|
+
st.add_argument('--minimum-separation', type=int, default=48)
|
|
39
|
+
|
|
40
|
+
cp = sub.add_parser('compose', help='compose without alignment')
|
|
41
|
+
cp.add_argument('images', nargs='+')
|
|
42
|
+
cp.add_argument('-o', '--output', required=True)
|
|
43
|
+
cp.add_argument(
|
|
44
|
+
'--layout', choices=['vertical', 'horizontal', 'grid', 'overlay'], default='vertical'
|
|
45
|
+
)
|
|
46
|
+
cp.add_argument('--columns', type=int, default=2)
|
|
47
|
+
cp.add_argument('--gap', type=int, default=0)
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args(argv)
|
|
50
|
+
if args.operation == 'compose':
|
|
51
|
+
output = compose(args.images, layout=args.layout, columns=args.columns, gap=args.gap)
|
|
52
|
+
output.save(args.output)
|
|
53
|
+
print(f'Saved {args.output}: {output.width}x{output.height}')
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
frames = [Image.open(path) for path in args.images]
|
|
57
|
+
viewport = None
|
|
58
|
+
if args.viewport is not None:
|
|
59
|
+
top, bottom = (int(value) for value in args.viewport.split(':'))
|
|
60
|
+
viewport = Viewport(top, bottom)
|
|
61
|
+
positions = [int(value) for value in args.positions.split(',')] if args.positions else None
|
|
62
|
+
|
|
63
|
+
if args.algorithm == 'basic':
|
|
64
|
+
if positions is None:
|
|
65
|
+
raise SystemExit('stitch --algorithm basic requires --positions')
|
|
66
|
+
result = stitch_basic(
|
|
67
|
+
frames,
|
|
68
|
+
positions=positions,
|
|
69
|
+
viewport=viewport,
|
|
70
|
+
options=AlignOptions(auto_trim=not args.no_trim),
|
|
71
|
+
diagnostics=True,
|
|
72
|
+
)
|
|
73
|
+
placed = len([position for position in result.positions if position is not None])
|
|
74
|
+
else:
|
|
75
|
+
if positions is None:
|
|
76
|
+
raise SystemExit('stitch requires --positions for layered reconstruction')
|
|
77
|
+
if viewport is None and args.no_trim:
|
|
78
|
+
viewport = Viewport(0, frames[0].height)
|
|
79
|
+
options = LayeredOptions(
|
|
80
|
+
appearance=not args.no_appearance,
|
|
81
|
+
segmentation=Segmentation(args.segmentation),
|
|
82
|
+
)
|
|
83
|
+
result = stitch_layered(
|
|
84
|
+
frames,
|
|
85
|
+
scroll=positions,
|
|
86
|
+
viewport=viewport,
|
|
87
|
+
options=options,
|
|
88
|
+
diagnostics=True,
|
|
89
|
+
)
|
|
90
|
+
placed = len(result.scroll)
|
|
91
|
+
|
|
92
|
+
result.save(args.output)
|
|
93
|
+
if args.debug_dir:
|
|
94
|
+
result.save_debug(args.debug_dir)
|
|
95
|
+
for count, goal in (
|
|
96
|
+
(args.recommend_views, 'verification'),
|
|
97
|
+
(args.recommend_repair_views, 'repair'),
|
|
98
|
+
):
|
|
99
|
+
if count == 0:
|
|
100
|
+
continue
|
|
101
|
+
if not hasattr(result, 'scroll'):
|
|
102
|
+
raise SystemExit('planning requires the layered pipeline')
|
|
103
|
+
planners = plan_views(
|
|
104
|
+
result,
|
|
105
|
+
PlanOptions(
|
|
106
|
+
count=count,
|
|
107
|
+
goal=goal,
|
|
108
|
+
candidates=(
|
|
109
|
+
[int(value) for value in args.candidate_positions.split(',')]
|
|
110
|
+
if args.candidate_positions
|
|
111
|
+
else None
|
|
112
|
+
),
|
|
113
|
+
minimum_separation=args.minimum_separation,
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
for view in planners:
|
|
117
|
+
print(
|
|
118
|
+
f'Recommend {view.goal.value}: scroll={view.scroll_position}, '
|
|
119
|
+
f'estimated_gain={view.estimated_gain:.1f}'
|
|
120
|
+
)
|
|
121
|
+
print(
|
|
122
|
+
f'Saved {args.output}: {result.image.width}x{result.image.height}, '
|
|
123
|
+
f'{placed}/{len(frames)} frames placed'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__':
|
|
128
|
+
main()
|