object-remove 0.1.0__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.
Files changed (36) hide show
  1. object_remove/__init__.py +37 -0
  2. object_remove/fillengines/__init__.py +20 -0
  3. object_remove/fillengines/patch_comparator.py +182 -0
  4. object_remove/fillengines/patchmatch/__init__.py +41 -0
  5. object_remove/fillengines/patchmatch/group_solver.py +127 -0
  6. object_remove/fillengines/patchmatch/patch.py +48 -0
  7. object_remove/fillengines/patchmatch/patch_scorer.py +38 -0
  8. object_remove/fillengines/patchmatch/patch_search_index.py +56 -0
  9. object_remove/fillengines/patchmatch/solver.py +152 -0
  10. object_remove/fillengines/self_similar_shift.py +368 -0
  11. object_remove/imageutil.py +59 -0
  12. object_remove/mask/__init__.py +22 -0
  13. object_remove/mask/auto_select.py +84 -0
  14. object_remove/mask/brush_rasterizer.py +97 -0
  15. object_remove/mask/connected_components.py +110 -0
  16. object_remove/mask/polygon_selection.py +219 -0
  17. object_remove/mask/selection_enhancer.py +127 -0
  18. object_remove/pipeline/__init__.py +7 -0
  19. object_remove/pipeline/remove_object_job.py +154 -0
  20. object_remove/py.typed +0 -0
  21. object_remove/pyramid/__init__.py +15 -0
  22. object_remove/pyramid/image_pyramid.py +101 -0
  23. object_remove/pyramid/scale_scheduler.py +56 -0
  24. object_remove/pyramid/tile_scheduler.py +80 -0
  25. object_remove/render/__init__.py +16 -0
  26. object_remove/render/compositor.py +62 -0
  27. object_remove/render/multiband_blender.py +75 -0
  28. object_remove/render/poisson_blender.py +132 -0
  29. object_remove/session/__init__.py +11 -0
  30. object_remove/session/empty_session_sweeper.py +49 -0
  31. object_remove/session/undo_session_manager.py +178 -0
  32. object_remove-0.1.0.dist-info/METADATA +130 -0
  33. object_remove-0.1.0.dist-info/RECORD +36 -0
  34. object_remove-0.1.0.dist-info/WHEEL +5 -0
  35. object_remove-0.1.0.dist-info/licenses/LICENSE +21 -0
  36. object_remove-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,132 @@
1
+ """Poisson (gradient domain) blending — ``PoissonBlend`` / ``PoissonBlend2``.
2
+
3
+ A first class alternative to multi band blending, not a fallback. Parameter
4
+ surface:
5
+
6
+ =========== ========= ==================================================
7
+ parameter range meaning
8
+ =========== ========= ==================================================
9
+ ``sigma`` 0 to 10 sigma of the edge detector (edge aware guidance)
10
+ ``multiplier`` 0 to 10 gradient domain blend strength
11
+ ``algo_n`` 1 or 2 selects which of two Poisson solver variants to run
12
+ ``selection`` n/a region to blend
13
+ =========== ========= ==================================================
14
+
15
+ Variant 1 imports the *source* (filled) gradients; variant 2 uses *mixed*
16
+ gradients (whichever of source/target is stronger per edge) — the two classic
17
+ Poisson cloning modes, mapped onto ``algo_n``. ``sigma`` drives an edge detector
18
+ that attenuates guidance across strong target edges; ``multiplier`` scales the
19
+ imported gradient.
20
+
21
+ Solves the sparse Poisson system ``L f = div(g)`` with Dirichlet boundary equal
22
+ to the background, per channel, via ``scipy.sparse``.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import numpy as np
28
+ import scipy.sparse as sp
29
+ import scipy.sparse.linalg as spla
30
+
31
+ from ..pyramid.image_pyramid import gaussian_blur
32
+
33
+
34
+ class PoissonBlend:
35
+ def __init__(self, sigma: float = 1.0, multiplier: float = 1.0, algo_n: int = 1):
36
+ self.sigma = float(np.clip(sigma, 0.0, 10.0))
37
+ self.multiplier = float(np.clip(multiplier, 0.0, 10.0))
38
+ self.algo_n = 2 if int(algo_n) == 2 else 1
39
+
40
+ def blend(self, foreground: np.ndarray, background: np.ndarray,
41
+ mask: np.ndarray) -> np.ndarray:
42
+ """Seamlessly clone ``foreground`` into ``background`` over ``mask``."""
43
+ fg = foreground.astype(np.float64)
44
+ bg = background.astype(np.float64)
45
+ region = np.ascontiguousarray(mask > 0)
46
+ H, W = region.shape
47
+ if not region.any():
48
+ return bg.copy()
49
+
50
+ idx = -np.ones((H, W), dtype=np.int64)
51
+ ys, xs = np.nonzero(region)
52
+ idx[ys, xs] = np.arange(len(ys))
53
+ N = len(ys)
54
+
55
+ edge_w = self._edge_weights(bg)
56
+
57
+ # sparse Laplacian: 4 on diagonal, -1 to in region neighbours
58
+ neighbours = ((-1, 0), (1, 0), (0, -1), (0, 1))
59
+ rows = [np.arange(N)]
60
+ cols = [np.arange(N)]
61
+ data = [np.full(N, 4.0)]
62
+ for dy, dx in neighbours:
63
+ ny, nx = ys + dy, xs + dx
64
+ inb = (ny >= 0) & (ny < H) & (nx >= 0) & (nx < W)
65
+ nin = np.zeros(N, dtype=bool)
66
+ nin[inb] = region[ny[inb], nx[inb]]
67
+ src = np.nonzero(nin)[0]
68
+ rows.append(src)
69
+ cols.append(idx[ny[src], nx[src]])
70
+ data.append(np.full(len(src), -1.0))
71
+ A = sp.csr_matrix((np.concatenate(data),
72
+ (np.concatenate(rows), np.concatenate(cols))),
73
+ shape=(N, N))
74
+ lu = spla.splu(sp.csc_matrix(A))
75
+
76
+ out = bg.copy()
77
+ for c in range(fg.shape[2]):
78
+ b = self._rhs(fg[:, :, c], bg[:, :, c], region, idx, ys, xs,
79
+ neighbours, edge_w)
80
+ f = lu.solve(b)
81
+ out[ys, xs, c] = np.clip(f, 0.0, 1.0)
82
+ return out
83
+
84
+ def _edge_weights(self, bg: np.ndarray) -> np.ndarray:
85
+ """1 in flat areas, dropping to 0 across strong target edges (controlled by sigma)."""
86
+ if self.sigma <= 0:
87
+ return np.ones(bg.shape[:2])
88
+ gray = bg.mean(axis=2)
89
+ sm = gray
90
+ for _ in range(int(round(self.sigma))):
91
+ sm = gaussian_blur(sm)
92
+ gy, gx = np.gradient(sm)
93
+ mag = np.hypot(gx, gy)
94
+ scale = mag.mean() + 1e-6
95
+ return np.exp(-mag / (scale * (1.0 + self.sigma)))
96
+
97
+ def _rhs(self, fg_c, bg_c, region, idx, ys, xs, neighbours, edge_w):
98
+ H, W = region.shape
99
+ N = len(ys)
100
+ b = np.zeros(N)
101
+ for dy, dx in neighbours:
102
+ ny, nx = ys + dy, xs + dx
103
+ inb = (ny >= 0) & (ny < H) & (nx >= 0) & (nx < W)
104
+ # guidance gradient across this edge
105
+ grad_src = np.zeros(N)
106
+ grad_dst = np.zeros(N)
107
+ grad_src[inb] = fg_c[ys[inb], xs[inb]] - fg_c[ny[inb], nx[inb]]
108
+ grad_dst[inb] = bg_c[ys[inb], xs[inb]] - bg_c[ny[inb], nx[inb]]
109
+ if self.algo_n == 2:
110
+ # mixed gradients: keep whichever is steeper (variant 2)
111
+ use_dst = np.abs(grad_dst) > np.abs(grad_src)
112
+ edge_gradient = np.where(use_dst, grad_dst, grad_src)
113
+ else:
114
+ edge_gradient = grad_src
115
+ edge_gradient = edge_gradient * self.multiplier
116
+ # edge aware attenuation from the target
117
+ w = np.ones(N)
118
+ w[inb] = edge_w[ys[inb], xs[inb]]
119
+ edge_gradient = edge_gradient * w
120
+ b += edge_gradient
121
+ # Dirichlet: neighbours outside the region contribute background value
122
+ nin = np.zeros(N, dtype=bool)
123
+ nin[inb] = region[ny[inb], nx[inb]]
124
+ boundary = inb & ~nin
125
+ b[boundary] += bg_c[ny[boundary], nx[boundary]]
126
+ return b
127
+
128
+
129
+ # ``PoissonBlend2`` — the tier 3 default (algo_n=2, mixed gradients).
130
+ class PoissonBlend2(PoissonBlend):
131
+ def __init__(self, sigma: float = 1.0, multiplier: float = 1.0):
132
+ super().__init__(sigma=sigma, multiplier=multiplier, algo_n=2)
@@ -0,0 +1,11 @@
1
+ """Stage F — session / undo persistence."""
2
+
3
+ from .undo_session_manager import (
4
+ SessionManager, EditSession, EditStep, rle_encode, rle_decode,
5
+ )
6
+ from .empty_session_sweeper import EmptySessionSweeper
7
+
8
+ __all__ = [
9
+ "SessionManager", "EditSession", "EditStep",
10
+ "rle_encode", "rle_decode", "EmptySessionSweeper",
11
+ ]
@@ -0,0 +1,49 @@
1
+ """Orphaned empty session GC.
2
+
3
+ A startup/periodic sweep, distinct from the explicit per session cleanup path,
4
+ that removes session directories left behind by aborted or crashed edits (empty
5
+ or with no valid recorded step). Deletions go through the same queued/locked
6
+ delete task as explicit removal.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from typing import List
14
+
15
+ from .undo_session_manager import SessionManager, _RemoveTask
16
+
17
+
18
+ class EmptySessionSweeper:
19
+ def __init__(self, manager: SessionManager):
20
+ self.manager = manager
21
+
22
+ def sweep(self) -> List[str]:
23
+ """Queue removal of empty/orphaned session dirs; return the ids swept."""
24
+ root = self.manager.root
25
+ swept: List[str] = []
26
+ if not os.path.isdir(root):
27
+ return swept
28
+ for name in os.listdir(root):
29
+ path = os.path.join(root, name)
30
+ if not os.path.isdir(path):
31
+ continue
32
+ if self._is_orphan(path):
33
+ self.manager._queue.submit(
34
+ _RemoveTask("remove_session_directory", path))
35
+ swept.append(name)
36
+ return swept
37
+
38
+ @staticmethod
39
+ def _is_orphan(path: str) -> bool:
40
+ manifest = os.path.join(path, "manifest.json")
41
+ if not os.path.isfile(manifest):
42
+ # a directory with no manifest at all is an aborted/crashed edit
43
+ return True
44
+ try:
45
+ with open(manifest) as fh:
46
+ data = json.load(fh)
47
+ except Exception:
48
+ return True # corrupt, so discard
49
+ return len(data.get("steps", [])) == 0
@@ -0,0 +1,178 @@
1
+ """Session / undo persistence.
2
+
3
+ * each edit gets a session id and a directory holding the mask history
4
+ (RLE encoded) and step metadata;
5
+ * session directory removal is **not** an inline synchronous delete — it is
6
+ dispatched as a task onto a queue and run under an explicit lock, so the UI
7
+ thread never blocks on filesystem I/O and two removals can't race;
8
+ * corrupt sessions are validated and discarded rather than trusted.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import queue
16
+ import shutil
17
+ import threading
18
+ import time
19
+ import uuid
20
+ from dataclasses import dataclass, field
21
+ from typing import List, Optional
22
+
23
+ import numpy as np
24
+
25
+
26
+ # RLE mask codec (compact mask history)
27
+ def rle_encode(mask: np.ndarray) -> dict:
28
+ flat = np.ascontiguousarray(mask > 0).ravel()
29
+ if flat.size == 0:
30
+ return {"h": 0, "w": 0, "runs": []}
31
+ changes = np.flatnonzero(np.diff(flat)) + 1
32
+ bounds = np.concatenate(([0], changes, [flat.size]))
33
+ runs = np.diff(bounds).tolist()
34
+ return {"h": int(mask.shape[0]), "w": int(mask.shape[1]),
35
+ "first": int(flat[0]), "runs": runs}
36
+
37
+
38
+ def rle_decode(obj: dict) -> np.ndarray:
39
+ h, w = obj["h"], obj["w"]
40
+ flat = np.zeros(h * w, dtype=bool)
41
+ val = bool(obj.get("first", 0))
42
+ pos = 0
43
+ for run in obj["runs"]:
44
+ if val:
45
+ flat[pos:pos + run] = True
46
+ pos += run
47
+ val = not val
48
+ return flat.reshape(h, w)
49
+
50
+
51
+ # background delete task queue
52
+ @dataclass
53
+ class _RemoveTask:
54
+ kind: str # "remove_session_directory"
55
+ path: str
56
+
57
+
58
+ class _TaskQueue:
59
+ """Serialises removals under a lock on a background worker thread."""
60
+
61
+ def __init__(self):
62
+ self._q: "queue.Queue[Optional[_RemoveTask]]" = queue.Queue()
63
+ self._lock = threading.Lock()
64
+ self._thread = threading.Thread(target=self._run, daemon=True)
65
+ self._thread.start()
66
+
67
+ def submit(self, task: _RemoveTask) -> None:
68
+ self._q.put(task)
69
+
70
+ def _run(self) -> None:
71
+ while True:
72
+ task = self._q.get()
73
+ if task is None:
74
+ return
75
+ try:
76
+ with self._lock:
77
+ if task.kind == "remove_session_directory" and os.path.isdir(task.path):
78
+ shutil.rmtree(task.path, ignore_errors=True)
79
+ finally:
80
+ self._q.task_done()
81
+
82
+ def flush(self) -> None:
83
+ self._q.join()
84
+
85
+ def stop(self) -> None:
86
+ self._q.put(None)
87
+
88
+
89
+ @dataclass
90
+ class EditStep:
91
+ index: int
92
+ mask_rle: dict
93
+ note: str = ""
94
+ ts: float = field(default_factory=time.time)
95
+
96
+
97
+ class SessionManager:
98
+ """One instance per app; owns the sessions root and the delete queue."""
99
+
100
+ def __init__(self, root: str):
101
+ self.root = root
102
+ os.makedirs(root, exist_ok=True)
103
+ self._queue = _TaskQueue()
104
+ self._sessions: dict[str, "EditSession"] = {}
105
+
106
+ def new_session(self) -> "EditSession":
107
+ session_id = uuid.uuid4().hex[:16]
108
+ path = os.path.join(self.root, session_id)
109
+ os.makedirs(path, exist_ok=True)
110
+ sess = EditSession(session_id, path)
111
+ self._sessions[session_id] = sess
112
+ return sess
113
+
114
+ # explicit per session cleanup (queued/locked, not inline)
115
+ def remove_session_directory(self, session: "EditSession") -> None:
116
+ """Queue removal of a specific (committed/exported) session."""
117
+ self._sessions.pop(session.session_id, None)
118
+ self._queue.submit(_RemoveTask("remove_session_directory", session.path))
119
+
120
+ def flush(self) -> None:
121
+ self._queue.flush()
122
+
123
+ def close(self) -> None:
124
+ self._queue.flush()
125
+ self._queue.stop()
126
+
127
+ @staticmethod
128
+ def validate_session(path: str) -> bool:
129
+ """A session is valid iff its manifest parses and its steps decode."""
130
+ manifest = os.path.join(path, "manifest.json")
131
+ if not os.path.isfile(manifest):
132
+ return False
133
+ try:
134
+ with open(manifest) as fh:
135
+ data = json.load(fh)
136
+ for step in data.get("steps", []):
137
+ rle_decode(step["mask_rle"])
138
+ return True
139
+ except Exception:
140
+ return False
141
+
142
+
143
+ class EditSession:
144
+ def __init__(self, session_id: str, path: str):
145
+ self.session_id = session_id
146
+ self.path = path
147
+ self.steps: List[EditStep] = []
148
+
149
+ def record(self, mask: np.ndarray, note: str = "") -> EditStep:
150
+ step = EditStep(len(self.steps), rle_encode(mask), note)
151
+ self.steps.append(step)
152
+ self._persist()
153
+ return step
154
+
155
+ def undo(self) -> Optional[EditStep]:
156
+ if not self.steps:
157
+ return None
158
+ step = self.steps.pop()
159
+ self._persist()
160
+ return step
161
+
162
+ def mask_at(self, index: int) -> np.ndarray:
163
+ return rle_decode(self.steps[index].mask_rle)
164
+
165
+ def _persist(self) -> None:
166
+ manifest = {
167
+ "session_id": self.session_id,
168
+ "steps": [{"index": s.index, "mask_rle": s.mask_rle,
169
+ "note": s.note, "ts": s.ts} for s in self.steps],
170
+ }
171
+ tmp_manifest_path = os.path.join(self.path, "manifest.json.tmp")
172
+ with open(tmp_manifest_path, "w") as fh:
173
+ json.dump(manifest, fh)
174
+ # atomic rename so a crash mid write never leaves a truncated manifest
175
+ os.replace(tmp_manifest_path, os.path.join(self.path, "manifest.json"))
176
+
177
+ def is_empty(self) -> bool:
178
+ return len(self.steps) == 0
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: object_remove
3
+ Version: 0.1.0
4
+ Summary: From-scratch CPU reference implementation of a tiered on-device object-removal pipeline
5
+ Author: shalaga44
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shalaga44/object_remove
8
+ Project-URL: Issues, https://github.com/shalaga44/object_remove/issues
9
+ Keywords: image-processing,inpainting,object-removal,computer-vision
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=2.0
23
+ Requires-Dist: scipy>=1.10
24
+ Requires-Dist: Pillow>=9.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # object_remove
30
+
31
+ A from scratch, CPU implementation of an on device object removal pipeline (see
32
+ [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full design): a
33
+ polygon/scanline mask layer, **three fill engines** of increasing
34
+ sophistication, **two interchangeable blend backends**, and a locked/queued
35
+ session persistence layer.
36
+
37
+ Everything here is implemented from primitives (FAST corners, BRIEF descriptors,
38
+ Hamming matching, PatchMatch, Laplacian/Poisson blending) — no OpenCV, no learned
39
+ weights.
40
+
41
+ ## Quick start
42
+
43
+ ```bash
44
+ pip install -r requirements.txt # numpy, scipy, Pillow
45
+ PYTHONPATH=. python examples/demo.py # writes before/after PNGs to examples/out/
46
+ PYTHONPATH=. python -m pytest -q # 31 tests
47
+ ```
48
+
49
+ ```python
50
+ import numpy as np
51
+ from object_remove import RemoveObjectJob, EngineTier
52
+
53
+ result = RemoveObjectJob(image_rgb).run(mask, tier=EngineTier.AUTO,
54
+ progress=lambda stage, frac: ...)
55
+ removed = result.image # float32 RGB in [0, 1]
56
+ print(result.tier_used) # which engine AUTO picked
57
+ ```
58
+
59
+ `image_rgb` is any `(H, W, 3)` array (uint8 or float); `mask` is `(H, W)` with
60
+ nonzero marking the object to remove.
61
+
62
+ ## Module layout
63
+
64
+ | Stage | Concern | This repo |
65
+ |---|---|---|
66
+ | A — mask | selection, brush, auto select | `object_remove/mask/` |
67
+ | B/D — scale/tiles | pyramid + tile scheduling | `object_remove/pyramid/` |
68
+ | C1 — legacy CPU fill | segment aware patch comparator | `fillengines/patch_comparator.py` |
69
+ | C2 — texture synthesis | self similar rigid shift | `fillengines/self_similar_shift.py` |
70
+ | C3 — patch match | multi scale PatchMatch | `fillengines/patchmatch/` |
71
+ | E — blend | multi band + Poisson | `object_remove/render/` |
72
+ | F — session | undo / persistence | `object_remove/session/` |
73
+ | — orchestration | tier selection, progress, cancellation | `pipeline/remove_object_job.py` |
74
+
75
+ ## The three fill engines
76
+
77
+ - **Tier 1 — `SegmentAwarePatchFillEngine`** — greedy onion peel exemplar fill
78
+ scored by segment aware SSD with byte mask validity. Cheap, correct on small
79
+ holes; the low end / tiny hole fallback.
80
+ - **Tier 2 — `SelfSimilarShiftEngine`** — the fast default for repetitive texture.
81
+ FAST corners + 256 bit BRIEF + Hamming matching find a *rigid self similar
82
+ offset*; a 1D DP seam cuts the copied block in cleanly, run for x then y via
83
+ a transpose trick.
84
+ - **Tier 3 — `PatchMatchEngine`** — multi scale, multi group PatchMatch inpainting
85
+ (random init → propagation → random search, then patch voting reconstruction,
86
+ coarse to fine). The quality path.
87
+
88
+ `EngineTier.AUTO` sends truly tiny holes to tier 1, otherwise tries the cheap
89
+ tier 2 rigid shift first and falls back to tier 3 PatchMatch when the shift's
90
+ boundary continuity score is poor.
91
+
92
+ ## The two blend backends
93
+
94
+ - **`MultiBandBlender`** — Laplacian pyramid blend with a fixed **4 tap
95
+ kernel**, `[0.15439, 0.15133, 0.14252, 0.12896]`, and the mask feathered
96
+ *per pyramid level*.
97
+ - **`PoissonBlend` / `PoissonBlend2`** — gradient domain blend (`sigma`,
98
+ `multiplier`, `algo_n` 1|2 variant), solved as a sparse Poisson system.
99
+ Poisson is the safe default for single image inpainting because it reads
100
+ only the (valid) hole boundary; the compositor repairs the background under
101
+ the hole so no backend ever samples the object being removed.
102
+
103
+ ## Session persistence
104
+
105
+ `SessionManager` gives each edit a session id and an RLE mask history. Directory
106
+ removal is dispatched onto a background queue and run **under a lock** — never
107
+ an inline delete from the caller. A separate `EmptySessionSweeper` GCs orphaned
108
+ empty session dirs left by aborted/crashed edits.
109
+
110
+ ## Status & scope
111
+
112
+ Implemented and tested: the full CPU pipeline across all stages. Auto select
113
+ is explicitly an *interface + CPU fallback*: a real segmentation model can be
114
+ dropped in behind `AutoSelectModel`, which ships a working color flood
115
+ fallback so tap to select runs end to end without one.
116
+
117
+ Engines run in pure Python/numpy for clarity; they are correct, not tuned for
118
+ speed. The hot inner loops (PatchMatch sweep, Poisson solve) are the obvious
119
+ targets for a native/GPU port.
120
+
121
+ ```
122
+ object_remove/
123
+ mask/ brush_rasterizer, polygon_selection, selection_enhancer,
124
+ connected_components, auto_select
125
+ pyramid/ image_pyramid, scale_scheduler, tile_scheduler
126
+ fillengines/ patch_comparator (T1), self_similar_shift (T2), patchmatch/ (T3)
127
+ render/ multiband_blender, poisson_blender, compositor
128
+ session/ undo_session_manager, empty_session_sweeper
129
+ pipeline/ remove_object_job
130
+ ```
@@ -0,0 +1,36 @@
1
+ object_remove/__init__.py,sha256=3NybrvSQ27HDkS4oNTSWCbF_d4mISopZczUV1Fo5oYo,1385
2
+ object_remove/imageutil.py,sha256=dytMnESHndVBjBuiWbyRM3ms7Q-eFfLJfynhIQR5GVM,1828
3
+ object_remove/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ object_remove/fillengines/__init__.py,sha256=h6U2VtdJ8axVIPglhWp9KtVe68BBJAPX-oNBHJeWxd0,754
5
+ object_remove/fillengines/patch_comparator.py,sha256=2OYau1BVwp-PMkhaNZrQQdkFtM5MYTOiuGWuq-3aThM,7193
6
+ object_remove/fillengines/self_similar_shift.py,sha256=VwCRW2ru5bJXx_hdO1f9kmFvweBmL1KHixZTD6GeVK8,14272
7
+ object_remove/fillengines/patchmatch/__init__.py,sha256=GQZNoALA6L_7rJCz2d576cmzVB9_WoQknGtCvlGK1dg,1644
8
+ object_remove/fillengines/patchmatch/group_solver.py,sha256=3SlSaqzv1IGZ7jokWomqGq9i0nGM40wAxcNHt-9CKrs,4930
9
+ object_remove/fillengines/patchmatch/patch.py,sha256=2zMCOoIcJuq_a-rWuznWsB0uy2pxYFMzzXmgHmckRXQ,1371
10
+ object_remove/fillengines/patchmatch/patch_scorer.py,sha256=RgZHnQ2sR5aW-Gw4iBqSgSp25Ib3W8v6IF-ijNtZJZc,1455
11
+ object_remove/fillengines/patchmatch/patch_search_index.py,sha256=_43IQEVHD5OK0l1hI5cnrJDylyBAZztFsotTZt2mmvk,2178
12
+ object_remove/fillengines/patchmatch/solver.py,sha256=WcK_ikt6a3r6JrxkY5xaNtg4KW3TwFLCtGbXv0auH2Y,6419
13
+ object_remove/mask/__init__.py,sha256=bQVvd1UM5kd76fnooH2z4a_P9T7_NJI3_J6DNT3rXgE,733
14
+ object_remove/mask/auto_select.py,sha256=KZ6NdwcCqXitI7GD0Ub2Oy3_Q-li0la2Bq2rV0Z1PJ0,3267
15
+ object_remove/mask/brush_rasterizer.py,sha256=R0C48hFWl444qVd-fw8FrBxnyuoAfFAZfKCFOMfHaz4,3861
16
+ object_remove/mask/connected_components.py,sha256=4m4nXnlfxeDTjtn8SgemF9LH5e7uOgHlE44VLReGI4U,3521
17
+ object_remove/mask/polygon_selection.py,sha256=qOMl14O1oNTU9arersIUBFe_fZBcEB4dcD7tTErMuSg,7868
18
+ object_remove/mask/selection_enhancer.py,sha256=uX5IXw34ham9SawOxDcwWVKcxFd6o1UjoT8iAg696to,4787
19
+ object_remove/pipeline/__init__.py,sha256=qeSxaz5HzpyP4moqd7Rcx3acW5W7V3OoUNiaY4siHqs,216
20
+ object_remove/pipeline/remove_object_job.py,sha256=THvnUwEHv5X4vUv7pdqsj_ZuC0IKB6BL0gcWBXgTFro,6344
21
+ object_remove/pyramid/__init__.py,sha256=zw5zsE1A1xJjChM07snD8XpZxQjI6m-isQQfwQRnsVA,525
22
+ object_remove/pyramid/image_pyramid.py,sha256=iLxDZGN6qEvbh9-u3nAEK_RUJBBo1HBzOmd6LNSCWCQ,3686
23
+ object_remove/pyramid/scale_scheduler.py,sha256=WIVymIiku1UVGLJ8bZhiTwbjU5lA8RIbEZvls_xXTDE,2052
24
+ object_remove/pyramid/tile_scheduler.py,sha256=xpvK19y_JSaxRwkMcNicKao1dZ0flfyvcMS_n3rXcfs,2954
25
+ object_remove/render/__init__.py,sha256=3821iqB146nXdeUtLz5xhvDivEkPmO-rSdOecvERtTU,523
26
+ object_remove/render/compositor.py,sha256=Wuma1RJ4YK9yEo6KHNThAeWHCLDjlZMzXrFkl1Mf6lc,2592
27
+ object_remove/render/multiband_blender.py,sha256=bmHB5ul5V2BoUEaKHgudXcS6_cN8Ht_y6kryfVJQM0E,2887
28
+ object_remove/render/poisson_blender.py,sha256=0ebhm0e1wtrayybTQesFEjVzEBA1dvwvfd4GW8dC2q0,5434
29
+ object_remove/session/__init__.py,sha256=54sYX7WqDWlEfq8WY-yUOboiKChNa2wzflwbmJeDXE8,326
30
+ object_remove/session/empty_session_sweeper.py,sha256=QKoCzs5svKM6kK8F7GsBTR4OGB_lHbAAskpm_O7WHO4,1616
31
+ object_remove/session/undo_session_manager.py,sha256=c2f9xC9h5596EWX2BCzYmEuSf61IV6tkHrqMggLVVGM,5473
32
+ object_remove-0.1.0.dist-info/licenses/LICENSE,sha256=MXtYKLz9evYRq3bIl3mXLvBOjGbGLe-O9ofEH5SFQlY,1066
33
+ object_remove-0.1.0.dist-info/METADATA,sha256=tDpw3g7BghCEdZUHFWDMMN30egmDCUkTxQm5z79Ob3U,5726
34
+ object_remove-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
35
+ object_remove-0.1.0.dist-info/top_level.txt,sha256=qaadAwnEePg4QvC7T59W-ucRB7lXHekDw3J-nET_aPk,14
36
+ object_remove-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shalaga44
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.
@@ -0,0 +1 @@
1
+ object_remove