filter-crop 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.
filter_crop/__init__.py
ADDED
|
File without changes
|
filter_crop/filter.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import cv2
|
|
3
|
+
import numpy as np
|
|
4
|
+
import ast
|
|
5
|
+
from openfilter.filter_runtime.filter import FilterConfig, Filter, Frame
|
|
6
|
+
from typing import List, Tuple, Dict, Any, Optional
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
__all__ = ['FilterCropConfig', 'FilterCrop']
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
SKIP_OCR_FLAG = 'skip_ocr'
|
|
14
|
+
DEFAULT_CLASS_NAME = 'cropped'
|
|
15
|
+
|
|
16
|
+
class FilterCropConfig(FilterConfig):
|
|
17
|
+
"""
|
|
18
|
+
Configuration class for the FilterCrop.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
polygon_points (str): String representation of polygon coordinates for cropping.
|
|
22
|
+
Format: "[[(x1,y1), (x2,y2), ...]]"
|
|
23
|
+
mutate_original_frames (bool): If True, modify original frames instead of creating new ones.
|
|
24
|
+
output_prefix (Optional[str]): Prefix for output topics when not mutating original frames.
|
|
25
|
+
|
|
26
|
+
# Topic handling modes
|
|
27
|
+
topic_mode (str): How to handle multiple topics:
|
|
28
|
+
- "all": Process all topics
|
|
29
|
+
- "main_only": Only process 'main' topic
|
|
30
|
+
- "selected": Only process topics in 'topics' list
|
|
31
|
+
topics (List[str]): List of topics to process when topic_mode='selected'.
|
|
32
|
+
|
|
33
|
+
# Detection handling
|
|
34
|
+
detection_key (str): Key to look for in frame.data['meta'] for detections.
|
|
35
|
+
detection_class_field (str): Field name for detection class in metadata.
|
|
36
|
+
detection_roi_field (str): Field name for detection ROIs in metadata.
|
|
37
|
+
|
|
38
|
+
# Custom naming
|
|
39
|
+
custom_name (Optional[str]): Custom name for output when not using detection classes.
|
|
40
|
+
|
|
41
|
+
# For backward compatibility
|
|
42
|
+
crop_from_env (bool): Legacy parameter for environment-based configuration.
|
|
43
|
+
class_name (Optional[str]): Legacy parameter for custom class name.
|
|
44
|
+
cropped_frame_prefix (Optional[str]): Legacy parameter for output_prefix.
|
|
45
|
+
"""
|
|
46
|
+
# Basic configuration
|
|
47
|
+
polygon_points: Optional[str] = None
|
|
48
|
+
mutate_original_frames: bool = False
|
|
49
|
+
output_prefix: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
# Topic handling
|
|
52
|
+
topic_mode: str = "all"
|
|
53
|
+
topics: List[str] = ["main"]
|
|
54
|
+
|
|
55
|
+
# Detection handling
|
|
56
|
+
detection_key: str = "detections"
|
|
57
|
+
detection_class_field: str = "class"
|
|
58
|
+
detection_roi_field: str = "rois"
|
|
59
|
+
|
|
60
|
+
# Custom naming
|
|
61
|
+
custom_name: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
# Legacy support
|
|
64
|
+
crop_from_env: bool = False
|
|
65
|
+
class_name: Optional[str] = None
|
|
66
|
+
cropped_frame_prefix: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FilterCrop(Filter):
|
|
70
|
+
"""
|
|
71
|
+
A filter that crops video frames based on polygon coordinates or detection ROIs.
|
|
72
|
+
|
|
73
|
+
This filter is designed to work within a video processing pipeline, supporting both real-time and batch processing.
|
|
74
|
+
It provides flexible cropping capabilities with multiple operation modes and output options.
|
|
75
|
+
|
|
76
|
+
Features:
|
|
77
|
+
1. Multiple Operation Modes:
|
|
78
|
+
- Polygon Mode: Crop frames using predefined polygon coordinates
|
|
79
|
+
- Detection Mode: Crop frames based on detection ROIs with class labels
|
|
80
|
+
- Environment Mode: Configure cropping through environment variables
|
|
81
|
+
|
|
82
|
+
2. Flexible Output Options:
|
|
83
|
+
- Create new frames for cropped regions
|
|
84
|
+
- Modify original frames in-place
|
|
85
|
+
- Support for labeled output topics
|
|
86
|
+
- Maintain original frame as "main" stream
|
|
87
|
+
|
|
88
|
+
3. Configuration Options:
|
|
89
|
+
- Polygon coordinates can be provided as string or through environment
|
|
90
|
+
- Customizable output topic prefixes
|
|
91
|
+
- Configurable class names for labeled detections
|
|
92
|
+
- Customizable detection metadata keys for class and ROIs
|
|
93
|
+
|
|
94
|
+
4. Topic Selection Modes:
|
|
95
|
+
- "all": Process all incoming topic frames
|
|
96
|
+
- "main_only": Only process the 'main' topic frame
|
|
97
|
+
- "selected": Only process frames from topics specified in configuration
|
|
98
|
+
|
|
99
|
+
5. Frame Processing Behavior:
|
|
100
|
+
- For frames with detections: Creates separate output frames for each detection
|
|
101
|
+
- For frames without detections: Uses polygon-based cropping
|
|
102
|
+
- Sets appropriate metadata for downstream processing (e.g., SKIP_OCR_FLAG)
|
|
103
|
+
|
|
104
|
+
6. Backward Compatibility:
|
|
105
|
+
- Supports legacy configuration via environment variables
|
|
106
|
+
- Maintains compatibility with previous crop filter implementations
|
|
107
|
+
|
|
108
|
+
Parameters:
|
|
109
|
+
polygon_points (str, optional): String representation of polygon coordinates for cropping
|
|
110
|
+
Format: "[[(x1,y1), (x2,y2), ...]]"
|
|
111
|
+
mutate_original_frames (bool): If True, modify original frames instead of creating new ones
|
|
112
|
+
output_prefix (str, optional): Prefix for output topics when not mutating original frames
|
|
113
|
+
topic_mode (str): How to handle multiple topics ("all", "main_only", or "selected")
|
|
114
|
+
topics (List[str]): List of topics to process when topic_mode='selected'
|
|
115
|
+
detection_key (str): Key to look for in frame.data['meta'] for detections
|
|
116
|
+
detection_class_field (str): Field name for detection class in metadata
|
|
117
|
+
detection_roi_field (str): Field name for detection ROIs in metadata
|
|
118
|
+
custom_name (str, optional): Custom name for output when not using detection classes
|
|
119
|
+
crop_from_env (bool): Legacy parameter for environment-based configuration
|
|
120
|
+
class_name (str, optional): Legacy parameter for custom class name
|
|
121
|
+
cropped_frame_prefix (str, optional): Legacy parameter for output_prefix
|
|
122
|
+
|
|
123
|
+
Example Configurations:
|
|
124
|
+
|
|
125
|
+
1. Basic Polygon Crop:
|
|
126
|
+
```python
|
|
127
|
+
config = FilterCropConfig(
|
|
128
|
+
polygon_points="[[(100, 100), (400, 100), (400, 300), (100, 300)]]",
|
|
129
|
+
output_prefix="cropped_",
|
|
130
|
+
topic_mode="all"
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
2. Detection-based Cropping:
|
|
135
|
+
```python
|
|
136
|
+
config = FilterCropConfig(
|
|
137
|
+
detection_key="detections",
|
|
138
|
+
detection_class_field="class",
|
|
139
|
+
detection_roi_field="rois",
|
|
140
|
+
topic_mode="main_only"
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
3. In-place Modification:
|
|
145
|
+
```python
|
|
146
|
+
config = FilterCropConfig(
|
|
147
|
+
polygon_points="[[(100, 100), (400, 100), (400, 300), (100, 300)]]",
|
|
148
|
+
mutate_original_frames=True
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
4. Topic-selective Processing:
|
|
153
|
+
```python
|
|
154
|
+
config = FilterCropConfig(
|
|
155
|
+
polygon_points="[[(100, 100), (400, 100), (400, 300), (100, 300)]]",
|
|
156
|
+
topic_mode="selected",
|
|
157
|
+
topics=["camera1", "camera2"],
|
|
158
|
+
output_prefix="region_"
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
5. Custom Detection Fields:
|
|
163
|
+
```python
|
|
164
|
+
config = FilterCropConfig(
|
|
165
|
+
detection_key="objects",
|
|
166
|
+
detection_class_field="label",
|
|
167
|
+
detection_roi_field="bounding_boxes",
|
|
168
|
+
topic_mode="all"
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def normalize_config(cls, config: FilterCropConfig) -> FilterCropConfig:
|
|
175
|
+
"""
|
|
176
|
+
Normalize and validate the filter configuration.
|
|
177
|
+
|
|
178
|
+
This method:
|
|
179
|
+
1. Loads configuration from environment variables if specified
|
|
180
|
+
2. Converts polygon points from string to list format
|
|
181
|
+
3. Validates polygon points and configuration parameters
|
|
182
|
+
4. Ensures required parameters are present based on operation mode
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
config (FilterCropConfig): The configuration to normalize
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
FilterCropConfig: The normalized configuration
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ValueError: If polygon points are invalid or required parameters are missing
|
|
192
|
+
"""
|
|
193
|
+
config = FilterCropConfig(super().normalize_config(config))
|
|
194
|
+
|
|
195
|
+
# Handle legacy parameters
|
|
196
|
+
if config.cropped_frame_prefix and not config.output_prefix:
|
|
197
|
+
config.output_prefix = config.cropped_frame_prefix
|
|
198
|
+
|
|
199
|
+
env_mapping = {
|
|
200
|
+
"mutate_original_frames": bool,
|
|
201
|
+
"output_prefix": str,
|
|
202
|
+
"polygon_points": str,
|
|
203
|
+
"topic_mode": str,
|
|
204
|
+
"topics": list,
|
|
205
|
+
"detection_key": str,
|
|
206
|
+
"detection_class_field": str,
|
|
207
|
+
"detection_roi_field": str,
|
|
208
|
+
"custom_name": str,
|
|
209
|
+
"crop_from_env": bool,
|
|
210
|
+
"class_name": str,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for key, expected_type in env_mapping.items():
|
|
214
|
+
env_key = f"FILTER_{key.upper()}"
|
|
215
|
+
env_val = os.getenv(env_key)
|
|
216
|
+
if env_val is not None:
|
|
217
|
+
if expected_type is bool:
|
|
218
|
+
setattr(config, key, env_val.strip().lower() == "true")
|
|
219
|
+
else:
|
|
220
|
+
setattr(config, key, env_val.strip())
|
|
221
|
+
|
|
222
|
+
# Convert polygon_points from string to list if it is a string
|
|
223
|
+
if isinstance(config.polygon_points, str):
|
|
224
|
+
config.polygon_points = ast.literal_eval(config.polygon_points)
|
|
225
|
+
if isinstance(config.polygon_points[0], tuple):
|
|
226
|
+
config.polygon_points = [config.polygon_points]
|
|
227
|
+
if len(config.polygon_points[0]) < 3:
|
|
228
|
+
raise ValueError("Polygon must have at least three vertices.")
|
|
229
|
+
else:
|
|
230
|
+
# Assume polygon will be provided elsewhere or not used
|
|
231
|
+
config.polygon_points = None
|
|
232
|
+
|
|
233
|
+
# Only validate output_prefix if we're not using detections and not mutating frames
|
|
234
|
+
if not config.mutate_original_frames and not config.output_prefix and config.polygon_points is not None:
|
|
235
|
+
raise ValueError("output_prefix cannot be empty when mutate_original_frames is False and using polygon mode.")
|
|
236
|
+
|
|
237
|
+
return config
|
|
238
|
+
|
|
239
|
+
def setup(self, config: FilterCropConfig):
|
|
240
|
+
"""
|
|
241
|
+
Initialize the filter with the given configuration.
|
|
242
|
+
|
|
243
|
+
This method:
|
|
244
|
+
1. Sets up instance variables from the configuration
|
|
245
|
+
2. Processes polygon points if provided
|
|
246
|
+
3. Computes bounding box for cropping
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
config (FilterCropConfig): The configuration to use for setup
|
|
250
|
+
"""
|
|
251
|
+
logger.info("===========================================")
|
|
252
|
+
logger.info(f"FilterPolygonCrop setup: {config}")
|
|
253
|
+
logger.info("===========================================")
|
|
254
|
+
|
|
255
|
+
self.mutate_original_frames = config.mutate_original_frames
|
|
256
|
+
self.output_prefix = config.output_prefix
|
|
257
|
+
self.polygon_points = config.polygon_points
|
|
258
|
+
self.topic_mode = config.topic_mode
|
|
259
|
+
self.topics = config.topics
|
|
260
|
+
self.detection_key = config.detection_key
|
|
261
|
+
self.detection_class_field = config.detection_class_field
|
|
262
|
+
self.detection_roi_field = config.detection_roi_field
|
|
263
|
+
self.custom_name = config.custom_name
|
|
264
|
+
|
|
265
|
+
# Legacy support
|
|
266
|
+
self.crop_from_env = config.crop_from_env
|
|
267
|
+
self.class_name = config.class_name
|
|
268
|
+
|
|
269
|
+
self.config = config
|
|
270
|
+
|
|
271
|
+
if config.polygon_points is None:
|
|
272
|
+
self.polygon_points = None
|
|
273
|
+
else:
|
|
274
|
+
# Parse the string into a list of points
|
|
275
|
+
if isinstance(config.polygon_points, str):
|
|
276
|
+
self.polygon_points = ast.literal_eval(config.polygon_points)
|
|
277
|
+
else:
|
|
278
|
+
self.polygon_points = config.polygon_points
|
|
279
|
+
|
|
280
|
+
# Convert to numpy array
|
|
281
|
+
self.polygon_points = np.array(self.polygon_points, dtype=np.int32)
|
|
282
|
+
|
|
283
|
+
# Compute bounding box around the polygon for crop dimensions
|
|
284
|
+
self.x, self.y, self.w, self.h = self.polygon_to_bbox(self.polygon_points)
|
|
285
|
+
|
|
286
|
+
logger.info("Setup Completed for Crop Filter")
|
|
287
|
+
|
|
288
|
+
def shutdown(self):
|
|
289
|
+
"""Clean up resources when the filter is shut down."""
|
|
290
|
+
logger.info("Shutdown completed!")
|
|
291
|
+
|
|
292
|
+
def process(self, frames: Dict[str, Frame]) -> Dict[str, Frame]:
|
|
293
|
+
"""Process incoming frames and apply cropping based on configuration.
|
|
294
|
+
|
|
295
|
+
The filter operates in three modes:
|
|
296
|
+
1. Detection Mode: Uses detection ROIs to crop frames
|
|
297
|
+
2. Polygon Mode: Uses predefined polygon coordinates to crop frames
|
|
298
|
+
3. Environment Mode: Uses environment configuration for cropping
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
frames: Dictionary of input frames
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dictionary of processed frames with appropriate keys
|
|
305
|
+
"""
|
|
306
|
+
output_frames = {}
|
|
307
|
+
|
|
308
|
+
# Tag frames with their topics and identify frames with detections
|
|
309
|
+
for topic, frame in frames.items():
|
|
310
|
+
if 'meta' not in frame.data:
|
|
311
|
+
frame.data['meta'] = {}
|
|
312
|
+
frame.data['meta']['topic'] = topic
|
|
313
|
+
|
|
314
|
+
# Always return original frames if mutating
|
|
315
|
+
if self.mutate_original_frames:
|
|
316
|
+
output_frames[topic] = frame
|
|
317
|
+
|
|
318
|
+
# Filter frames based on topic_mode
|
|
319
|
+
filtered_frames = {}
|
|
320
|
+
for topic, frame in frames.items():
|
|
321
|
+
if self.topic_mode == "main_only":
|
|
322
|
+
if topic == "main":
|
|
323
|
+
filtered_frames[topic] = frame
|
|
324
|
+
# If no 'main' topic exists, use the first frame
|
|
325
|
+
elif 'main' not in frames and len(filtered_frames) == 0:
|
|
326
|
+
filtered_frames[topic] = frame
|
|
327
|
+
elif self.topic_mode == "selected":
|
|
328
|
+
if topic in self.topics:
|
|
329
|
+
filtered_frames[topic] = frame
|
|
330
|
+
else: # "all" mode
|
|
331
|
+
filtered_frames[topic] = frame
|
|
332
|
+
|
|
333
|
+
# Group frames by detection presence
|
|
334
|
+
detection_frames = {}
|
|
335
|
+
non_detection_frames = {}
|
|
336
|
+
|
|
337
|
+
for topic, frame in filtered_frames.items():
|
|
338
|
+
# Use configured metadata key to check for detections
|
|
339
|
+
has_detections = (
|
|
340
|
+
self.detection_key in frame.data.get('meta', {}) and
|
|
341
|
+
frame.data['meta'][self.detection_key]
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if has_detections:
|
|
345
|
+
detection_frames[topic] = frame
|
|
346
|
+
else:
|
|
347
|
+
non_detection_frames[topic] = frame
|
|
348
|
+
|
|
349
|
+
# Process frames with detections
|
|
350
|
+
for topic, frame in detection_frames.items():
|
|
351
|
+
processed = self._process_detection_frame(frame)
|
|
352
|
+
output_frames.update(processed)
|
|
353
|
+
|
|
354
|
+
# Process frames without detections
|
|
355
|
+
for topic, frame in non_detection_frames.items():
|
|
356
|
+
processed = self._process_polygon_frame(frame)
|
|
357
|
+
output_frames.update(processed)
|
|
358
|
+
|
|
359
|
+
def sanitize_frame_data(frames: Dict[str, Frame]):
|
|
360
|
+
for frame in frames.values():
|
|
361
|
+
for k, v in list(frame.data.items()):
|
|
362
|
+
if isinstance(v, np.ndarray):
|
|
363
|
+
logger.warning(f"Removing unserializable key '{k}' from frame.data")
|
|
364
|
+
del frame.data[k]
|
|
365
|
+
|
|
366
|
+
sanitize_frame_data(output_frames)
|
|
367
|
+
return output_frames
|
|
368
|
+
|
|
369
|
+
def _process_detection_frame(self, frame: Frame) -> Dict[str, Frame]:
|
|
370
|
+
"""Process a frame with detections."""
|
|
371
|
+
output_frames = {}
|
|
372
|
+
image = frame.rw_bgr.image
|
|
373
|
+
topic = frame.data.get('meta', {}).get('topic', 'main')
|
|
374
|
+
|
|
375
|
+
# Always create main frame first
|
|
376
|
+
if self.mutate_original_frames:
|
|
377
|
+
frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
378
|
+
else:
|
|
379
|
+
main_frame = Frame(image, {**frame.data}, 'BGR')
|
|
380
|
+
main_frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
381
|
+
output_frames['main'] = main_frame
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# Get detections using configured key
|
|
385
|
+
detections = frame.data['meta'].get(self.detection_key, [])
|
|
386
|
+
|
|
387
|
+
if detections:
|
|
388
|
+
# Track class counts to generate unique keys
|
|
389
|
+
class_counts = {}
|
|
390
|
+
|
|
391
|
+
# Process each detection
|
|
392
|
+
for detection in detections:
|
|
393
|
+
# Use configured keys for class and ROI
|
|
394
|
+
class_name = detection.get(self.detection_class_field, DEFAULT_CLASS_NAME)
|
|
395
|
+
rois = detection.get(self.detection_roi_field, [])
|
|
396
|
+
|
|
397
|
+
for roi in rois:
|
|
398
|
+
# Update count for this class
|
|
399
|
+
count = class_counts.get(class_name, 0) + 1
|
|
400
|
+
class_counts[class_name] = count
|
|
401
|
+
|
|
402
|
+
# Create a unique key for this detection
|
|
403
|
+
unique_key = class_name if count == 1 else f"{class_name}_{count}"
|
|
404
|
+
|
|
405
|
+
# Crop the frame using the ROI coordinates
|
|
406
|
+
x1, y1, x2, y2 = roi
|
|
407
|
+
cropped_frame = image[y1:y2, x1:x2]
|
|
408
|
+
|
|
409
|
+
if self.mutate_original_frames:
|
|
410
|
+
frame = Frame(cropped_frame, {**frame.data}, 'BGR')
|
|
411
|
+
frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
412
|
+
output_frames[topic] = frame
|
|
413
|
+
else:
|
|
414
|
+
# Create a new frame for the cropped image
|
|
415
|
+
crop_frame = Frame(cropped_frame, {**frame.data}, 'BGR')
|
|
416
|
+
crop_frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
417
|
+
output_frames[unique_key] = crop_frame
|
|
418
|
+
else:
|
|
419
|
+
# No detections, just set SKIP_OCR_FLAG
|
|
420
|
+
frame.data['meta'][SKIP_OCR_FLAG] = True
|
|
421
|
+
|
|
422
|
+
return output_frames
|
|
423
|
+
|
|
424
|
+
def _process_polygon_frame(self, frame: Frame) -> Dict[str, Frame]:
|
|
425
|
+
"""Process a frame without detections using polygon mode or environment mode."""
|
|
426
|
+
output_frames = {}
|
|
427
|
+
image = frame.rw_bgr.image
|
|
428
|
+
topic = frame.data.get('meta', {}).get('topic', 'main')
|
|
429
|
+
|
|
430
|
+
# Always create main frame first
|
|
431
|
+
if self.mutate_original_frames:
|
|
432
|
+
frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
433
|
+
else:
|
|
434
|
+
main_frame = Frame(image, {**frame.data}, 'BGR')
|
|
435
|
+
main_frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
436
|
+
output_frames['main'] = main_frame
|
|
437
|
+
|
|
438
|
+
# Handle environment mode (for legacy support)
|
|
439
|
+
if self.crop_from_env:
|
|
440
|
+
# Use class_name if specified, otherwise use DEFAULT_CLASS_NAME
|
|
441
|
+
class_name = self.class_name or DEFAULT_CLASS_NAME
|
|
442
|
+
|
|
443
|
+
# Apply output prefix if specified
|
|
444
|
+
if self.output_prefix:
|
|
445
|
+
output_key = f"{self.output_prefix}{class_name}"
|
|
446
|
+
else:
|
|
447
|
+
output_key = class_name
|
|
448
|
+
|
|
449
|
+
# Create a new frame with the original image
|
|
450
|
+
env_frame = Frame(image, {**frame.data}, 'BGR')
|
|
451
|
+
env_frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
452
|
+
output_frames[output_key] = env_frame
|
|
453
|
+
return output_frames
|
|
454
|
+
|
|
455
|
+
# Handle polygon mode
|
|
456
|
+
if self.polygon_points is not None:
|
|
457
|
+
# Create mask from polygon
|
|
458
|
+
mask = np.zeros_like(image, dtype=np.uint8)
|
|
459
|
+
cv2.fillPoly(mask, [self.polygon_points], (255, 255, 255))
|
|
460
|
+
|
|
461
|
+
# Apply mask and crop
|
|
462
|
+
masked_frame = cv2.bitwise_and(image, mask)
|
|
463
|
+
cropped_frame = masked_frame[self.y:self.y + self.h, self.x:self.x + self.w]
|
|
464
|
+
|
|
465
|
+
if self.mutate_original_frames:
|
|
466
|
+
frame = Frame(cropped_frame, {**frame.data}, 'BGR')
|
|
467
|
+
frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
468
|
+
output_frames[topic] = frame
|
|
469
|
+
else:
|
|
470
|
+
# Create a new frame with the cropped image
|
|
471
|
+
if self.custom_name:
|
|
472
|
+
output_key = self.custom_name
|
|
473
|
+
else:
|
|
474
|
+
output_key = topic
|
|
475
|
+
|
|
476
|
+
if self.output_prefix:
|
|
477
|
+
output_key = f"{self.output_prefix}{output_key}"
|
|
478
|
+
|
|
479
|
+
crop_frame = Frame(cropped_frame, {**frame.data}, 'BGR')
|
|
480
|
+
crop_frame.data['meta'][SKIP_OCR_FLAG] = False
|
|
481
|
+
output_frames[output_key] = crop_frame
|
|
482
|
+
else:
|
|
483
|
+
# No polygon points, just pass through the frame
|
|
484
|
+
frame.data['meta'][SKIP_OCR_FLAG] = True
|
|
485
|
+
output_frames[topic] = frame
|
|
486
|
+
|
|
487
|
+
return output_frames
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def polygon_to_bbox(cls, polygon_points: List[Tuple[int, int]]) -> Tuple[int, int, int, int]:
|
|
491
|
+
"""
|
|
492
|
+
Convert a list of polygon points to a bounding box.
|
|
493
|
+
|
|
494
|
+
This method:
|
|
495
|
+
1. Computes the minimum bounding rectangle that contains all polygon points
|
|
496
|
+
2. Returns the top-left corner coordinates and dimensions of the rectangle
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
polygon_points (List[Tuple[int, int]]): List of (x, y) coordinates defining the polygon
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Tuple[int, int, int, int]: (x, y, width, height) of the bounding box
|
|
503
|
+
"""
|
|
504
|
+
x, y, w, h = cv2.boundingRect(polygon_points)
|
|
505
|
+
return x, y, w, h
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
if __name__ == '__main__':
|
|
509
|
+
FilterCrop.run()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: filter_crop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License-Expression: Apache-2.0
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Requires-Python: <3.13,>=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: openfilter[all]==0.1.1
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: build==1.2.1; extra == "dev"
|
|
15
|
+
Requires-Dist: setuptools==72.2.0; extra == "dev"
|
|
16
|
+
Requires-Dist: twine<7,>=6.1.0; extra == "dev"
|
|
17
|
+
Requires-Dist: wheel==0.44.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest==8.3.4; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov==6.0.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# 🪚 Crop Filter
|
|
23
|
+
|
|
24
|
+
**Crop Filter** is a modular [OpenFilter](https://github.com/PlainsightAI/openfilter)-based filter for extracting regions of interest (ROIs) from video frames using either detection metadata or predefined polygons.
|
|
25
|
+
|
|
26
|
+
It supports polygon-based cropping, detection-based cropping, and legacy environment-based configurations — all while integrating seamlessly into OpenFilter pipelines.
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/filter-crop/)
|
|
29
|
+
[](https://github.com/PlainsightAI/filter-crop/blob/main/LICENSE)
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ✨ Features
|
|
35
|
+
|
|
36
|
+
- 🎯 Crop using polygon coordinates or detection bounding boxes
|
|
37
|
+
- 🔀 Supports in-place modification or creation of new cropped frames
|
|
38
|
+
- 🧠 Configurable via CLI args, code, or environment variables
|
|
39
|
+
- 🧩 Plug-and-play compatibility with [OpenFilter](https://github.com/PlainsightAI/openfilter)
|
|
40
|
+
- ⚙️ Supports topic selection modes and legacy configuration
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 📦 Installation
|
|
45
|
+
|
|
46
|
+
Install the latest version from PyPI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install filter-crop
|
|
50
|
+
````
|
|
51
|
+
|
|
52
|
+
Or install from source:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/PlainsightAI/filter-crop.git
|
|
56
|
+
cd filter-crop
|
|
57
|
+
make install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🚀 Quick Start (CLI)
|
|
63
|
+
|
|
64
|
+
Use the OpenFilter CLI to run the Crop Filter in a processing pipeline:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
openfilter run \
|
|
68
|
+
- VideoIn --sources file://filter_example_video.mp4!loop \
|
|
69
|
+
- filter_crop.filter.FilterCrop \
|
|
70
|
+
--polygon_points "[[(0, 500), (0, 720), (1280, 720), (1280, 500)]]" \
|
|
71
|
+
--mutate_original_frames true \
|
|
72
|
+
--topic_mode main_only \
|
|
73
|
+
- Webvis
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or simply:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
make run
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then open [http://localhost:8000](http://localhost:8000) to view the output.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 🧰 Using from PyPI
|
|
87
|
+
|
|
88
|
+
After installing with:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install filter-crop
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You can use the Crop Filter directly in code:
|
|
95
|
+
|
|
96
|
+
### Standalone usage
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from filter_crop.filter import FilterCrop
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
FilterCrop.run()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### In a multi-filter OpenFilter pipeline
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from openfilter.filter_runtime.filter import Filter
|
|
109
|
+
from openfilter.filter_runtime.filters.video_in import VideoIn
|
|
110
|
+
from openfilter.filter_runtime.filters.webvis import Webvis
|
|
111
|
+
from filter_crop.filter import FilterCrop
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
Filter.run_multi([
|
|
115
|
+
(VideoIn, dict(sources='file://example.mp4!loop')),
|
|
116
|
+
(FilterCrop, dict(
|
|
117
|
+
polygon_points=[[(100, 100), (400, 100), (400, 300), (100, 300)]],
|
|
118
|
+
output_prefix="crop_",
|
|
119
|
+
topic_mode="main_only"
|
|
120
|
+
)),
|
|
121
|
+
(Webvis, {}),
|
|
122
|
+
])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🧪 Testing
|
|
128
|
+
|
|
129
|
+
Run tests locally:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
make test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or run a specific file:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pytest -v tests/test_filter_crop.py
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Tests cover:
|
|
142
|
+
|
|
143
|
+
* Polygon cropping
|
|
144
|
+
* Detection metadata
|
|
145
|
+
* ROI handling
|
|
146
|
+
* Edge cases (missing metadata, malformed inputs)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 🔧 Example Configs
|
|
151
|
+
|
|
152
|
+
### Polygon Crop
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
FilterCropConfig(
|
|
156
|
+
polygon_points="[[(100, 100), (400, 100), (400, 300), (100, 300)]]",
|
|
157
|
+
output_prefix="cropped_",
|
|
158
|
+
topic_mode="all"
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Detection-based Crop
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
FilterCropConfig(
|
|
166
|
+
detection_key="detections",
|
|
167
|
+
detection_class_field="class",
|
|
168
|
+
detection_roi_field="rois",
|
|
169
|
+
topic_mode="main_only"
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### In-place Modification
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
FilterCropConfig(
|
|
177
|
+
polygon_points="[[(100, 100), (400, 100), (400, 300), (100, 300)]]",
|
|
178
|
+
mutate_original_frames=True
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🧩 How it Works
|
|
185
|
+
|
|
186
|
+
The filter supports three main cropping strategies:
|
|
187
|
+
|
|
188
|
+
| Mode | Input Needed | Output Behavior |
|
|
189
|
+
| ------------------ | --------------------------------- | ------------------------------------------ |
|
|
190
|
+
| Detection | Metadata with `rois` and `class` | Crops and emits one frame per detection |
|
|
191
|
+
| Polygon | List of polygon points | Applies polygon mask and crops the region |
|
|
192
|
+
| Env-based (legacy) | ENV vars like `FILTER_CLASS_NAME` | Emits a labeled copy of the original frame |
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🤝 Contributing
|
|
197
|
+
|
|
198
|
+
We welcome PRs! Please read our [CONTRIBUTING.md](https://github.com/PlainsightAI/filter-crop/blob/main/CONTRIBUTING.md) for instructions.
|
|
199
|
+
|
|
200
|
+
**Highlights**:
|
|
201
|
+
|
|
202
|
+
* Format code with `black`
|
|
203
|
+
* Lint with `ruff`
|
|
204
|
+
* Use type hints on public methods
|
|
205
|
+
* Sign commits using DCO (`git commit -s`)
|
|
206
|
+
* Include tests when relevant
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 📄 License
|
|
211
|
+
|
|
212
|
+
Licensed under the [Apache 2.0 License](https://github.com/PlainsightAI/filter-crop/blob/main/LICENSE).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 🙏 Acknowledgements
|
|
217
|
+
|
|
218
|
+
Thanks for using and improving the Crop Filter!
|
|
219
|
+
For issues or suggestions, [open a GitHub issue](https://github.com/PlainsightAI/filter-crop/issues/new/choose).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
filter_crop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
filter_crop/filter.py,sha256=naa5dFHHXW2LK5JGReqimH9DcnDQweI-iEKyLgwl7DE,20780
|
|
3
|
+
filter_crop-0.1.0.dist-info/licenses/LICENSE,sha256=Hun-T5YKdxKbQ1lfbGCu8YiknxeFsTnWAuSWz6CKQAQ,11343
|
|
4
|
+
filter_crop-0.1.0.dist-info/METADATA,sha256=PX3obkmhGyCL5DwwmuW0JVX-RGwVnazilHPCeVGzyqs,5561
|
|
5
|
+
filter_crop-0.1.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
6
|
+
filter_crop-0.1.0.dist-info/top_level.txt,sha256=F1XUSTXB-lp7PAMAUYvz2UCZsoO1ummLYhCOgRzEzzI,12
|
|
7
|
+
filter_crop-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [2025] [Plainsight]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
filter_crop
|