OTVision 0.6.5__py3-none-any.whl → 0.6.6__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.
- OTVision/application/detect/detected_frame_factory.py +1 -0
- OTVision/detect/otdet_file_writer.py +1 -1
- OTVision/detect/rtsp_input_source.py +36 -28
- OTVision/detect/timestamper.py +1 -0
- OTVision/detect/video_input_source.py +7 -4
- OTVision/domain/frame.py +6 -0
- OTVision/track/parser/chunk_parser_plugins.py +1 -0
- OTVision/track/tracker/tracker_plugin_iou.py +1 -0
- OTVision/version.py +1 -1
- {otvision-0.6.5.dist-info → otvision-0.6.6.dist-info}/METADATA +1 -1
- {otvision-0.6.5.dist-info → otvision-0.6.6.dist-info}/RECORD +13 -13
- {otvision-0.6.5.dist-info → otvision-0.6.6.dist-info}/WHEEL +0 -0
- {otvision-0.6.5.dist-info → otvision-0.6.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -62,7 +62,7 @@ class OtdetFileWriter:
|
|
|
62
62
|
detect_config = config.detect
|
|
63
63
|
|
|
64
64
|
actual_frames = len(event.frames)
|
|
65
|
-
if
|
|
65
|
+
if expected_duration := detect_config.expected_duration:
|
|
66
66
|
actual_fps = actual_frames / expected_duration.total_seconds()
|
|
67
67
|
else:
|
|
68
68
|
actual_fps = actual_frames / source_metadata.duration.total_seconds()
|
|
@@ -31,6 +31,7 @@ RETRY_SECONDS = 1
|
|
|
31
31
|
|
|
32
32
|
class Counter:
|
|
33
33
|
def __init__(self, start_value: int = 0) -> None:
|
|
34
|
+
self._start_value = start_value
|
|
34
35
|
self.__counter = start_value
|
|
35
36
|
|
|
36
37
|
def increment(self) -> None:
|
|
@@ -39,6 +40,9 @@ class Counter:
|
|
|
39
40
|
def get(self) -> int:
|
|
40
41
|
return self.__counter
|
|
41
42
|
|
|
43
|
+
def reset(self) -> None:
|
|
44
|
+
self.__counter = self._start_value
|
|
45
|
+
|
|
42
46
|
|
|
43
47
|
class RtspInputSource(InputSourceDetect):
|
|
44
48
|
|
|
@@ -68,6 +72,10 @@ class RtspInputSource(InputSourceDetect):
|
|
|
68
72
|
def flush_buffer_size(self) -> int:
|
|
69
73
|
return self.stream_config.flush_buffer_size
|
|
70
74
|
|
|
75
|
+
@property
|
|
76
|
+
def fps(self) -> float:
|
|
77
|
+
return self.config.convert.output_fps
|
|
78
|
+
|
|
71
79
|
def __init__(
|
|
72
80
|
self,
|
|
73
81
|
subject: Subject[FlushEvent],
|
|
@@ -82,6 +90,9 @@ class RtspInputSource(InputSourceDetect):
|
|
|
82
90
|
self._get_current_config = get_current_config
|
|
83
91
|
self._current_stream: str | None = None
|
|
84
92
|
self._current_video_capture: VideoCapture | None = None
|
|
93
|
+
self._stream_start_time: datetime = self._datetime_provider.provide()
|
|
94
|
+
self._current_video_start_time = self._stream_start_time
|
|
95
|
+
self._outdated = True
|
|
85
96
|
|
|
86
97
|
@property
|
|
87
98
|
def _video_capture(self) -> VideoCapture:
|
|
@@ -106,20 +117,30 @@ class RtspInputSource(InputSourceDetect):
|
|
|
106
117
|
return self._current_video_capture
|
|
107
118
|
|
|
108
119
|
def produce(self) -> Generator[Frame, None, None]:
|
|
109
|
-
|
|
120
|
+
self._stream_start_time = self._datetime_provider.provide()
|
|
121
|
+
self._current_video_start_time = self._stream_start_time
|
|
110
122
|
while not self.should_stop():
|
|
111
123
|
if (frame := self._read_next_frame()) is not None:
|
|
112
124
|
self._frame_counter.increment()
|
|
125
|
+
occurrence = self._datetime_provider.provide()
|
|
126
|
+
|
|
127
|
+
if self._outdated:
|
|
128
|
+
self._current_video_start_time = occurrence
|
|
129
|
+
self._outdated = False
|
|
113
130
|
|
|
114
131
|
yield Frame(
|
|
115
132
|
data=convert_frame_to_rgb(frame), # YOLO expects RGB
|
|
116
133
|
frame=self.current_frame_number,
|
|
117
134
|
source=self.rtsp_url,
|
|
118
|
-
|
|
135
|
+
output=self.create_output(),
|
|
136
|
+
occurrence=occurrence,
|
|
119
137
|
)
|
|
120
138
|
if self.flush_condition_met():
|
|
121
|
-
self._notify(
|
|
122
|
-
|
|
139
|
+
self._notify()
|
|
140
|
+
self._outdated = True
|
|
141
|
+
self._frame_counter.reset()
|
|
142
|
+
|
|
143
|
+
self._notify()
|
|
123
144
|
|
|
124
145
|
def _init_video_capture(self, source: str) -> VideoCapture:
|
|
125
146
|
cap = VideoCapture(source)
|
|
@@ -152,24 +173,16 @@ class RtspInputSource(InputSourceDetect):
|
|
|
152
173
|
def flush_condition_met(self) -> bool:
|
|
153
174
|
return self.current_frame_number % self.flush_buffer_size == 0
|
|
154
175
|
|
|
155
|
-
def _notify(self
|
|
176
|
+
def _notify(self) -> None:
|
|
156
177
|
frame_width = int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
|
|
157
178
|
frame_height = int(self._video_capture.get(CAP_PROP_FRAME_HEIGHT))
|
|
158
|
-
fps = self.config.convert.output_fps
|
|
159
|
-
_start_time = calculate_start_time(
|
|
160
|
-
start_time, self.current_frame_number, fps, self.flush_buffer_size
|
|
161
|
-
)
|
|
162
179
|
frames = (
|
|
163
180
|
self.flush_buffer_size
|
|
164
181
|
if self.current_frame_number % self.flush_buffer_size == 0
|
|
165
182
|
else self.current_frame_number % self.flush_buffer_size
|
|
166
183
|
)
|
|
167
|
-
duration = timedelta(seconds=round(frames / fps))
|
|
168
|
-
|
|
169
|
-
f"{self.stream_config.name}_FR{round(fps)}"
|
|
170
|
-
f"_{_start_time.strftime(DATETIME_FORMAT)}.mp4"
|
|
171
|
-
)
|
|
172
|
-
output = str(self.stream_config.save_dir / output_filename)
|
|
184
|
+
duration = timedelta(seconds=round(frames / self.fps))
|
|
185
|
+
output = self.create_output()
|
|
173
186
|
self._subject.notify(
|
|
174
187
|
FlushEvent.create(
|
|
175
188
|
source=self.rtsp_url,
|
|
@@ -177,22 +190,17 @@ class RtspInputSource(InputSourceDetect):
|
|
|
177
190
|
duration=duration,
|
|
178
191
|
source_width=frame_width,
|
|
179
192
|
source_height=frame_height,
|
|
180
|
-
source_fps=fps,
|
|
181
|
-
start_time=
|
|
193
|
+
source_fps=self.fps,
|
|
194
|
+
start_time=self._current_video_start_time,
|
|
182
195
|
)
|
|
183
196
|
)
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
) * flush_buffer_size
|
|
192
|
-
if offset_in_frames == 0:
|
|
193
|
-
return start
|
|
194
|
-
offset_in_seconds = offset_in_frames / fps
|
|
195
|
-
return start + timedelta(seconds=offset_in_seconds)
|
|
198
|
+
def create_output(self) -> str:
|
|
199
|
+
output_filename = (
|
|
200
|
+
f"{self.stream_config.name}_FR{round(self.fps)}"
|
|
201
|
+
f"_{self._current_video_start_time.strftime(DATETIME_FORMAT)}.mp4"
|
|
202
|
+
)
|
|
203
|
+
return str(self.stream_config.save_dir / output_filename)
|
|
196
204
|
|
|
197
205
|
|
|
198
206
|
def convert_frame_to_rgb(frame: ndarray) -> ndarray:
|
OTVision/detect/timestamper.py
CHANGED
|
@@ -116,6 +116,7 @@ class VideoSource(InputSourceDetect):
|
|
|
116
116
|
FrameKeys.data: rotated_image,
|
|
117
117
|
FrameKeys.frame: frame_number,
|
|
118
118
|
FrameKeys.source: str(video_file),
|
|
119
|
+
FrameKeys.output: str(video_file),
|
|
119
120
|
}
|
|
120
121
|
)
|
|
121
122
|
else:
|
|
@@ -124,6 +125,7 @@ class VideoSource(InputSourceDetect):
|
|
|
124
125
|
FrameKeys.data: None,
|
|
125
126
|
FrameKeys.frame: frame_number,
|
|
126
127
|
FrameKeys.source: str(video_file),
|
|
128
|
+
FrameKeys.output: str(video_file),
|
|
127
129
|
}
|
|
128
130
|
)
|
|
129
131
|
counter += 1
|
|
@@ -206,8 +208,9 @@ class VideoSource(InputSourceDetect):
|
|
|
206
208
|
def __add_occurrence(self, timestamper: Timestamper, frame: dict) -> Frame:
|
|
207
209
|
updated = timestamper.stamp(frame)
|
|
208
210
|
return Frame(
|
|
209
|
-
data=updated[
|
|
210
|
-
frame=updated[
|
|
211
|
-
source=updated[
|
|
212
|
-
|
|
211
|
+
data=updated[FrameKeys.data],
|
|
212
|
+
frame=updated[FrameKeys.frame],
|
|
213
|
+
source=updated[FrameKeys.source],
|
|
214
|
+
output=updated[FrameKeys.output],
|
|
215
|
+
occurrence=updated[FrameKeys.occurrence],
|
|
213
216
|
)
|
OTVision/domain/frame.py
CHANGED
|
@@ -20,6 +20,7 @@ class FrameKeys:
|
|
|
20
20
|
frame: Literal["frame"] = "frame"
|
|
21
21
|
source: Literal["source"] = "source"
|
|
22
22
|
occurrence: Literal["occurrence"] = "occurrence"
|
|
23
|
+
output: Literal["output"] = "output"
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class Frame(TypedDict):
|
|
@@ -35,6 +36,7 @@ class Frame(TypedDict):
|
|
|
35
36
|
data: Optional[ndarray]
|
|
36
37
|
frame: int
|
|
37
38
|
source: str
|
|
39
|
+
output: str
|
|
38
40
|
occurrence: datetime
|
|
39
41
|
|
|
40
42
|
|
|
@@ -49,6 +51,7 @@ class DetectedFrame:
|
|
|
49
51
|
no (FrameNo): Frame number.
|
|
50
52
|
occurrence (datetime): Time stamp, at which frame was recorded.
|
|
51
53
|
source (str): Source from where frame was obtained, e.g. video file path.
|
|
54
|
+
output (str): Output file name, e.g. video file name.
|
|
52
55
|
detections (Sequence[Detection]): A sequence of Detections occurring in frame.
|
|
53
56
|
image (Optional[ndarray]): Optional image data of frame.
|
|
54
57
|
"""
|
|
@@ -56,6 +59,7 @@ class DetectedFrame:
|
|
|
56
59
|
no: FrameNo
|
|
57
60
|
occurrence: datetime
|
|
58
61
|
source: str
|
|
62
|
+
output: str
|
|
59
63
|
detections: Sequence[Detection]
|
|
60
64
|
image: Optional[ndarray] = None
|
|
61
65
|
|
|
@@ -64,6 +68,7 @@ class DetectedFrame:
|
|
|
64
68
|
no=self.no,
|
|
65
69
|
occurrence=self.occurrence,
|
|
66
70
|
source=self.source,
|
|
71
|
+
output=self.output,
|
|
67
72
|
detections=self.detections,
|
|
68
73
|
image=None,
|
|
69
74
|
)
|
|
@@ -153,6 +158,7 @@ class TrackedFrame(DetectedFrame):
|
|
|
153
158
|
no=self.no,
|
|
154
159
|
occurrence=self.occurrence,
|
|
155
160
|
source=self.source,
|
|
161
|
+
output=self.output,
|
|
156
162
|
finished_tracks=self.finished_tracks,
|
|
157
163
|
detections=detections,
|
|
158
164
|
image=self.image,
|
OTVision/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OTVision
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Summary: OTVision is a core module of the OpenTrafficCam framework to perform object detection and tracking.
|
|
5
5
|
Project-URL: Homepage, https://opentrafficcam.org/
|
|
6
6
|
Project-URL: Documentation, https://opentrafficcam.org/overview/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
OTVision/__init__.py,sha256=CLnfgTlVHM4_nzDacvy06Z_Crc3hU6usd0mUyEvBf24,781
|
|
2
2
|
OTVision/config.py,sha256=0ecnI0N2rS2q0Ld6gBpK4iU2iyuUw683XWjj4g-L1m4,5336
|
|
3
3
|
OTVision/dataformat.py,sha256=BHF7qHzyNb80hI1EKfwcdJ9bgG_X4bp_hCXzdg7_MSA,1941
|
|
4
|
-
OTVision/version.py,sha256=
|
|
4
|
+
OTVision/version.py,sha256=TePRHn7igC5gC-d_U37x7TlN7c1dsVfR5nB0hJk7lY0,175
|
|
5
5
|
OTVision/abstraction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
OTVision/abstraction/defaults.py,sha256=ftETDe25gmr563RPSbG6flcEiNiHnRb0iXK1Zj_zdNg,442
|
|
7
7
|
OTVision/abstraction/observer.py,sha256=ZFGxUUjI3wUpf5ogXg2yDe-QjCcXre6SxH5zOogOx2U,1350
|
|
@@ -18,7 +18,7 @@ OTVision/application/update_current_config.py,sha256=iW1rpCClTHn8tnmVSpLVxdEB0nh
|
|
|
18
18
|
OTVision/application/detect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
OTVision/application/detect/current_object_detector.py,sha256=J4U5k1s902agN90FYR4M_gO8MYYa_5XkYGREjStWCxQ,1343
|
|
20
20
|
OTVision/application/detect/current_object_detector_metadata.py,sha256=xai0UBEzxr-rxXCc8mTmNDECds7mdsw2sem5HZxvQ4Q,1017
|
|
21
|
-
OTVision/application/detect/detected_frame_factory.py,sha256=
|
|
21
|
+
OTVision/application/detect/detected_frame_factory.py,sha256=sW_l0xaPz44_lPEmCPKz4Xg1Mv4ZGKN9CyBCG_iN8dQ,1007
|
|
22
22
|
OTVision/application/detect/detected_frame_producer.py,sha256=LD4AnQG04YGE68TpxmaRuWRZeCZfhp8oixk6SHTru7c,906
|
|
23
23
|
OTVision/application/detect/detection_file_save_path_provider.py,sha256=nUyzgR7imrH8PkUl_72kdUDiolPXq1_RQqbpFwLI5Cs,2165
|
|
24
24
|
OTVision/application/detect/factory.py,sha256=UCnLtgpWdNqwwjW0v2yzKF9Gacx6gewjTyy43wXs2Jg,938
|
|
@@ -38,12 +38,12 @@ OTVision/detect/detect.py,sha256=YaVS-DJXdEmh-OzwE31UPNl2uk7mcFyO_CKKTgMeiuM,132
|
|
|
38
38
|
OTVision/detect/detected_frame_buffer.py,sha256=TrLbImpvCm1B09Z3c000e2uDO5WguyVwoMmFAqH8Zvk,1505
|
|
39
39
|
OTVision/detect/file_based_detect_builder.py,sha256=C6BqVuknbJzX-B4N4nSwJgEpt9Nf79Oen8H6so9AflU,754
|
|
40
40
|
OTVision/detect/otdet.py,sha256=-8rZGY9NVRAwIHeVcNCjm665SmnW5UVIO_PSKdHegDA,6274
|
|
41
|
-
OTVision/detect/otdet_file_writer.py,sha256=
|
|
41
|
+
OTVision/detect/otdet_file_writer.py,sha256=idgPKwhgEd18HT7HshaT304JPlSw0kT4Isn2gWJRaSk,4304
|
|
42
42
|
OTVision/detect/pyav_frame_count_provider.py,sha256=w7p9iM3F2fljV8SD7q491gQhIHANbVczqtalcUiKj-E,453
|
|
43
43
|
OTVision/detect/rtsp_based_detect_builder.py,sha256=5-jg4ivJhiWi3PVIILVThorDNKg-i4Z-rFqZ2n01RDY,1322
|
|
44
|
-
OTVision/detect/rtsp_input_source.py,sha256=
|
|
45
|
-
OTVision/detect/timestamper.py,sha256=
|
|
46
|
-
OTVision/detect/video_input_source.py,sha256=
|
|
44
|
+
OTVision/detect/rtsp_input_source.py,sha256=0BGMTFZqAf_8qjha_3BrNhjDoS-ejyNlU_Z9h3DqG54,6865
|
|
45
|
+
OTVision/detect/timestamper.py,sha256=VvDTzHu9fTI7qQL9x775Gc27r47R8D5Pb040ffwO04k,5288
|
|
46
|
+
OTVision/detect/video_input_source.py,sha256=GLzG4LeZNYcOE1tHyebL51HxBHzJOVaDfJKsLiAtu4A,8775
|
|
47
47
|
OTVision/detect/yolo.py,sha256=Ksj8X7DZmONalaMB_iz-AtXwhEk4Fu7nZNzrXpqfhQw,10451
|
|
48
48
|
OTVision/detect/plugin_av/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
49
|
OTVision/detect/plugin_av/rotate_frame.py,sha256=4wJqTYI2HRlfa4p2Ffap33vLmKIzE_EwFvQraEkQ4R8,1055
|
|
@@ -52,7 +52,7 @@ OTVision/domain/cli.py,sha256=VNZZ_zc1x6RzndQlOFp5HSeWTg_VAiF1G3IhYymdrzA,1781
|
|
|
52
52
|
OTVision/domain/current_config.py,sha256=Q38NCktoGNU1Z_miXNoJXLH8-NDbVszwVOMGR1aAwWM,286
|
|
53
53
|
OTVision/domain/detect_producer_consumer.py,sha256=gD7NwscQZLmCxMDZpZkGql0oMrpGHDBBNvdTXs58Vvw,855
|
|
54
54
|
OTVision/domain/detection.py,sha256=SZLP-87XE3NcTkeYz7GTqp4oPMiqI1P5gILp1_yHtxY,3761
|
|
55
|
-
OTVision/domain/frame.py,sha256=
|
|
55
|
+
OTVision/domain/frame.py,sha256=Hv1v9cegfhVGgl2MB4uKYbvngfkOLrvES_w4gD0HtMo,6410
|
|
56
56
|
OTVision/domain/input_source_detect.py,sha256=9DzkTg5dh7_KmxE9oxdmxrcTYhvZY8hHLZwhrh7Gz2o,1245
|
|
57
57
|
OTVision/domain/object_detection.py,sha256=kyrTbP9sZBKtGo54vCNfluDMM8wpWZST9Oqf8m8Q1y4,1394
|
|
58
58
|
OTVision/domain/serialization.py,sha256=S7gb648z_W8U3Fb6TSk7hVU4qHlGwOZ7D6FeYSLXQwM,257
|
|
@@ -81,11 +81,11 @@ OTVision/track/model/filebased/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
81
81
|
OTVision/track/model/filebased/frame_chunk.py,sha256=rXhQCHXWGJbePy5ZW3JZCdltGz5mZxFdcrW0mgez-2k,6771
|
|
82
82
|
OTVision/track/model/filebased/frame_group.py,sha256=f-hXS1Vc5U_qf2cgNbYVeSTZ3dg5NUJhasOEHuuX1HE,2977
|
|
83
83
|
OTVision/track/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
84
|
-
OTVision/track/parser/chunk_parser_plugins.py,sha256=
|
|
84
|
+
OTVision/track/parser/chunk_parser_plugins.py,sha256=X86W_TBQN20ldZIEuv63FSvWBacG0wEaD0YkC9jcJVg,3142
|
|
85
85
|
OTVision/track/parser/frame_group_parser_plugins.py,sha256=TWnGhM-N7ldN8LHZ1YYecEjn4xo2o91PyOGgr4Jzh9M,5479
|
|
86
86
|
OTVision/track/tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
87
|
OTVision/track/tracker/filebased_tracking.py,sha256=H3WYbsSca-67898diBoixpLjBqQDnOSiqnbvvySE6fc,6576
|
|
88
|
-
OTVision/track/tracker/tracker_plugin_iou.py,sha256=
|
|
88
|
+
OTVision/track/tracker/tracker_plugin_iou.py,sha256=AecE4CXRf4qUdN3_AvSFcsW4so-zDUGAVXqzfjSb-i0,7517
|
|
89
89
|
OTVision/transform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
90
|
OTVision/transform/get_homography.py,sha256=29waW61uzCCB7tlBAS2zck9sliAxZqnjjOa4jOOHHIc,5970
|
|
91
91
|
OTVision/transform/reference_points_picker.py,sha256=LOmwzCaqbJnXVhaTSaZtkCzTMDamYjbTpY8I99pN0rg,16578
|
|
@@ -98,7 +98,7 @@ OTVision/view/view_helpers.py,sha256=a5yV_6ZxO5bxsSymOmxdHqzOEv0VFq4wFBopVRGuVRo
|
|
|
98
98
|
OTVision/view/view_track.py,sha256=vmfMqpbUfnzg_EsWiL-IIKNOApVF09dzSojHpUfYY6M,5393
|
|
99
99
|
OTVision/view/view_transform.py,sha256=HvRd8g8geKRy0OoiZUDn_oC3SJC5nuXhZf3uZelfGKg,5473
|
|
100
100
|
OTVision/view/helpers/OTC.ico,sha256=G9kwlDtgBXmXO3yxW6Z-xVFV2q4nUGuz9E1VPHSu_I8,21662
|
|
101
|
-
otvision-0.6.
|
|
102
|
-
otvision-0.6.
|
|
103
|
-
otvision-0.6.
|
|
104
|
-
otvision-0.6.
|
|
101
|
+
otvision-0.6.6.dist-info/METADATA,sha256=RzNTAEFsKAPXOo7ze8bsD_wrxIt7RtPSP6C_uWgZ7QA,6262
|
|
102
|
+
otvision-0.6.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
103
|
+
otvision-0.6.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
104
|
+
otvision-0.6.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|