easydetect 0.2.2__tar.gz → 0.2.3__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.
- {easydetect-0.2.2 → easydetect-0.2.3}/PKG-INFO +1 -1
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/__init__.py +1 -1
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/cli.py +1 -1
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/model.py +10 -3
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/predictor.py +56 -2
- {easydetect-0.2.2 → easydetect-0.2.3}/pyproject.toml +1 -1
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_predictor.py +54 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/.gitignore +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/LICENSE +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/NOTICE +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/README.md +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/__init__.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/augment.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/dataset.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/data/labels.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/downloads.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/errors.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/exporter.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/metrics.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/__init__.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/decoder.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/denoising.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/dfine_net.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/encoder.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/hgnetv2.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/nn/ops.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/plotting.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/results.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/sources.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/tracker.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/trainer.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/utils/__init__.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/utils/loss.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/utils/ops.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/easydetect/validator.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/__init__.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/conftest.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_augment.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_cli.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_coco2yolo.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_dataset.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_downloads.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_lab_api.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_model.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_nn.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_onnxruntime.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_results.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_sources.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_tracker.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tests/test_train_export.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/bench.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/build_mirror.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/coco2yolo.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/compare_yolo.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/convert_dfine.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/hub_README.md +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/make_demo.py +0 -0
- {easydetect-0.2.2 → easydetect-0.2.3}/tools/make_toyset.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easydetect
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Easy real-time object detection you can ship: D-FINE with PyTorch training, OpenVINO inference on CPU/GPU/NPU, and a browser app to label and train. Apache-2.0 throughout.
|
|
5
5
|
Project-URL: Homepage, https://github.com/themakerrobot/easydetect
|
|
6
6
|
Project-URL: Source, https://github.com/themakerrobot/easydetect
|
|
@@ -24,7 +24,7 @@ needs numpy/opencv/openvino/pyyaml; training adds
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
-
__version__ = "0.2.
|
|
27
|
+
__version__ = "0.2.3"
|
|
28
28
|
|
|
29
29
|
from .errors import DownloadError, EasyDetectError, ModelNotFoundError
|
|
30
30
|
from .metrics import BoxMetrics, DetMetrics
|
|
@@ -29,7 +29,7 @@ Modes:
|
|
|
29
29
|
Common keys:
|
|
30
30
|
model=dfine-n|s|m|l|x|best.pt|model.xml source=bus.jpg|dir|video.mp4|0|url
|
|
31
31
|
data=data.yaml epochs=100 imgsz=640 batch=8 conf=0.25 iou=0.7|none device=0|cpu|AUTO
|
|
32
|
-
project=runs name=predict save=true show=false format=openvino half=true
|
|
32
|
+
contain=0.8 project=runs name=predict save=true show=false format=openvino half=true
|
|
33
33
|
|
|
34
34
|
Examples:
|
|
35
35
|
easydetect predict model=dfine-s source=bus.jpg conf=0.5
|
|
@@ -120,6 +120,7 @@ class Detector:
|
|
|
120
120
|
max_det: int = 300,
|
|
121
121
|
classes: list[int] | None = None,
|
|
122
122
|
iou: float | None = 0.7,
|
|
123
|
+
contain: float | None = None,
|
|
123
124
|
stream: bool = False,
|
|
124
125
|
save: bool = False,
|
|
125
126
|
show: bool = False,
|
|
@@ -136,6 +137,9 @@ class Detector:
|
|
|
136
137
|
only sane option for a long video or a live camera). ``iou`` drops a
|
|
137
138
|
box covering a higher-scoring one by more than that IoU — the same
|
|
138
139
|
object found twice, sometimes under two classes; ``None`` keeps all.
|
|
140
|
+
``contain=0.8`` also merges a box with 80% of its area inside another
|
|
141
|
+
of its class — the visible pieces of a half hidden object reported
|
|
142
|
+
beside the whole of it — into one box. Off by default.
|
|
139
143
|
"""
|
|
140
144
|
if source is None:
|
|
141
145
|
raise ValueError("predict() needs a source (image, folder, video, url, camera index)")
|
|
@@ -152,6 +156,7 @@ class Detector:
|
|
|
152
156
|
max_det=max_det,
|
|
153
157
|
classes=classes,
|
|
154
158
|
iou=iou,
|
|
159
|
+
contain=contain,
|
|
155
160
|
save=save,
|
|
156
161
|
show=show,
|
|
157
162
|
project=project,
|
|
@@ -168,6 +173,7 @@ class Detector:
|
|
|
168
173
|
source: Any = None,
|
|
169
174
|
conf: float = 0.25,
|
|
170
175
|
iou: float | None = 0.7,
|
|
176
|
+
contain: float | None = None,
|
|
171
177
|
match_iou: float = 0.3,
|
|
172
178
|
max_age: int = 30,
|
|
173
179
|
persist: bool = False,
|
|
@@ -176,7 +182,7 @@ class Detector:
|
|
|
176
182
|
) -> Any:
|
|
177
183
|
"""Predict, and keep an id on each box across frames (``boxes.id``).
|
|
178
184
|
|
|
179
|
-
``iou``
|
|
185
|
+
``iou`` and ``contain`` filter duplicates, as in :meth:`predict`. ``match_iou``
|
|
180
186
|
is how much a box must overlap a track in the previous frame to carry
|
|
181
187
|
its id on; ``max_age`` is how many frames a track waits for its object.
|
|
182
188
|
"""
|
|
@@ -185,7 +191,7 @@ class Detector:
|
|
|
185
191
|
if self.tracker is None or not persist:
|
|
186
192
|
self.tracker = IoUTracker(iou=match_iou, max_age=max_age)
|
|
187
193
|
kwargs.setdefault("name", "track")
|
|
188
|
-
gen = self._run(source, conf=conf, iou=iou, tracker=self.tracker,
|
|
194
|
+
gen = self._run(source, conf=conf, iou=iou, contain=contain, tracker=self.tracker,
|
|
189
195
|
**self._track_kwargs(kwargs))
|
|
190
196
|
return _Stream(gen, "track") if stream else list(gen)
|
|
191
197
|
|
|
@@ -209,6 +215,7 @@ class Detector:
|
|
|
209
215
|
max_det: int = 300,
|
|
210
216
|
classes: list[int] | None = None,
|
|
211
217
|
iou: float | None = 0.7,
|
|
218
|
+
contain: float | None = None,
|
|
212
219
|
save: bool = False,
|
|
213
220
|
show: bool = False,
|
|
214
221
|
project: str = "runs",
|
|
@@ -230,7 +237,7 @@ class Detector:
|
|
|
230
237
|
try:
|
|
231
238
|
for frame in loader:
|
|
232
239
|
det, speed = predictor(frame.img, conf=conf, max_det=max_det, classes=classes,
|
|
233
|
-
iou=iou)
|
|
240
|
+
iou=iou, contain=contain)
|
|
234
241
|
if tracker is not None:
|
|
235
242
|
det = tracker.update(det)
|
|
236
243
|
result = Results(
|
|
@@ -158,6 +158,52 @@ def drop_duplicates(xyxy: np.ndarray, iou: float = IOU) -> np.ndarray:
|
|
|
158
158
|
return np.nonzero(alive)[0]
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
#: How much less sure an enclosing box may be than the box inside it and still
|
|
162
|
+
#: count as the whole object rather than a loose box around several.
|
|
163
|
+
CONTAIN_MARGIN = 0.1
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def drop_contained(xyxy: np.ndarray, scores: np.ndarray, cls: np.ndarray,
|
|
167
|
+
contain: float) -> np.ndarray:
|
|
168
|
+
"""Indices of the boxes to keep when a box lies inside another of its class.
|
|
169
|
+
|
|
170
|
+
IoU cannot see a part inside a whole: a chair half hidden behind a person
|
|
171
|
+
can come back as the whole chair (0.64) plus its two visible pieces (0.67,
|
|
172
|
+
0.61), and each piece overlaps the whole by an IoU of only its share of the
|
|
173
|
+
area. Measured as the overlap over the *smaller* box instead, a piece sits
|
|
174
|
+
near 1.0, so a pair of one class sharing at least ``contain`` of the
|
|
175
|
+
smaller box is one object, and one box goes:
|
|
176
|
+
|
|
177
|
+
* the inner one, when the enclosing box is about as sure (within
|
|
178
|
+
``CONTAIN_MARGIN``) — the whole object and a piece of it;
|
|
179
|
+
* the enclosing one, when it is much less sure than the box inside — a
|
|
180
|
+
loose box around a group, which must not erase the members.
|
|
181
|
+
|
|
182
|
+
NMS's "keep the best" would not do here: the best box is a piece, and once
|
|
183
|
+
it has removed the whole, the other piece no longer overlaps anything and
|
|
184
|
+
stays — two half chairs. Same class only: a cup on a table is not part of
|
|
185
|
+
the table. The cost is a real object inside another of its class, a child
|
|
186
|
+
held by an adult, so this is off unless asked for.
|
|
187
|
+
"""
|
|
188
|
+
x1, y1, x2, y2 = xyxy.T
|
|
189
|
+
area = np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None)
|
|
190
|
+
gone = np.zeros(len(xyxy), bool)
|
|
191
|
+
for i in range(len(xyxy)):
|
|
192
|
+
for j in range(i + 1, len(xyxy)):
|
|
193
|
+
if cls[i] != cls[j]:
|
|
194
|
+
continue
|
|
195
|
+
iw = max(0.0, min(x2[i], x2[j]) - max(x1[i], x1[j]))
|
|
196
|
+
ih = max(0.0, min(y2[i], y2[j]) - max(y1[i], y1[j]))
|
|
197
|
+
if iw * ih < contain * max(min(area[i], area[j]), 1e-9):
|
|
198
|
+
continue
|
|
199
|
+
big, small = (i, j) if area[i] >= area[j] else (j, i)
|
|
200
|
+
if scores[big] >= scores[small] - CONTAIN_MARGIN:
|
|
201
|
+
gone[small] = True
|
|
202
|
+
else:
|
|
203
|
+
gone[big] = True
|
|
204
|
+
return np.nonzero(~gone)[0]
|
|
205
|
+
|
|
206
|
+
|
|
161
207
|
def _split_outputs(arrays: list[np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
|
|
162
208
|
"""``(boxes, scores)`` for the one image in a batch, whatever order they come in."""
|
|
163
209
|
boxes = next((a for a in arrays if a.shape[-1] == 4), None)
|
|
@@ -235,12 +281,15 @@ class Predictor:
|
|
|
235
281
|
max_det: int = 300,
|
|
236
282
|
classes: list[int] | None = None,
|
|
237
283
|
iou: float | None = IOU,
|
|
284
|
+
contain: float | None = None,
|
|
238
285
|
) -> np.ndarray:
|
|
239
286
|
"""-> ``(N, 6)`` array of ``x1 y1 x2 y2 conf cls`` in pixels.
|
|
240
287
|
|
|
241
288
|
``iou`` drops a box that covers a higher-scoring one by more than that
|
|
242
289
|
IoU, whatever the two classes (``None`` keeps them all): see
|
|
243
|
-
:func:`drop_duplicates`.
|
|
290
|
+
:func:`drop_duplicates`. ``contain`` (off by default) also drops a box
|
|
291
|
+
lying mostly inside another of its class: see
|
|
292
|
+
:func:`drop_contained`.
|
|
244
293
|
"""
|
|
245
294
|
if _looks_like_logits(scores):
|
|
246
295
|
scores = 1.0 / (1.0 + np.exp(-scores))
|
|
@@ -263,6 +312,9 @@ class Predictor:
|
|
|
263
312
|
if iou is not None and len(xyxy) > 1:
|
|
264
313
|
kept = drop_duplicates(xyxy, iou)
|
|
265
314
|
xyxy, best, cls = xyxy[kept], best[kept], cls[kept]
|
|
315
|
+
if contain is not None and len(xyxy) > 1:
|
|
316
|
+
kept = drop_contained(xyxy, best, cls, contain)
|
|
317
|
+
xyxy, best, cls = xyxy[kept], best[kept], cls[kept]
|
|
266
318
|
return np.concatenate(
|
|
267
319
|
[xyxy, best.astype(np.float32)[:, None], cls.astype(np.float32)[:, None]], axis=1
|
|
268
320
|
)
|
|
@@ -274,6 +326,7 @@ class Predictor:
|
|
|
274
326
|
max_det: int = 300,
|
|
275
327
|
classes: list[int] | None = None,
|
|
276
328
|
iou: float | None = IOU,
|
|
329
|
+
contain: float | None = None,
|
|
277
330
|
) -> tuple[np.ndarray, dict[str, float]]:
|
|
278
331
|
"""Run one image; returns ``(detections (N,6), speed dict in ms)``."""
|
|
279
332
|
import time
|
|
@@ -283,7 +336,8 @@ class Predictor:
|
|
|
283
336
|
t1 = time.perf_counter()
|
|
284
337
|
boxes, scores = self.infer(tensor)
|
|
285
338
|
t2 = time.perf_counter()
|
|
286
|
-
det = self.postprocess(boxes, scores, img.shape[:2], conf, max_det, classes, iou
|
|
339
|
+
det = self.postprocess(boxes, scores, img.shape[:2], conf, max_det, classes, iou,
|
|
340
|
+
contain)
|
|
287
341
|
t3 = time.perf_counter()
|
|
288
342
|
speed = {
|
|
289
343
|
"preprocess": (t1 - t0) * 1e3,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "easydetect"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Easy real-time object detection you can ship: D-FINE with PyTorch training, OpenVINO inference on CPU/GPU/NPU, and a browser app to label and train. Apache-2.0 throughout."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -147,3 +147,57 @@ def test_one_predictor_can_be_shared_between_threads(tiny_ir):
|
|
|
147
147
|
for thread in threads:
|
|
148
148
|
thread.join()
|
|
149
149
|
assert failures == []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_a_half_hidden_object_and_its_pieces_become_one_box():
|
|
153
|
+
from easydetect.predictor import drop_contained, drop_duplicates
|
|
154
|
+
|
|
155
|
+
# best first: a piece (0.67), the whole chair (0.64), another piece (0.61),
|
|
156
|
+
# and a second chair beside it that only touches the first
|
|
157
|
+
xyxy = np.array([[320, 520, 372, 640], # lower right piece
|
|
158
|
+
[285, 455, 373, 640], # the whole chair
|
|
159
|
+
[285, 470, 330, 640], # left piece
|
|
160
|
+
[380, 455, 470, 640]], # another chair
|
|
161
|
+
np.float32)
|
|
162
|
+
scores = np.array([0.67, 0.64, 0.61, 0.60])
|
|
163
|
+
cls = np.array([56, 56, 56, 56])
|
|
164
|
+
assert list(drop_duplicates(xyxy, 0.7)) == [0, 1, 2, 3] # IoU sees four chairs
|
|
165
|
+
# the whole chair stays, its pieces go; the chair beside it is left alone
|
|
166
|
+
assert list(drop_contained(xyxy, scores, cls, 0.8)) == [1, 3]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_a_loose_box_around_a_group_does_not_erase_its_members():
|
|
170
|
+
from easydetect.predictor import drop_contained
|
|
171
|
+
|
|
172
|
+
xyxy = np.array([[0, 0, 50, 100], [50, 0, 100, 100], [0, 0, 100, 100]], np.float32)
|
|
173
|
+
scores = np.array([0.9, 0.85, 0.3]) # two people, and a box around both
|
|
174
|
+
assert list(drop_contained(xyxy, scores, np.zeros(3), 0.8)) == [0, 1]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_containment_never_crosses_classes():
|
|
178
|
+
from easydetect.predictor import drop_contained
|
|
179
|
+
|
|
180
|
+
xyxy = np.array([[0, 0, 100, 100], [10, 10, 40, 40]], np.float32) # a cup on a table
|
|
181
|
+
scores = np.array([0.8, 0.7])
|
|
182
|
+
assert list(drop_contained(xyxy, scores, np.array([60, 41]), 0.8)) == [0, 1]
|
|
183
|
+
assert list(drop_contained(xyxy, scores, np.array([60, 60]), 0.8)) == [0]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_the_whole_path_merges_pieces_only_when_asked():
|
|
187
|
+
"""postprocess as predict() runs it: model outputs in, boxes out, at 640 x 720."""
|
|
188
|
+
from easydetect.predictor import Predictor
|
|
189
|
+
|
|
190
|
+
pieces = [(320, 520, 372, 640, 0.67), (285, 455, 373, 640, 0.64), (285, 470, 330, 640, 0.61),
|
|
191
|
+
(40, 100, 340, 640, 0.97)] # three chair boxes and a person
|
|
192
|
+
boxes = np.array([[(x1 + x2) / 2 / 640, (y1 + y2) / 2 / 720, (x2 - x1) / 640,
|
|
193
|
+
(y2 - y1) / 720] for x1, y1, x2, y2, _ in pieces], np.float32)
|
|
194
|
+
scores = np.zeros((4, 80), np.float32)
|
|
195
|
+
for i, (*_, s) in enumerate(pieces):
|
|
196
|
+
scores[i, 0 if i == 3 else 56] = s
|
|
197
|
+
post = Predictor.postprocess.__get__(object.__new__(Predictor))
|
|
198
|
+
default = post(boxes, scores, (720, 640), conf=0.5)
|
|
199
|
+
merged = post(boxes, scores, (720, 640), conf=0.5, contain=0.8)
|
|
200
|
+
assert len(default) == 4 # NMS at 0.7 keeps every piece
|
|
201
|
+
assert sorted(merged[:, 5].tolist()) == [0, 56] # one person, one chair
|
|
202
|
+
chair = merged[merged[:, 5] == 56][0]
|
|
203
|
+
assert chair[:4].round().tolist() == [285, 455, 373, 640] # the whole of it
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|