Phase 1: Add foundation improvements - logging, configuration, testing, CI/CD
Co-authored-by: Askill <16598120+Askill@users.noreply.github.com>
This commit is contained in:
parent
4f4652ef4a
commit
883e05f2a5
|
|
@ -0,0 +1,110 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install black isort flake8 mypy
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Check code formatting with Black
|
||||
run: |
|
||||
black --check --line-length 140 .
|
||||
|
||||
- name: Check import sorting with isort
|
||||
run: |
|
||||
isort --check-only --profile black --line-length 140 .
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# Stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# Exit-zero treats all errors as warnings
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=140 --statistics --extend-ignore=E203,W503
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg libsm6 libxext6 libxrender-dev
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-cov
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Run tests (if they exist)
|
||||
run: |
|
||||
if [ -d tests ]; then
|
||||
pytest --cov=Application --cov-report=xml --cov-report=term-missing
|
||||
else
|
||||
echo "No tests directory found, skipping tests"
|
||||
fi
|
||||
|
||||
- name: Upload coverage reports
|
||||
if: matrix.python-version == '3.12'
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Check package
|
||||
run: |
|
||||
pip install twine
|
||||
twine check dist/*
|
||||
|
|
@ -1,18 +1,87 @@
|
|||
|
||||
# Test footage and media files
|
||||
generate test footage/images/
|
||||
|
||||
generate test footage/3.MP4
|
||||
input/*
|
||||
short.mp4
|
||||
|
||||
__pycache__/
|
||||
.vscode/
|
||||
*.mp4
|
||||
|
||||
*.weights
|
||||
*.m4v
|
||||
|
||||
/output/*.txt
|
||||
|
||||
docs/ueberblick.drawio.png
|
||||
|
||||
# Output and cache
|
||||
output/
|
||||
/output/*.txt
|
||||
tmp/tmp.prof
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
*.manifest
|
||||
*.spec
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv
|
||||
|
||||
# Testing
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.log
|
||||
.pytest_cache/
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
.pyre/
|
||||
.pytype/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints
|
||||
*.ipynb
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Pre-commit
|
||||
.pre-commit-config.yaml.bak
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
# Pre-commit hooks for code quality
|
||||
# Install with: pip install pre-commit && pre-commit install
|
||||
|
||||
repos:
|
||||
# General file checks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
|
||||
# Python code formatting
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['--line-length=140']
|
||||
|
||||
# Import sorting
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ['--profile', 'black', '--line-length', '140']
|
||||
|
||||
# Linting
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--max-line-length=140', '--extend-ignore=E203,W503']
|
||||
|
||||
# Type checking (optional - can be slow)
|
||||
# Uncomment to enable mypy checks
|
||||
# - repo: https://github.com/pre-commit/mirrors-mypy
|
||||
# rev: v1.8.0
|
||||
# hooks:
|
||||
# - id: mypy
|
||||
# additional_dependencies: [types-all]
|
||||
# args: [--ignore-missing-imports]
|
||||
|
|
@ -106,7 +106,7 @@ class Classifier(ClassifierInterface):
|
|||
image_np_expanded = np.expand_dims(image, axis=0)
|
||||
# Actual detection.
|
||||
|
||||
(boxes, scores, classes, num) = self.sess.run(
|
||||
boxes, scores, classes, num = self.sess.run(
|
||||
[self.detection_boxes, self.detection_scores, self.detection_classes, self.num_detections],
|
||||
feed_dict={self.image_tensor: image_np_expanded},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from Application.Logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class Config:
|
||||
|
|
@ -20,24 +25,32 @@ class Config:
|
|||
"avgNum": 10,
|
||||
}
|
||||
|
||||
def __init__(self, config_path):
|
||||
"""This is basically just a wrapper for a json / python dict"""
|
||||
if os.path.isfile(config_path):
|
||||
print("using supplied configuration at", config_path)
|
||||
# fail if config can not be parsed
|
||||
with open(config_path) as file:
|
||||
self.c = json.load(file)
|
||||
def __init__(self, config_path: Optional[str]):
|
||||
"""
|
||||
Initialize configuration from file or use defaults.
|
||||
|
||||
Args:
|
||||
config_path: Path to JSON configuration file. If None or invalid, uses defaults.
|
||||
"""
|
||||
if config_path and os.path.isfile(config_path):
|
||||
logger.info(f"Using supplied configuration at {config_path}")
|
||||
try:
|
||||
with open(config_path) as file:
|
||||
self.c = json.load(file)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
logger.error(f"Failed to parse config file: {e}")
|
||||
logger.warning("Falling back to default configuration")
|
||||
else:
|
||||
print("using default configuration")
|
||||
logger.info("Using default configuration")
|
||||
|
||||
print("Current Config:")
|
||||
logger.info("Current Configuration:")
|
||||
for key, value in self.c.items():
|
||||
print(f"{key}:\t\t{value}")
|
||||
logger.info(f" {key}: {value}")
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
if key not in self.c:
|
||||
return None
|
||||
return self.c[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
self.c[key] = value
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class ContourExtractor:
|
|||
masks = []
|
||||
for c in cnts:
|
||||
ca = cv2.contourArea(c)
|
||||
(x, y, w, h) = cv2.boundingRect(c)
|
||||
x, y, w, h = cv2.boundingRect(c)
|
||||
if ca < self.min_area or ca > self.max_area:
|
||||
continue
|
||||
contours.append((x, y, w, h))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class Exporter:
|
|||
def export_layers(self, layers):
|
||||
list_of_frames = self.make_list_of_frames(layers)
|
||||
with VideoReader(self.config, list_of_frames) as video_reader:
|
||||
|
||||
|
||||
underlay = cv2.VideoCapture(self.footage_path).read()[1]
|
||||
underlay = cv2.cvtColor(underlay, cv2.COLOR_BGR2RGB)
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class Exporter:
|
|||
frame_count, frame = video_reader.pop()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
frame2 = np.copy(underlay)
|
||||
for (x, y, w, h) in layer.bounds[frame_count - layer.start_frame]:
|
||||
for x, y, w, h in layer.bounds[frame_count - layer.start_frame]:
|
||||
if x is None:
|
||||
continue
|
||||
factor = video_reader.w / self.resize_width
|
||||
|
|
@ -105,7 +105,9 @@ class Exporter:
|
|||
cv2.imshow("changes x", background)
|
||||
cv2.waitKey(10) & 0xFF
|
||||
|
||||
self.add_timestamp(frames[frame_count - layer.start_frame + layer.export_offset], videoReader, frame_count, x, y, w, h)
|
||||
self.add_timestamp(
|
||||
frames[frame_count - layer.start_frame + layer.export_offset], videoReader, frame_count, x, y, w, h
|
||||
)
|
||||
except:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
|
||||
class HeatMap:
|
||||
def __init__(self, x, y, contours, resize_factor=1):
|
||||
self.image_bw = np.zeros(shape=[y, x, 3], dtype=np.float64)
|
||||
|
|
@ -25,4 +26,4 @@ class HeatMap:
|
|||
plt.show()
|
||||
|
||||
def save_image(self, path):
|
||||
plt.imsave(path, (255 * self.image_bw).astype(np.uint8))
|
||||
plt.imsave(path, (255 * self.image_bw).astype(np.uint8))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import pickle
|
||||
import os.path
|
||||
import pickle
|
||||
|
||||
|
||||
class Importer:
|
||||
def __init__(self, config):
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ class Layer:
|
|||
for b1s, b2s in zip(bounds[::10], layer2.bounds[:max_len:10]):
|
||||
for b1 in b1s:
|
||||
for b2 in b2s:
|
||||
if self.contours_overlay((b1[0], b1[1] + b1[3]), (b1[0] + b1[2], b1[1]), (b2[0], b2[1] + b2[3]), (b2[0] + b2[2], b2[1])):
|
||||
if self.contours_overlay(
|
||||
(b1[0], b1[1] + b1[3]), (b1[0] + b1[2], b1[1]), (b2[0], b2[1] + b2[3]), (b2[0] + b2[2], b2[1])
|
||||
):
|
||||
overlap = True
|
||||
break
|
||||
return overlap
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class LayerFactory:
|
|||
frame_number = data[0]
|
||||
bounds = data[1]
|
||||
mask = data[2]
|
||||
(x, y, w, h) = bounds
|
||||
x, y, w, h = bounds
|
||||
tol = self.tolerance
|
||||
|
||||
found_layer_i_ds = set()
|
||||
|
|
@ -75,7 +75,7 @@ class LayerFactory:
|
|||
for j, bounds in enumerate(sorted(last_bounds, reverse=True)):
|
||||
if bounds is None:
|
||||
break
|
||||
(x2, y2, w2, h2) = bounds
|
||||
x2, y2, w2, h2 = bounds
|
||||
if self.contours_overlay((x - tol, y + h + tol), (x + w + tol, y - tol), (x2, y2 + h2), (x2 + w2, y2)):
|
||||
layer.add(frame_number, (x, y, w, h), mask)
|
||||
found_layer_i_ds.add(i)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,20 @@ from multiprocessing.pool import ThreadPool
|
|||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from Application.Classifiers.Classifier import Classifier
|
||||
from Application.Config import Config
|
||||
from Application.Exporter import Exporter
|
||||
from Application.Layer import Layer
|
||||
from Application.VideoReader import VideoReader
|
||||
|
||||
# Optional: Classifier (requires TensorFlow)
|
||||
try:
|
||||
from Application.Classifiers.Classifier import Classifier
|
||||
|
||||
CLASSIFIER_AVAILABLE = True
|
||||
except ImportError:
|
||||
CLASSIFIER_AVAILABLE = False
|
||||
Classifier = None
|
||||
|
||||
|
||||
class LayerManager:
|
||||
def __init__(self, config, layers):
|
||||
|
|
@ -36,7 +44,7 @@ class LayerManager:
|
|||
print("Before deleting sparse layers ", len(self.layers))
|
||||
self.delete_sparse()
|
||||
print("after deleting sparse layers ", len(self.layers))
|
||||
#self.calcTimeOffset()
|
||||
# self.calcTimeOffset()
|
||||
|
||||
def delete_sparse(self):
|
||||
to_delete = []
|
||||
|
|
@ -82,7 +90,7 @@ class LayerManager:
|
|||
frame_count, frame = video_reader.pop()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
data = []
|
||||
for (x, y, w, h) in layer.bounds[frame_count - layer.start_frame]:
|
||||
for x, y, w, h in layer.bounds[frame_count - layer.start_frame]:
|
||||
if x is None:
|
||||
break
|
||||
factor = video_reader.w / self.resize_width
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
"""Logging configuration for Video Summary application."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def setup_logger(name: str = "video_summary", level: int = logging.INFO, log_file: Optional[str] = None) -> logging.Logger:
|
||||
"""
|
||||
Configure and return a logger instance.
|
||||
|
||||
Args:
|
||||
name: Logger name
|
||||
level: Logging level (default: INFO)
|
||||
log_file: Optional log file path. If None, logs to stdout only.
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
# Prevent duplicate handlers
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# Create formatter
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(level)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# File handler (optional)
|
||||
if log_file:
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str = "video_summary") -> logging.Logger:
|
||||
"""
|
||||
Get an existing logger instance.
|
||||
|
||||
Args:
|
||||
name: Logger name
|
||||
|
||||
Returns:
|
||||
Logger instance
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
"""Video Summary Application Package.
|
||||
|
||||
This package provides tools for video summarization through contour extraction
|
||||
and layer-based processing.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Askill"
|
||||
|
||||
# Core imports
|
||||
from Application.Config import Config
|
||||
from Application.Layer import Layer
|
||||
|
||||
# Import optional components that may have additional dependencies
|
||||
__all__ = ["Config", "Layer"]
|
||||
|
||||
# Try to import video processing components
|
||||
try:
|
||||
from Application.ContourExctractor import ContourExtractor
|
||||
from Application.Exporter import Exporter
|
||||
from Application.HeatMap import HeatMap
|
||||
from Application.Importer import Importer
|
||||
from Application.LayerFactory import LayerFactory
|
||||
from Application.VideoReader import VideoReader
|
||||
|
||||
__all__.extend(["ContourExtractor", "Exporter", "HeatMap", "Importer", "LayerFactory", "VideoReader"])
|
||||
except ImportError as e:
|
||||
import warnings
|
||||
|
||||
warnings.warn(f"Some video processing components could not be imported: {e}")
|
||||
|
||||
# Try to import LayerManager (may require TensorFlow for classification features)
|
||||
try:
|
||||
from Application.LayerManager import LayerManager
|
||||
|
||||
__all__.append("LayerManager")
|
||||
except ImportError:
|
||||
import warnings
|
||||
|
||||
warnings.warn("LayerManager could not be imported. TensorFlow may be required for classification features.")
|
||||
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
# Contributing to Video-Summary
|
||||
|
||||
Thank you for considering contributing to Video-Summary! This document provides guidelines and instructions for contributing.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8 or higher
|
||||
- Git
|
||||
- ffmpeg (for video processing)
|
||||
|
||||
### Setting up Development Environment
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/Askill/Video-Summary.git
|
||||
cd Video-Summary
|
||||
```
|
||||
|
||||
2. **Create a virtual environment**
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. **Install dependencies**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install -e ".[dev]" # Install development dependencies
|
||||
```
|
||||
|
||||
4. **Install pre-commit hooks**
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Style
|
||||
|
||||
We use the following tools to maintain code quality:
|
||||
|
||||
- **Black**: Code formatting (line length: 140)
|
||||
- **isort**: Import sorting
|
||||
- **flake8**: Linting
|
||||
- **mypy**: Type checking (optional but recommended)
|
||||
|
||||
Run these tools before committing:
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
black .
|
||||
isort .
|
||||
|
||||
# Check for issues
|
||||
flake8 .
|
||||
mypy Application/ main.py
|
||||
```
|
||||
|
||||
Or simply commit - pre-commit hooks will run automatically!
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. **Create a feature branch**
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
2. **Make your changes**
|
||||
- Write clean, readable code
|
||||
- Add type hints where applicable
|
||||
- Update documentation as needed
|
||||
- Add tests for new functionality
|
||||
|
||||
3. **Test your changes**
|
||||
```bash
|
||||
# Run tests (if available)
|
||||
pytest
|
||||
|
||||
# Test the CLI
|
||||
python main.py path/to/test/video.mp4 output_test
|
||||
```
|
||||
|
||||
4. **Commit your changes**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: Add your feature description"
|
||||
```
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
- `feat:` - New feature
|
||||
- `fix:` - Bug fix
|
||||
- `docs:` - Documentation changes
|
||||
- `refactor:` - Code refactoring
|
||||
- `test:` - Adding tests
|
||||
- `chore:` - Maintenance tasks
|
||||
|
||||
5. **Push and create a Pull Request**
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
- Keep PRs focused on a single feature or fix
|
||||
- Write a clear description of what the PR does
|
||||
- Reference any related issues
|
||||
- Ensure CI passes (linting, tests, build)
|
||||
- Update documentation if needed
|
||||
- Add screenshots for UI changes
|
||||
|
||||
## Testing
|
||||
|
||||
While we're building out the test suite, please ensure:
|
||||
|
||||
1. Your code runs without errors
|
||||
2. You've tested with sample videos
|
||||
3. Edge cases are handled (missing files, corrupt videos, etc.)
|
||||
4. Memory usage is reasonable
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
When reporting issues, please include:
|
||||
|
||||
1. **Environment details**
|
||||
- OS and version
|
||||
- Python version
|
||||
- Dependency versions
|
||||
|
||||
2. **Steps to reproduce**
|
||||
- Exact commands run
|
||||
- Input file characteristics (if applicable)
|
||||
|
||||
3. **Expected vs. actual behavior**
|
||||
|
||||
4. **Error messages and logs**
|
||||
|
||||
5. **Screenshots** (if applicable)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Video-Summary/
|
||||
├── Application/ # Core processing modules
|
||||
│ ├── Config.py # Configuration management
|
||||
│ ├── ContourExctractor.py # Extract contours from video
|
||||
│ ├── LayerFactory.py # Group contours into layers
|
||||
│ ├── LayerManager.py # Manage and clean layers
|
||||
│ ├── Exporter.py # Export processed results
|
||||
│ └── ...
|
||||
├── main.py # CLI entry point
|
||||
├── pyproject.toml # Project configuration
|
||||
└── requirements.txt # Dependencies
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **ContourExtractor**: Analyzes video frames to detect movement
|
||||
2. **LayerFactory**: Groups related contours across frames
|
||||
3. **LayerManager**: Filters and optimizes layers
|
||||
4. **Exporter**: Generates output videos
|
||||
|
||||
## Code Review Process
|
||||
|
||||
1. Maintainers will review your PR
|
||||
2. Address any requested changes
|
||||
3. Once approved, your PR will be merged
|
||||
4. Your contribution will be credited in releases
|
||||
|
||||
## Questions?
|
||||
|
||||
- Open an issue for questions
|
||||
- Tag maintainers for urgent matters
|
||||
- Be patient and respectful
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Video-Summary! 🎥✨
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Askill
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
231
README.md
231
README.md
|
|
@ -1,55 +1,204 @@
|
|||
# Video-Summary
|
||||
|
||||
# Video Summary and Classification
|
||||
[](https://github.com/Askill/Video-Summary/actions)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Example:
|
||||
A Python-based video summarization tool that extracts contours from video frames to create condensed summaries. Perfect for analyzing surveillance footage, time-lapse videos, or any static camera recording where you want to extract and visualize movement over time.
|
||||
|
||||
usage:
|
||||
main.py input_video.mp4 output_dir ?config_file.json
|
||||
|
||||

|
||||
What you see above is a 15 second excerpt of a 2 minute overlayed synopsis of a 2.5h video from an on campus web cam.
|
||||
The synopsis took 40 minutes from start to finish on a 8 core machine and used a maximum of 6Gb of RAM.
|
||||
## ✨ Features
|
||||
|
||||
However since the contour extraction could be performed on a video stream, the benchmark results show that a single core would be enough to process a video faster than real time.
|
||||
- **Movement Detection**: Automatically detects and extracts moving objects from static camera footage
|
||||
- **Layer-Based Processing**: Groups related movements across frames into coherent layers
|
||||
- **Heatmap Generation**: Visualizes areas of activity in the video
|
||||
- **Configurable**: Extensive configuration options for fine-tuning detection sensitivity
|
||||
- **Efficient**: Processes video faster than real-time on modern hardware
|
||||
- **Caching**: Saves intermediate results for faster re-processing with different parameters
|
||||
|
||||
## Heatmap
|
||||

|
||||
## 🚀 Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
## Benchmark
|
||||
Below you can find the benchmark results for a 10 minutes clip, with the stacked time per component on the x-axis.
|
||||
The tests were done on a machine with a Ryzen 3700X with 8 cores 16 threads and 32 Gb of RAM.
|
||||
On my configuration 1 minutes of of the original Video can be processed in about 20 seconds, the expected processing time is about 1/3 of the orignial video length.
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Askill/Video-Summary.git
|
||||
cd Video-Summary
|
||||
|
||||
- CE = Contour Extractor
|
||||
- LE = LayerFactory
|
||||
- LM = LayerManager
|
||||
- EX = Exporter
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||

|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
### Configuration
|
||||
# Install system dependencies (Linux)
|
||||
sudo apt-get install ffmpeg libsm6 libxext6 libxrender-dev
|
||||
```
|
||||
|
||||
./Application/Config.py
|
||||
### Basic Usage
|
||||
|
||||
"min_area": 100, min area in pixels, of a single contour, smaller is ignored
|
||||
"max_area": 9000000, max area in pixels, of a single contour, larger is ignored
|
||||
"threshold": 6, luminance difference threshold, sensitivity of movement detection
|
||||
"resizeWidth": 600, video is scaled down internally
|
||||
"inputPath": None, overwritten in main.py
|
||||
"outputPath": None, overwritten in main.py
|
||||
"maxLayerLength": 5000, max length of Layer in frames
|
||||
"minLayerLength": 10, min length of Layer in frames
|
||||
"tolerance": 100, max distance (in pixels) between contours to be aggragated into layer
|
||||
"maxLength": None,
|
||||
"ttolerance": 60, number of frames movement can be apart until a new layer is created
|
||||
"videoBufferLength": 100, Buffer Length of Video Reader Componenent
|
||||
"LayersPerContour": 2, number of layers a single contour can belong to
|
||||
"avgNum": 10, number of images that should be averaged before calculating the difference
|
||||
(computationally expensive, needed in outdoor scenarios due to clouds, leaves moving in the wind ...)
|
||||
|
||||
```bash
|
||||
# Process a video with default settings
|
||||
python main.py input_video.mp4 output_dir
|
||||
|
||||
### notes:
|
||||
optional:
|
||||
# Use custom configuration
|
||||
python main.py input_video.mp4 output_dir config.json
|
||||
|
||||
install tensorflow==1.15.0 and tensorflow-gpu==1.15.0, cuda 10.2 and 10.0, copy missing files from 10.0 to 10.2, restart computer, set maximum vram
|
||||
# Enable verbose logging
|
||||
python main.py input_video.mp4 output_dir --verbose
|
||||
```
|
||||
|
||||
## 📊 Example Output
|
||||
|
||||

|
||||
|
||||
*A 15-second excerpt of a 2-minute overlaid synopsis of a 2.5-hour video from a campus webcam.*
|
||||
|
||||
### Heatmap Visualization
|
||||
|
||||

|
||||
|
||||
The heatmap shows areas of activity throughout the video, with brighter regions indicating more movement.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Create a JSON configuration file to customize processing parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"min_area": 300,
|
||||
"max_area": 900000,
|
||||
"threshold": 7,
|
||||
"resizeWidth": 700,
|
||||
"maxLayerLength": 5000,
|
||||
"minLayerLength": 40,
|
||||
"tolerance": 20,
|
||||
"ttolerance": 50,
|
||||
"videoBufferLength": 250,
|
||||
"LayersPerContour": 220,
|
||||
"avgNum": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `min_area` | Minimum contour area in pixels (smaller ignored) | 300 |
|
||||
| `max_area` | Maximum contour area in pixels (larger ignored) | 900000 |
|
||||
| `threshold` | Luminance difference threshold for movement detection | 7 |
|
||||
| `resizeWidth` | Video is scaled to this width internally for processing | 700 |
|
||||
| `maxLayerLength` | Maximum length of a layer in frames | 5000 |
|
||||
| `minLayerLength` | Minimum length of a layer in frames | 40 |
|
||||
| `tolerance` | Max distance (pixels) between contours to aggregate into layer | 20 |
|
||||
| `ttolerance` | Number of frames movement can be apart before creating new layer | 50 |
|
||||
| `videoBufferLength` | Buffer length of Video Reader component | 250 |
|
||||
| `LayersPerContour` | Number of layers a single contour can belong to | 220 |
|
||||
| `avgNum` | Number of images to average before calculating difference | 10 |
|
||||
|
||||
> **Note**: `avgNum` is computationally expensive but needed in outdoor scenarios with clouds, leaves moving in wind, etc.
|
||||
|
||||
## 📈 Performance Benchmarks
|
||||
|
||||
**Test Configuration:**
|
||||
- Hardware: Ryzen 3700X (8 cores, 16 threads), 32GB RAM
|
||||
- Video: 10-minute clip
|
||||
- Processing Speed: ~20 seconds per minute of video (1:3 ratio)
|
||||
- Memory Usage: Max 6GB RAM
|
||||
|
||||
**Component Breakdown:**
|
||||
- CE = Contour Extractor
|
||||
- LF = Layer Factory
|
||||
- LM = Layer Manager
|
||||
- EX = Exporter
|
||||
|
||||

|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
Video-Summary/
|
||||
├── Application/ # Core processing modules
|
||||
│ ├── Config.py # Configuration management
|
||||
│ ├── ContourExctractor.py # Movement detection
|
||||
│ ├── LayerFactory.py # Layer extraction
|
||||
│ ├── LayerManager.py # Layer optimization
|
||||
│ ├── Exporter.py # Output generation
|
||||
│ ├── VideoReader.py # Video I/O
|
||||
│ ├── HeatMap.py # Heatmap generation
|
||||
│ ├── Importer.py # Cache loading
|
||||
│ ├── Layer.py # Layer data structure
|
||||
│ └── Logger.py # Logging utilities
|
||||
├── main.py # CLI entry point
|
||||
├── pyproject.toml # Package configuration
|
||||
└── requirements.txt # Dependencies
|
||||
```
|
||||
|
||||
### Processing Pipeline
|
||||
|
||||
1. **Video Reading**: Load and preprocess video frames
|
||||
2. **Contour Extraction**: Detect movement by comparing consecutive frames
|
||||
3. **Layer Creation**: Group related contours across frames
|
||||
4. **Layer Management**: Filter and optimize layers based on configuration
|
||||
5. **Export**: Generate output video with overlaid movement and heatmap
|
||||
|
||||
## 🧪 Development
|
||||
|
||||
### Code Quality Tools
|
||||
|
||||
We use modern Python development tools:
|
||||
|
||||
- **Black**: Code formatting
|
||||
- **isort**: Import sorting
|
||||
- **flake8**: Linting
|
||||
- **mypy**: Type checking
|
||||
- **pre-commit**: Automated checks
|
||||
|
||||
```bash
|
||||
# Install development dependencies
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Install pre-commit hooks
|
||||
pre-commit install
|
||||
|
||||
# Run formatting
|
||||
black .
|
||||
isort .
|
||||
|
||||
# Run linting
|
||||
flake8 .
|
||||
mypy Application/ main.py
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=Application --cov-report=html
|
||||
```
|
||||
|
||||
## 📝 Contributing
|
||||
|
||||
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
The original Creative Commons licensed documentation can be found in [licens.txt](licens.txt).
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Built with OpenCV, NumPy, and imageio
|
||||
- Inspired by video synopsis research in computer vision
|
||||
|
||||
## 📮 Contact
|
||||
|
||||
For questions or issues, please [open an issue](https://github.com/Askill/Video-Summary/issues) on GitHub.
|
||||
|
||||
---
|
||||
|
||||
**Note**: TensorFlow support is optional and not required for core functionality. The project works perfectly fine without GPU acceleration, though processing times will be longer for large videos.
|
||||
|
|
|
|||
149
main.py
149
main.py
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import time
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from Application.Config import Config
|
||||
from Application.ContourExctractor import ContourExtractor
|
||||
|
|
@ -9,58 +10,130 @@ from Application.HeatMap import HeatMap
|
|||
from Application.Importer import Importer
|
||||
from Application.LayerFactory import LayerFactory
|
||||
from Application.LayerManager import LayerManager
|
||||
from Application.Logger import get_logger, setup_logger
|
||||
from Application.VideoReader import VideoReader
|
||||
|
||||
# Setup logging
|
||||
setup_logger()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
def main(config):
|
||||
|
||||
def main(config: Config) -> int:
|
||||
"""
|
||||
Main processing pipeline for video summarization.
|
||||
|
||||
Args:
|
||||
config: Configuration object with processing parameters
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
start_total = time.time()
|
||||
|
||||
if os.path.exists(config["cachePath"] + "_layers.txt"):
|
||||
layers, contours, masks = Importer(config).import_raw_data()
|
||||
layers = LayerFactory(config).extract_layers(contours, masks)
|
||||
else:
|
||||
contours, masks = ContourExtractor(config).extract_contours()
|
||||
layers = LayerFactory(config).extract_layers(contours, masks)
|
||||
try:
|
||||
# Check if cached data exists
|
||||
cache_path = config["cachePath"] + "_layers.txt"
|
||||
if os.path.exists(cache_path):
|
||||
logger.info(f"Loading cached data from {cache_path}")
|
||||
layers, contours, masks = Importer(config).import_raw_data()
|
||||
layers = LayerFactory(config).extract_layers(contours, masks)
|
||||
else:
|
||||
logger.info("Extracting contours from video...")
|
||||
contours, masks = ContourExtractor(config).extract_contours()
|
||||
logger.info("Extracting layers from contours...")
|
||||
layers = LayerFactory(config).extract_layers(contours, masks)
|
||||
|
||||
layer_manager = LayerManager(config, layers)
|
||||
layer_manager.clean_layers()
|
||||
logger.info("Cleaning layers...")
|
||||
layer_manager = LayerManager(config, layers)
|
||||
layer_manager.clean_layers()
|
||||
|
||||
# layerManager.tagLayers()
|
||||
if len(layer_manager.layers) == 0:
|
||||
exit(1)
|
||||
# Check if we have any layers to process
|
||||
if len(layer_manager.layers) == 0:
|
||||
logger.error("No layers found to process. Exiting.")
|
||||
return 1
|
||||
|
||||
heatmap = HeatMap(
|
||||
config["w"], config["h"], [contour for layer in layer_manager.layers for contour in layer.bounds], 1920 / config["resizeWidth"]
|
||||
)
|
||||
heatmap.show_image()
|
||||
#heatmap.save_image(config["outputPath"].split(".")[0] + "_heatmap.png") # not working yet
|
||||
# Generate heatmap
|
||||
logger.info("Generating heatmap...")
|
||||
heatmap = HeatMap(
|
||||
config["w"], config["h"], [contour for layer in layer_manager.layers for contour in layer.bounds], 1920 / config["resizeWidth"]
|
||||
)
|
||||
heatmap.show_image()
|
||||
|
||||
print(f"Exporting {len(contours)} Contours and {len(layer_manager.layers)} Layers")
|
||||
Exporter(config).export(layer_manager.layers, contours, masks, raw=True, overlayed=True)
|
||||
print("Total time: ", time.time() - start_total)
|
||||
# Export results
|
||||
logger.info(f"Exporting {len(contours)} Contours and {len(layer_manager.layers)} Layers")
|
||||
Exporter(config).export(layer_manager.layers, contours, masks, raw=True, overlayed=True)
|
||||
|
||||
total_time = time.time() - start_total
|
||||
logger.info(f"Total processing time: {total_time:.2f} seconds")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during processing: {e}", exc_info=True)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract movement from static camera recording",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s input_video.mp4 output_dir
|
||||
%(prog)s input_video.mp4 output_dir custom_config.json
|
||||
""",
|
||||
)
|
||||
parser.add_argument("input", metavar="input_file", type=str, help="Input video file to extract movement from")
|
||||
parser.add_argument(
|
||||
"output",
|
||||
metavar="output_dir",
|
||||
type=str,
|
||||
nargs="?",
|
||||
default="output",
|
||||
help="Output directory to save results and cached files (default: output)",
|
||||
)
|
||||
parser.add_argument("config", metavar="config", type=str, nargs="?", default=None, help="Path to configuration JSON file (optional)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
|
||||
|
||||
parser = argparse.ArgumentParser(description='Extract movement from static camera recording')
|
||||
parser.add_argument('input', metavar='input_file', type=str,
|
||||
help='input video to extract movement from')
|
||||
parser.add_argument('output', metavar='output_dir', type=str, nargs="?", default="output",
|
||||
help='output directory to save results and cached files into')
|
||||
parser.add_argument('config', metavar='config', type=str, nargs="?", default=None,
|
||||
help='relative path to config.json')
|
||||
args = parser.parse_args()
|
||||
|
||||
config = Config(args.config)
|
||||
# Setup logging level
|
||||
if args.verbose:
|
||||
setup_logger(level=10) # DEBUG level
|
||||
|
||||
input_path = os.path.join(os.path.dirname(__file__), args.input)
|
||||
output_path = os.path.join(os.path.dirname(__file__), args.output)
|
||||
try:
|
||||
# Load configuration
|
||||
config = Config(args.config)
|
||||
|
||||
file_name = input_path.split("/")[-1]
|
||||
# Resolve paths
|
||||
input_path = os.path.join(os.path.dirname(__file__), args.input)
|
||||
output_path = os.path.join(os.path.dirname(__file__), args.output)
|
||||
|
||||
config["inputPath"] = input_path
|
||||
config["outputPath"] = os.path.join(output_path, file_name)
|
||||
config["cachePath"] = os.path.join(output_path, file_name.split(".")[0])
|
||||
config["w"], config["h"] = VideoReader(config).get_wh()
|
||||
# Validate input file exists
|
||||
if not os.path.exists(input_path):
|
||||
logger.error(f"Input file not found: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
main(config)
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
|
||||
file_name = input_path.split("/")[-1]
|
||||
|
||||
# Configure paths
|
||||
config["inputPath"] = input_path
|
||||
config["outputPath"] = os.path.join(output_path, file_name)
|
||||
config["cachePath"] = os.path.join(output_path, file_name.split(".")[0])
|
||||
|
||||
# Get video dimensions
|
||||
logger.info("Reading video dimensions...")
|
||||
config["w"], config["h"] = VideoReader(config).get_wh()
|
||||
|
||||
# Run main processing
|
||||
exit_code = main(config)
|
||||
sys.exit(exit_code)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("Processing interrupted by user")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
logger.error(f"Unhandled exception: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
104
pyproject.toml
104
pyproject.toml
|
|
@ -1,11 +1,101 @@
|
|||
# Example configuration for Black.
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
# NOTE: you have to use single-quoted strings in TOML for regular expressions.
|
||||
# It's the equivalent of r-strings in Python. Multiline strings are treated as
|
||||
# verbose regular expressions by Black. Use [ ] to denote a significant space
|
||||
# character.
|
||||
[project]
|
||||
name = "video-summary"
|
||||
version = "0.1.0"
|
||||
description = "A Python-based video summarization tool that extracts contours from video frames to create condensed summaries"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Askill", email = ""}
|
||||
]
|
||||
keywords = ["video", "summarization", "computer-vision", "opencv", "contour-extraction"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Science/Research",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Scientific/Engineering :: Image Recognition",
|
||||
]
|
||||
dependencies = [
|
||||
"opencv-python>=4.5.0,<5.0.0",
|
||||
"numpy>=1.21.0,<2.0.0",
|
||||
"imutils>=0.5.4",
|
||||
"imageio>=2.9.0",
|
||||
"imageio-ffmpeg>=0.4.0",
|
||||
"matplotlib>=3.3.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"black>=23.0.0",
|
||||
"isort>=5.12.0",
|
||||
"flake8>=6.0.0",
|
||||
"mypy>=1.0.0",
|
||||
"pytest>=7.0.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
"pre-commit>=3.0.0",
|
||||
]
|
||||
tensorflow = [
|
||||
"tensorflow>=2.10.0,<3.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
video-summary = "main:main"
|
||||
|
||||
[tool.black]
|
||||
line-length = 140
|
||||
target-version = ['py36', 'py37', 'py38', 'py39']
|
||||
include = '\.pyi?$'
|
||||
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
/(
|
||||
# directories
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 140
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = true
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = true
|
||||
ensure_newline_before_comments = true
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = false
|
||||
disallow_incomplete_defs = false
|
||||
check_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_no_return = true
|
||||
follow_imports = "normal"
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
opencv-python
|
||||
numpy
|
||||
imutils
|
||||
imageio
|
||||
tensorflow
|
||||
matplotlib
|
||||
imageio-ffmpeg
|
||||
# Core dependencies for video processing
|
||||
opencv-python>=4.5.0,<5.0.0
|
||||
numpy>=1.21.0,<2.0.0
|
||||
imutils>=0.5.4
|
||||
|
||||
# Video I/O
|
||||
imageio>=2.9.0
|
||||
imageio-ffmpeg>=0.4.0
|
||||
|
||||
# Visualization
|
||||
matplotlib>=3.3.0
|
||||
|
||||
# Optional: Machine Learning (for classification features)
|
||||
# Uncomment if you need TensorFlow support:
|
||||
# tensorflow>=2.10.0,<3.0.0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
"""Setup script for Video-Summary package.
|
||||
|
||||
This is a shim for backward compatibility.
|
||||
The real configuration is in pyproject.toml.
|
||||
"""
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup()
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Test package initialization."""
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
"""Tests for Config module."""
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from Application.Config import Config
|
||||
|
||||
|
||||
class TestConfig:
|
||||
"""Test suite for Config class."""
|
||||
|
||||
def test_default_config(self):
|
||||
"""Test that default config is loaded when no file provided."""
|
||||
config = Config(None)
|
||||
assert config["min_area"] == 300
|
||||
assert config["max_area"] == 900000
|
||||
assert config["threshold"] == 7
|
||||
|
||||
def test_load_config_from_file(self):
|
||||
"""Test loading config from a JSON file."""
|
||||
test_config = {"min_area": 500, "max_area": 1000000, "threshold": 10}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump(test_config, f)
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
config = Config(temp_path)
|
||||
assert config["min_area"] == 500
|
||||
assert config["max_area"] == 1000000
|
||||
assert config["threshold"] == 10
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_config_with_invalid_file(self):
|
||||
"""Test that default config is used when file doesn't exist."""
|
||||
config = Config("/nonexistent/path/config.json")
|
||||
assert config["min_area"] == 300 # Should use defaults
|
||||
|
||||
def test_config_getitem(self):
|
||||
"""Test __getitem__ method."""
|
||||
config = Config(None)
|
||||
assert config["min_area"] is not None
|
||||
assert config["nonexistent_key"] is None
|
||||
|
||||
def test_config_setitem(self):
|
||||
"""Test __setitem__ method."""
|
||||
config = Config(None)
|
||||
config["new_key"] = "new_value"
|
||||
assert config["new_key"] == "new_value"
|
||||
|
||||
def test_config_with_malformed_json(self):
|
||||
"""Test handling of malformed JSON file."""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
f.write("{invalid json content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
config = Config(temp_path)
|
||||
# Should fall back to defaults
|
||||
assert config["min_area"] == 300
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
Loading…
Reference in New Issue