├── .github ├── ISSUE_TEMPLATE │ ├── scenedetect_app.md │ ├── scenedetect_package.md │ └── scenedetect_request.md ├── actions │ └── setup-ffmpeg │ │ └── action.yml └── workflows │ ├── build-windows.yml │ ├── build.yml │ ├── check-code-format.yml │ ├── check-docs.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── generate-docs.yml │ └── generate-website.yml ├── .gitignore ├── CITATION.cff ├── LICENSE ├── MANIFEST.in ├── README.md ├── THIRD-PARTY.md ├── appveyor.yml ├── benchmark ├── AutoShot │ └── .gitkeep ├── BBC │ └── .gitkeep ├── README.md ├── __main__.py ├── autoshot_dataset.py ├── bbc_dataset.py └── evaluator.py ├── dist ├── installer │ ├── Generated Images │ │ ├── installer_banner.jpg │ │ ├── installer_banner.scale-125.jpg │ │ ├── installer_banner.scale-150.jpg │ │ ├── installer_banner.scale-200.jpg │ │ ├── installer_banner.svg │ │ ├── installer_logo.jpg │ │ ├── installer_logo.scale-125.jpg │ │ ├── installer_logo.scale-150.jpg │ │ ├── installer_logo.scale-200.jpg │ │ └── installer_logo.svg │ ├── Prerequisites │ │ └── Visual C++ Redistributable for Visual Studio 2015-2019 │ │ │ └── VC_redist.x64.exe │ ├── PySceneDetect.aip │ ├── installer_banner.png │ ├── installer_banner.svg │ ├── installer_logo.png │ ├── installer_logo.svg │ ├── license65.dat.enc │ └── psd_square_small.ico ├── package-info.rst ├── pre_release.py ├── pyscenedetect.ico ├── requirements_windows.txt ├── scenedetect.spec ├── windows │ ├── LICENSE-PYTHON │ └── README.txt └── windows_thirdparty.7z ├── docs ├── .readthedocs.yaml ├── Makefile ├── _static │ ├── pyscenedetect.css │ ├── pyscenedetect_logo.png │ └── pyscenedetect_logo_small.png ├── _templates │ └── navigation.html ├── api.rst ├── api │ ├── backends.rst │ ├── common.rst │ ├── detector.rst │ ├── detectors.rst │ ├── output.rst │ ├── platform.rst │ ├── scene_manager.rst │ ├── stats_manager.rst │ └── video_stream.rst ├── cli.rst ├── cli │ ├── backends.rst │ └── config_file.rst ├── conf.py ├── generate_cli_docs.py ├── index.rst ├── make.bat └── requirements.txt ├── pyproject.toml ├── requirements.txt ├── requirements_headless.txt ├── scenedetect.cfg ├── scenedetect ├── __init__.py ├── __main__.py ├── _cli │ ├── __init__.py │ ├── commands.py │ ├── config.py │ ├── context.py │ └── controller.py ├── _thirdparty │ ├── LICENSE-CLICK │ ├── LICENSE-MOVIEPY │ ├── LICENSE-NUMPY │ ├── LICENSE-OPENCV │ ├── LICENSE-PYAV │ ├── LICENSE-PYTEST │ ├── LICENSE-SIMPLETABLE │ ├── LICENSE-TQDM │ ├── __init__.py │ └── simpletable.py ├── backends │ ├── __init__.py │ ├── moviepy.py │ ├── opencv.py │ └── pyav.py ├── common.py ├── detector.py ├── detectors │ ├── __init__.py │ ├── adaptive_detector.py │ ├── content_detector.py │ ├── hash_detector.py │ ├── histogram_detector.py │ └── threshold_detector.py ├── output │ ├── __init__.py │ ├── image.py │ └── video.py ├── platform.py ├── scene_manager.py ├── stats_manager.py └── video_stream.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_backend_opencv.py ├── test_backend_pyav.py ├── test_cli.py ├── test_detectors.py ├── test_frame_timecode.py ├── test_output.py ├── test_platform.py ├── test_scene_manager.py ├── test_stats_manager.py └── test_video_stream.py └── website ├── mkdocs.yml ├── overrides └── main.html ├── pages ├── api.md ├── changelog.md ├── cli.md ├── contributing.md ├── copyright.md ├── docs.md ├── download.md ├── faq.md ├── features.md ├── img │ ├── 0.6.4-score-comparison.png │ ├── goldeneye-stats.png │ ├── params.png │ ├── pyscenedetect_logo.png │ └── pyscenedetect_logo_small.png ├── index.md ├── literature.md ├── similar.md ├── style.css └── supporting.md └── requirements.txt /.github/ISSUE_TEMPLATE/scenedetect_app.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Application Bug 3 | about: Help us fix and improve PySceneDetect by reporting bugs or other issues. 4 | 5 | --- 6 | 7 | **Description:** 8 | 9 | Describe what the bug or issue is (e.g. crashes when setting X) and how it can be reproduced. 10 | 11 | **Command:** 12 | 13 | Place a full copy of the command line options you are using here, for example: 14 | 15 | `scenedetect -i some_video.mp4 -s some_video.stats.csv -o outdir detect-content --threshold 28 list-scenes save-images` 16 | 17 | **Output:** 18 | 19 | Copy the output of running the application here. Where possible, generate a debug log by adding `-v debug -l BUG_REPORT.txt` to the beginning of your command, and **attach `BUG_REPORT.txt` to your issue**. 20 | 21 | **Environment:** 22 | 23 | The operating system and how you installed PySceneDetect may be relevant to the issue. Please run `scenedetect version` and copy the output here, or provide other details on how PySceneDetect was installed. 24 | 25 | **Media/Files:** 26 | 27 | Attach or link to any files relevant to the issue, including videos (or YouTube links), scene files, stats files, and log output. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/scenedetect_package.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Python API 3 | about: Programmers using the `scenedetect` package. 4 | 5 | --- 6 | 7 | **Description:** 8 | 9 | Describe the issue (unexpected result, exception thrown, etc...) and any relevant output. 10 | 11 | **Example:** 12 | 13 | Include code samples that demonstrate the issue: 14 | 15 | ```python 16 | from scenedetect import detect, ContentDetector, split_video_ffmpeg 17 | scene_list = detect('my_video.mp4', ContentDetector()) 18 | split_video_ffmpeg('my_video.mp4', scene_list) 19 | ``` 20 | 21 | **Environment:** 22 | 23 | Run `scenedetect version` and include the output. This will describe the environment/OS/platform and versions of dependencies you have installed. 24 | 25 | **Media/Files:** 26 | 27 | Attach or link to any files relevant to the issue, including videos (or YouTube links), scene files, stats files, and log output. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/scenedetect_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature or Enhancement 3 | about: Submit an idea or feature request to make PySceneDetect better. 4 | 5 | --- 6 | 7 | **Problem/Use Case** 8 | 9 | Describe what problem you want to solve, or what use case you want to achieve. Ex. PySceneDetect doesn't work well in cases X or Y but if it could detect Z [...], PySceneDetect is slow because of X but could be faster if it did Y [...], or I need PySceneDetect to do X because of some condition Y [...]. 10 | 11 | **Solutions** 12 | 13 | Discuss any potential solutions here. 14 | 15 | **Proposed Implementation:** 16 | 17 | Description of what you want to happen, and how you think the feature/enhancement should be implemented. Ex. what command line options/arguments should be added to PySceneDetect, and examples of how you expect them to function. 18 | 19 | **Alternatives:** 20 | 21 | List any alternative solutions or related ideas you've considered. 22 | 23 | **Examples:** 24 | 25 | Attach or link to any relevant videos or images that are relevant. 26 | -------------------------------------------------------------------------------- /.github/actions/setup-ffmpeg/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup FFmpeg' 2 | inputs: 3 | github-token: 4 | required: true 5 | 6 | runs: 7 | using: 'composite' 8 | steps: 9 | - name: Setup FFmpeg (latest) 10 | id: latest 11 | continue-on-error: true 12 | uses: FedericoCarboni/setup-ffmpeg@v3 13 | with: 14 | github-token: ${{ inputs.github-token }} 15 | 16 | - name: Setup FFmpeg (7.0.0) 17 | if: ${{ steps.latest.outcome == 'failure' }} 18 | id: v7-0-0 19 | continue-on-error: true 20 | uses: FedericoCarboni/setup-ffmpeg@v3 21 | with: 22 | github-token: ${{ inputs.github-token }} 23 | ffmpeg-version: "7.0.0" 24 | 25 | - name: Setup FFmpeg (6.1.1) 26 | if: ${{ steps.v7-0-0.outcome == 'failure' }} 27 | id: v6-1-1 28 | continue-on-error: true 29 | uses: FedericoCarboni/setup-ffmpeg@v3 30 | with: 31 | github-token: ${{ inputs.github-token }} 32 | ffmpeg-version: "6.1.1" 33 | 34 | # The oldest version we allow falling back to must not have `continue-on-error: true` 35 | - name: Setup FFmpeg (6.1.0) 36 | if: ${{ steps.v6-1-1.outcome == 'failure' }} 37 | id: v6-1-0 38 | uses: FedericoCarboni/setup-ffmpeg@v3 39 | with: 40 | github-token: ${{ inputs.github-token }} 41 | ffmpeg-version: "6.1.0" 42 | -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | # Build Portable Windows EXE (x64) Distribution for PySceneDetect 2 | 3 | name: Windows Distribution 4 | 5 | on: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | pull_request: 9 | paths: 10 | - dist/** 11 | - scenedetect/** 12 | - tests/** 13 | push: 14 | paths: 15 | - dist/** 16 | - scenedetect/** 17 | - tests/** 18 | branches: 19 | - main 20 | - 'releases/**' 21 | tags: 22 | - v*-release 23 | workflow_dispatch: 24 | 25 | jobs: 26 | build: 27 | runs-on: windows-latest 28 | strategy: 29 | matrix: 30 | python-version: ["3.13"] 31 | 32 | env: 33 | ffmpeg-version: "7.1" 34 | IMAGEIO_FFMPEG_EXE: "" 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Set up Python ${{ matrix.python-version }} 40 | uses: actions/setup-python@v5 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | cache: 'pip' 44 | 45 | - name: Install Dependencies 46 | run: | 47 | python -m pip install --upgrade pip build wheel virtualenv setuptools 48 | pip install -r docs/requirements.txt 49 | pip install --upgrade -r dist/requirements_windows.txt --no-binary imageio-ffmpeg 50 | 51 | - name: Download Resources 52 | run: | 53 | git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources 54 | git checkout refs/remotes/origin/resources -- tests/resources/ 55 | 56 | - name: Download FFMPEG ${{ env.ffmpeg-version }} 57 | uses: dsaltares/fetch-gh-release-asset@1.1.2 58 | with: 59 | repo: 'GyanD/codexffmpeg' 60 | version: 'tags/${{ env.ffmpeg-version }}' 61 | file: 'ffmpeg-${{ env.ffmpeg-version }}-full_build.7z' 62 | 63 | - name: Unit Test 64 | shell: bash 65 | run: | 66 | 7z e ffmpeg-${{ env.ffmpeg-version }}-full_build.7z ffmpeg.exe -r 67 | echo "IMAGEIO_FFMPEG_EXE=`realpath ffmpeg.exe`" >> "$GITHUB_ENV" 68 | python -m pytest -vv 69 | 70 | - name: Build PySceneDetect 71 | run: | 72 | python dist/pre_release.py 73 | pyinstaller dist/scenedetect.spec 74 | 75 | - name: Build Documentation 76 | run: | 77 | sphinx-build -b singlehtml docs dist/scenedetect/docs 78 | rm -r dist/scenedetect/docs/.doctrees 79 | 80 | - name: Assemble Portable Distribution 81 | run: | 82 | Move-Item -Path LICENSE -Destination dist/scenedetect/ 83 | New-Item -Path dist/scenedetect/ -Name thirdparty -ItemType Directory 84 | Move-Item -Path dist/windows/README* -Destination dist/scenedetect/ 85 | Move-Item -Path dist/windows/LICENSE* -Destination dist/scenedetect/thirdparty/ 86 | Move-Item -Path scenedetect/_thirdparty/LICENSE* -Destination dist/scenedetect/thirdparty/ 87 | 7z e -odist/ffmpeg ffmpeg-${{ env.ffmpeg-version }}-full_build.7z LICENSE -r 88 | Move-Item -Path ffmpeg.exe -Destination dist/scenedetect/ffmpeg.exe 89 | Move-Item -Path dist/ffmpeg/LICENSE -Destination dist/scenedetect/thirdparty/LICENSE-FFMPEG 90 | 91 | - name: Test Portable Distribution 92 | run: | 93 | ./dist/scenedetect/scenedetect -i tests/resources/goldeneye.mp4 detect-content time -e 2s 94 | 95 | - name: Upload Artifact 96 | uses: actions/upload-artifact@v4 97 | with: 98 | name: PySceneDetect-win64_portable 99 | path: dist/scenedetect 100 | include-hidden-files: true 101 | 102 | test: 103 | runs-on: windows-latest 104 | needs: build 105 | steps: 106 | - uses: actions/checkout@v4 107 | with: 108 | ref: resources 109 | 110 | - uses: actions/download-artifact@v4.1.7 111 | with: 112 | name: PySceneDetect-win64_portable 113 | path: build 114 | 115 | - name: Test 116 | run: | 117 | echo Testing binary 118 | ./build/scenedetect version 119 | echo Test OpenCV 120 | ./build/scenedetect -i tests/resources/goldeneye.mp4 -b opencv detect-content time --end 10s 121 | echo Test PyAV 122 | ./build/scenedetect -i tests/resources/goldeneye.mp4 -b pyav detect-content time --end 10s 123 | echo Test moviepy 124 | ./build/scenedetect -i tests/resources/goldeneye.mp4 -b moviepy detect-content time --end 10s 125 | echo Test split-video + ffmpeg 126 | ./build/scenedetect -i tests/resources/goldeneye.mp4 detect-content time --end 10s split-video 127 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Test PySceneDetect on Linux/OSX/Windows and generate Python distribution (sdist/wheel). 2 | name: Python Distribution 3 | 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | pull_request: 8 | paths: 9 | - dist/** 10 | - scenedetect/** 11 | - tests/** 12 | push: 13 | paths: 14 | - dist/** 15 | - scenedetect/** 16 | - tests/** 17 | branches: 18 | - main 19 | - 'releases/**' 20 | tags: 21 | - v*-release 22 | workflow_dispatch: 23 | 24 | jobs: 25 | build: 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | os: [macos-13, macos-14, ubuntu-22.04, ubuntu-latest, windows-latest] 30 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 31 | exclude: 32 | # macos-14 builders use M1 (ARM64) which does not have a Python 3.7 package available. 33 | - os: macos-14 34 | python-version: "3.7" 35 | # ubuntu 24+ does not have Python 3.7 36 | - os: ubuntu-latest 37 | python-version: "3.7" 38 | 39 | env: 40 | # Version is extracted below and used to find correct package install path. 41 | scenedetect_version: "" 42 | # Setuptools must be pinned for the Python 3.7 builders. 43 | setuptools_version: "${{ matrix.python-version == '3.7' && '==62.3.4' || '' }}" 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Setup FFmpeg 49 | # TODO: This action currently does not work for non-x64 builders (e.g. macos-14): 50 | # https://github.com/federicocarboni/setup-ffmpeg/issues/21 51 | if: ${{ runner.arch == 'X64' }} 52 | uses: ./.github/actions/setup-ffmpeg 53 | with: 54 | github-token: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Set up Python ${{ matrix.python-version }} 57 | uses: actions/setup-python@v5 58 | with: 59 | python-version: ${{ matrix.python-version }} 60 | cache: 'pip' 61 | 62 | - name: Install Dependencies 63 | run: | 64 | python -m pip install --upgrade pip build wheel virtualenv setuptools${{ env.setuptools_version }} 65 | pip install -r requirements_headless.txt --only-binary av,opencv-python-headless 66 | 67 | - name: Install MoviePy 68 | # TODO: We can only run MoviePy tests on systems that have ffmpeg. 69 | if: ${{ runner.arch == 'X64' }} 70 | run: | 71 | pip install moviepy 72 | 73 | - name: Checkout test resources 74 | run: | 75 | git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources 76 | git checkout refs/remotes/origin/resources -- tests/resources/ 77 | 78 | - name: Unit Tests 79 | run: | 80 | python -m pytest -vv 81 | 82 | - name: Smoke Test (Module) 83 | run: | 84 | python -m scenedetect version 85 | python -m scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s 86 | python -m scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s 87 | python -m pip uninstall -y scenedetect 88 | 89 | - name: Build Package 90 | shell: bash 91 | run: | 92 | python -m build 93 | echo "scenedetect_version=`python -c \"import scenedetect; print(scenedetect.__version__.replace('-', '.'))\"`" >> "$GITHUB_ENV" 94 | 95 | - name: Smoke Test Package (Source Dist) 96 | run: | 97 | python -m pip install dist/scenedetect-${{ env.scenedetect_version }}.tar.gz 98 | scenedetect version 99 | scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s 100 | scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s 101 | python -m pip uninstall -y scenedetect 102 | 103 | - name: Smoke Test Package (Wheel) 104 | run: | 105 | python -m pip install dist/scenedetect-${{ env.scenedetect_version }}-py3-none-any.whl 106 | scenedetect version 107 | scenedetect -i tests/resources/testvideo.mp4 -b opencv time --end 2s 108 | scenedetect -i tests/resources/testvideo.mp4 -b pyav time --end 2s 109 | python -m pip uninstall -y scenedetect 110 | 111 | - name: Upload Package 112 | if: ${{ matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest' }} 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: scenedetect-dist 116 | path: | 117 | dist/*.tar.gz 118 | dist/*.whl 119 | -------------------------------------------------------------------------------- /.github/workflows/check-code-format.yml: -------------------------------------------------------------------------------- 1 | # Check PySceneDetect code lint warnings and formatting. 2 | name: Static Analysis 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - scenedetect/** 8 | - tests/** 9 | push: 10 | paths: 11 | - scenedetect/** 12 | - tests/** 13 | 14 | jobs: 15 | check_format: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 3.12 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.12' 24 | cache: 'pip' 25 | 26 | - name: Install Dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install -r requirements_headless.txt --only-binary av,opencv-python-headless 30 | 31 | - name: Check Code Format (yapf) 32 | if: ${{ hashFiles('.style.yapf') != '' }} 33 | run: | 34 | python -m pip install --upgrade yapf toml 35 | python -m yapf --diff --recursive scenedetect/ tests/ 36 | 37 | - name: Static Analysis (ruff) 38 | if: ${{ hashFiles('.style.yapf') == '' }} 39 | run: | 40 | python -m pip install --upgrade ruff 41 | python -m ruff check 42 | python -m ruff format --check 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/check-docs.yml: -------------------------------------------------------------------------------- 1 | # Checks that the CLI docs are up-to-date. If this fails on your PR, there may be some changes 2 | # to the command-line docs that were not updated. Run `python docs/generate_cli_docs.py` from 3 | # the root PySceneDetect source folder and commit the changes to resolve the issue. 4 | name: Check Documentation 5 | 6 | on: 7 | schedule: 8 | - cron: '0 0 * * *' 9 | pull_request: 10 | paths: 11 | - docs/** 12 | - scenedetect/** 13 | push: 14 | paths: 15 | - docs/** 16 | - scenedetect/** 17 | branches: 18 | - main 19 | - 'releases/**' 20 | tags: 21 | - v*-release 22 | workflow_dispatch: 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Set up Python 3.12 32 | uses: actions/setup-python@v5 33 | with: 34 | python-version: '3.12' 35 | cache: 'pip' 36 | 37 | - name: Install Dependencies 38 | run: | 39 | python -m pip install --upgrade pip build wheel virtualenv 40 | pip install -r docs/requirements.txt 41 | pip install -r dist/requirements_windows.txt 42 | 43 | 44 | - name: Check CLI Documentation 45 | shell: bash 46 | run: | 47 | if [[ `git status --porcelain=1 | wc -l` -ne 0 ]]; then 48 | echo "CLI documentation is of date: docs/cli.rst does not match output after running docs/generate_cli_docs.py!" 49 | echo "Re-run `python docs/generate_cli_docs.py` to update and commit the result." 50 | exit 1 51 | fi 52 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # CodeQL for PySceneDetect 2 | name: "CodeQL" 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - releases/** 9 | paths: 10 | - scenedetect/** 11 | - tests/** 12 | pull_request: 13 | branches: 14 | - main 15 | - releases/* 16 | paths: 17 | - scenedetect/** 18 | - tests/** 19 | schedule: 20 | - cron: "20 7 * * 4" 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | runs-on: ubuntu-latest 26 | permissions: 27 | actions: read 28 | contents: read 29 | security-events: write 30 | 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | language: [ python ] 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v3 42 | with: 43 | languages: ${{ matrix.language }} 44 | queries: +security-and-quality 45 | 46 | - name: Autobuild 47 | uses: github/codeql-action/autobuild@v3 48 | 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v3 51 | with: 52 | category: "/language:${{ matrix.language }}" 53 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v4 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v4 21 | -------------------------------------------------------------------------------- /.github/workflows/generate-docs.yml: -------------------------------------------------------------------------------- 1 | # Generate PySceneDetect documentation and updates the gh-pages branch. 2 | name: Generate Documentation 3 | 4 | on: 5 | push: 6 | branches: 7 | - main # docs/head 8 | - 'releases/**' # docs/** 9 | paths: 10 | - 'docs/**' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | update_docs: 15 | runs-on: ubuntu-latest 16 | env: 17 | # TODO: Figure out a better way to handle figuring out what version /latest should be, 18 | # e.g. add a latest version file in main. 19 | scenedetect_docs_latest: '0.6.6' 20 | scenedetect_docs_dest: '' 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Python 3.12 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: '3.12' 29 | cache: 'pip' 30 | 31 | - name: Set Destination (Releases) 32 | if: ${{ contains(github.ref_name, 'releases') }} 33 | run: | 34 | echo "scenedetect_docs_dest=$(echo ${{ github.ref_name }} | cut -b 10-)" >> "$GITHUB_ENV" 35 | 36 | - name: Set Destination (Head) 37 | if: ${{ contains(github.ref_name, 'main') }} 38 | run: | 39 | echo "scenedetect_docs_dest=head" >> "$GITHUB_ENV" 40 | 41 | - name: Check Destination 42 | if: ${{ env.scenedetect_docs_dest == '' }} 43 | run: | 44 | echo "Failing build: destination must be set!" 45 | 46 | - name: Setup Environment 47 | run: | 48 | python -m pip install --upgrade pip build wheel virtualenv 49 | pip install -r docs/requirements.txt 50 | pip install -r dist/requirements_windows.txt 51 | git config --global user.name github-actions 52 | git config --global user.email github-actions@github.com 53 | 54 | - name: Check CLI Documentation 55 | shell: bash 56 | run: | 57 | if [[ `git status --porcelain=1 | wc -l` -ne 0 ]]; then 58 | echo "CLI documentation is of date: docs/cli.rst does not match output after running docs/generate_cli_docs.py!" 59 | echo "Re-run `python docs/generate_cli_docs.py` to update and commit the result." 60 | exit 1 61 | fi 62 | 63 | - name: Generate Docs 64 | run: | 65 | sphinx-build -b html docs build 66 | 67 | - name: Update gh-pages Branch 68 | run: | 69 | git fetch origin gh-pages 70 | git checkout gh-pages 71 | git rm "docs/${{ env.scenedetect_docs_dest }}" -r -f --ignore-unmatch 72 | git add build/ 73 | git mv build "docs/${{ env.scenedetect_docs_dest }}" 74 | 75 | - name: Update Latest 76 | if: ${{ env.scenedetect_docs_dest == env.scenedetect_docs_latest }} 77 | run: | 78 | git rm "docs/latest" -r -f --ignore-unmatch 79 | mkdir -p latest 80 | cp -r -f "docs/${{ env.scenedetect_docs_dest }}" docs/latest 81 | git add docs/latest 82 | echo "scenedetect_docs_dest='${{ env.scenedetect_docs_dest }} (latest)'" >> "$GITHUB_ENV" 83 | 84 | - name: Commit and Push 85 | run: | 86 | git commit -a -m "[docs] @${{ github.triggering_actor }}: Generate Documentation" \ 87 | -m "Source: ${{ github.ref_name }} (${{ github.sha }})" \ 88 | -m "Destination: ${{ env.scenedetect_docs_dest }}" 89 | git push 90 | -------------------------------------------------------------------------------- /.github/workflows/generate-website.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Generate Website 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'website/**' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | update_site: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 3.12 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.12' 23 | cache: 'pip' 24 | 25 | - name: Install Dependencies 26 | run: | 27 | python -m pip install --upgrade pip build wheel virtualenv 28 | pip install -r website/requirements.txt 29 | 30 | - name: Generate Website 31 | run: | 32 | mkdocs build -f website/mkdocs.yml 33 | 34 | - name: Update Website 35 | run: | 36 | git fetch origin gh-pages 37 | git checkout gh-pages 38 | git rm * -r -f --ignore-unmatch 39 | git checkout HEAD -- .nojekyll 40 | git checkout HEAD -- CNAME 41 | git checkout HEAD -- docs/ 42 | git rm docs/index.html --ignore-unmatch 43 | git add website/build/ 44 | git mv website/build/* . -f -k 45 | git mv website/build/docs/index.html docs/index.html -k 46 | git rm website/build/* -r -f --ignore-unmatch 47 | git config --global user.name github-actions 48 | git config --global user.email github-actions@github.com 49 | git commit -a -m "[docs] @${{ github.triggering_actor }}: Generate Website" \ 50 | -m "Commit: ${{ github.sha }}" 51 | git push 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/_build/ 2 | website/build/ 3 | tests/resources/* 4 | *.mp4 5 | *.jpg 6 | *.jpeg 7 | *.patch 8 | *.exe 9 | *.mkv 10 | *.m4v 11 | *.csv 12 | benchmarks/BCC/*.mp4 13 | *.txt 14 | benchmarks/RAI/*.mp4 15 | *.txt 16 | 17 | 18 | # From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore 19 | 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | *.so 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | *.manifest 43 | *.spec 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | *.mo 60 | *.pot 61 | .scrapy 62 | docs/_build/ 63 | .pybuilder/ 64 | target/ 65 | .ipynb_checkpoints 66 | profile_default/ 67 | ipython_config.py 68 | .pdm.toml 69 | __pypackages__/ 70 | celerybeat-schedule 71 | celerybeat.pid 72 | *.sage.py 73 | .env 74 | .venv 75 | env/ 76 | venv/ 77 | ENV/ 78 | env.bak/ 79 | venv.bak/ 80 | .spyderproject 81 | .spyproject 82 | .ropeproject 83 | /site 84 | .mypy_cache/ 85 | .dmypy.json 86 | dmypy.json 87 | .pyre/ 88 | .pytype/ 89 | cython_debug/ 90 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: PySceneDetect 3 | message: www.scenedetect.com 4 | type: software 5 | authors: 6 | - given-names: Brandon 7 | family-names: Castellano 8 | affiliation: www.bcastell.com 9 | repository-code: 'https://github.com/Breakthrough/PySceneDetect' 10 | url: 'https://www.scenedetect.com' 11 | abstract: Video Cut Detection and Analysis Tool 12 | license: BSD-3-Clause 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (C) 2024, Brandon Castellano 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude .github * 2 | recursive-exclude dist * 3 | recursive-exclude docs * 4 | recursive-exclude website * 5 | exclude * 6 | include README.md 7 | include LICENSE 8 | include pyproject.toml 9 | include setup.cfg 10 | include scenedetect.cfg 11 | include dist/package-info.rst 12 | recursive-include docs * 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![PySceneDetect](https://raw.githubusercontent.com/Breakthrough/PySceneDetect/main/website/pages/img/pyscenedetect_logo_small.png) 3 | ========================================================== 4 | Video Cut Detection and Analysis Tool 5 | ---------------------------------------------------------- 6 | 7 | [![Build Status](https://img.shields.io/github/actions/workflow/status/Breakthrough/PySceneDetect/build.yml)](https://github.com/Breakthrough/PySceneDetect/actions) 8 | [![PyPI Status](https://img.shields.io/pypi/status/scenedetect.svg)](https://pypi.python.org/pypi/scenedetect/) 9 | [![PyPI Version](https://img.shields.io/pypi/v/scenedetect?color=blue)](https://pypi.python.org/pypi/scenedetect/) 10 | [![PyPI License](https://img.shields.io/pypi/l/scenedetect.svg)](https://scenedetect.com/copyright/) 11 | 12 | ---------------------------------------------------------- 13 | 14 | ### Latest Release: v0.6.6 (March 9, 2025) 15 | 16 | **Website**: [scenedetect.com](https://www.scenedetect.com) 17 | 18 | **Quickstart Example**: [scenedetect.com/cli/](https://www.scenedetect.com/cli/) 19 | 20 | **Documentation**: [scenedetect.com/docs/](https://www.scenedetect.com/docs/) 21 | 22 | **Discord**: https://discord.gg/H83HbJngk7 23 | 24 | ---------------------------------------------------------- 25 | 26 | **Quick Install**: 27 | 28 | pip install scenedetect[opencv] --upgrade 29 | 30 | Requires ffmpeg/mkvmerge for video splitting support. Windows builds (MSI installer/portable ZIP) can be found on [the download page](https://scenedetect.com/download/). 31 | 32 | ---------------------------------------------------------- 33 | 34 | **Quick Start (Command Line)**: 35 | 36 | Split input video on each fast cut using `ffmpeg`: 37 | 38 | scenedetect -i video.mp4 split-video 39 | 40 | Save some frames from each cut: 41 | 42 | scenedetect -i video.mp4 save-images 43 | 44 | Skip the first 10 seconds of the input video: 45 | 46 | scenedetect -i video.mp4 time -s 10s 47 | 48 | More examples can be found throughout [the documentation](https://www.scenedetect.com/docs/latest/cli.html). 49 | 50 | **Quick Start (Python API)**: 51 | 52 | To get started, there is a high level function in the library that performs content-aware scene detection on a video (try it from a Python prompt): 53 | 54 | ```python 55 | from scenedetect import detect, ContentDetector 56 | scene_list = detect('my_video.mp4', ContentDetector()) 57 | ``` 58 | 59 | `scene_list` will now be a list containing the start/end times of all scenes found in the video. There also exists a two-pass version `AdaptiveDetector` which handles fast camera movement better, and `ThresholdDetector` for handling fade out/fade in events. 60 | 61 | Try calling `print(scene_list)`, or iterating over each scene: 62 | 63 | ```python 64 | from scenedetect import detect, ContentDetector 65 | scene_list = detect('my_video.mp4', ContentDetector()) 66 | for i, scene in enumerate(scene_list): 67 | print(' Scene %2d: Start %s / Frame %d, End %s / Frame %d' % ( 68 | i+1, 69 | scene[0].get_timecode(), scene[0].get_frames(), 70 | scene[1].get_timecode(), scene[1].get_frames(),)) 71 | ``` 72 | 73 | We can also split the video into each scene if `ffmpeg` is installed (`mkvmerge` is also supported): 74 | 75 | ```python 76 | from scenedetect import detect, ContentDetector, split_video_ffmpeg 77 | scene_list = detect('my_video.mp4', ContentDetector()) 78 | split_video_ffmpeg('my_video.mp4', scene_list) 79 | ``` 80 | 81 | For more advanced usage, the API is highly configurable, and can easily integrate with any pipeline. This includes using different detection algorithms, splitting the input video, and much more. The following example shows how to implement a function similar to the above, but using [the `scenedetect` API](https://www.scenedetect.com/docs/latest/api.html): 82 | 83 | ```python 84 | from scenedetect import open_video, SceneManager, split_video_ffmpeg 85 | from scenedetect.detectors import ContentDetector 86 | from scenedetect.video_splitter import split_video_ffmpeg 87 | 88 | def split_video_into_scenes(video_path, threshold=27.0): 89 | # Open our video, create a scene manager, and add a detector. 90 | video = open_video(video_path) 91 | scene_manager = SceneManager() 92 | scene_manager.add_detector( 93 | ContentDetector(threshold=threshold)) 94 | scene_manager.detect_scenes(video, show_progress=True) 95 | scene_list = scene_manager.get_scene_list() 96 | split_video_ffmpeg(video_path, scene_list, show_progress=True) 97 | ``` 98 | 99 | See [the documentation](https://www.scenedetect.com/docs/latest/api.html) for more examples. 100 | 101 | **Benchmark**: 102 | 103 | We evaluate the performance of different detectors in terms of accuracy and processing speed. See the [benchmark report](benchmark/README.md) for details. 104 | 105 | ## Reference 106 | 107 | - [Documentation](https://www.scenedetect.com/docs/) (covers application and Python API) 108 | - [CLI Example](https://www.scenedetect.com/cli/) 109 | - [Config File](https://www.scenedetect.com/docs/0.6.4/cli/config_file.html) 110 | 111 | ## Help & Contributing 112 | 113 | Please submit any bugs/issues or feature requests to [the Issue Tracker](https://github.com/Breakthrough/PySceneDetect/issues). Before submission, ensure you search through existing issues (both open and closed) to avoid creating duplicate entries. 114 | Pull requests are welcome and encouraged. PySceneDetect is released under the BSD 3-Clause license, and submitted code should be compliant. 115 | 116 | For help or other issues, you can join [the official PySceneDetect Discord Server](https://discord.gg/H83HbJngk7), submit an issue/bug report [here on Github](https://github.com/Breakthrough/PySceneDetect/issues), or contact me via [my website](http://www.bcastell.com/about/). 117 | 118 | ## Code Signing 119 | 120 | This program uses free code signing provided by [SignPath.io](https://signpath.io?utm_source=foundation&utm_medium=github&utm_campaign=PySceneDetect), and a free code signing certificate by the [SignPath Foundation](https://signpath.org?utm_source=foundation&utm_medium=github&utm_campaign=PySceneDetect) 121 | 122 | ## License 123 | 124 | BSD-3-Clause; see [`LICENSE`](LICENSE) and [`THIRD-PARTY.md`](THIRD-PARTY.md) for details. 125 | 126 | ---------------------------------------------------------- 127 | 128 | Copyright (C) 2014-2024 Brandon Castellano. 129 | All rights reserved. 130 | -------------------------------------------------------------------------------- /THIRD-PARTY.md: -------------------------------------------------------------------------------- 1 | # Ancillary Software Licenses 2 | 3 | This file includes license information for various open-source projects 4 | that are imported, derived into, or distributed with PySceneDetect. 5 | See [LICENSE](LICENSE) for the main PySceneDetect license. 6 | 7 | Depending on the features being used, PySceneDetect uses the following 8 | third-party software which are released under the terms detailed below. 9 | By downloading, copying, installing or using this software, you agree 10 | to these terms. 11 | 12 | In no particular order: 13 | 14 | ----------------------------------------------------------------------- 15 | 16 | > click [Copyright (C) 2017, Armin Ronacher]: 17 | This software uses OpenCV; see thirdparty/LICENSE-CLICK or visit: 18 | [ http://click.pocoo.org/license/ ] 19 | 20 | > NumPy [Copyright (C) 2005-2016, Numpy Developers]: 21 | This software uses Numpy; see thirdparty/LICENSE-NUMPY or visit: 22 | [ http://www.numpy.org/license.html ] 23 | 24 | > OpenCV [Copyright (C) 2017, Itseez]: 25 | This software uses OpenCV; see thirdparty/LICENSE-OPENCV or visit: 26 | [ http://opencv.org/license.html ] 27 | 28 | > PyAV [Copyright (C) 2017, Mike Boers and others]: 29 | This software uses PyAV; see thirdparty/LICENSE-PYAV or visit: 30 | [ https://github.com/PyAV-Org/PyAV/blob/main/LICENSE.txt ] 31 | 32 | > pytest [Copyright (C) 2004-2017, Holger Krekel and others]: 33 | This software uses pytest; see thirdparty/LICENSE-PYTEST or visit: 34 | [ https://docs.pytest.org/en/latest/license.html ] 35 | 36 | > simpletable [Copyright (C) 2014-2019, Matheus Vieira Portela and others]: 37 | This software uses simpletable; see thirdparty/LICENSE-SIMPLETABLE or visit: 38 | [ https://github.com/matheusportela/simpletable/blob/master/LICENSE ] 39 | 40 | > tqdm [Copyright (C) 2013-2018, Casper da Costa-Luis, 41 | Google Inc., and Noam Yorav-Raphael]: 42 | This software uses tqdm; see thirdparty/LICENSE-TQDM or visit: 43 | [ https://github.com/tqdm/tqdm/blob/master/LICENCE ] 44 | 45 | > MoviePy [ Copyright (C) 2015 Zulko ] 46 | This software uses tqdm; see thirdparty/LICENSE-TQDM or visit: 47 | [ https://github.com/Zulko/moviepy/blob/master/LICENCE.txt ] 48 | 49 | ----------------------------------------------------------------------- 50 | 51 | This software may also invoke FFmpeg or mkvmerge, if available. If required, 52 | these programs can be obtained from following URLs: 53 | 54 | FFmpeg: [ https://ffmpeg.org/download.html ] 55 | mkvmerge: [ https://mkvtoolnix.download/downloads.html ] 56 | 57 | Once installed, ensure the program is in your PATH variable (i.e. you can 58 | run the `ffmpeg` or `mkvmerge` command from any location). 59 | 60 | Certain distributions of PySceneDetect may include ffmpeg. See 61 | thirdparty/LICENSE-FFMPEG file or visit [ https://ffmpeg.org ] 62 | 63 | FFmpeg is a trademark of Fabrice Bellard 64 | mkvmerge is Copyright (C) 2005-2016, Matroska 65 | 66 | Windows distributions may include a compiled Python distribution. For license 67 | information regarding the distributed version of Python, see the 68 | thirdparty/LICENSE-PYTHON file, or visit [ https://docs.python.org/3/license.html ] 69 | 70 | ----------------------------------------------------------------------- 71 | 72 | If any information above is incorrect, please let us know. 73 | Visit [ https://www.scenedetect.com ] for contact information. 74 | 75 | 76 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Build signed releases for PySceneDetect Windows x64 2 | 3 | build: false 4 | 5 | # Branches applies to tags as well. We only build on tagged releases of the form vX.Y.Z-release 6 | branches: 7 | only: 8 | - main 9 | - /releases\/.+/ 10 | - /v.+-release/ 11 | 12 | skip_tags: false 13 | skip_non_tags: true 14 | 15 | environment: 16 | matrix: 17 | - PYTHON: "C:\\Python313-x64" 18 | # Encrypted AdvancedInstaller License 19 | ai_license_secret: 20 | secure: of3o1pInqCJYwKLFsiadbsRYazCmCuZq7r2roaYvYXmBvm6e6JHsRU47waylTmhm 21 | ai_license_salt: 22 | secure: +NKWwlkEptlThgfeL35pLo7EsnkJc+4WODm8tTg1aO5fc0duQ4r100fHQYj6nzhyUdy3Dhs/mOLkxD8rNbBiEQ== 23 | 24 | # SignPath Config for Code Signing 25 | deploy: 26 | - provider: Webhook 27 | url: https://app.signpath.io/API/v1/f2efa44c-5b5c-45f2-b44f-8f9dde708313/Integrations/AppVeyor?ProjectSlug=PySceneDetect&SigningPolicySlug=release-signing 28 | authorization: 29 | secure: FBgWCaxCCKOqc2spYf5NGWSNUGLbT5WeuC5U0k4Of1Ids9n51YWxhGlMyzLbdNBFe64RUcOSzk/N3emlQzbsJg== 30 | on: 31 | APPVEYOR_REPO_TAG: true # keep casing this way for Linux builds where variables are case-sensitive 32 | 33 | install: 34 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 35 | - echo * * SETTING UP PYTHON ENVIRONMENT * * 36 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 37 | - 'SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%' 38 | - python --version 39 | - python -m pip install --upgrade pip build wheel virtualenv setuptools 40 | - python -m pip install -r docs/requirements.txt 41 | - python -m pip install --upgrade -r dist/requirements_windows.txt --no-binary imageio-ffmpeg 42 | # Checkout build resources and third party software used for testing. 43 | - git checkout refs/remotes/origin/resources -- dist/ 44 | - appveyor DownloadFile https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-full_build.7z 45 | - 7z e ffmpeg-7.1-full_build.7z -odist/ffmpeg ffmpeg.exe LICENSE -r 46 | - 'SET IMAGEIO_FFMPEG_EXE=%APPVEYOR_BUILD_FOLDER%\\dist\\ffmpeg\\ffmpeg.exe' 47 | 48 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 49 | - echo * * BUILDING WINDOWS EXE * * 50 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 51 | # Build Windows .EXE and create portable .ZIP 52 | - python dist/pre_release.py --release 53 | - pyinstaller dist/scenedetect.spec 54 | - sphinx-build -b singlehtml docs dist/scenedetect/docs 55 | - mkdir dist\scenedetect\thirdparty 56 | - move LICENSE dist\scenedetect\ 57 | - move dist\windows\README* dist\scenedetect\ 58 | - move dist\windows\LICENSE* dist\scenedetect\thirdparty\ 59 | - move scenedetect\_thirdparty\LICENSE* dist\scenedetect\thirdparty\ 60 | - copy dist\ffmpeg\ffmpeg.exe dist\scenedetect\ 61 | - move dist\ffmpeg\LICENSE dist\scenedetect\thirdparty\LICENSE-FFMPEG 62 | - cd dist/scenedetect 63 | - 7z a ../scenedetect-win64.zip * 64 | - cd ../.. 65 | 66 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 67 | - echo * * BUILDING MSI INSTALLER * * 68 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 69 | # Download, install, and register AdvancedInstaller 70 | - cd dist/installer 71 | - ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1')) 72 | - appveyor-tools\secure-file -decrypt license65.dat.enc -secret %ai_license_secret% -salt %ai_license_salt% 73 | - appveyor DownloadFile https://www.advancedinstaller.com/downloads/advinst.msi 74 | - msiexec /i advinst.msi /qn 75 | - 'SET PATH=%PATH%;C:\\Program Files (x86)\\Caphyon\\Advanced Installer 22.5\\bin\\x86' 76 | # License path must be absolute 77 | - AdvancedInstaller.com /RegisterOffline "%cd%\license65.dat" 78 | # Create MSI installer 79 | - AdvancedInstaller.com /build PySceneDetect.aip 80 | - cd ../.. 81 | 82 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 83 | - echo * * PACKAGING BUILD ARTIFACTS * * 84 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 85 | # Zip all resources together for code signing 86 | - cd dist 87 | - move installer\PySceneDetect-*.msi . 88 | - cp scenedetect\scenedetect.exe . 89 | - 7z a scenedetect-signed.zip scenedetect.exe PySceneDetect-*.msi 90 | - cd .. 91 | 92 | test_script: 93 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 94 | - echo * * TESTING BUILD * * 95 | - echo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 96 | # Checkout required test resources 97 | - git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources 98 | - git checkout refs/remotes/origin/resources -- tests/resources/ 99 | - move dist\scenedetect\ffmpeg.exe ffmpeg.exe 100 | # Run unit tests 101 | - pytest 102 | # Test Windows build 103 | - move ffmpeg.exe dist\scenedetect\ffmpeg.exe 104 | - cd dist/scenedetect 105 | - scenedetect.exe version 106 | - scenedetect.exe -i ../../tests/resources/testvideo.mp4 -b opencv detect-content time -e 2s 107 | - scenedetect.exe -i ../../tests/resources/testvideo.mp4 -b pyav detect-content time -e 2s 108 | 109 | artifacts: 110 | # Portable ZIP 111 | - path: dist/scenedetect-win64.zip 112 | name: PySceneDetect-win64_portable 113 | # MSI Installer + .EXE Bundle for Signing 114 | - path: dist/scenedetect-signed.zip 115 | name: PySceneDetect-win64_installer 116 | -------------------------------------------------------------------------------- /benchmark/AutoShot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/benchmark/AutoShot/.gitkeep -------------------------------------------------------------------------------- /benchmark/BBC/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/benchmark/BBC/.gitkeep -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarking PySceneDetect 2 | This repository benchmarks the performance of PySceneDetect in terms of both latency and accuracy. 3 | We evaluate it using the standard dataset for video shot detection: [BBC](https://zenodo.org/records/14865504) and [AutoShot](https://drive.google.com/file/d/17diRkLlNUUjHDooXdqFUTXYje2-x4Yt6/view?usp=sharing). 4 | 5 | ## Dataset Download 6 | ### BBC 7 | ``` 8 | # annotation 9 | wget -O BBC/fixed.zip https://zenodo.org/records/14873790/files/fixed.zip 10 | unzip BBC/fixed.zip -d BBC 11 | rm -rf BBC/fixed.zip 12 | 13 | # videos 14 | wget -O BBC/videos.zip https://zenodo.org/records/14873790/files/videos.zip 15 | unzip BBC/videos.zip -d BBC 16 | rm -rf BBC/videos.zip 17 | ``` 18 | 19 | ### AutoShot 20 | Download `AutoShot_test.tar.gz` from [Google drive](https://drive.google.com/file/d/17diRkLlNUUjHDooXdqFUTXYje2-x4Yt6/view?usp=sharing). 21 | ``` 22 | tar -zxvf AutoShot.tar.gz 23 | rm AutoShot.tar.gz 24 | ``` 25 | 26 | ## Evaluation 27 | To evaluate PySceneDetect on a dataset, run the following command from the root of the repo: 28 | ``` 29 | python -m benchmark --dataset --detector 30 | ``` 31 | For example, to evaluate ContentDetector on the BBC dataset: 32 | ``` 33 | python -m benchmark --dataset BBC --detector detect-content 34 | ``` 35 | To run all detectors on all datasets: 36 | ``` 37 | python -m benchmark --all 38 | ``` 39 | The `--all` flag can also be combined with `--dataset` or `--detector`. 40 | 41 | ### Result 42 | The performance is computed as recall, precision, f1, and elapsed time. 43 | 44 | #### BBC 45 | 46 | | Detector | Recall | Precision | F1 | Elapsed time (second) | 47 | |:-----------------:|:------:|:---------:|:-----:|:---------------------:| 48 | | AdaptiveDetector | 87.12 | 96.55 | 91.59 | 27.84 | 49 | | ContentDetector | 84.70 | 88.77 | 86.69 | 28.20 | 50 | | HashDetector | 92.30 | 75.56 | 83.10 | 16.00 | 51 | | HistogramDetector | 89.84 | 72.03 | 79.96 | 15.13 | 52 | | ThresholdDetector | 0.00 | 0.00 | 0.00 | 18.95 | 53 | 54 | #### AutoShot 55 | 56 | | Detector | Recall | Precision | F1 | Elapsed time (second) | 57 | |:-----------------:|:------:|:---------:|:-----:|:---------------------:| 58 | | AdaptiveDetector | 70.77 | 77.65 | 74.05 | 1.23 | 59 | | ContentDetector | 63.67 | 76.40 | 69.46 | 1.21 | 60 | | HashDetector | 56.66 | 76.35 | 65.05 | 1.16 | 61 | | HistogramDetector | 63.36 | 53.34 | 57.92 | 1.23 | 62 | | ThresholdDetector | 0.75 | 38.64 | 1.47 | 1.24 | 63 | 64 | ## Citation 65 | ### BBC 66 | ``` 67 | @InProceedings{bbc_dataset, 68 | author = {Lorenzo Baraldi and Costantino Grana and Rita Cucchiara}, 69 | title = {A Deep Siamese Network for Scene Detection in Broadcast Videos}, 70 | booktitle = {Proceedings of the 23rd ACM International Conference on Multimedia}, 71 | year = {2015}, 72 | } 73 | ``` 74 | 75 | ### AutoShot 76 | ``` 77 | @InProceedings{autoshot_dataset, 78 | author = {Wentao Zhu and Yufang Huang and Xiufeng Xie and Wenxian Liu and Jincan Deng and Debing Zhang and Zhangyang Wang and Ji Liu}, 79 | title = {AutoShot: A Short Video Dataset and State-of-the-Art Shot Boundary Detection}, 80 | booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR) Workshops}, 81 | year = {2023}, 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /benchmark/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import time 4 | import typing as ty 5 | 6 | from tqdm import tqdm 7 | 8 | from benchmark.autoshot_dataset import AutoShotDataset 9 | from benchmark.bbc_dataset import BBCDataset 10 | from benchmark.evaluator import Evaluator 11 | from scenedetect import ( 12 | AdaptiveDetector, 13 | ContentDetector, 14 | HashDetector, 15 | HistogramDetector, 16 | ThresholdDetector, 17 | detect, 18 | ) 19 | 20 | _DETECTORS = { 21 | "detect-adaptive": AdaptiveDetector, 22 | "detect-content": ContentDetector, 23 | "detect-hash": HashDetector, 24 | "detect-hist": HistogramDetector, 25 | "detect-threshold": ThresholdDetector, 26 | } 27 | 28 | 29 | _DATASETS = { 30 | "BBC": BBCDataset("benchmark/BBC"), 31 | "AutoShot": AutoShotDataset("benchmark/AutoShot"), 32 | } 33 | 34 | _DEFAULT_DETECTOR = "detect-content" 35 | _DEFAULT_DATASET = "BBC" 36 | 37 | _RESULT_PRINT_FORMAT = ( 38 | "Recall: {recall:.2f}, Precision: {precision:.2f}, F1: {f1:.2f} Elapsed time: {elapsed:.2f}\n" 39 | ) 40 | 41 | 42 | def _detect_scenes(detector: str, dataset: str, detailed: bool): 43 | pred_scenes = {} 44 | for video_file, scene_file in tqdm(_DATASETS[dataset]): 45 | start = time.time() 46 | pred_scene_list = detect(video_file, _DETECTORS[detector]()) 47 | elapsed = time.time() - start 48 | filename = os.path.basename(video_file) 49 | scenes = { 50 | scene_file: { 51 | "video_file": filename, 52 | "elapsed": elapsed, 53 | "pred_scenes": [scene[1].frame_num for scene in pred_scene_list], 54 | } 55 | } 56 | result = Evaluator().evaluate_performance(scenes) 57 | if detailed: 58 | print(f"\n{filename} results:") 59 | print(_RESULT_PRINT_FORMAT.format(**result) + "\n") 60 | pred_scenes.update(scenes) 61 | 62 | return pred_scenes 63 | 64 | 65 | def run_benchmark(detector: str, dataset: str, detailed: bool): 66 | print(f"Evaluating {detector} on dataset {dataset}...\n") 67 | pred_scenes = _detect_scenes(detector=detector, dataset=dataset, detailed=detailed) 68 | result = Evaluator().evaluate_performance(pred_scenes) 69 | # Print extra separators in detailed output to identify overall results vs individual videos. 70 | if detailed: 71 | print("------------------------------------------------------------") 72 | print(f"\nOverall Results for {detector} on dataset {dataset}:") 73 | print(_RESULT_PRINT_FORMAT.format(**result)) 74 | if detailed: 75 | print("------------------------------------------------------------") 76 | 77 | 78 | def create_parser(): 79 | parser = argparse.ArgumentParser(description="Benchmarking PySceneDetect performance.") 80 | parser.add_argument( 81 | "--dataset", 82 | type=str, 83 | choices=[ 84 | "BBC", 85 | "AutoShot", 86 | ], 87 | help="Dataset name. Supported datasets are BBC and AutoShot.", 88 | ) 89 | parser.add_argument( 90 | "--detector", 91 | type=str, 92 | choices=[ 93 | "detect-adaptive", 94 | "detect-content", 95 | "detect-hash", 96 | "detect-hist", 97 | "detect-threshold", 98 | ], 99 | help="Detector name. Implemented detectors are listed: " 100 | "https://www.scenedetect.com/docs/latest/cli.html", 101 | ) 102 | parser.add_argument( 103 | "--detailed", 104 | action="store_const", 105 | const=True, 106 | help="Print results for each video, in addition to overall summary.", 107 | ) 108 | parser.add_argument( 109 | "--all", 110 | action="store_const", 111 | const=True, 112 | help="Benchmark all detectors on all datasets. If --detector or --dataset are specified, " 113 | "will only run with those.", 114 | ) 115 | return parser 116 | 117 | 118 | def run_all_benchmarks(detector: ty.Optional[str], dataset: ty.Optional[str], detailed: bool): 119 | detectors = {detector: _DETECTORS[detector]} if detector else _DETECTORS 120 | datasets = {dataset: _DATASETS[dataset]} if dataset else _DATASETS 121 | print( 122 | "Running benchmarks for:\n" 123 | f" - Detectors: {', '.join(detectors.keys())}\n" 124 | f" - Datasets: {', '.join(datasets.keys())}\n" 125 | ) 126 | for detector in detectors: 127 | for dataset in datasets: 128 | run_benchmark(detector=detector, dataset=dataset, detailed=detailed) 129 | 130 | 131 | def main(): 132 | parser = create_parser() 133 | args = parser.parse_args() 134 | if args.all: 135 | run_all_benchmarks( 136 | detector=args.detector, dataset=args.dataset, detailed=bool(args.detailed) 137 | ) 138 | else: 139 | run_benchmark( 140 | detector=args.detector if args.detector else _DEFAULT_DETECTOR, 141 | dataset=args.dataset if args.dataset else _DEFAULT_DATASET, 142 | detailed=bool(args.detailed), 143 | ) 144 | 145 | 146 | if __name__ == "__main__": 147 | main() 148 | -------------------------------------------------------------------------------- /benchmark/autoshot_dataset.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | class AutoShotDataset: 6 | """ 7 | The AutoShot Dataset (test splits) proposed by Zhu et al. in AutoShot: A Short Video Dataset and State-of-the-Art Shot Boundary Detection 8 | Link: https://openaccess.thecvf.com/content/CVPR2023W/NAS/html/Zhu_AutoShot_A_Short_Video_Dataset_and_State-of-the-Art_Shot_Boundary_Detection_CVPRW_2023_paper.html 9 | The original test set consists of 200 videos, but 36 videos are missing (AutoShot/videos/.mp4). 10 | The annotated scenes are provided in corresponding files (AutoShot/annotations/.txt) 11 | """ 12 | 13 | def __init__(self, dataset_dir: str): 14 | self._video_files = [ 15 | file for file in sorted(glob.glob(os.path.join(dataset_dir, "videos", "*.mp4"))) 16 | ] 17 | self._scene_files = [ 18 | file for file in sorted(glob.glob(os.path.join(dataset_dir, "annotations", "*.txt"))) 19 | ] 20 | for video_file, scene_file in zip(self._video_files, self._scene_files): 21 | video_id = os.path.basename(video_file).split(".")[0] 22 | scene_id = os.path.basename(scene_file).split(".")[0] 23 | assert video_id == scene_id 24 | 25 | def __getitem__(self, index): 26 | video_file = self._video_files[index] 27 | scene_file = self._scene_files[index] 28 | return video_file, scene_file 29 | 30 | def __len__(self): 31 | return len(self._video_files) 32 | -------------------------------------------------------------------------------- /benchmark/bbc_dataset.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | class BBCDataset: 6 | """ 7 | The BBC Dataset, proposed by Baraldi et al. in A deep siamese network for scene detection in broadcast videos 8 | Link: https://arxiv.org/abs/1510.08893 9 | The dataset consists of 11 videos (BBC/videos/bbc_01.mp4 to BBC/videos/bbc_11.mp4). 10 | The annotated scenes are provided in corresponding files (BBC/fixed/[i]-scenes.txt). 11 | """ 12 | 13 | def __init__(self, dataset_dir: str): 14 | self._video_files = [ 15 | file for file in sorted(glob.glob(os.path.join(dataset_dir, "videos", "*.mp4"))) 16 | ] 17 | self._scene_files = [ 18 | file for file in sorted(glob.glob(os.path.join(dataset_dir, "fixed", "*.txt"))) 19 | ] 20 | assert len(self._video_files) == len(self._scene_files) 21 | for video_file, scene_file in zip(self._video_files, self._scene_files): 22 | video_id = os.path.basename(video_file).replace("bbc_", "").split(".")[0] 23 | scene_id = os.path.basename(scene_file).split("-")[0] 24 | assert video_id == scene_id 25 | 26 | def __getitem__(self, index): 27 | video_file = self._video_files[index] 28 | scene_file = self._scene_files[index] 29 | return video_file, scene_file 30 | 31 | def __len__(self): 32 | return len(self._video_files) 33 | -------------------------------------------------------------------------------- /benchmark/evaluator.py: -------------------------------------------------------------------------------- 1 | from statistics import mean 2 | 3 | 4 | class Evaluator: 5 | def __init__(self): 6 | pass 7 | 8 | def _load_scenes(self, scene_filename): 9 | with open(scene_filename) as f: 10 | gt_scene_list = [x.strip().split("\t")[1] for x in f.readlines()] 11 | gt_scene_list = [int(x) + 1 for x in gt_scene_list] 12 | return gt_scene_list 13 | 14 | def evaluate_performance(self, pred_scenes): 15 | total_correct = 0 16 | total_pred = 0 17 | total_gt = 0 18 | assert pred_scenes 19 | 20 | for scene_file, pred in pred_scenes.items(): 21 | gt_scene_list = self._load_scenes(scene_file) 22 | pred_list = pred["pred_scenes"] 23 | total_correct += len(set(pred_list) & set(gt_scene_list)) 24 | total_pred += len(pred_list) 25 | total_gt += len(gt_scene_list) 26 | 27 | recall = total_correct / total_gt 28 | precision = total_correct / total_pred if total_pred != 0 else 0 29 | f1 = 2 * recall * precision / (recall + precision) if (recall + precision) != 0 else 0 30 | avg_elapsed = mean([x["elapsed"] for x in pred_scenes.values()]) 31 | result = { 32 | "recall": recall * 100, 33 | "precision": precision * 100, 34 | "f1": f1 * 100, 35 | "elapsed": avg_elapsed, 36 | } 37 | return result 38 | -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_banner.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_banner.scale-125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_banner.scale-125.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_banner.scale-150.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_banner.scale-150.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_banner.scale-200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_banner.scale-200.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_logo.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_logo.scale-125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_logo.scale-125.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_logo.scale-150.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_logo.scale-150.jpg -------------------------------------------------------------------------------- /dist/installer/Generated Images/installer_logo.scale-200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Generated Images/installer_logo.scale-200.jpg -------------------------------------------------------------------------------- /dist/installer/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2019/VC_redist.x64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2019/VC_redist.x64.exe -------------------------------------------------------------------------------- /dist/installer/installer_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/installer_banner.png -------------------------------------------------------------------------------- /dist/installer/installer_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/installer_logo.png -------------------------------------------------------------------------------- /dist/installer/license65.dat.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/license65.dat.enc -------------------------------------------------------------------------------- /dist/installer/psd_square_small.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/installer/psd_square_small.ico -------------------------------------------------------------------------------- /dist/package-info.rst: -------------------------------------------------------------------------------- 1 | 2 | PySceneDetect 3 | ========================================================== 4 | 5 | Video Scene Cut Detection and Analysis Tool 6 | ---------------------------------------------------------- 7 | 8 | .. image:: https://img.shields.io/pypi/status/scenedetect.svg 9 | :target: https://github.com/Breakthrough/PySceneDetect 10 | 11 | .. image:: https://img.shields.io/github/release/Breakthrough/PySceneDetect.svg 12 | :target: https://github.com/Breakthrough/PySceneDetect 13 | 14 | .. image:: https://img.shields.io/pypi/l/scenedetect.svg 15 | :target: http://pyscenedetect.readthedocs.org/en/latest/copyright/ 16 | 17 | .. image:: https://img.shields.io/github/stars/Breakthrough/PySceneDetect.svg?style=social&label=View%20on%20Github 18 | :target: https://github.com/Breakthrough/PySceneDetect 19 | 20 | ---------------------------------------------------------- 21 | 22 | Documentation: https://www.scenedetect.com/docs 23 | 24 | Github Repo: https://github.com/Breakthrough/PySceneDetect/ 25 | 26 | Install: ``pip install --upgrade scenedetect[opencv]`` 27 | 28 | ---------------------------------------------------------- 29 | 30 | **PySceneDetect** is a tool for detecting shot changes in videos, and can automatically split videos into separate clips. PySceneDetect is free and open-source software, and has several detection methods to find fast-cuts and threshold-based fades. 31 | 32 | For example, to split a video: ``scenedetect -i video.mp4 split-video`` 33 | 34 | You can also use the Python API (`docs `_) to do the same: 35 | 36 | .. code-block:: python 37 | 38 | from scenedetect import detect, AdaptiveDetector, split_video_ffmpeg 39 | scene_list = detect('my_video.mp4', AdaptiveDetector()) 40 | split_video_ffmpeg('my_video.mp4', scene_list) 41 | 42 | ---------------------------------------------------------- 43 | 44 | Licensed under BSD 3-Clause (see the ``LICENSE`` file for details). 45 | 46 | Copyright (C) 2014-2024 Brandon Castellano. 47 | All rights reserved. 48 | 49 | -------------------------------------------------------------------------------- /dist/pre_release.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | 13 | # Pre-release script to run before invoking `pyinstaller`: 14 | # 15 | # python dist/pre_release.py 16 | # pyinstaller dist/scenedetect.spec 17 | # 18 | import os 19 | import sys 20 | sys.path.append(os.path.abspath(".")) 21 | 22 | import scenedetect 23 | 24 | 25 | VERSION = scenedetect.__version__ 26 | 27 | run_version_check = ("--release" in sys.argv) 28 | 29 | if run_version_check: 30 | installer_aip = '' 31 | with open("dist/installer/PySceneDetect.aip", "r") as f: 32 | installer_aip = f.read() 33 | aip_version = f"" 34 | assert aip_version in installer_aip, f"Installer project version does not match {VERSION}." 35 | 36 | with open("dist/.version_info", "wb") as f: 37 | v = VERSION.split(".") 38 | assert 2 <= len(v) <= 4, f"Unrecognized version format: {VERSION}" 39 | while len(v) < 4: 40 | v.append("0") 41 | (maj, min, pat, bld) = v[0], v[1], v[2], v[3] 42 | # If either major or minor have suffixes, assume it's a dev/beta build and set 43 | # the final component to 999. 44 | if not min.isdigit(): 45 | assert "-" in min 46 | min = min[:min.find("-")] 47 | bld = 999 48 | if not pat.isdigit(): 49 | assert "-" in pat 50 | pat = pat[:pat.find("-")] 51 | bld = 999 52 | f.write(f"""# UTF-8 53 | # 54 | # For more details about fixed file info 'ffi' see: 55 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx 56 | VSVersionInfo( 57 | ffi=FixedFileInfo( 58 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) 59 | # Set not needed items to zero 0. 60 | filevers=({maj}, {min}, {pat}, {bld}), 61 | prodvers=({maj}, {min}, {pat}, {bld}), 62 | # Contains a bitmask that specifies the valid bits 'flags'r 63 | mask=0x3f, 64 | # Contains a bitmask that specifies the Boolean attributes of the file. 65 | flags=0x0, 66 | # The operating system for which this file was designed. 67 | # 0x4 - NT and there is no need to change it. 68 | OS=0x4, 69 | # The general type of file. 70 | # 0x1 - the file is an application. 71 | fileType=0x1, 72 | # The function of the file. 73 | # 0x0 - the function is not defined for this fileType 74 | subtype=0x0, 75 | # Creation date and time stamp. 76 | date=(0, 0) 77 | ), 78 | kids=[ 79 | StringFileInfo( 80 | [ 81 | StringTable( 82 | u'040904B0', 83 | [StringStruct(u'CompanyName', u'github.com/Breakthrough'), 84 | StringStruct(u'FileDescription', u'www.scenedetect.com'), 85 | StringStruct(u'FileVersion', u'{VERSION}'), 86 | StringStruct(u'InternalName', u'PySceneDetect'), 87 | StringStruct(u'LegalCopyright', u'Copyright © 2024 Brandon Castellano'), 88 | StringStruct(u'OriginalFilename', u'scenedetect.exe'), 89 | StringStruct(u'ProductName', u'PySceneDetect'), 90 | StringStruct(u'ProductVersion', u'{VERSION}')]) 91 | ]), 92 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) 93 | ] 94 | ) 95 | """.encode()) 96 | -------------------------------------------------------------------------------- /dist/pyscenedetect.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/pyscenedetect.ico -------------------------------------------------------------------------------- /dist/requirements_windows.txt: -------------------------------------------------------------------------------- 1 | # PySceneDetect Requirements for Windows Build 2 | av==14.2.0 3 | click==8.1.8 4 | opencv-python-headless==4.11.0.86 5 | imageio-ffmpeg==0.6.0 6 | moviepy==2.1.2 7 | numpy==2.2.3 8 | platformdirs==4.3.6 9 | tqdm==4.67.1 10 | 11 | # Build-only and test-only requirements. 12 | pyinstaller 13 | pytest 14 | -------------------------------------------------------------------------------- /dist/scenedetect.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['../scenedetect/__main__.py'], 7 | pathex=['.'], 8 | binaries=None, 9 | datas=[ 10 | ('windows/*', '.'), 11 | ('../LICENSE', '.'), 12 | ('../scenedetect.cfg', '.') 13 | ], 14 | hiddenimports=[], 15 | hookspath=[], 16 | runtime_hooks=[], 17 | excludes=[], 18 | win_no_prefer_redirects=False, 19 | win_private_assemblies=False, 20 | cipher=block_cipher) 21 | 22 | pyz = PYZ(a.pure, a.zipped_data, 23 | cipher=block_cipher) 24 | exe = EXE(pyz, 25 | a.scripts, 26 | exclude_binaries=True, 27 | name='scenedetect', 28 | debug=False, 29 | strip=False, 30 | upx=True, 31 | console=True, 32 | version='.version_info', 33 | icon='pyscenedetect.ico') 34 | coll = COLLECT(exe, 35 | a.binaries, 36 | a.zipfiles, 37 | a.datas, 38 | strip=False, 39 | upx=True, 40 | name='scenedetect') 41 | -------------------------------------------------------------------------------- /dist/windows/README.txt: -------------------------------------------------------------------------------- 1 | Run `scenedetect --help` for usage examples. For documentation, see `docs/index.html` or visit 2 | https://www.scenedetect.com/docs. 3 | -------------------------------------------------------------------------------- /dist/windows_thirdparty.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/dist/windows_thirdparty.7z -------------------------------------------------------------------------------- /docs/.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | # TODO: Support Python 3.11. 7 | python: "3.10" 8 | 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | python: 13 | install: 14 | - requirements: docs/requirements.txt 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = PySceneDetect 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/pyscenedetect.css: -------------------------------------------------------------------------------- 1 | .sig { 2 | padding-bottom: 8px; 3 | } 4 | .field-list { 5 | padding-bottom: 8px; 6 | } 7 | .class { 8 | padding-bottom: 12px; 9 | padding-top: 4px; 10 | } 11 | .function { 12 | padding-bottom: 12px; 13 | padding-top: 12px; 14 | } 15 | .data { 16 | padding-bottom: 12px; 17 | padding-top: 12px; 18 | } 19 | .method { 20 | padding-bottom: 12px; 21 | padding-top: 12px; 22 | } 23 | .attribute { 24 | padding-bottom: 12px; 25 | padding-top: 12px; 26 | } 27 | .exception { 28 | padding-bottom: 12px; 29 | padding-top: 12px; 30 | } 31 | .property { 32 | padding-bottom: 12px; 33 | padding-top: 12px; 34 | } 35 | -------------------------------------------------------------------------------- /docs/_static/pyscenedetect_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/docs/_static/pyscenedetect_logo.png -------------------------------------------------------------------------------- /docs/_static/pyscenedetect_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/docs/_static/pyscenedetect_logo_small.png -------------------------------------------------------------------------------- /docs/_templates/navigation.html: -------------------------------------------------------------------------------- 1 |

{{ _('Navigation') }}

2 | {{ toctree(maxdepth=2, includehidden=theme_sidebar_includehidden, collapse=theme_sidebar_collapse) }} 3 | {% if theme_extra_nav_links %} 4 |
5 |
    6 | {% for text, uri in theme_extra_nav_links.items() %} 7 |
  • {{ text }}
  • 8 | {% endfor %} 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | *********************************************************************** 3 | ``scenedetect`` 🎬 Package 4 | *********************************************************************** 5 | 6 | The `scenedetect` API is easy to integrate with most application workflows, while also being highly extensible. See the `Getting Started`_ section below for some common use cases and integrations. The `scenedetect` package is organized into several sub-modules: 7 | 8 | * :ref:`scenedetect 🎬 `: high-level functions like :func:`scenedetect.detect() ` to quickly analyze a video with any :ref:`detection algorithm ` (:ref:`example `) and get a list of timecode pairs as a result 9 | 10 | * :ref:`scenedetect.detectors 🕵️ `: detection algorithms: 11 | 12 | * :mod:`ContentDetector `: detects fast cuts using weighted average of HSV changes 13 | 14 | * :mod:`ThresholdDetector `: finds fades in/out using average pixel intensity changes in RGB 15 | 16 | * :mod:`AdaptiveDetector ` finds fast cuts using rolling average of HSL changes 17 | 18 | * :mod:`HistogramDetector ` finds fast cuts using HSV histogram changes 19 | 20 | * :mod:`HashDetector `: finds fast cuts using perceptual image hashing 21 | 22 | * :ref:`scenedetect.output ✂️ `: Output formats: 23 | 24 | * :func:`split_video_ffmpeg ` and :func:`split_video_mkvmerge ` split a video based on the detected scenes 25 | 26 | * :func:`save_images ` can save an arbitrary number of images from each scene 27 | 28 | * :func:`write_scene_list ` can be used to save scene/cut info as CSV, :func:`write_scene_list_html ` for HTML 29 | 30 | * :ref:`scenedetect.backends 🎥 `: PySceneDetect supports multiple libraries as an input backend: 31 | 32 | * OpenCV: :class:`VideoStreamCv2 ` 33 | 34 | * PyAV: :class:`VideoStreamAv ` 35 | 36 | * MoviePy: :class:`VideoStreamMoviePy ` 37 | 38 | * :ref:`scenedetect.common ⏱️ `: common functionality such as :class:`FrameTimecode ` for timecode handling 39 | 40 | * :ref:`scenedetect.scene_manager 🎞️ `: the :class:`SceneManager ` coordinates performing scene detection on a video with one or more detectors 41 | 42 | * :ref:`scenedetect.detector 🌐 `: the interface (:class:`SceneDetector `) that detectors must implement to be compatible with PySceneDetect 43 | 44 | * :ref:`scenedetect.video_stream `: the interface (:class:`VideoStream `) that detectors must implement to be compatible with PySceneDetect 45 | 46 | * :ref:`scenedetect.stats_manager 🧮 `: the :class:`StatsManager ` allows you to store detection metrics for each frame and save them to CSV for further analysis 47 | 48 | * :ref:`scenedetect.platform 🐱‍💻 `: logging and utility functions 49 | 50 | 51 | Most types/functions are also available directly from the `scenedetect` package to make imports simpler. 52 | 53 | .. warning:: 54 | 55 | The PySceneDetect API is still under development. It is recommended that you pin the `scenedetect` version in your requirements to below the next major release: 56 | 57 | .. code:: python 58 | 59 | scenedetect<0.8 60 | 61 | 62 | .. _scenedetect-quickstart: 63 | 64 | ======================================================================= 65 | Getting Started 66 | ======================================================================= 67 | 68 | PySceneDetect makes it very easy to find scene transitions in a video with the :func:`scenedetect.detect` function: 69 | 70 | .. code:: python 71 | 72 | from scenedetect import detect, ContentDetector 73 | path = "video.mp4" 74 | scenes = detect(path, ContentDetector()) 75 | for (scene_start, scene_end) in scenes: 76 | print(f"{scene_start}-{scene_end}") 77 | 78 | ``scenes`` now contains a list of :class:`FrameTimecode ` pairs representing the start/end of each scene. Note that you can set ``show_progress=True`` when calling :func:`detect ` to display a progress bar with estimated time remaining. 79 | 80 | Here, we use :mod:`ContentDetector ` to detect fast cuts. There are :ref:`many detector types ` which can be used to find fast cuts and fades in/out. PySceneDetect can also export scene data in various formats, and can :ref:`split the input video ` automatically if `ffmpeg` is available: 81 | 82 | .. code:: python 83 | 84 | from scenedetect import detect, ContentDetector, split_video_ffmpeg 85 | scene_list = detect("my_video.mp4", ContentDetector()) 86 | split_video_ffmpeg("my_video.mp4", scenes) 87 | 88 | Recipes for common use cases can be `found on Github `_ including limiting detection time and storing per-frame metrics. For advanced workflows, start with the :ref:`SceneManager usage examples `. 89 | 90 | .. _scenedetect-functions: 91 | 92 | ======================================================================= 93 | Functions 94 | ======================================================================= 95 | 96 | .. automodule:: scenedetect 97 | :members: 98 | 99 | ======================================================================= 100 | Module Reference 101 | ======================================================================= 102 | 103 | .. toctree:: 104 | :maxdepth: 3 105 | :caption: PySceneDetect Module Documentation 106 | :name: fullapitoc 107 | 108 | api/detectors 109 | api/scene_manager 110 | api/common 111 | api/output 112 | api/backends 113 | api/stats_manager 114 | api/detector 115 | api/video_stream 116 | api/platform 117 | 118 | 119 | ======================================================================= 120 | Logging 121 | ======================================================================= 122 | 123 | PySceneDetect outputs messages to a logger named ``pyscenedetect`` which does not have any default handlers. You can use :func:`scenedetect.init_logger ` with ``show_stdout=True`` or specify a log file (verbosity can also be specified) to attach some common handlers, or use ``logging.getLogger("pyscenedetect")`` and attach log handlers manually. 124 | -------------------------------------------------------------------------------- /docs/api/backends.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-backends: 3 | 4 | ---------------------------------------- 5 | Video Backends 6 | ---------------------------------------- 7 | 8 | .. automodule:: scenedetect.backends 9 | :members: 10 | 11 | .. automodule:: scenedetect.backends.opencv 12 | :members: 13 | 14 | .. automodule:: scenedetect.backends.pyav 15 | :members: 16 | 17 | .. automodule:: scenedetect.backends.moviepy 18 | :members: 19 | -------------------------------------------------------------------------------- /docs/api/common.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-common: 3 | 4 | --------------------------------------------------------------- 5 | Common 6 | --------------------------------------------------------------- 7 | 8 | .. automodule:: scenedetect.common 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/api/detector.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-detector: 3 | 4 | ------------------------------------------------- 5 | Detector Interface 6 | ------------------------------------------------- 7 | 8 | .. automodule:: scenedetect.detector 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/api/detectors.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-detectors: 3 | 4 | ---------------------------------------- 5 | Detectors 6 | ---------------------------------------- 7 | 8 | .. automodule:: scenedetect.detectors 9 | :members: 10 | 11 | .. automodule:: scenedetect.detectors.adaptive_detector 12 | :members: 13 | 14 | .. automodule:: scenedetect.detectors.content_detector 15 | :members: 16 | 17 | .. automodule:: scenedetect.detectors.hash_detector 18 | :members: 19 | 20 | .. automodule:: scenedetect.detectors.histogram_detector 21 | :members: 22 | 23 | .. automodule:: scenedetect.detectors.threshold_detector 24 | :members: 25 | -------------------------------------------------------------------------------- /docs/api/output.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | .. _scenedetect-output: 4 | 5 | ------------------------------------------------- 6 | Ouptut 7 | ------------------------------------------------- 8 | 9 | .. autofunction:: scenedetect.output.save_images 10 | 11 | .. autofunction:: scenedetect.output.is_ffmpeg_available 12 | 13 | .. autofunction:: scenedetect.output.split_video_ffmpeg 14 | 15 | .. autofunction:: scenedetect.output.is_mkvmerge_available 16 | 17 | .. autofunction:: scenedetect.output.split_video_mkvmerge 18 | 19 | .. autofunction:: scenedetect.output.write_scene_list_html 20 | 21 | .. autofunction:: scenedetect.output.write_scene_list 22 | 23 | .. autoclass:: scenedetect.output.SceneMetadata 24 | 25 | .. autoclass:: scenedetect.output.VideoMetadata 26 | 27 | .. autofunction:: scenedetect.output.default_formatter 28 | 29 | -------------------------------------------------------------------------------- /docs/api/platform.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-platform: 3 | 4 | --------------------------------------------------------------- 5 | Platform & Logging 6 | --------------------------------------------------------------- 7 | 8 | .. automodule:: scenedetect.platform 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/api/scene_manager.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-scene_manager: 3 | 4 | ----------------------------------------------------------------------- 5 | Scene Manager 6 | ----------------------------------------------------------------------- 7 | 8 | .. automodule:: scenedetect.scene_manager 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/api/stats_manager.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-stats_manager: 3 | 4 | ----------------------------------------------------------------------- 5 | Stats Manager 6 | ----------------------------------------------------------------------- 7 | 8 | .. automodule:: scenedetect.stats_manager 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/api/video_stream.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect-video_stream: 3 | 4 | --------------------------------------------------------------- 5 | Stream Interface 6 | --------------------------------------------------------------- 7 | 8 | .. automodule:: scenedetect.video_stream 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/cli/backends.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _cli-backends: 3 | 4 | *********************************************************************** 5 | Backends 6 | *********************************************************************** 7 | 8 | PySceneDetect supports multiple backends for video input. Some can be configured by using :ref:`a config file `. Installed backends can be verified by running ``scenedetect version --all``. 9 | 10 | Note that the `scenedetect` command output is generated as a post-processing step, after scene detection completes. Most commands require the ability for the input to be replayed, and preferably it should also support seeking. Network streams and other input types are supported with certain backends, however integration with live streams requires use of the Python API. 11 | 12 | 13 | ======================================================================= 14 | OpenCV 15 | ======================================================================= 16 | 17 | *[Default]* 18 | The `OpenCV `_ backend (usually `opencv-python `_) uses OpenCV's ``VideoCapture`` for video input. Can be used by specifying ``-b opencv`` via command line, or setting ``backend = opencv`` under the ``[global]`` section of your :ref:`config file `. 19 | 20 | It is mostly reliable and fast, although can occasionally run into issues processing videos with multiple audio tracks or small amounts of frame corruption. You can use a custom version of the ``cv2`` package, or install either the `opencv-python` or `opencv-python-headless` packages from `pip`. 21 | 22 | The OpenCV backend also supports image sequences as inputs (e.g. ``frame%02d.jpg`` if you want to load frame001.jpg, frame002.jpg, frame003.jpg...). Make sure to specify the framerate manually (``-f``/``--framerate``) to ensure accurate timing calculations. 23 | 24 | 25 | ======================================================================= 26 | PyAV 27 | ======================================================================= 28 | 29 | The `PyAV `_ backend (`av package `_) is a more robust backend that handles multiple audio tracks and frame decode errors gracefully. 30 | 31 | This backend can be used by specifying ``-b pyav`` via command line, or setting ``backend = pyav`` under the ``[global]`` section of your :ref:`config file `. 32 | 33 | 34 | ======================================================================= 35 | MoviePy 36 | ======================================================================= 37 | 38 | MoviePy launches ffmpeg as a subprocess, and can be used with various types of inputs. If the input supports seeking it should work fine with most operations, for example, image sequences or AviSynth scripts. 39 | 40 | .. warning:: 41 | 42 | The MoviePy backend is still under development and is not included with current Windows distribution. To enable MoviePy support, you must install PySceneDetect using `python` and `pip`. 43 | 44 | This backend can be used by specifying ``-b moviepy`` via command line, or setting ``backend = moviepy`` under the ``[global]`` section of your :ref:`config file `. 45 | -------------------------------------------------------------------------------- /docs/cli/config_file.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _scenedetect_cli-config_file: 3 | 4 | *********************************************************************** 5 | Configuration File 6 | *********************************************************************** 7 | 8 | A configuration file path can be specified using the ``-c``/``--config`` argument. PySceneDetect also looks for a config file named `scenedetect.cfg` in one of the following locations: 9 | 10 | * Windows: 11 | * ``C:/Users/%USERNAME%/AppData/Local/PySceneDetect/scenedetect.cfg`` 12 | 13 | * Linux: 14 | * ``~/.config/PySceneDetect/scenedetect.cfg`` 15 | * ``$XDG_CONFIG_HOME/scenedetect.cfg`` 16 | 17 | * Mac: 18 | * ``~/Library/Preferences/PySceneDetect/scenedetect.cfg`` 19 | 20 | Run `scenedetect --help` to see the exact path on your system which will be used. Values set on the command line take precedence over those set in the config file. Most (but not all) command line parameters can be set using a configuration file, and some options can *only* be set using a config file. See the :ref:`Template ` below for a ``scenedetect.cfg`` file that describes each option, which you can use to create a new config file. Note that lines starting with a ``#`` are comments and will be ignored. 21 | 22 | The syntax of a configuration file is: 23 | 24 | .. code:: ini 25 | 26 | [command] 27 | option_a = value 28 | #comment 29 | option_b = 1 30 | 31 | 32 | ======================================================================= 33 | Example 34 | ======================================================================= 35 | 36 | .. code:: ini 37 | 38 | [global] 39 | default-detector = detect-content 40 | min-scene-len = 0.8s 41 | 42 | [detect-content] 43 | threshold = 26 44 | 45 | [split-video] 46 | # Use higher quality encoding 47 | preset = slow 48 | rate-factor = 17 49 | filename = $VIDEO_NAME-Clip-$SCENE_NUMBER 50 | 51 | [save-images] 52 | format = jpeg 53 | quality = 80 54 | num-images = 3 55 | 56 | 57 | .. _config_file Template: 58 | 59 | ======================================================================= 60 | Template 61 | ======================================================================= 62 | 63 | This template shows every possible configuration option and default values. It can be used as a ``scenedetect.cfg`` file. You can also `download it from Github `_. 64 | 65 | .. literalinclude:: ../../scenedetect.cfg 66 | :language: ini 67 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration file for the Sphinx documentation builder. 3 | # 4 | # This file does only contain a selection of the most common options. For a 5 | # full list see the documentation: 6 | # http://www.sphinx-doc.org/en/master/config 7 | 8 | # -- Path setup -------------------------------------------------------------- 9 | 10 | # If extensions (or modules to document with autodoc) are in another directory, 11 | # add these directories to sys.path here. If the directory is relative to the 12 | # documentation root, use os.path.abspath to make it absolute, like shown here. 13 | # 14 | import os 15 | import sys 16 | 17 | sys.path.insert(0, os.path.abspath("..")) 18 | 19 | from scenedetect import __version__ as scenedetect_version 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "PySceneDetect" 24 | copyright = "2014-2024, Brandon Castellano" 25 | author = "Brandon Castellano" 26 | 27 | # The short X.Y version 28 | version = scenedetect_version 29 | # The full version, including alpha/beta/rc tags 30 | release = scenedetect_version 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | "sphinx.ext.napoleon", 39 | "sphinx.ext.autodoc", 40 | ] 41 | 42 | autoclass_content = "both" 43 | autodoc_member_order = "groupwise" 44 | autodoc_typehints = "description" 45 | autodoc_typehints_format = "short" 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ["_templates"] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = ".rst" 55 | 56 | # The root toctree document. 57 | root_doc = "index" 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = "en" 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path . 69 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = "sphinx" 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # Add any paths that contain custom static files (such as style sheets) here, 77 | # relative to this directory. They are copied after the builtin static files, 78 | # so a file named "default.css" will overwrite the builtin "default.css". 79 | html_static_path = ["_static"] 80 | html_css_files = ["pyscenedetect.css"] 81 | 82 | # Custom sidebar templates, must be a dictionary that maps document names 83 | # to template names. 84 | # 85 | # The default sidebars (for documents that don't match any pattern) are 86 | # defined by theme itself. Builtin themes are using these templates by 87 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 88 | # 'searchbox.html']``. 89 | # 90 | # html_sidebars = {} 91 | 92 | # -- Options for HTMLHelp output --------------------------------------------- 93 | 94 | # Output file base name for HTML help builder. 95 | htmlhelp_basename = "PySceneDetectdoc" 96 | 97 | # -- Options for LaTeX output ------------------------------------------------ 98 | 99 | latex_elements = { 100 | # The paper size ('letterpaper' or 'a4paper'). 101 | # 102 | # 'papersize': 'letterpaper', 103 | # The font size ('10pt', '11pt' or '12pt'). 104 | # 105 | # 'pointsize': '10pt', 106 | # Additional stuff for the LaTeX preamble. 107 | # 108 | # 'preamble': '', 109 | # Latex figure (float) alignment 110 | # 111 | # 'figure_align': 'htbp', 112 | } 113 | 114 | # Grouping the document tree into LaTeX files. List of tuples 115 | # (source start file, target name, title, 116 | # author, documentclass [howto, manual, or own class]). 117 | latex_documents = [ 118 | (root_doc, "PySceneDetect.tex", "PySceneDetect Documentation", "Brandon Castellano", "manual"), 119 | ] 120 | 121 | # -- Options for manual page output ------------------------------------------ 122 | 123 | # One entry per manual page. List of tuples 124 | # (source start file, name, description, authors, manual section). 125 | man_pages = [(root_doc, "pyscenedetect", "PySceneDetect Documentation", [author], 1)] 126 | 127 | # -- Options for Texinfo output ---------------------------------------------- 128 | 129 | # Grouping the document tree into Texinfo files. List of tuples 130 | # (source start file, target name, title, author, 131 | # dir menu entry, description, category) 132 | texinfo_documents = [ 133 | ( 134 | root_doc, 135 | "PySceneDetect", 136 | "PySceneDetect Documentation", 137 | author, 138 | "PySceneDetect", 139 | "Python API and `scenedetect` command reference.", 140 | "Miscellaneous", 141 | ), 142 | ] 143 | 144 | # -- Theme ------------------------------------------------- 145 | 146 | # TODO: Consider switching to sphinx_material. 147 | 148 | html_theme = "alabaster" 149 | html_theme_options = { 150 | "sidebar_width": "235px", 151 | "description": "Version: [%s]" % (release), 152 | "show_relbar_bottom": True, 153 | "show_relbar_top": False, 154 | "github_user": "Breakthrough", 155 | "github_repo": "PySceneDetect", 156 | "github_type": "star", 157 | "tip_bg": "#f0f6fa", 158 | "tip_border": "#c2dcf2", 159 | "hint_bg": "#f0faf0", 160 | "hint_border": "#d3ebdc", 161 | "warn_bg": "#f5ebd0", 162 | "warn_border": "#f2caa2", 163 | "attention_bg": "#f5dcdc", 164 | "attention_border": "#ffaaaa", 165 | "logo": "pyscenedetect_logo.png", 166 | "logo_name": False, 167 | } 168 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. PySceneDetect documentation index file (contains toctree directive). 3 | Copyright (C) 2014-2024 Brandon Castellano. All rights reserved. 4 | 5 | ####################################################################### 6 | PySceneDetect Documentation 7 | ####################################################################### 8 | 9 | Welcome to the PySceneDetect docs. The docs are split into two separate parts: one for the command-line interface (the `scenedetect` command) and another for the Python API (the `scenedetect` module). 10 | 11 | You can install the latest release of PySceneDetect by running `pip install scenedetect[opencv]` or downloading the Windows build from `scenedetect.com/download `_. PySceneDetect requires `ffmpeg` or `mkvmerge` for video splitting support. 12 | 13 | .. note:: 14 | 15 | If you see any errors in the documentation, or want to suggest improvements, feel free to raise an issue on `the PySceneDetect issue tracker `_. 16 | 17 | PySceneDetect development happens on Github at `github.com/Breakthrough/PySceneDetect `_. 18 | 19 | 20 | *********************************************************************** 21 | Table of Contents 22 | *********************************************************************** 23 | 24 | ======================================================================= 25 | ``scenedetect`` Command Reference 🖥️ 26 | ======================================================================= 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | :caption: Command-Line Interface: 31 | :name: clitoc 32 | 33 | cli 34 | cli/config_file 35 | cli/backends 36 | 37 | 38 | ======================================================================= 39 | ``scenedetect`` Python Module 🐍 40 | ======================================================================= 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | :caption: API Documentation: 45 | :name: apitoc 46 | 47 | api 48 | api/detectors 49 | api/output 50 | api/backends 51 | api/common 52 | api/scene_manager 53 | api/detector 54 | api/video_stream 55 | api/stats_manager 56 | api/platform 57 | 58 | ======================================================================= 59 | Indices and Tables 60 | ======================================================================= 61 | 62 | * :ref:`genindex` 63 | * :ref:`modindex` 64 | * :ref:`search` 65 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=PySceneDetect 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # These are requirements only for the docs. 2 | Sphinx == 7.0.1 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # --------------------------------------------------------------- 4 | # [ Site: http://www.bcastell.com/projects/PySceneDetect/ ] 5 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 6 | # [ Documentation: http://www.scenedetect.com/docs/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # 10 | 11 | [build-system] 12 | requires = ["setuptools"] 13 | build-backend = "setuptools.build_meta" 14 | 15 | [tool.ruff] 16 | exclude = [ 17 | "docs" 18 | ] 19 | line-length = 100 20 | indent-width = 4 21 | 22 | [tool.ruff.format] 23 | quote-style = "double" 24 | indent-style = "space" 25 | skip-magic-trailing-comma = false 26 | docstring-code-format = true 27 | 28 | [tool.ruff.lint] 29 | select = [ 30 | # flake8-bugbear 31 | "B", 32 | # pycodestyle 33 | "E", 34 | # Pyflakes 35 | "F", 36 | # isort 37 | "I", 38 | # TODO - Add additional rule sets (https://docs.astral.sh/ruff/rules/): 39 | # pyupgrade 40 | #"UP", 41 | # flake8-simplify 42 | #"SIM", 43 | ] 44 | ignore = [ 45 | # TODO: Determine if we should use __all__, a reudndant alias, or keep this suppressed. 46 | "F401", 47 | # TODO: Line too long 48 | "E501", 49 | # TODO: Do not assign a `lambda` expression, use a `def` 50 | "E731", 51 | ] 52 | fixable = ["ALL"] 53 | unfixable = [] 54 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect Requirements 3 | # 4 | av>=9.2 5 | click>=8.0 6 | numpy 7 | opencv-python 8 | platformdirs 9 | pytest>=7.0 10 | tqdm 11 | -------------------------------------------------------------------------------- /requirements_headless.txt: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect Requirements for Headless Machines 3 | # 4 | av>=9.2 5 | click>=8.0 6 | numpy 7 | opencv-python-headless 8 | platformdirs 9 | pytest>=7.0 10 | tqdm 11 | -------------------------------------------------------------------------------- /scenedetect/__main__.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """Entry point for PySceneDetect's command-line interface.""" 13 | 14 | import sys 15 | from logging import getLogger 16 | 17 | from scenedetect._cli import scenedetect 18 | from scenedetect._cli.context import CliContext 19 | from scenedetect._cli.controller import run_scenedetect 20 | from scenedetect.platform import FakeTqdmLoggingRedirect, logging_redirect_tqdm 21 | 22 | 23 | def main(): 24 | """PySceneDetect command-line interface (CLI) entry point.""" 25 | context = CliContext() 26 | try: 27 | # Process command line arguments and subcommands to initialize the context. 28 | scenedetect.main(obj=context) # Parse CLI arguments with registered callbacks. 29 | except SystemExit as exit: 30 | help_command = any(arg in sys.argv for arg in ["-h", "--help"]) 31 | if help_command or exit.code != 0: 32 | raise 33 | 34 | # If we get here, processing the command line and loading the context worked. Let's run 35 | # the controller if we didn't process any help requests. 36 | logger = getLogger("pyscenedetect") 37 | # Ensure log messages don't conflict with any progress bars. If we're in quiet mode, where 38 | # no progress bars get created, we instead create a fake context manager. This is done here 39 | # to avoid needing a separate context manager at each point a progress bar is created. 40 | log_redirect = ( 41 | FakeTqdmLoggingRedirect() if context.quiet_mode else logging_redirect_tqdm(loggers=[logger]) 42 | ) 43 | 44 | with log_redirect: 45 | try: 46 | run_scenedetect(context) 47 | except KeyboardInterrupt: 48 | logger.info("Stopped.") 49 | if __debug__: 50 | raise 51 | except BaseException as ex: 52 | if __debug__: 53 | raise 54 | else: 55 | logger.critical("ERROR: Unhandled exception:", exc_info=ex) 56 | raise SystemExit(1) from None 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-CLICK: -------------------------------------------------------------------------------- 1 | click license 2 | Copyright (C) 2017, Armin Ronacher. 3 | 4 | Source Code: http://click.pocoo.org 5 | License URL: http://click.pocoo.org/license/ 6 | 7 | ----------------------------------------------------------------------- 8 | 9 | License 10 | --------------- 11 | 12 | Click is licensed under a three-clause BSD License. It basically means: do 13 | whatever you want with it as long as the copyright in Click sticks around, the 14 | conditions are not modified and the disclaimer is present. Furthermore, you 15 | must not use the names of the authors to promote derivatives of the software 16 | without written consent. 17 | 18 | License Text 19 | --------------------- 20 | 21 | Copyright (c) 2014 by Armin Ronacher. 22 | 23 | Click uses parts of optparse written by Gregory P. Ward and maintained by the 24 | Python software foundation. This is limited to code in the parser.py module: 25 | 26 | Copyright (c) 2001-2006 Gregory P. Ward. All rights reserved. 27 | Copyright (c) 2002-2006 Python Software Foundation. All rights reserved. 28 | 29 | Some rights reserved. 30 | 31 | Redistribution and use in source and binary forms, with or without 32 | modification, are permitted provided that the following conditions are met: 33 | 34 | - Redistributions of source code must retain the above copyright notice, 35 | this list of conditions and the following disclaimer. 36 | 37 | - Redistributions in binary form must reproduce the above copyright notice, 38 | this list of conditions and the following disclaimer in the 39 | documentation and/or other materials provided with the distribution. 40 | 41 | - The names of the contributors may not be used to endorse or promote 42 | products derived from this software without specific prior written 43 | permission. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 46 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 47 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 48 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 49 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 50 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 51 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 52 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 53 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 54 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 55 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-MOVIEPY: -------------------------------------------------------------------------------- 1 | MoviePy license 2 | Copyright (c) 2015 Zulko 3 | 4 | URL: https://github.com/Zulko/moviepy/blob/master/LICENCE.txt 5 | 6 | ----------------------------------------------------------------------- 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2015 Zulko 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in 20 | all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. 29 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-NUMPY: -------------------------------------------------------------------------------- 1 | Numpy license 2 | Copyright (C) 2005-2016, NumPy Developers. 3 | 4 | URL: http://www.numpy.org/license.html 5 | 6 | ----------------------------------------------------------------------- 7 | 8 | 9 | Copyright (C) 2005-2016, NumPy Developers. 10 | All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | * Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | * Redistributions in binary form must reproduce the above copyright notice, 19 | this list of conditions and the following disclaimer in the documentation 20 | and/or other materials provided with the distribution. 21 | 22 | * Neither the name of the NumPy Developers nor the names of any 23 | contributors may be used to endorse or promote products derived from this 24 | software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 27 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 30 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 31 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 32 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 33 | AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | 37 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-OPENCV: -------------------------------------------------------------------------------- 1 | OpenCV license 2 | Copyright (C) 2017, Itseez. 3 | 4 | URL: http://opencv.org/license.html 5 | 6 | ----------------------------------------------------------------------- 7 | 8 | License Agreement 9 | For Open Source Computer Vision Library 10 | (3-clause BSD License) 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | * Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | * Redistributions in binary form must reproduce the above copyright notice, 19 | this list of conditions and the following disclaimer in the documentation 20 | and/or other materials provided with the distribution. 21 | 22 | * Neither the names of the copyright holders nor the names of the 23 | contributors may be used to endorse or promote products derived from this 24 | software without specific prior written permission. 25 | 26 | This software is provided by the copyright holders and contributors "as is" and 27 | any express or implied warranties, including, but not limited to, the implied 28 | warranties of merchantability and fitness for a particular purpose are 29 | disclaimed. In no event shall copyright holders or contributors be liable for 30 | any direct, indirect, incidental, special, exemplary, or consequential damages 31 | (including, but not limited to, procurement of substitute goods or services; 32 | loss of use, data, or profits; or business interruption) however caused 33 | and on any theory of liability, whether in contract, strict liability, 34 | or tort (including negligence or otherwise) arising in any way out of 35 | the use of this software, even if advised of the possibility of such damage. 36 | 37 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-PYAV: -------------------------------------------------------------------------------- 1 | Copyright retained by original committers. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * Neither the name of the project nor the names of its contributors may be 11 | used to endorse or promote products derived from this software without 12 | specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-PYTEST: -------------------------------------------------------------------------------- 1 | pytest license 2 | Copyright (C) 2004-2017, Holger Krekel and others. 3 | 4 | URL: https://docs.pytest.org/en/latest/license.html 5 | 6 | ----------------------------------------------------------------------- 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2004-2017 Holger Krekel and others 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 16 | of the Software, and to permit persons to whom the Software is furnished to do 17 | so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-SIMPLETABLE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matheus Vieira Portela 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/LICENSE-TQDM: -------------------------------------------------------------------------------- 1 | tqdm license 2 | Copyright (C) Copyright (C) 2013-2018, Casper da Costa-Luis, 3 | Google Inc., and Noam Yorav-Raphael. 4 | 5 | URL: https://raw.githubusercontent.com/tqdm/tqdm/master/LICENCE 6 | 7 | ----------------------------------------------------------------------- 8 | 9 | `tqdm` is a product of collaborative work. 10 | Unless otherwise stated, all authors (see commit logs) retain copyright 11 | for their respective work, and release the work under the MIT licence 12 | (text below). 13 | 14 | Exceptions or notable authors are listed below 15 | in reverse chronological order: 16 | 17 | * files: * 18 | MPLv2.0 2015-2018 (c) Casper da Costa-Luis 19 | [casperdcl](https://github.com/casperdcl). 20 | * files: tqdm/_tqdm.py 21 | MIT 2016 (c) [PR #96] on behalf of Google Inc. 22 | * files: tqdm/_tqdm.py setup.py README.rst MANIFEST.in .gitignore 23 | MIT 2013 (c) Noam Yorav-Raphael, original author. 24 | 25 | [PR #96]: https://github.com/tqdm/tqdm/pull/96 26 | 27 | 28 | Mozilla Public Licence (MPL) v. 2.0 - Exhibit A 29 | ----------------------------------------------- 30 | 31 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. 32 | If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 33 | 34 | 35 | MIT License (MIT) 36 | ----------------- 37 | 38 | Copyright (c) 2013 noamraph 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining a copy of 41 | this software and associated documentation files (the "Software"), to deal in 42 | the Software without restriction, including without limitation the rights to 43 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 44 | the Software, and to permit persons to whom the Software is furnished to do so, 45 | subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all 48 | copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 51 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 52 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 53 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 54 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 55 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 56 | -------------------------------------------------------------------------------- /scenedetect/_thirdparty/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """Includes third-party libraries distributed with PySceneDetect. To simplify distribution of binary 13 | builds, the source directory also includes license files for the packages PySceneDetect depends on. 14 | """ 15 | -------------------------------------------------------------------------------- /scenedetect/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """``scenedetect.backends`` Module 13 | 14 | This module contains :class:`VideoStream ` implementations 15 | backed by various Python multimedia libraries. In addition to creating backend objects directly, 16 | :func:`scenedetect.open_video` can be used to open a video with a specified backend, falling 17 | back to OpenCV if not available. 18 | 19 | All backends available on the current system can be found via :data:`AVAILABLE_BACKENDS`. 20 | 21 | If you already have a `cv2.VideoCapture` object you want to use for scene detection, you can 22 | use a :class:`VideoCaptureAdapter ` instead 23 | of a backend. This is useful when working with devices or streams, for example. 24 | 25 | =============================================================== 26 | Video Files 27 | =============================================================== 28 | 29 | Assuming we have a file `video.mp4` in our working directory, we can load it and perform scene 30 | detection on it using :func:`open_video`: 31 | 32 | .. code:: python 33 | 34 | from scenedetect import open_video 35 | video = open_video("video.mp4") 36 | 37 | An optional backend from :data:`AVAILABLE_BACKENDS` can be passed to :func:`open_video` 38 | (e.g. `backend="opencv"`). Additional keyword arguments passed to :func:`open_video` 39 | will be forwarded to the backend constructor. If the specified backend is unavailable, or 40 | loading the video fails, ``opencv`` will be tried as a fallback. 41 | 42 | Lastly, to use a specific backend directly: 43 | 44 | .. code:: python 45 | 46 | # Manually importing and constructing a backend: 47 | from scenedetect.backends.opencv import VideoStreamCv2 48 | video = VideoStreamCv2("video.mp4") 49 | 50 | In both examples above, the resulting ``video`` can be used with 51 | :meth:`SceneManager.detect_scenes() `. 52 | 53 | =============================================================== 54 | Devices / Cameras / Pipes 55 | =============================================================== 56 | 57 | You can use an existing `cv2.VideoCapture` object with the PySceneDetect API using a 58 | :class:`VideoCaptureAdapter `. For example, 59 | to use a :class:`SceneManager ` with a webcam device: 60 | 61 | .. code:: python 62 | 63 | from scenedetect import SceneManager, ContentDetector 64 | from scenedetect.backends import VideoCaptureAdapter 65 | # Open device ID 2. 66 | cap = cv2.VideoCapture(2) 67 | video = VideoCaptureAdapter(cap) 68 | total_frames = 1000 69 | scene_manager = SceneManager() 70 | scene_manager.add_detector(ContentDetector()) 71 | scene_manager.detect_scenes(video=video, duration=total_frames) 72 | 73 | When working with live inputs, note that you can pass a callback to 74 | :meth:`detect_scenes() ` to be 75 | called on every scene detection event. See the :mod:`SceneManager ` 76 | examples for details. 77 | """ 78 | 79 | # TODO(v1.0): Consider removing and making this a namespace package so that additional backends can 80 | # be dynamically added. The preferred approach for this should probably be: 81 | # https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-namespace-packages 82 | 83 | # TODO: Future VideoStream implementations under consideration: 84 | # - Nvidia VPF: https://developer.nvidia.com/blog/vpf-hardware-accelerated-video-processing-framework-in-python/ 85 | 86 | import typing as ty 87 | 88 | # OpenCV must be available at minimum. 89 | from scenedetect.backends.opencv import VideoCaptureAdapter, VideoStreamCv2 90 | 91 | try: 92 | from scenedetect.backends.pyav import VideoStreamAv 93 | except ImportError: 94 | VideoStreamAv = None 95 | 96 | try: 97 | from scenedetect.backends.moviepy import VideoStreamMoviePy 98 | except ImportError: 99 | VideoStreamMoviePy = None 100 | 101 | # TODO: Lazy-loading backends would improve startup performance. However, this requires removing 102 | # some of the re-exported types above from the public API. 103 | AVAILABLE_BACKENDS: ty.Dict[str, ty.Type] = { 104 | backend.BACKEND_NAME: backend 105 | for backend in filter( 106 | None, 107 | [ 108 | VideoStreamCv2, 109 | VideoStreamAv, 110 | VideoStreamMoviePy, 111 | ], 112 | ) 113 | } 114 | """All available backends that :func:`scenedetect.open_video` can consider for the `backend` 115 | parameter. These backends must support construction with the following signature: 116 | 117 | BackendType(path: str, framerate: ty.Optional[float]) 118 | """ 119 | -------------------------------------------------------------------------------- /scenedetect/detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """``scenedetect.detector`` Module 13 | 14 | This module contains the :class:`SceneDetector` interface, from which all scene detectors in 15 | :mod:`scenedetect.detectors` module are derived from. 16 | 17 | The SceneDetector class represents the interface which detection algorithms are expected to provide 18 | in order to be compatible with PySceneDetect. 19 | 20 | .. warning:: 21 | 22 | This API is still unstable, and changes and design improvements are planned for the v1.0 23 | release. Instead of just timecodes, detection algorithms will also provide a specific type of 24 | event (in, out, cut, etc...). 25 | """ 26 | 27 | import typing as ty 28 | from enum import Enum 29 | 30 | import numpy 31 | 32 | from scenedetect.common import _USE_PTS_IN_DEVELOPMENT, FrameTimecode 33 | from scenedetect.stats_manager import StatsManager 34 | 35 | 36 | class SceneDetector: 37 | """Base class to inherit from when implementing a scene detection algorithm. 38 | 39 | This API is not yet stable and subject to change. 40 | """ 41 | 42 | # TODO(v0.7): Make this a proper abstract base class. 43 | 44 | # TODO(v0.7): This should be a property. 45 | stats_manager: ty.Optional[StatsManager] = None 46 | """Optional :class:`StatsManager ` to 47 | use for caching frame metrics to and from.""" 48 | 49 | def stats_manager_required(self) -> bool: 50 | """Stats Manager Required: Prototype indicating if detector requires stats. 51 | 52 | Returns: 53 | True if a StatsManager is required for the detector, False otherwise. 54 | """ 55 | return False 56 | 57 | def get_metrics(self) -> ty.List[str]: 58 | """Returns a list of all metric names/keys used by this detector. 59 | 60 | Returns: 61 | List of strings of frame metric key names that will be used by 62 | the detector when a StatsManager is passed to process_frame. 63 | """ 64 | return [] 65 | 66 | def process_frame( 67 | self, timecode: FrameTimecode, frame_img: numpy.ndarray 68 | ) -> ty.List[FrameTimecode]: 69 | """Process the next frame. `frame_num` is assumed to be sequential. 70 | 71 | Args: 72 | timecode: Timecode corresponding to the frame being processed. 73 | frame_img: Video frame as a 24-bit BGR image. 74 | 75 | Returns: 76 | List of timecodes where scene cuts have been detected, if any. 77 | """ 78 | return [] 79 | 80 | def post_process(self, timecode: int) -> ty.List[FrameTimecode]: 81 | """Called after there are no more frames to process. 82 | 83 | Args: 84 | timecode: The last position in the video which was read. 85 | 86 | Returns: 87 | List of timecodes where scene cuts have been detected, if any. 88 | """ 89 | return [] 90 | 91 | @property 92 | def event_buffer_length(self) -> int: 93 | """The amount of frames a given event can be buffered for, in time. Represents maximum 94 | amount any event can be behind `frame_number` in the result of :meth:`process_frame`. 95 | """ 96 | return 0 97 | 98 | 99 | class FlashFilter: 100 | """Filters fast-cuts to enforce minimum scene length.""" 101 | 102 | class Mode(Enum): 103 | """Which mode the filter should use for enforcing minimum scene length.""" 104 | 105 | MERGE = 0 106 | """Merge consecutive cuts shorter than filter length.""" 107 | SUPPRESS = 1 108 | """Suppress consecutive cuts until the filter length has passed.""" 109 | 110 | def __init__(self, mode: Mode, length: int): 111 | """ 112 | Arguments: 113 | mode: The mode to use when enforcing `length`. 114 | length: Number of frames to use when filtering cuts. 115 | """ 116 | self._mode = mode 117 | self._filter_length = length # Number of frames to use for activating the filter. 118 | self._last_above = None # Last frame above threshold. 119 | self._merge_enabled = False # Used to disable merging until at least one cut was found. 120 | self._merge_triggered = False # True when the merge filter is active. 121 | self._merge_start = None # Frame number where we started the merge filter. 122 | 123 | @property 124 | def max_behind(self) -> int: 125 | return 0 if self._mode == FlashFilter.Mode.SUPPRESS else self._filter_length 126 | 127 | def filter(self, timecode: FrameTimecode, above_threshold: bool) -> ty.List[FrameTimecode]: 128 | if not self._filter_length > 0: 129 | return [timecode] if above_threshold else [] 130 | if _USE_PTS_IN_DEVELOPMENT: 131 | raise NotImplementedError("TODO: Change filter to use units of time instead of frames.") 132 | if self._last_above is None: 133 | self._last_above = timecode 134 | if self._mode == FlashFilter.Mode.MERGE: 135 | return self._filter_merge(frame_num=timecode, above_threshold=above_threshold) 136 | elif self._mode == FlashFilter.Mode.SUPPRESS: 137 | return self._filter_suppress(frame_num=timecode, above_threshold=above_threshold) 138 | raise RuntimeError("Unhandled FlashFilter mode.") 139 | 140 | def _filter_suppress(self, frame_num: int, above_threshold: bool) -> ty.List[int]: 141 | min_length_met: bool = (frame_num - self._last_above) >= self._filter_length 142 | if not (above_threshold and min_length_met): 143 | return [] 144 | # Both length and threshold requirements were satisfied. Emit the cut, and wait until both 145 | # requirements are met again. 146 | self._last_above = frame_num 147 | return [frame_num] 148 | 149 | def _filter_merge(self, frame_num: int, above_threshold: bool) -> ty.List[int]: 150 | min_length_met: bool = (frame_num - self._last_above) >= self._filter_length 151 | # Ensure last frame is always advanced to the most recent one that was above the threshold. 152 | if above_threshold: 153 | self._last_above = frame_num 154 | if self._merge_triggered: 155 | # This frame was under the threshold, see if enough frames passed to disable the filter. 156 | num_merged_frames = self._last_above - self._merge_start 157 | if min_length_met and not above_threshold and num_merged_frames >= self._filter_length: 158 | self._merge_triggered = False 159 | return [self._last_above] 160 | # Keep merging until enough frames pass below the threshold. 161 | return [] 162 | # Wait for next frame above the threshold. 163 | if not above_threshold: 164 | return [] 165 | # If we met the minimum length requirement, no merging is necessary. 166 | if min_length_met: 167 | # Only allow the merge filter once the first cut is emitted. 168 | self._merge_enabled = True 169 | return [frame_num] 170 | # Start merging cuts until the length requirement is met. 171 | if self._merge_enabled: 172 | self._merge_triggered = True 173 | self._merge_start = frame_num 174 | return [] 175 | -------------------------------------------------------------------------------- /scenedetect/detectors/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """``scenedetect.detectors`` Module 13 | 14 | This module contains the following scene detection algorithms: 15 | 16 | * :mod:`ContentDetector `: 17 | Detects shot changes using weighted average of pixel changes in the HSV colorspace. 18 | 19 | * :mod:`ThresholdDetector `: 20 | Detects slow transitions using average pixel intensity in RGB (fade in/fade out) 21 | 22 | * :mod:`AdaptiveDetector `: 23 | Performs rolling average on differences in HSV colorspace. In some cases, this can improve 24 | handling of fast motion. 25 | 26 | * :mod:`HistogramDetector `: 27 | Uses histogram differences for Y channel in YUV space to find fast cuts. 28 | 29 | * :mod:`HashDetector `: 30 | Uses perceptual hashing to calculate similarity between adjacent frames. 31 | 32 | Detection algorithms are created by implementing the 33 | :class:`SceneDetector ` interface. Detectors are 34 | typically attached to a :class:`SceneManager ` when 35 | processing videos, however they can also be used to process frames directly. 36 | """ 37 | 38 | from scenedetect.detectors.content_detector import ContentDetector # noqa: I001 39 | from scenedetect.detectors.threshold_detector import ThresholdDetector 40 | from scenedetect.detectors.adaptive_detector import AdaptiveDetector 41 | from scenedetect.detectors.hash_detector import HashDetector 42 | from scenedetect.detectors.histogram_detector import HistogramDetector 43 | 44 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 45 | # # 46 | # Detection Methods & Algorithms Planned or In Development # 47 | # # 48 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 49 | # 50 | # class DissolveDetector(SceneDetector): 51 | # """Detects slow fades (dissolve cuts) via changes in the HSV colour space. 52 | # 53 | # Detects slow fades only; to detect fast cuts between content scenes, the 54 | # ContentDetector should be used instead. 55 | # """ 56 | # 57 | # def __init__(self): 58 | # super(DissolveDetector, self).__init__() 59 | # 60 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 61 | # 62 | # class MotionDetector(SceneDetector): 63 | # """Detects motion events in scenes containing a static background. 64 | # 65 | # Uses background subtraction followed by noise removal (via morphological 66 | # opening) to generate a frame score compared against the set threshold. 67 | # """ 68 | # 69 | # def __init__(self): 70 | # super(MotionDetector, self).__init__() 71 | # 72 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 73 | -------------------------------------------------------------------------------- /scenedetect/detectors/adaptive_detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """:class:`AdaptiveDetector` compares the difference in content between adjacent frames similar 13 | to `ContentDetector` except the threshold isn't fixed, but is a rolling average of adjacent frame 14 | changes. This can help mitigate false detections in situations such as fast camera motions. 15 | 16 | This detector is available from the command-line as the `detect-adaptive` command. 17 | """ 18 | 19 | import typing as ty 20 | from logging import getLogger 21 | 22 | import numpy as np 23 | 24 | from scenedetect.common import FrameTimecode 25 | from scenedetect.detectors import ContentDetector 26 | 27 | logger = getLogger("pyscenedetect") 28 | 29 | 30 | class AdaptiveDetector(ContentDetector): 31 | """Two-pass detector that calculates frame scores with ContentDetector, and then applies 32 | a rolling average when processing the result that can help mitigate false detections 33 | in situations such as camera movement. 34 | """ 35 | 36 | ADAPTIVE_RATIO_KEY_TEMPLATE = "adaptive_ratio{luma_only} (w={window_width})" 37 | 38 | def __init__( 39 | self, 40 | adaptive_threshold: float = 3.0, 41 | min_scene_len: int = 15, 42 | window_width: int = 2, 43 | min_content_val: float = 15.0, 44 | weights: ContentDetector.Components = ContentDetector.DEFAULT_COMPONENT_WEIGHTS, 45 | luma_only: bool = False, 46 | kernel_size: ty.Optional[int] = None, 47 | min_delta_hsv: ty.Optional[float] = None, 48 | ): 49 | """ 50 | Arguments: 51 | adaptive_threshold: Threshold (float) that score ratio must exceed to trigger a 52 | new scene (see frame metric adaptive_ratio in stats file). 53 | min_scene_len: Once a cut is detected, this many frames must pass before a new one can 54 | be added to the scene list. Can be an int or FrameTimecode type. 55 | window_width: Size of window (number of frames) before and after each frame to 56 | average together in order to detect deviations from the mean. Must be at least 1. 57 | min_content_val: Minimum threshold (float) that the content_val must exceed in order to 58 | register as a new scene. This is calculated the same way that `detect-content` 59 | calculates frame score based on `weights`/`luma_only`/`kernel_size`. 60 | weights: Weight to place on each component when calculating frame score 61 | (`content_val` in a statsfile, the value `threshold` is compared against). 62 | If omitted, the default ContentDetector weights are used. 63 | luma_only: If True, only considers changes in the luminance channel of the video. 64 | Equivalent to specifying `weights` as :data:`ContentDetector.LUMA_ONLY`. 65 | Overrides `weights` if both are set. 66 | kernel_size: Size of kernel to use for post edge detection filtering. If None, 67 | automatically set based on video resolution. 68 | min_delta_hsv: [DEPRECATED] DO NOT USE. Use `min_content_val` instead. 69 | """ 70 | if min_delta_hsv is not None: 71 | logger.error("min_delta_hsv is deprecated, use min_content_val instead.") 72 | min_content_val = min_delta_hsv 73 | if window_width < 1: 74 | raise ValueError("window_width must be at least 1.") 75 | 76 | super().__init__( 77 | threshold=255.0, 78 | min_scene_len=0, 79 | weights=weights, 80 | luma_only=luma_only, 81 | kernel_size=kernel_size, 82 | ) 83 | 84 | # TODO: Turn these public options into properties. 85 | self.min_scene_len = min_scene_len 86 | self.adaptive_threshold = adaptive_threshold 87 | self.min_content_val = min_content_val 88 | self.window_width = window_width 89 | 90 | self._adaptive_ratio_key = AdaptiveDetector.ADAPTIVE_RATIO_KEY_TEMPLATE.format( 91 | window_width=window_width, luma_only="" if not luma_only else "_lum" 92 | ) 93 | self._buffer: ty.List[ty.Tuple[FrameTimecode, float]] = [] 94 | # NOTE: The name of last cut is different from `self._last_scene_cut` from our base class, 95 | # and serves a different purpose! 96 | self._last_cut: ty.Optional[FrameTimecode] = None 97 | 98 | @property 99 | def event_buffer_length(self) -> int: 100 | return self.window_width 101 | 102 | def get_metrics(self) -> ty.List[str]: 103 | return super().get_metrics() + [self._adaptive_ratio_key] 104 | 105 | def process_frame( 106 | self, timecode: FrameTimecode, frame_img: np.ndarray 107 | ) -> ty.List[FrameTimecode]: 108 | # TODO(#283): Merge this with ContentDetector and turn it on by default. 109 | 110 | super().process_frame(timecode=timecode, frame_img=frame_img) 111 | 112 | # Initialize last scene cut point at the beginning of the frames of interest. 113 | if self._last_cut is None: 114 | self._last_cut = timecode 115 | 116 | required_frames = 1 + (2 * self.window_width) 117 | self._buffer.append((timecode, self._frame_score)) 118 | if not len(self._buffer) >= required_frames: 119 | return [] 120 | self._buffer = self._buffer[-required_frames:] 121 | (target_timecode, target_score) = self._buffer[self.window_width] 122 | average_window_score = sum( 123 | score for i, (_frame, score) in enumerate(self._buffer) if i != self.window_width 124 | ) / (2.0 * self.window_width) 125 | 126 | average_is_zero = abs(average_window_score) < 0.00001 127 | 128 | adaptive_ratio = 0.0 129 | if not average_is_zero: 130 | adaptive_ratio = min(target_score / average_window_score, 255.0) 131 | elif average_is_zero and target_score >= self.min_content_val: 132 | # if we would have divided by zero, set adaptive_ratio to the max (255.0) 133 | adaptive_ratio = 255.0 134 | if self.stats_manager is not None: 135 | self.stats_manager.set_metrics( 136 | target_timecode, {self._adaptive_ratio_key: adaptive_ratio} 137 | ) 138 | 139 | # Check to see if adaptive_ratio exceeds the adaptive_threshold as well as there 140 | # being a large enough content_val to trigger a cut 141 | threshold_met: bool = ( 142 | adaptive_ratio >= self.adaptive_threshold and target_score >= self.min_content_val 143 | ) 144 | min_length_met: bool = (timecode - self._last_cut) >= self.min_scene_len 145 | if threshold_met and min_length_met: 146 | self._last_cut = target_timecode 147 | return [target_timecode] 148 | return [] 149 | -------------------------------------------------------------------------------- /scenedetect/detectors/hash_detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2022 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """:py:class:`HashDetector` calculates a hash for each frame of a video using a perceptual 13 | hashing algorithm. The differences (distance) in hash value between frames is calculated. 14 | If this difference exceeds a set threshold, a scene cut is triggered. 15 | 16 | This detector is available from the command-line interface by using the `detect-hash` command. 17 | """ 18 | 19 | import typing as ty 20 | 21 | import cv2 22 | import numpy 23 | 24 | from scenedetect.common import FrameTimecode 25 | from scenedetect.detector import SceneDetector 26 | 27 | 28 | class HashDetector(SceneDetector): 29 | """Detects cuts using a perceptual hashing algorithm. Applies a direct cosine transform (DCT) 30 | and lowpass filter, followed by binary thresholding on the median. See references below: 31 | 32 | 1. https://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html 33 | 2. https://github.com/JohannesBuchner/imagehash 34 | 35 | Arguments: 36 | threshold: Value from 0.0 and 1.0 representing the relative hamming distance between 37 | the perceptual hashes of adjacent frames. A distance of 0 means the image is the same, 38 | and 1 means no correlation. Smaller threshold values thus require more correlation, 39 | making the detector more sensitive. The hamming distance is divided by `size` x `size` 40 | before comparing to `threshold` for normalization. 41 | size: Size of square of low frequency data to use for the DCT 42 | lowpass: How much high frequency information to filter from the DCT. A value of 2 means 43 | keep lower 1/2 of the frequency data, 4 means only keep 1/4, etc... 44 | min_scene_len: Once a cut is detected, this many frames must pass before a new one can 45 | be added to the scene list. Can be an int or FrameTimecode type. 46 | """ 47 | 48 | def __init__( 49 | self, 50 | threshold: float = 0.395, 51 | size: int = 16, 52 | lowpass: int = 2, 53 | min_scene_len: int = 15, 54 | ): 55 | super(HashDetector, self).__init__() 56 | self._threshold = threshold 57 | self._min_scene_len = min_scene_len 58 | self._size = size 59 | self._size_sq = float(size * size) 60 | self._factor = lowpass 61 | self._last_frame: numpy.ndarray = None 62 | self._last_scene_cut: FrameTimecode = None 63 | self._last_hash = numpy.array([]) 64 | self._metric_key = f"hash_dist [size={self._size} lowpass={self._factor}]" 65 | 66 | def get_metrics(self): 67 | return [self._metric_key] 68 | 69 | def process_frame( 70 | self, timecode: FrameTimecode, frame_img: numpy.ndarray 71 | ) -> ty.List[FrameTimecode]: 72 | """Similar to ContentDetector, but using a perceptual hashing algorithm 73 | to calculate a hash for each frame and then calculate a hash difference 74 | frame to frame.""" 75 | 76 | cut_list = [] 77 | 78 | # Initialize last scene cut point at the beginning of the frames of interest. 79 | if self._last_scene_cut is None: 80 | self._last_scene_cut = timecode 81 | 82 | # We can only start detecting once we have a frame to compare with. 83 | if self._last_frame is not None: 84 | # We obtain the change in hash value between subsequent frames. 85 | curr_hash = self.hash_frame( 86 | frame_img=frame_img, hash_size=self._size, factor=self._factor 87 | ) 88 | 89 | last_hash = self._last_hash 90 | 91 | if last_hash.size == 0: 92 | # Calculate hash of last frame 93 | last_hash = self.hash_frame( 94 | frame_img=self._last_frame, hash_size=self._size, factor=self._factor 95 | ) 96 | 97 | # Hamming distance is calculated to compare to last frame 98 | hash_dist = numpy.count_nonzero(curr_hash.flatten() != last_hash.flatten()) 99 | 100 | # Normalize based on size of the hash 101 | hash_dist_norm = hash_dist / self._size_sq 102 | 103 | if self.stats_manager is not None: 104 | self.stats_manager.set_metrics(timecode, {self._metric_key: hash_dist_norm}) 105 | 106 | self._last_hash = curr_hash 107 | 108 | # We consider any frame over the threshold a new scene, but only if 109 | # the minimum scene length has been reached (otherwise it is ignored). 110 | if hash_dist_norm >= self._threshold and ( 111 | (timecode - self._last_scene_cut) >= self._min_scene_len 112 | ): 113 | cut_list.append(timecode) 114 | self._last_scene_cut = timecode 115 | 116 | self._last_frame = frame_img.copy() 117 | 118 | return cut_list 119 | 120 | @staticmethod 121 | def hash_frame(frame_img, hash_size, factor) -> numpy.ndarray: 122 | """Calculates the perceptual hash of a frame and returns it. Based on phash from 123 | https://github.com/JohannesBuchner/imagehash. 124 | """ 125 | 126 | # Transform to grayscale 127 | gray_img = cv2.cvtColor(frame_img, cv2.COLOR_BGR2GRAY) 128 | 129 | # Resize image to square to help with DCT 130 | imsize = hash_size * factor 131 | resized_img = cv2.resize(gray_img, (imsize, imsize), interpolation=cv2.INTER_AREA) 132 | 133 | # Check to avoid dividing by zero 134 | max_value = numpy.max(numpy.max(resized_img)) 135 | if max_value == 0: 136 | # Just set the max to 1 to not change the values 137 | max_value = 1 138 | 139 | # Calculate discrete cosine tranformation of the image 140 | resized_img = numpy.float32(resized_img) / max_value 141 | dct_complete = cv2.dct(resized_img) 142 | 143 | # Only keep the low frequency information 144 | dct_low_freq = dct_complete[:hash_size, :hash_size] 145 | 146 | # Calculate the median of the low frequency informations 147 | med = numpy.median(dct_low_freq) 148 | 149 | # Transform the low frequency information into a binary image based on > or < median 150 | hash_img = dct_low_freq > med 151 | 152 | return hash_img 153 | -------------------------------------------------------------------------------- /scenedetect/detectors/histogram_detector.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # --------------------------------------------------------------- 4 | # [ Site: http://www.scenedetect.scenedetect.com/ ] 5 | # [ Docs: http://manual.scenedetect.scenedetect.com/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2022 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """:py:class:`HistogramDetector` compares the difference in the YUV histograms of subsequent 13 | frames. If the difference exceeds a given threshold, a cut is detected. 14 | 15 | This detector is available from the command-line as the `detect-hist` command. 16 | """ 17 | 18 | import typing as ty 19 | 20 | import cv2 21 | import numpy 22 | 23 | from scenedetect.common import FrameTimecode 24 | from scenedetect.detector import SceneDetector 25 | 26 | 27 | class HistogramDetector(SceneDetector): 28 | """Compares the difference in the Y channel of YUV histograms for adjacent frames. When the 29 | difference exceeds a given threshold, a cut is detected.""" 30 | 31 | METRIC_KEYS = ["hist_diff"] 32 | 33 | def __init__(self, threshold: float = 0.05, bins: int = 256, min_scene_len: int = 15): 34 | """ 35 | Arguments: 36 | threshold: maximum relative difference between 0.0 and 1.0 that the histograms can 37 | differ. Histograms are calculated on the Y channel after converting the frame to 38 | YUV, and normalized based on the number of bins. Higher dicfferences imply greater 39 | change in content, so larger threshold values are less sensitive to cuts. 40 | bins: Number of bins to use for the histogram. 41 | min_scene_len: Once a cut is detected, this many frames must pass before a new one can 42 | be added to the scene list. Can be an int or FrameTimecode type. 43 | """ 44 | super().__init__() 45 | # Internally, threshold represents the correlation between two histograms and has values 46 | # between -1.0 and 1.0. 47 | self._threshold = max(0.0, min(1.0, 1.0 - threshold)) 48 | self._bins = bins 49 | self._min_scene_len = min_scene_len 50 | self._last_hist = None 51 | self._last_cut = None 52 | self._metric_key = f"hist_diff [bins={self._bins}]" 53 | 54 | def process_frame(self, timecode: FrameTimecode, frame_img: numpy.ndarray) -> ty.List[int]: 55 | """Computes the histogram of the luma channel of the frame image and compares it with the 56 | histogram of the luma channel of the previous frame. If the difference between the histograms 57 | exceeds the threshold, a scene cut is detected. 58 | Histogram difference is computed using the correlation metric. 59 | 60 | Arguments: 61 | frame_num: Frame number of frame that is being passed. 62 | frame_img: Decoded frame image (numpy.ndarray) to perform scene 63 | detection on. 64 | 65 | Returns: 66 | List of frames where scene cuts have been detected. There may be 0 67 | or more frames in the list, and not necessarily the same as frame_num. 68 | """ 69 | cut_list = [] 70 | 71 | np_data_type = frame_img.dtype 72 | 73 | if np_data_type != numpy.uint8: 74 | raise ValueError("Image must be 8-bit rgb for HistogramDetector") 75 | 76 | if frame_img.shape[2] != 3: 77 | raise ValueError("Image must have three color channels for HistogramDetector") 78 | 79 | # Initialize last scene cut point at the beginning of the frames of interest. 80 | if not self._last_cut: 81 | self._last_cut = timecode 82 | 83 | hist = self.calculate_histogram(frame_img, bins=self._bins) 84 | 85 | # We can only start detecting once we have a frame to compare with. 86 | if self._last_hist is not None: 87 | # TODO: We can have EMA of histograms to make it more robust 88 | # ema_hist = alpha * hist + (1 - alpha) * ema_hist 89 | 90 | # Compute histogram difference between frames 91 | hist_diff = cv2.compareHist(self._last_hist, hist, cv2.HISTCMP_CORREL) 92 | 93 | # Check if a new scene should be triggered 94 | # Set a correlation threshold to determine scene changes. 95 | # The threshold value should be between -1 (perfect negative correlation, not applicable here) 96 | # and +1 (perfect positive correlation, identical histograms). 97 | # Values close to 1 indicate very similar frames, while lower values suggest changes. 98 | # Example: If `_threshold` is set to 0.8, it implies that only changes resulting in a correlation 99 | # less than 0.8 between histograms will be considered significant enough to denote a scene change. 100 | if hist_diff <= self._threshold and ( 101 | (timecode - self._last_cut) >= self._min_scene_len 102 | ): 103 | cut_list.append(timecode) 104 | self._last_cut = timecode 105 | 106 | # Save stats to a StatsManager if it is being used 107 | if self.stats_manager is not None: 108 | self.stats_manager.set_metrics(timecode, {self._metric_key: hist_diff}) 109 | 110 | self._last_hist = hist 111 | 112 | return cut_list 113 | 114 | @staticmethod 115 | def calculate_histogram( 116 | frame_img: numpy.ndarray, bins: int = 256, normalize: bool = True 117 | ) -> numpy.ndarray: 118 | """ 119 | Calculates and optionally normalizes the histogram of the luma (Y) channel of an image 120 | converted from BGR to YUV color space. 121 | 122 | This function extracts the Y channel from the given BGR image, computes its histogram with 123 | the specified number of bins, and optionally normalizes this histogram to have a sum of one 124 | across all bins. 125 | 126 | Args: 127 | ----- 128 | frame_img : np.ndarray 129 | The input image in BGR color space, assumed to have shape (height, width, 3) 130 | where the last dimension represents the BGR channels. 131 | bins : int, optional (default=256) 132 | The number of bins to use for the histogram. 133 | normalize : bool, optional (default=True) 134 | A boolean flag that determines whether the histogram should be normalized 135 | such that the sum of all histogram bins equals 1. 136 | 137 | Returns: 138 | -------- 139 | np.ndarray 140 | A 1D numpy array of length equal to `bins`, representing the histogram of the luma 141 | channel. Each element in the array represents the count (or frequency) of a particular 142 | luma value in the image. If normalized, these values represent the relative frequency. 143 | 144 | Examples: 145 | --------- 146 | >>> img = cv2.imread("path_to_image.jpg") 147 | >>> hist = calculate_histogram(img, bins=256, normalize=True) 148 | >>> print(hist.shape) 149 | (256,) 150 | """ 151 | # Extract Luma channel from the frame image 152 | y, _, _ = cv2.split(cv2.cvtColor(frame_img, cv2.COLOR_BGR2YUV)) 153 | 154 | # Create the histogram with a bin for every rgb value 155 | hist = cv2.calcHist([y], [0], None, [bins], [0, 256]) 156 | 157 | if normalize: 158 | # Normalize the histogram 159 | hist = cv2.normalize(hist, hist).flatten() 160 | 161 | return hist 162 | 163 | def get_metrics(self) -> ty.List[str]: 164 | return [self._metric_key] 165 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [metadata] 3 | name = scenedetect 4 | version = attr: scenedetect.__version__ 5 | license = BSD 3-Clause License 6 | author = Brandon Castellano 7 | author_email = brandon248@gmail.com 8 | description = Video scene cut/shot detection program and Python library. 9 | long_description = file: dist/package-info.rst 10 | long_description_content_type = text/x-rst 11 | url = https://www.scenedetect.com 12 | project_urls = 13 | Homepage = https://www.scenedetect.com 14 | Repository = https://github.com/Breakthrough/PySceneDetect/ 15 | Documentation = https://www.scenedetect.com/docs/ 16 | Bug Tracker = https://github.com/Breakthrough/PySceneDetect/issues/ 17 | classifiers = 18 | Development Status :: 5 - Production/Stable 19 | Environment :: Console 20 | Environment :: Console :: Curses 21 | Intended Audience :: Developers 22 | Intended Audience :: End Users/Desktop 23 | Intended Audience :: System Administrators 24 | License :: OSI Approved :: MIT License 25 | Operating System :: OS Independent 26 | Programming Language :: Python :: 3 27 | Programming Language :: Python :: 3.7 28 | Programming Language :: Python :: 3.8 29 | Programming Language :: Python :: 3.9 30 | Programming Language :: Python :: 3.10 31 | Programming Language :: Python :: 3.11 32 | Programming Language :: Python :: 3.12 33 | Programming Language :: Python :: 3.13 34 | Topic :: Multimedia :: Video 35 | Topic :: Multimedia :: Video :: Conversion 36 | Topic :: Multimedia :: Video :: Non-Linear Editor 37 | Topic :: Utilities 38 | keywords = video computer-vision analysis 39 | 40 | [options] 41 | install_requires = 42 | Click 43 | numpy 44 | platformdirs 45 | tqdm 46 | packages = 47 | scenedetect 48 | scenedetect._cli 49 | scenedetect._thirdparty 50 | scenedetect.backends 51 | scenedetect.detectors 52 | scenedetect.output 53 | python_requires = >=3.7 54 | 55 | [options.extras_require] 56 | opencv = opencv-python 57 | opencv-headless = opencv-python-headless 58 | pyav = av 59 | moviepy = moviepy 60 | 61 | [options.entry_points] 62 | console_scripts = 63 | scenedetect = scenedetect.__main__:main 64 | 65 | [aliases] 66 | test = pytest 67 | 68 | [tool:pytest] 69 | addopts = --verbose 70 | python_files = tests/*.py 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # PySceneDetect: Python-Based Video Scene Detector 4 | # --------------------------------------------------------------- 5 | # [ Site: http://www.bcastell.com/projects/PySceneDetect/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # [ Documentation: http://www.scenedetect.com/docs/ ] 8 | # 9 | # Copyright (C) 2014-2024 Brandon Castellano . 10 | # 11 | """PySceneDetect setup.py - DEPRECATED. 12 | 13 | Build using `python -m build` and installing the resulting .whl using `pip`. 14 | """ 15 | 16 | import setuptools 17 | 18 | if __name__ == "__main__": 19 | setuptools.setup(name="scenedetect") 20 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """PySceneDetect Unit Test Suite 13 | 14 | To run all available tests run `pytest -v` from the parent directory 15 | (i.e. the root project folder of PySceneDetect containing the scenedetect/ 16 | and tests/ folders). This will automatically find and run all of the 17 | test cases in the tests/ folder and display the results. 18 | """ 19 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """PySceneDetect Test Configuration 13 | 14 | This file includes all pytest configuration for running PySceneDetect's tests. 15 | 16 | These tests rely on the files in the tests/resources/ folder in the "resources" branch of 17 | the PySceneDetect git repository. These files can be checked out via git by running the 18 | following from the root of the repo: 19 | 20 | git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources 21 | git checkout refs/remotes/origin/resources -- tests/resources/ 22 | git reset 23 | 24 | Note that currently these tests create some temporary files which are not yet cleaned up. 25 | """ 26 | 27 | # TODO: Properly cleanup temporary files. 28 | 29 | import logging 30 | import os 31 | import typing as ty 32 | 33 | import pytest 34 | 35 | # 36 | # Helper Functions 37 | # 38 | 39 | 40 | def check_exists(path: ty.AnyStr) -> ty.AnyStr: 41 | """Returns the absolute path to a (relative) path of a file that 42 | should exist within the tests/ directory. 43 | 44 | Throws FileNotFoundError if the file could not be found. 45 | """ 46 | if not os.path.exists(path): 47 | raise FileNotFoundError( 48 | """ 49 | Test video file (%s) must be present to run test case. This file can be obtained by running the following commands from the root of the repository: 50 | 51 | git fetch --depth=1 https://github.com/Breakthrough/PySceneDetect.git refs/heads/resources:refs/remotes/origin/resources 52 | git checkout refs/remotes/origin/resources -- tests/resources/ 53 | git reset 54 | """ 55 | % path 56 | ) 57 | return path 58 | 59 | 60 | # 61 | # Pytest Hooks 62 | # 63 | 64 | 65 | def pytest_assertrepr_compare(op, left, right): 66 | if isinstance(left, str) and isinstance(right, str) and op == "in": 67 | return [ 68 | "Did not find expected output in test.", 69 | "", 70 | "Expected to find:", 71 | "", 72 | *left.splitlines(), 73 | "", 74 | "Actual output:", 75 | "", 76 | *right.splitlines(), 77 | ] 78 | return [] 79 | 80 | 81 | # 82 | # Test Case Fixtures 83 | # 84 | 85 | 86 | @pytest.fixture(autouse=True) 87 | def no_logs_gte_error(caplog): 88 | """Ensure no log messages with error severity or higher were reported during test execution.""" 89 | EXCLUDED_MODULES = set() 90 | yield 91 | errors = [ 92 | record 93 | for record in caplog.get_records("call") 94 | if record.levelno >= logging.ERROR and record.module not in EXCLUDED_MODULES 95 | ] 96 | assert not errors, "Test failed due to presence of one or more logs with ERROR severity." 97 | 98 | 99 | @pytest.fixture 100 | def test_video_file() -> str: 101 | """Simple test video containing both fast cuts and fades/dissolves.""" 102 | return check_exists("tests/resources/testvideo.mp4") 103 | 104 | 105 | @pytest.fixture 106 | def test_movie_clip() -> str: 107 | """Movie clip containing fast cuts.""" 108 | return check_exists("tests/resources/goldeneye.mp4") 109 | 110 | 111 | @pytest.fixture 112 | def test_vfr_video() -> str: 113 | """Movie clip containing fast cut, but encoded as variable framerate.""" 114 | return check_exists("tests/resources/goldeneye-vfr.mp4") 115 | 116 | 117 | @pytest.fixture 118 | def corrupt_video_file() -> str: 119 | """Video containing a corrupted frame causing a decode failure.""" 120 | return check_exists("tests/resources/corrupt_frame.mp4") 121 | 122 | 123 | @pytest.fixture 124 | def rotated_video_file() -> str: 125 | """Video containing a corrupted frame causing a decode failure.""" 126 | return check_exists("tests/resources/issue-134-rotate.mp4") 127 | 128 | 129 | @pytest.fixture 130 | def test_image_sequence() -> str: 131 | """Path to a short image sequence (from counter.mp4).""" 132 | return "tests/resources/counter/frame%03d.png" 133 | 134 | 135 | @pytest.fixture 136 | def test_fades_clip() -> str: 137 | """Clip containing fades in/out.""" 138 | return check_exists("tests/resources/fades.mp4") 139 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """PySceneDetect API Tests 13 | 14 | These tests demonstrate common workflow patterns used when integrating the PySceneDetect API.""" 15 | 16 | 17 | def test_api_detect(test_video_file: str): 18 | """Demonstrate usage of the `detect()` function to process a complete video.""" 19 | from scenedetect import ContentDetector, detect 20 | 21 | scene_list = detect(test_video_file, ContentDetector()) 22 | for i, scene in enumerate(scene_list): 23 | print("Scene %d: %s - %s" % (i + 1, scene[0].get_timecode(), scene[1].get_timecode())) 24 | 25 | 26 | def test_api_detect_start_end_time(test_video_file: str): 27 | """Demonstrate usage of the `detect()` function to process a subset of a video.""" 28 | from scenedetect import ContentDetector, detect 29 | 30 | # Times can be seconds (float), frames (int), or timecode 'HH:MM:SSS.nnn' (str). 31 | # See test_api_timecode_types() for examples of each format. 32 | scene_list = detect(test_video_file, ContentDetector(), start_time=10.5, end_time=15.9) 33 | for i, scene in enumerate(scene_list): 34 | print("Scene %d: %s - %s" % (i + 1, scene[0].get_timecode(), scene[1].get_timecode())) 35 | 36 | 37 | def test_api_detect_stats(test_video_file: str): 38 | """Demonstrate usage of the `detect()` function to generate a statsfile.""" 39 | from scenedetect import ContentDetector, detect 40 | 41 | detect(test_video_file, ContentDetector(), stats_file_path="frame_metrics.csv") 42 | 43 | 44 | def test_api_scene_manager(test_video_file: str): 45 | """Demonstrate how to use a SceneManager to implement a function similar to `detect()`.""" 46 | from scenedetect import ContentDetector, SceneManager, open_video 47 | 48 | video = open_video(test_video_file) 49 | scene_manager = SceneManager() 50 | scene_manager.add_detector(ContentDetector()) 51 | scene_manager.detect_scenes(video=video) 52 | scene_list = scene_manager.get_scene_list() 53 | for i, scene in enumerate(scene_list): 54 | print("Scene %d: %s - %s" % (i + 1, scene[0].get_timecode(), scene[1].get_timecode())) 55 | 56 | 57 | def test_api_scene_manager_start_end_time(test_video_file: str): 58 | """Demonstrate how to use a SceneManager to process a subset of the input video.""" 59 | from scenedetect import ContentDetector, SceneManager, open_video 60 | 61 | video = open_video(test_video_file) 62 | scene_manager = SceneManager() 63 | scene_manager.add_detector(ContentDetector()) 64 | # Times can be seconds (float), frames (int), or timecode 'HH:MM:SSS.nnn' (str). 65 | # See test_api_timecode_types() for examples of each format. 66 | start_time = 200 # Start at frame (int) 200 67 | end_time = 15.0 # End at 15 seconds (float) 68 | video.seek(start_time) 69 | scene_manager.detect_scenes(video=video, end_time=end_time) 70 | scene_list = scene_manager.get_scene_list() 71 | for i, scene in enumerate(scene_list): 72 | print("Scene %d: %s - %s" % (i + 1, scene[0].get_timecode(), scene[1].get_timecode())) 73 | 74 | 75 | def test_api_timecode_types(): 76 | """Demonstrate all different types of timecodes that can be used.""" 77 | from scenedetect import FrameTimecode 78 | 79 | base_timecode = FrameTimecode(timecode=0, fps=10.0) 80 | # Frames (int) 81 | timecode = base_timecode + 1 82 | assert timecode.get_frames() == 1 83 | # Seconds (float) 84 | timecode = base_timecode + 1.0 85 | assert timecode.get_frames() == 10 86 | # Timecode (str, 'HH:MM:SS' or 'HH:MM:SSS.nnn') 87 | timecode = base_timecode + "00:00:01.500" 88 | assert timecode.get_frames() == 15 89 | # Seconds (str, 'SSSs' or 'SSSS.SSSs') 90 | timecode = base_timecode + "1.5s" 91 | assert timecode.get_frames() == 15 92 | 93 | 94 | def test_api_stats_manager(test_video_file: str): 95 | """Demonstrate using a StatsManager to save per-frame statistics to disk.""" 96 | from scenedetect import ContentDetector, SceneManager, StatsManager, open_video 97 | 98 | video = open_video(test_video_file) 99 | scene_manager = SceneManager(stats_manager=StatsManager()) 100 | scene_manager.add_detector(ContentDetector()) 101 | scene_manager.detect_scenes(video=video) 102 | # Save per-frame statistics to disk. 103 | filename = "%s.stats.csv" % test_video_file 104 | scene_manager.stats_manager.save_to_csv(csv_file=filename) 105 | 106 | 107 | def test_api_scene_manager_callback(test_video_file: str): 108 | """Demonstrate how to use a callback with the SceneManager detect_scenes method.""" 109 | import numpy 110 | 111 | from scenedetect import ContentDetector, SceneManager, open_video 112 | 113 | # Callback to invoke on the first frame of every new scene detection. 114 | def on_new_scene(frame_img: numpy.ndarray, frame_num: int): 115 | print("New scene found at frame %d." % frame_num) 116 | 117 | video = open_video(test_video_file) 118 | scene_manager = SceneManager() 119 | scene_manager.add_detector(ContentDetector()) 120 | scene_manager.detect_scenes(video=video, callback=on_new_scene) 121 | 122 | 123 | def test_api_device_callback(test_video_file: str): 124 | """Demonstrate how to use a webcam/device/pipe and a callback function. 125 | Instead of calling `open_video()`, an existing `cv2.VideoCapture` can be used by 126 | wrapping it with a `VideoCaptureAdapter.`""" 127 | import cv2 128 | import numpy 129 | 130 | from scenedetect import ContentDetector, SceneManager, VideoCaptureAdapter 131 | 132 | # Callback to invoke on the first frame of every new scene detection. 133 | def on_new_scene(frame_img: numpy.ndarray, frame_num: int): 134 | print("New scene found at frame %d." % frame_num) 135 | 136 | # We open a file just for test purposes, but we can also use a device or pipe here. 137 | cap = cv2.VideoCapture(test_video_file) 138 | video = VideoCaptureAdapter(cap) 139 | # Now `video` can be used as normal with a `SceneManager`. If the input is non-terminating, 140 | # either set `end_time/duration` when calling `detect_scenes`, or call `scene_manager.stop()`. 141 | total_frames = 1000 142 | scene_manager = SceneManager() 143 | scene_manager.add_detector(ContentDetector()) 144 | scene_manager.detect_scenes(video=video, duration=total_frames, callback=on_new_scene) 145 | -------------------------------------------------------------------------------- /tests/test_backend_opencv.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """PySceneDetect scenedetect.backend.opencv Tests 13 | 14 | This file includes unit tests for the scenedetect.backend.opencv module that implements the 15 | VideoStreamCv2 ('opencv') backend. These tests validate behaviour specific to this backend. 16 | 17 | For VideoStream tests that validate conformance, see test_video_stream.py. 18 | """ 19 | 20 | import cv2 21 | 22 | from scenedetect import ContentDetector, SceneManager 23 | from scenedetect.backends.opencv import VideoCaptureAdapter, VideoStreamCv2 24 | 25 | GROUND_TRUTH_CAPTURE_ADAPTER_TEST = [1, 90, 210] 26 | GROUND_TRUTH_CAPTURE_ADAPTER_CALLBACK_TEST = [180, 394] 27 | 28 | 29 | def test_open_image_sequence(test_image_sequence: str): 30 | """Test opening an image sequence. Currently, only VideoStreamCv2 supports this.""" 31 | sequence = VideoStreamCv2(test_image_sequence, framerate=25.0) 32 | assert sequence.is_seekable 33 | assert sequence.frame_size[0] > 0 and sequence.frame_size[1] > 0 34 | assert sequence.duration.frame_num == 30 35 | assert sequence.read() is not False 36 | sequence.seek(100) 37 | assert sequence.position == 29 38 | 39 | 40 | def test_capture_adapter(test_movie_clip: str): 41 | """Test that the VideoCaptureAdapter works with SceneManager.""" 42 | cap = cv2.VideoCapture(test_movie_clip) 43 | assert cap.isOpened() 44 | adapter = VideoCaptureAdapter(cap) 45 | assert adapter.read() is not False 46 | 47 | scene_manager = SceneManager() 48 | scene_manager.add_detector(ContentDetector()) 49 | assert scene_manager.detect_scenes(video=adapter, duration=adapter.base_timecode + 10.0) 50 | scenes = scene_manager.get_scene_list() 51 | assert len(scenes) == len(GROUND_TRUTH_CAPTURE_ADAPTER_TEST) 52 | assert [start.get_frames() for (start, _) in scenes] == GROUND_TRUTH_CAPTURE_ADAPTER_TEST 53 | -------------------------------------------------------------------------------- /tests/test_backend_pyav.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """PySceneDetect scenedetect.backend.pyav Tests 13 | 14 | This file includes unit tests for the scenedetect.backend.pyav module that implements the 15 | VideoStreamAv ('pyav') backend. These tests validate behaviour specific to this backend. 16 | 17 | For VideoStream tests that validate conformance, see test_video_stream.py. 18 | """ 19 | 20 | from scenedetect.backends.pyav import VideoStreamAv 21 | 22 | 23 | def test_video_stream_pyav_bytesio(test_video_file: str): 24 | """Test that VideoStreamAv works with a BytesIO input in addition to a path.""" 25 | # Mode must be binary! 26 | with open(test_video_file, mode="rb") as video_file: 27 | stream = VideoStreamAv(path_or_io=video_file, threading_mode=None) 28 | assert stream.is_seekable 29 | stream.seek(50) 30 | for _ in range(10): 31 | assert stream.read() is not False 32 | -------------------------------------------------------------------------------- /tests/test_output.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """Tests for scenedetect.output module.""" 13 | 14 | from pathlib import Path 15 | 16 | import pytest 17 | 18 | from scenedetect import ( 19 | ContentDetector, 20 | FrameTimecode, 21 | SceneManager, 22 | VideoStreamCv2, 23 | open_video, 24 | save_images, 25 | ) 26 | from scenedetect.output import ( 27 | SceneMetadata, 28 | VideoMetadata, 29 | is_ffmpeg_available, 30 | split_video_ffmpeg, 31 | ) 32 | 33 | FFMPEG_ARGS = ( 34 | "-vf crop=128:128:0:0 -map 0:v:0 -c:v libx264 -preset ultrafast -qp 0 -tune zerolatency" 35 | ) 36 | """Only encodes a small crop of the frame and tuned for performance to speed up tests.""" 37 | 38 | 39 | @pytest.mark.skipif(condition=not is_ffmpeg_available(), reason="ffmpeg is not available") 40 | def test_split_video_ffmpeg_default(tmp_path, test_movie_clip): 41 | video = open_video(test_movie_clip) 42 | # Extract three hard-coded scenes for testing, each 30 frames. 43 | scenes = [ 44 | (video.base_timecode + 30, video.base_timecode + 60), 45 | (video.base_timecode + 60, video.base_timecode + 90), 46 | (video.base_timecode + 90, video.base_timecode + 120), 47 | ] 48 | assert ( 49 | split_video_ffmpeg(test_movie_clip, scenes, output_dir=tmp_path, arg_override=FFMPEG_ARGS) 50 | == 0 51 | ) 52 | # The default filename format should be VIDEO_NAME-Scene-SCENE_NUMBER.mp4. 53 | video_name = Path(test_movie_clip).stem 54 | entries = sorted(tmp_path.glob(f"{video_name}-Scene-*")) 55 | assert len(entries) == len(scenes) 56 | 57 | 58 | @pytest.mark.skipif(condition=not is_ffmpeg_available(), reason="ffmpeg is not available") 59 | def test_split_video_ffmpeg_formatter(tmp_path, test_movie_clip): 60 | video = open_video(test_movie_clip) 61 | # Extract three hard-coded scenes for testing, each 30 frames. 62 | scenes = [ 63 | (video.base_timecode + 30, video.base_timecode + 60), 64 | (video.base_timecode + 60, video.base_timecode + 90), 65 | (video.base_timecode + 90, video.base_timecode + 120), 66 | ] 67 | 68 | # Custom filename formatter: 69 | def name_formatter(video: VideoMetadata, scene: SceneMetadata): 70 | return "abc" + video.name + "-123-" + str(scene.index) + ".mp4" 71 | 72 | assert ( 73 | split_video_ffmpeg( 74 | test_movie_clip, 75 | scenes, 76 | output_dir=tmp_path, 77 | arg_override=FFMPEG_ARGS, 78 | formatter=name_formatter, 79 | ) 80 | == 0 81 | ) 82 | video_name = Path(test_movie_clip).stem 83 | entries = sorted(tmp_path.glob(f"abc{video_name}-123-*")) 84 | assert len(entries) == len(scenes) 85 | 86 | 87 | # TODO: Add tests for `split_video_mkvmerge`. 88 | 89 | 90 | def test_save_images(test_video_file, tmp_path: Path): 91 | """Test scenedetect.scene_manager.save_images function.""" 92 | video = VideoStreamCv2(test_video_file) 93 | sm = SceneManager() 94 | sm.add_detector(ContentDetector()) 95 | 96 | image_name_glob = "scenedetect.tempfile.*.jpg" 97 | image_name_template = ( 98 | "scenedetect.tempfile.$SCENE_NUMBER.$IMAGE_NUMBER.$FRAME_NUMBER.$TIMESTAMP_MS.$TIMECODE" 99 | ) 100 | 101 | video_fps = video.frame_rate 102 | scene_list = [ 103 | (FrameTimecode(start, video_fps), FrameTimecode(end, video_fps)) 104 | for start, end in [(0, 100), (200, 300), (300, 400)] 105 | ] 106 | 107 | image_filenames = save_images( 108 | scene_list=scene_list, 109 | output_dir=tmp_path, 110 | video=video, 111 | num_images=3, 112 | image_extension="jpg", 113 | image_name_template=image_name_template, 114 | threading=False, 115 | ) 116 | 117 | # Ensure images got created, and the proper number got created. 118 | total_images = 0 119 | for scene_number in image_filenames: 120 | for path in image_filenames[scene_number]: 121 | assert tmp_path.joinpath(path).exists(), f"expected {path} to exist" 122 | total_images += 1 123 | 124 | assert total_images == len([path for path in tmp_path.glob(image_name_glob)]) 125 | 126 | 127 | def test_save_images_singlethreaded(test_video_file, tmp_path: Path): 128 | """Test scenedetect.scene_manager.save_images function.""" 129 | video = VideoStreamCv2(test_video_file) 130 | sm = SceneManager() 131 | sm.add_detector(ContentDetector()) 132 | 133 | image_name_glob = "scenedetect.tempfile.*.jpg" 134 | image_name_template = ( 135 | "scenedetect.tempfile.$SCENE_NUMBER.$IMAGE_NUMBER.$FRAME_NUMBER.$TIMESTAMP_MS.$TIMECODE" 136 | ) 137 | 138 | video_fps = video.frame_rate 139 | scene_list = [ 140 | (FrameTimecode(start, video_fps), FrameTimecode(end, video_fps)) 141 | for start, end in [(0, 100), (200, 300), (300, 400)] 142 | ] 143 | 144 | image_filenames = save_images( 145 | scene_list=scene_list, 146 | output_dir=tmp_path, 147 | video=video, 148 | num_images=3, 149 | image_extension="jpg", 150 | image_name_template=image_name_template, 151 | threading=True, 152 | ) 153 | 154 | # Ensure images got created, and the proper number got created. 155 | total_images = 0 156 | for scene_number in image_filenames: 157 | for path in image_filenames[scene_number]: 158 | assert tmp_path.joinpath(path).exists(), f"expected {path} to exist" 159 | total_images += 1 160 | 161 | assert total_images == len([path for path in tmp_path.glob(image_name_glob)]) 162 | 163 | 164 | # TODO: Test other functionality against zero width scenes. 165 | def test_save_images_zero_width_scene(test_video_file, tmp_path: Path): 166 | """Test scenedetect.scene_manager.save_images guards against zero width scenes.""" 167 | video = VideoStreamCv2(test_video_file) 168 | image_name_glob = "scenedetect.tempfile.*.jpg" 169 | image_name_template = "scenedetect.tempfile.$SCENE_NUMBER.$IMAGE_NUMBER" 170 | 171 | video_fps = video.frame_rate 172 | scene_list = [ 173 | (FrameTimecode(start, video_fps), FrameTimecode(end, video_fps)) 174 | for start, end in [(0, 0), (1, 1), (2, 3)] 175 | ] 176 | NUM_IMAGES = 10 177 | image_filenames = save_images( 178 | scene_list=scene_list, 179 | output_dir=tmp_path, 180 | video=video, 181 | num_images=10, 182 | image_extension="jpg", 183 | image_name_template=image_name_template, 184 | ) 185 | assert len(image_filenames) == 3 186 | assert all(len(image_filenames[scene]) == NUM_IMAGES for scene in image_filenames) 187 | total_images = 0 188 | for scene_number in image_filenames: 189 | for path in image_filenames[scene_number]: 190 | assert tmp_path.joinpath(path).exists(), f"expected {path} to exist" 191 | total_images += 1 192 | 193 | assert total_images == len([path for path in tmp_path.glob(image_name_glob)]) 194 | -------------------------------------------------------------------------------- /tests/test_platform.py: -------------------------------------------------------------------------------- 1 | # 2 | # PySceneDetect: Python-Based Video Scene Detector 3 | # ------------------------------------------------------------------- 4 | # [ Site: https://scenedetect.com ] 5 | # [ Docs: https://scenedetect.com/docs/ ] 6 | # [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7 | # 8 | # Copyright (C) 2014-2024 Brandon Castellano . 9 | # PySceneDetect is licensed under the BSD 3-Clause License; see the 10 | # included LICENSE file, or visit one of the above pages for details. 11 | # 12 | """PySceneDetect scenedetect.platform Tests 13 | 14 | This file includes unit tests for the scenedetect.platform module, containing 15 | all platform/library/OS-specific compatibility fixes. 16 | """ 17 | 18 | import platform 19 | 20 | import pytest 21 | 22 | from scenedetect.platform import CommandTooLong, invoke_command 23 | 24 | 25 | def test_invoke_command(): 26 | """Ensures the function exists and is callable without throwing 27 | an exception.""" 28 | if platform.system() == "Windows": 29 | invoke_command(["cmd"]) 30 | else: 31 | invoke_command(["echo"]) 32 | 33 | 34 | def test_long_command(): 35 | """[Windows Only] Ensures that a command string too large to be handled 36 | is translated to the correct exception for error handling. 37 | """ 38 | if platform.system() == "Windows": 39 | with pytest.raises(CommandTooLong): 40 | invoke_command("x" * 2**15) 41 | -------------------------------------------------------------------------------- /website/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # PySceneDetect Website (https://www.scenedetect.com) 2 | # Copyright (C) 2014-2024 Brandon Castellano . 3 | site_name: PySceneDetect 4 | site_description: "Website and documentation for PySceneDetect, a program to automatically detect scene cuts and split videos. Written in Python, and also provides Python API in addition to command-line interface for use within other programs." 5 | site_author: "Brandon Castellano" 6 | docs_dir: "pages" 7 | site_dir: "build" 8 | repo_url: https://github.com/Breakthrough/PySceneDetect 9 | edit_uri: 'blob/main/website/pages/' 10 | repo_name: "PySceneDetect on Github" 11 | copyright: 'Copyright © 2014-2024 Brandon Castellano. All rights reserved.
Licensed under BSD 3-Clause (see the LICENSE file for details).' 12 | theme: 13 | name: readthedocs 14 | logo: img/pyscenedetect_logo_small.png 15 | custom_dir: overrides 16 | # TODO: deprecated option for this theme 17 | google_analytics: ['UA-72551323-1', 'auto'] 18 | 19 | nav: 20 | - 'PySceneDetect': 21 | - 'Home': 'index.md' 22 | - 'Features': 'features.md' 23 | - 'Download': 'download.md' 24 | - 'Changelog': 'changelog.md' 25 | - 'Reference:': 26 | - 'Documentation': 'docs.md' 27 | - 'Command-Line': 'cli.md' 28 | - 'Python API': 'api.md' 29 | - 'Support:': 30 | - 'FAQ': 'faq.md' 31 | - 'Bugs & Contributing': 'contributing.md' 32 | - 'Development & Support': 'supporting.md' 33 | - 'Resources': 34 | - 'Similar Projects': 'similar.md' 35 | - 'Research & Literature': 'literature.md' 36 | - 'License & Copyright': 'copyright.md' 37 | 38 | markdown_extensions: [fenced_code] 39 | 40 | extra_css: 41 | - style.css 42 | -------------------------------------------------------------------------------- /website/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block site_name %} 4 | {% if page.is_index %} 5 | 6 | {% else %} 7 | 8 | {% endif %} 9 | {% if page.is_index %} 10 | 🎥 {{ config.site_name }} 11 | {% else %} 12 | 13 | {% endif %} 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /website/pages/contributing.md: -------------------------------------------------------------------------------- 1 | 2 | ##   Bug Reports 3 | 4 | Bugs, issues, features, and improvements to PySceneDetect are handled through [the issue tracker on Github](https://github.com/Breakthrough/PySceneDetect/issues). If you run into any bugs using PySceneDetect, please [create a new issue](https://github.com/Breakthrough/PySceneDetect/issues/new/choose). 5 | 6 | Try to [find an existing issue](https://github.com/Breakthrough/PySceneDetect/issues?q=) before creating a new one, as there may be a workaround posted there. Additional information is also helpful for existing reports. 7 | 8 | ##   Contributing to Development 9 | 10 | Development of PySceneDetect happens on [github.com/Breakthrough/PySceneDetect](https://github.com/Breakthrough/PySceneDetect). Pull requests are accepted and encouraged. Where possible, PRs should be submitted with a dedicated entry in [the issue tracker](https://github.com/Breakthrough/PySceneDetect/issues?q=). Issues and features are typically grouped into version milestones. 11 | 12 | The following checklist covers the basics of pre-submission requirements: 13 | 14 | - Code passes all unit tests (run `pytest`) 15 | - Code passes static analysis and formatting checks (`ruff check` and `ruff format`) 16 | - Follows the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) 17 | 18 | Note that PySceneDetect is released under the BSD 3-Clause license, and submitted code should comply with this license (see [License & Copyright Information](copyright.md) for details). 19 | 20 | ##   Features That Need Help 21 | 22 | The following is a "wishlist" of features which PySceneDetect eventually should have, but does not currently due to lack of resources. Anyone who is able to contribute in any capacity to these items is encouraged to do so by starting a dialogue by opening a new issue on Github as per above. 23 | 24 | ### Flash Suppression 25 | 26 | Some detection methods struggle with bright flashes and fast camera movement. The detection pipeline has some filters in place to deal with these cases, but there are still drawbacks. We are actively seeking methods which can improve both performance and accuracy in these cases. 27 | 28 | ### Automatic Thresholding 29 | 30 | The `detect-content` command requires a manual threshold to be set currently. Methods to use peak detection to dynamically determine when scene cuts occur would allow for the program to work with a much wider amount of material without requiring manual tuning, but would require statistical analysis. 31 | 32 | Ideally, this would be something like `-threshold=auto` as a default. 33 | 34 | ### Dissolve Detection 35 | 36 | Depending on the length of the dissolve and parameters being used, detection accuracy for these types of cuts can vary widely. A method to improve accuracy with minimal performance loss is an open problem. 37 | 38 | ### Advanced Strategies 39 | 40 | Research into detection methods and performance are ongoing. All contributions in this regard are most welcome. 41 | 42 | ### GUI 43 | 44 | A graphical user interface will be crucial for making PySceneDetect approchable by a wider audience. There have been several suggested designs, but nothing concrete has been developed yet. Any proposed solution for the GUI should work across Windows, Linux, and OSX. 45 | 46 | ### Localization 47 | 48 | PySceneDetect currently is not localized for other languages. Anyone who can help improve how localization can be approached for development material is encouraged to contribute in any way possible. Whether it is the GUI program, the command line interface, or documentation, localization will allow PySceneDetect to be used by much more users in their native languages. -------------------------------------------------------------------------------- /website/pages/copyright.md: -------------------------------------------------------------------------------- 1 | 2 | ## PySceneDetect License Agreement 3 | 4 | ```md 5 | 6 | PySceneDetect License (BSD 3-Clause) 7 | < http://www.bcastell.com/projects/PySceneDetect > 8 | 9 | Copyright (C) 2014-2024, Brandon Castellano. 10 | All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions 14 | are met: 15 | 16 | 1. Redistributions of source code must retain the above copyright 17 | notice, this list of conditions and the following disclaimer. 18 | 19 | 2. Redistributions in binary form must reproduce the above 20 | copyright notice, this list of conditions and the following 21 | disclaimer in the documentation and/or other materials 22 | provided with the distribution. 23 | 24 | 3. Neither the name of the copyright holder nor the names of its 25 | contributors may be used to endorse or promote products derived 26 | from this software without specific prior written permission. 27 | 28 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 29 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 30 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 31 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 32 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 33 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 34 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 35 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 36 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 38 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | ``` 40 | 41 | 42 | ## Ancillary Software Licenses 43 | 44 | This section contains links to the license agreements for all third-party software libraries used and distributed with PySceneDetect. You can find copies of all the relevant license agreements referenced below if you installed a copy of PySceneDetect by looking at the LICENSE files in the installation directory. 45 | 46 | ----------------------------------------------------------------------- 47 | 48 | 49 | ### click 50 | 51 | - Copyright (C) 2017, Armin Ronacher. 52 | - URL: http://click.pocoo.org/license/ 53 | 54 | ### NumPy 55 | 56 | - Copyright (C) 2005-2016, NumPy Developers. 57 | - URL: http://www.numpy.org/license.html 58 | 59 | ### OpenCV 60 | 61 | - Copyright (C) 2017, Itseez. 62 | - URL: http://opencv.org/license.html 63 | 64 | ### PyAV 65 | - Copyright (C) 2017, Mike Boers and others 66 | - URL: https://github.com/PyAV-Org/PyAV/blob/main/LICENSE.txt 67 | 68 | ### simpletable 69 | 70 | - Copyright (C) 2014-2019, Matheus Vieira Portela and others 71 | - URL: https://github.com/matheusportela/simpletable/blob/master/LICENSE 72 | 73 | ### tqdm 74 | 75 | - Copyright (C) 2013-2018, Casper da Costa-Luis, Google Inc., and Noam Yorav-Raphael. 76 | - URL: https://raw.githubusercontent.com/tqdm/tqdm/master/LICENCE 77 | 78 | 79 | ----------------------------------------------------------------------- 80 | 81 | ### FFmpeg and mkvmerge 82 | 83 | This software may also invoke mkvmerge or FFmpeg, if available. 84 | 85 | FFmpeg is a trademark of Fabrice Bellard. 86 | mkvmerge is Copyright (C) 2005-2016, Matroska. 87 | 88 | Certain distributions of PySceneDetect may include ffmpeg. See the 89 | thirdparty/LICENSE-FFMPEG file or visit [ https://ffmpeg.org ]. 90 | 91 | -------------------------------------------------------------------------------- /website/pages/docs.md: -------------------------------------------------------------------------------- 1 | 2 | # Documentation 3 | 4 | ## Stable 5 | 6 | * [latest](latest/) 7 | * [v0.6.6](0.6.6/) 8 | * [v0.6.5](0.6.5/) 9 | * [v0.6.4](0.6.4/) 10 | * [v0.6.3](0.6.3/) 11 | * [v0.6.2](0.6.2/) 12 | * [v0.6.1](0.6.1/) 13 | 14 | ## In Development 15 | 16 | * [head](head/) 17 | -------------------------------------------------------------------------------- /website/pages/download.md: -------------------------------------------------------------------------------- 1 | 2 | # Download 3 | 4 | PySceneDetect is completely free software, and can be downloaded from the links below. See the [license and copyright information](copyright.md) page for details. If you have trouble running PySceneDetect, ensure that you have all the required dependencies listed in the [Dependencies](#dependencies) section below. 5 | 6 | PySceneDetect requires at least Python 3.7 or higher. 7 | 8 | 9 | ## Install via pip       10 | 11 |
12 |

Including OpenCV (recommended):

13 |

pip install --upgrade scenedetect[opencv]

14 |

Including Headless OpenCV (servers):

15 |

pip install --upgrade scenedetect[opencv-headless]

16 |
17 | 18 | PySceneDetect is available via `pip` as [the `scenedetect` package](https://pypi.org/project/scenedetect/). 19 | 20 | ## Windows Build (64-bit Only)   21 | 22 |
23 |

Latest Release: v0.6.6

24 |

  Release Date:  March 9, 2025

25 |   Installer  (recommended)      26 |   Portable .zip      27 |   Getting Started 28 |
29 | 30 | ## Post Installation 31 | 32 | After installation, you can call PySceneDetect from any terminal/command prompt by typing `scenedetect` (try running `scenedetect --help`, or `scenedetect version`). If you encounter any runtime errors while running PySceneDetect, ensure that you have all the required dependencies listed in the System Requirements section above (you should be able to `import numpy` and `import cv2`). If you encounter any issues or want to make a feature request, feel free to [report any bugs or share some feature requests/ideas](contributing.md) on the [issue tracker](https://github.com/Breakthrough/PySceneDetect/issues) and help make PySceneDetect even better. 33 | 34 | 35 | ## Dependencies 36 | 37 | ### Python Packages 38 | 39 | PySceneDetect requires [Python 3](https://www.python.org/) and the following packages: 40 | 41 | - [OpenCV](http://opencv.org/): `pip install opencv-python` 42 | - [Numpy](https://numpy.org/): `pip install numpy` 43 | - [Click](https://click.palletsprojects.com): `pip install Click` 44 | - [tqdm](https://github.com/tqdm/tqdm): `pip install tqdm` 45 | - [appdirs](https://github.com/ActiveState/appdirs): `pip install appdirs` 46 | 47 | Optional packages: 48 | 49 | - [PyAV](https://pyav.org/): `pip install av` 50 | 51 | ### Video Splitting Tools 52 | 53 | For video splitting support, you need to have one of the following tools available (included in Windows builds): 54 | 55 | - [ffmpeg](https://ffmpeg.org/download.html), required to split video files (`split-video` or `split-video -c/--copy`) 56 | - [mkvmerge](https://mkvtoolnix.download/), part of mkvtoolnix, command-line tool, required to split video files in stream copy mode (`split-video -c/--copy` only) 57 | 58 | The `ffmpeg` and/or `mkvmerge` command must be available system wide (e.g. in a directory in `PATH`, so it can be used from any terminal/console by typing the command), or alternatively, placed in the same directory where PySceneDetect is installed. On Windows this is usually `C:\PythonXY\Scripts`, where `XY` is your Python version. For more information, [see the CLI documentation](cli.md). 59 | 60 | ### Building OpenCV from Source 61 | 62 | If you have installed OpenCV using `pip`, you will need to uninstall it before installing a different version of OpenCV, or building and installing it from source. 63 | 64 | You can [click here](http://breakthrough.github.io/Installing-OpenCV/) for a quick guide (OpenCV + Numpy on Windows & Linux) on installing OpenCV/Numpy on [Windows (using pre-built binaries)](http://breakthrough.github.io/Installing-OpenCV/#installing-on-windows-pre-built-binaries) and [Linux (compiling from source)](http://breakthrough.github.io/Installing-OpenCV/#installing-on-linux-compiling-from-source). If the Python module that comes with OpenCV on Windows is incompatible with your system architecture or Python version, [see this page](http://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv) to obtain a pre-compiled (unofficial) module. 65 | 66 | To ensure you have all the requirements installed, open a `python` interpreter, and ensure you can run `import numpy` and `import cv2` without any errors. 67 | 68 | 69 | ## Code Signing Policy 70 | 71 | This program uses free code signing provided by [SignPath.io](https://signpath.io?utm_source=foundation&utm_medium=website&utm_campaign=PySceneDetect), and a free code signing certificate by the [SignPath Foundation](https://signpath.org?utm_source=foundation&utm_medium=website&utm_campaign=PySceneDetect) 72 | -------------------------------------------------------------------------------- /website/pages/faq.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ##   Frequently Asked Questions 4 | 5 | #### How can I fix `ImportError: No module named cv2`? 6 | 7 | You need to install OpenCV for PySceneDetect to properly work. If you're using `pip`, you can install it as follows: 8 | 9 | ```md 10 | pip install scenedetect[opencv] 11 | ``` 12 | 13 | Note that you may need to use a different/older version depending on your Python version. You can also use the headless package if you're running a server: 14 | 15 | 16 | ```md 17 | pip install scenedetect[opencv-headless] 18 | ``` 19 | 20 | Unlike calling `pip install opencv-python`, the above commands will download and install the correct OpenCV version based on the Python version you are running. 21 | 22 | #### How can I enable video splitting support? 23 | 24 | Video splitting is performed by `ffmpeg` ([https://ffmpeg.org/download.html](https://ffmpeg.org/download.html)) or `mkvmerge` (https://mkvtoolnix.download/downloads.html) depending on which command line arguments are used. Ensure the tool is available and somewhere in your system's PATH folder. 25 | 26 | #### How can I fix the error `Cannot split video due to too many scenes`? 27 | 28 | This error occurs on Windows platforms specifically when the number of detected scenes is too large. This is because PySceneDetect internally invokes other commands, such as those used for the `split-video` command. 29 | 30 | You can get around this issue by simply invoking those tools manually, using a smaller sub-set of scenes (or splitting the scene list into multiple parts). You can obtain a comma-separated list of timecodes by using the `list-scenes` command. 31 | 32 | See [Issue #164](https://github.com/Breakthrough/PySceneDetect/issues/164) for details, or if you have any further questions. 33 | 34 | #### How can I fix the error `Failed to read any frames from video file`? 35 | 36 | Unfortunately, the underlying library used to perform video I/O was unable to open the file. Try using a different backend by installing PyAV (`pip install av`) and see if the problem persists. 37 | 38 | This can also happen due to videos having multiple audio tracks (as per [#179](https://github.com/Breakthrough/PySceneDetect/issues/179)). If the PyAV backend does not succeed in processing the video, as a workaround you can remove the audio track using either `ffmpeg` or `mkvmerge`: 39 | 40 | ```md 41 | ffmpeg -i input.mp4 -c copy -an output.mp4 42 | ``` 43 | 44 | Or: 45 | 46 | ```md 47 | mkvmerge -o output.mkv input.mp4 48 | ``` 49 | -------------------------------------------------------------------------------- /website/pages/features.md: -------------------------------------------------------------------------------- 1 | 2 | ## Overview 3 | 4 |
5 |

  Content-Aware Scene Detection

6 |     Detects breaks in-between content, not only when the video fades to black (although a threshold mode is available as well for those cases). 7 |
8 | 9 |
10 |

  Compatible With Many External Tools

11 |     The detected scene boundaries/cuts can be exported in a variety of formats, with the default type (comma-separated HH:MM:SS.nnn values) being ready to copy-and-paste directly into other tools (such as ffmpeg, mkvmerge, etc...) for splitting and/or re-encoding the video. 12 |
13 | 14 |
15 |

  Statistical Video Analysis

16 |     Can output a spreadsheet-compatible file for analyzing trends in a particular video file, to determine the optimal threshold values to use with specific scene detection methods/algorithms. 17 |
18 | 19 |
20 |

  Extendible and Embeddable

21 |     Written in Python, and designed with an easy-to-use and extendable API, PySceneDetect is ideal for embedding into other programs, or to implement custom methods/algorithms of scene detection for specific applications (e.g. analyzing security camera footage). 22 |
23 | 24 | 25 | ------------------------------------------------------------------------ 26 | 27 | 28 | ## Features 29 | 30 | - exports timecodes in standard format (HH:MM:SS.nnn), comma-separated for easy copy-and-paste into external tools and analysis with spreadsheet software 31 | - statistics/analysis mode to export frame-by-frame video metrics via the `-s [FILE]`/`--stats [FILE]` argument (e.g. `--stats metrics.csv`) 32 | - output-suppression (quiet) mode for better automation with external scripts/programs (`-q`/`--quiet`) 33 | - save an image of the first and last frame of each detected scene via the `save-images` command 34 | - split the input video automatically if `ffmpeg` or `mkvmerge` is available via the `split-video` command 35 | 36 | ### Output Formats 37 | 38 | - **EDL**: `save-edl` command (save as edit decision list in CMX 3600 format, compatible with most editors) 39 | - **HTML**: `save-html` command (save HTML table that can be viewed with browser) 40 | - **OTIO**: `save-otio` command (save as [OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) file) 41 | - **QP**: `save-qp` command (can be used with x264 `--qpfile`) 42 | 43 | ### Detection Methods 44 | 45 | PySceneDetect implements a variety of different detection algorithms which can be used independently or combined depending on the source material being analyzed. 46 | 47 | - **adaptive content scene detection** (`detect-adaptive`): uses rolling average of differences in HSL colorspace combined with thresholding to detect shot changes (fast cut) 48 | - **content-aware scene detection** (`detect-content`): uses differences in HSL colorspace combined with filtering to detect shot changes (fast cut) 49 | - **content-aware scene detection** (`detect-hash`): uses perceptual hashing to determine differences between frames to find shot changes (fast cut) 50 | - **content-aware scene detection** (`detect-hist`): uses differences in histograms of Y channel of frames after conversion to YUV (fast cut) 51 | - **threshold scene detection** (`detect-threshold`): uses average frame intensity (brightness) to detect slow transitions (fade in/out) 52 | 53 | By default, detection methods are tuned to provide high performance during processing, while maintaining reasonable accuracy. Each detection method is configurable, and different parameters can be changed for specific use cases. See [the documentation](docs.md) for details. 54 | 55 | ------------------------------------------------------------------------ 56 | 57 | 58 | ## Version Roadmap 59 | 60 | Future version roadmaps are now [tracked as milestones (link)](https://github.com/Breakthrough/PySceneDetect/milestones). Specific issues/features that are queued up for the very next release will have [the `backlog` tag](https://github.com/Breakthrough/PySceneDetect/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+backlog%22), and issues/features being worked on will have [the `status: in progress` tag](https://github.com/Breakthrough/PySceneDetect/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+in+progress%22). Also note that bug reports as well as additional feature requests can be submitted via [the issue tracker](https://github.com/Breakthrough/PySceneDetect/issues); read [the Bug Reports and Contributing page](contributing.md) for details. 61 | 62 | 63 | ### Planned Features 64 | 65 | The following features are under consideration for future releases. Any contributions towards completing these features are most welcome (pull requests may be accpeted via Github). 66 | 67 | - graphical interface (GUI) 68 | - automatic threshold detection for the current scene detection methods (or just output message indicating "Predicted Threshold: X") 69 | - suppression of short-length flashes/bursts of light [#35](https://github.com/Breakthrough/PySceneDetect/issues/35) 70 | - histogram-based detection algorithm in HSV/HSL color space [#53](https://github.com/Breakthrough/PySceneDetect/issues/53) 71 | - [perceptual hash](https://en.wikipedia.org/wiki/Perceptual_hashing) based scene detection ([prototype by @wjs018 in PR#290](https://github.com/Breakthrough/PySceneDetect/pull/290)) 72 | - adaptive bias for fade in/out interpolation 73 | - export scenes in chapter/XML format [#323](https://github.com/Breakthrough/PySceneDetect/issues/323) 74 | -------------------------------------------------------------------------------- /website/pages/img/0.6.4-score-comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/website/pages/img/0.6.4-score-comparison.png -------------------------------------------------------------------------------- /website/pages/img/goldeneye-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/website/pages/img/goldeneye-stats.png -------------------------------------------------------------------------------- /website/pages/img/params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/website/pages/img/params.png -------------------------------------------------------------------------------- /website/pages/img/pyscenedetect_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/website/pages/img/pyscenedetect_logo.png -------------------------------------------------------------------------------- /website/pages/img/pyscenedetect_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Breakthrough/PySceneDetect/34dffabd8666bf4cbb94ff1995f74fcf593eb368/website/pages/img/pyscenedetect_logo_small.png -------------------------------------------------------------------------------- /website/pages/index.md: -------------------------------------------------------------------------------- 1 | 2 | PySceneDetect 3 | 4 |
5 |

  Latest Release: v0.6.6 (March 9, 2025)

6 |   Download        Changelog        Documentation        Getting Started 7 |
8 | See the changelog for the latest release notes and known issues. 9 |
10 | 11 | **PySceneDetect** is a tool for **detecting shot changes in videos** ([example](cli.md)), and can **automatically split the video into separate clips**. PySceneDetect is free and open-source software, and has several [detection methods](features.md#detection-methods) to find fast-cuts and threshold-based fades. 12 | 13 |

Quickstart

14 | 15 | Split video on each fast cut using [command line (more examples)](cli.md): 16 | 17 | ```rst 18 | scenedetect -i video.mp4 split-video 19 | ``` 20 | 21 | Split video on each fast cut using [Python API (docs)](docs.md): 22 | 23 | ```python 24 | from scenedetect import detect, AdaptiveDetector, split_video_ffmpeg 25 | scene_list = detect('my_video.mp4', AdaptiveDetector()) 26 | split_video_ffmpeg('my_video.mp4', scene_list) 27 | ``` 28 | 29 | 30 |

Examples and Use Cases

31 | 32 | Here are some of the things people are using PySceneDetect for: 33 | 34 | - splitting home videos or other source footage into individual scenes 35 | - automated detection and removal of commercials from PVR-saved video sources 36 | - processing and splitting surveillance camera footage 37 | - statistical analysis of videos to find suitable "loops" for looping GIFs/cinemagraphs 38 | - academic analysis of film and video (e.g. finding mean shot length) 39 | 40 | Of course, this is just a small slice of what you can do with PySceneDetect, so why not try it out for yourself! The timecode format used by default (`HH:MM:SS.nnnn`) is compatible with most popular video tools, so in most cases the output scene list from PySceneDetect can be directly copied and pasted into another tool of your choice (e.g. `ffmpeg`, `avconv` or the `mkvtoolnix` suite). 41 | -------------------------------------------------------------------------------- /website/pages/literature.md: -------------------------------------------------------------------------------- 1 | 2 | # PySceneDetect in Literature 3 | 4 | PySceneDetect is a useful tool for statistical analysis of video. Below are links to various research articles/papers which have either used PySceneDetect as a part of their analysis, or propose more accurate detection algorithms using the current implementation as a comparison. 5 | 6 | - [Panda-70M: Captioning 70M Videos with Multiple Cross-Modality Teachers](https://arxiv.org/abs/2402.19479) by Tsai-Shien Chen, Aliaksandr Siarohin, Willi Menapace, Ekaterina Deyneka, Hsiang-wei Chao, Byung Eun Jeon, Yuwei Fang, Hsin-Ying Lee, Jian Ren, Ming-Hsuan Yang, Sergey Tulyakov (2024) 7 | 8 | - [Stable Remaster: Bridging the Gap Between Old Content and New Displays](https://arxiv.org/pdf/2306.06803.pdf) by Nathan Paull, Shuvam Keshari, Yian Wong (2023) 9 | 10 | - [LoL-V2T: Large-Scale Esports Video Description Dataset](https://ieeexplore.ieee.org/abstract/document/9522986) by Tsunehiko Tanaka, Edgar Simo-Serra (2021) 11 | 12 | - [Online Detection of Action Start via Soft Computing for Smart City](https://ieeexplore.ieee.org/document/9099408) by Tian Wang, Yang Chen, Hongqiang Lv, Jing Teng, Hichem Snoussi, Fei Tao (2020) 13 | 14 | - [Thesis Project: Smart Shades and Cane for The Blind](https://www.linkedin.com/pulse/blind-people-dont-have-good-muhammad-hashim-1f/) by Muhammad Hashim (2020) 15 | 16 | - [Movienet: a movie multilayer network model using visual and textual semantic cues](https://appliednetsci.springeropen.com/articles/10.1007/s41109-019-0226-0) by Youssef Mourchid, Benjamin Renoust, Olivier Roupin, Lê Văn, Hocine Cherifi & Mohammed El Hassouni (2019) 17 | 18 | - [NLP-Enriched Automatic Video Segmentation](https://ieeexplore.ieee.org/document/8525880/) by Mohannad AlMousa, Rachid Benlamri, Richard Khoury (2018) 19 | 20 | - [Online Detection of Action Start in Untrimmed, Streaming Videos](https://arxiv.org/pdf/1802.06822) by Zheng Shou, Junting Pan, Jonathan Chan, Kazuyuki Miyazawa, Hassan Mansour, Anthony Vetro, Xavi Gir-i-Nieto, Shih-Fu Chang (2018) 21 | 22 | - [Story Understanding in Video Advertisements](https://arxiv.org/pdf/1807.11122) by Keren Ye, Kyle Buettner, Adriana Kovashka (2018) 23 | 24 | This list is only provided for academic and research purposes, and is far from an exhaustive source of the uses of PySceneDetect in literature. If you think a particular submission is relevant and should be added to this list, feel free to [raise an issue](https://github.com/Breakthrough/PySceneDetect/issues/new/choose) with your suggestion. Publicly available material is preferred, although not a requirement. 25 | 26 | 27 | # Scene Detection Methodology 28 | 29 | You can find the source code for each scene detector in [the scenedetect/detectors folder](https://github.com/Breakthrough/PySceneDetect/tree/main/scenedetect/detectors). Also see [Issue #62: Reference of paper for the methods used](https://github.com/Breakthrough/PySceneDetect/issues/62) on Github for a further discussion on detection methodologies. You are more than welcome to propose any new ideas on the [issue tracker](https://github.com/Breakthrough/PySceneDetect/issues), or share a proof of concept using the Python API by creating a pull request. 30 | -------------------------------------------------------------------------------- /website/pages/similar.md: -------------------------------------------------------------------------------- 1 | 2 | ## Alternative and Related Programs 3 | 4 | The following is a list of programs or commands also performing scene cut analysis of some kind on video files. [Additions/contributions to this list are welcome](contributing.md). 5 | 6 | - [Scenecut Extractor](https://github.com/slhck/scenecut-extractor) - uses ffmpeg `select` filter 7 | - ffmpeg `blackframe` filter ([thanks @tonycpsu](https://github.com/Breakthrough/PySceneDetect/issues/7)) - threshold mode only 8 | - [Shotdetect](http://johmathe.name/shotdetect.html) - appears to be only for *NIX, content mode only 9 | - [Matlab Scene Change Detection](http://www.mathworks.com/help/vision/examples/scene-change-detection.html) - requires Matlab and Simulink/Computer Vision Toolbox, uses feature extraction and edge detection 10 | - [chaptertool](https://github.com/Mtillmann/chaptertool) - CLI/Web tool that converts PySceneDetect output to other formats 11 | - [TransNetV2](https://github.com/soCzech/TransNetV2) - Shot Boundary Detection Neural Network (2020) 12 | - [AutoShot] https://github.com/wentaozhu/AutoShot - Shot Boundary Detection Neural Network, based on a neural architecture search (2023) 13 | 14 | -------------------------------------------------------------------------------- /website/pages/style.css: -------------------------------------------------------------------------------- 1 | .wy-side-nav-search { 2 | display: block; 3 | width: 300px; 4 | padding: .809em; 5 | margin-bottom: .809em; 6 | z-index: 200; 7 | background-color: #CCD7E2; 8 | text-align: center; 9 | color: #fcfcfc 10 | } 11 | 12 | .wy-side-nav-search .wy-dropdown>a, 13 | .wy-side-nav-search>a { 14 | color:#3B3F47; 15 | font-size:100%; 16 | font-weight:700; 17 | display:inline-block; 18 | padding:4px 6px; 19 | margin-bottom:.809em; 20 | max-width:100% 21 | } -------------------------------------------------------------------------------- /website/pages/supporting.md: -------------------------------------------------------------------------------- 1 | 2 | #   Supporting Development 3 | 4 | This page is dedicated to the various people, tools, technologies, and companies which support the development of PySceneDetect. This page has been created to give credit to the things which allow PySceneDetect to exist, and is not meant to imply any kind of endorsement. 5 | 6 | 7 | ##   Tools, Technologies, and Companies 8 | 9 | The development of PySceneDetect is supported by the following tools, technologies, and companies: 10 | 11 | - [Github](https://github.com/) - Git and website hosting, code review, issue tracking, Linux/Windows/OSX builds 12 | - [AppVeyor](https://www.appveyor.com/) - Windows builds (portable + MSI) 13 | - [AdvancedInstaller](https://www.advancedinstaller.com/) - MSI installer 14 | - [SignPath](https://signpath.io/) - Code signing for Windows builds 15 | 16 | Special thanks to these companies for their support. 17 | 18 | 19 | ##   Community Contributions 20 | 21 | PySceneDetect is an open source project which anyone can freely contribute to (see [this page for various ways you can contribute](contributing.md)). You can view [the contribution graph on Github](https://github.com/Breakthrough/PySceneDetect/graphs/contributors) or [visit PySceneDetect at libraries.io](https://libraries.io/github/Breakthrough/PySceneDetect/contributors) or view the to see a list detailing the contributions people have made to the PySceneDetect project. 22 | 23 | In addition to those who have made a direct contribution in the form of a pull request, a special thank you to *everyone* who has submitted an issue, bug report, feature request, and/or pull request (see the links above for complete lists), as well as for those who continue to help the ongoing development of PySceneDetect. 24 | 25 | Your contributions continue to improve PySceneDetect, highlight the assets and talents of the FOSS community, and help to make the project's goal of being the most accurate scene detection program/library become a reality. Lastly, thank you all for your help, support, feedback, and direction on the project. 26 | 27 | 28 | ##   Donations 29 | 30 | Monetary donations are not accepted for the project, but [there are ways you can contribute](contributing.md) to the project. Furthermore, if you have resources, tools, or technologies which may benefit the development of PySceneDetect, please feel free to contact me via the issue tracker or [my website](http://bcastell.com/contact/). 31 | 32 | Many thanks to everyone who continually supports the development of the project. 33 | -------------------------------------------------------------------------------- /website/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.5.2 2 | jinja2==3.1.5 3 | --------------------------------------------------------------------------------