diff --git a/iscc_sdk/__init__.py b/iscc_sdk/__init__.py index 82f35e1..ebaa806 100644 --- a/iscc_sdk/__init__.py +++ b/iscc_sdk/__init__.py @@ -20,3 +20,4 @@ from iscc_sdk.ipfs import * from iscc_sdk.audio import * from iscc_sdk.video import * +from iscc_sdk.mp7 import * diff --git a/iscc_sdk/mp7.py b/iscc_sdk/mp7.py new file mode 100644 index 0000000..b4acdd9 --- /dev/null +++ b/iscc_sdk/mp7.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +from dataclasses import dataclass +from fractions import Fraction +from functools import cache +from typing import Tuple, List +from bitarray import bitarray +import bitarray +from bitarray.util import ba2int +import numpy as np + + +__all__ = [ + "read_mp7_signature", +] + + +SIGELEM_SIZE = 380 + + +@dataclass +class Frame: + """Represents an MP7 Frame Signature.""" + + vector: np.ndarray # 380 dimensional vector, range: 0..2 + elapsed: Fraction # time elapsed since start of video + confidence: int # signature confidence, range: 0..255 + + +@cache +def calc_byte_to_bit3(): + # type: () -> np.ndarray + """ + Build lookup table. + + :return: table to convert a 8bit value into five three-bit-values + :rtype: np.ndarray + """ + table_3_bit = np.zeros((256, 5), dtype=np.uint8) + for i in range(256): + div3 = 3 * 3 * 3 * 3 + for iii in range(0, 5): + table_3_bit[i, iii] = (i // div3) % 3 + div3 //= 3 + return table_3_bit + + +def pop_bits(data_bits, pos, bits=32): + # type: (bitarray, int, int) -> Tuple[int, int] + """ + Take out 0/1 values and pack them again to an unsigned integer. + + :param bitarray data_bits: 0/1 data + :param int pos: position in 0/1 data + :param int bits: number of bits (default 32) + :return: value, new position + :rtype: Tuple[int, int] + """ + chunk = data_bits[pos : pos + bits] + value = ba2int(chunk, signed=False) + pos += bits + return value, pos + + +def read_mp7_signature(byte_data): + # type: (bytes) -> List[Frame] + """ + Decode binary MP7 video signature. + + :param bytes byte_data: Raw MP7 video signature (as extracted by ffmpeg) + :return: List of Frame Signatures + :rtype: List[Frame] + """ + table_3_bit = calc_byte_to_bit3() + data_bits = bitarray.bitarray() + data_bits.frombytes(byte_data) + pos = 0 + pos += 129 + num_of_frames, pos = pop_bits(data_bits, pos) + media_time_unit, pos = pop_bits(data_bits, pos, 16) + pos += 1 + 32 + 32 + num_of_segments, pos = pop_bits(data_bits, pos) + pos += num_of_segments * (4 * 32 + 1 + 5 * 243) + pos += 1 + frame_sigs_v = [] + frame_sigs_c = [] + frame_sigs_e = [] + frame_sigs_tu = [] + for i in range(num_of_frames): + pos += 1 + raw_media_time, pos = pop_bits(data_bits, pos) + frame_confidence, pos = pop_bits(data_bits, pos, 8) + pos += 5 * 8 + vec = np.zeros((SIGELEM_SIZE,), dtype=np.uint8) + p = 0 + for ii in range(SIGELEM_SIZE // 5): + dat, pos = pop_bits(data_bits, pos, 8) + vec[p : p + 5] = table_3_bit[dat] + p += 5 + frame_sigs_v.append(vec) + frame_sigs_e.append(raw_media_time) + frame_sigs_c.append(frame_confidence) + frame_sigs_tu.append(media_time_unit) + + fsigs = [] + r = (frame_sigs_v, frame_sigs_e, frame_sigs_c, frame_sigs_tu) + for v, e, c, tu in zip(*r): + fsigs.append(Frame(vector=v, elapsed=Fraction(e, tu), confidence=c)) + return fsigs diff --git a/iscc_sdk/options.py b/iscc_sdk/options.py index d19443e..8be123d 100644 --- a/iscc_sdk/options.py +++ b/iscc_sdk/options.py @@ -1,4 +1,6 @@ """*SDK configuration and options*.""" +from typing import Optional + from iscc_core.options import CoreOptions from pydantic import Field @@ -30,5 +32,10 @@ class SdkOptions(CoreOptions): 60, description="Thumbnail image compression setting (0-100)" ) + video_fps: int = Field( + 5, + description="Frames per second to process for video hash (ignored when 0).", + ) + sdk_opts = SdkOptions() diff --git a/iscc_sdk/video.py b/iscc_sdk/video.py index 5cf47da..6bbee0d 100644 --- a/iscc_sdk/video.py +++ b/iscc_sdk/video.py @@ -1,9 +1,15 @@ """*Video handling module*""" +import os +from typing import Sequence, Tuple, List + +from loguru import logger as log import io import subprocess import sys import tempfile from os.path import join, basename +from pathlib import Path +from secrets import token_hex from PIL import Image, ImageEnhance import iscc_sdk as idk import iscc_schema as iss @@ -13,6 +19,8 @@ "video_meta_extract", "video_meta_embed", "video_thumbnail", + "video_mp7sig_extract", + "video_features_extract", ] VIDEO_META_MAP = { @@ -169,3 +177,42 @@ def video_thumbnail(fp): result = subprocess.run(cmd, capture_output=True) img_obj = Image.open(io.BytesIO(result.stdout)) return ImageEnhance.Sharpness(img_obj.convert("RGB")).enhance(1.4) + + +def video_features_extract(fp): + # type: (str) -> List[Tuple[int, ...]] + """ + Extract video features. + + :param str fp: Filepath to video file. + :return: A sequence of frame signatures. + :rtype: Sequence[Tuple[int, ...]] + """ + sig = video_mp7sig_extract(fp) + frames = idk.read_mp7_signature(sig) + return [tuple(frame.vector.tolist()) for frame in frames] + + +def video_mp7sig_extract(fp): + # type: (str) -> bytes + """Extract MPEG-7 Video Signature. + + :param str fp: Filepath to video file. + :return: raw signature data + :rtype: bytes + """ + + sigfile_path = Path(tempfile.mkdtemp(), token_hex(16) + ".bin") + sigfile_path_escaped = sigfile_path.as_posix().replace(":", "\\\\:") + + # Extract MP7 Signature + vf = f"signature=format=binary:filename={sigfile_path_escaped}" + vf = f"fps=fps={idk.sdk_opts.video_fps}," + vf + cmd = [idk.ffmpeg_bin()] + cmd.extend(["-i", fp, "-vf", vf, "-f", "null", "-"]) + subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) + + with open(sigfile_path, "rb") as sig: + sigdata = sig.read() + os.remove(sigfile_path) + return sigdata diff --git a/poetry.lock b/poetry.lock index cef7a80..7a7cf1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -98,7 +98,6 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -115,14 +114,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "click" version = "8.0.4" @@ -133,7 +124,6 @@ python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -208,7 +198,6 @@ python-versions = ">=3.7" [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} [[package]] name = "importlib-metadata" @@ -219,7 +208,6 @@ optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] @@ -471,6 +459,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "numpy" +version = "1.22.3" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + [[package]] name = "packaging" version = "21.3" @@ -534,9 +530,6 @@ category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -641,7 +634,6 @@ python-versions = ">=3.7" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -714,8 +706,6 @@ python-versions = ">=3.7" [package.dependencies] astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} -cached-property = {version = ">=1.5", markers = "python_version < \"3.8\""} -typing-extensions = {version = ">=3.7", markers = "python_version < \"3.8\""} [package.extras] numpy-style = ["docstring_parser (>=0.7)"] @@ -764,7 +754,6 @@ optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] @@ -775,14 +764,6 @@ category = "dev" optional = false python-versions = ">=3.7" -[[package]] -name = "typed-ast" -version = "1.5.2" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "typing-extensions" version = "4.1.1" @@ -799,9 +780,6 @@ category = "main" optional = false python-versions = ">=3.7" -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] dev = ["mypy", "pylint", "pytest", "pytest-cov"] @@ -857,8 +835,8 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" -python-versions = ">=3.7,<4.0" -content-hash = "51b0d999a243176578b4fc7cfbfb799709767412b5a9c769b6a5ef3b47baee67" +python-versions = ">=3.8,<4.0" +content-hash = "d79659823dc0758ac77f18f068aaee6586892e998287ea9e19527074be5aa105" [metadata.files] astunparse = [ @@ -982,10 +960,6 @@ blake3 = [ {file = "blake3-0.3.1-cp39-none-win_amd64.whl", hash = "sha256:2393b81dcd307823238798b400ec1460f2a09a3e1c2f442b3b53dc040addd54f"}, {file = "blake3-0.3.1.tar.gz", hash = "sha256:b39709ce9da18e7bbd41353594796fb87e14c57fcd5c463ed43458349d34dfdc"}, ] -cached-property = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] click = [ {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, @@ -1208,6 +1182,28 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +numpy = [ + {file = "numpy-1.22.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75"}, + {file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"}, + {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e"}, + {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4"}, + {file = "numpy-1.22.3-cp310-cp310-win32.whl", hash = "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430"}, + {file = "numpy-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4"}, + {file = "numpy-1.22.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce"}, + {file = "numpy-1.22.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe"}, + {file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5"}, + {file = "numpy-1.22.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1"}, + {file = "numpy-1.22.3-cp38-cp38-win32.whl", hash = "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62"}, + {file = "numpy-1.22.3-cp38-cp38-win_amd64.whl", hash = "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676"}, + {file = "numpy-1.22.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123"}, + {file = "numpy-1.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802"}, + {file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d"}, + {file = "numpy-1.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168"}, + {file = "numpy-1.22.3-cp39-cp39-win32.whl", hash = "sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa"}, + {file = "numpy-1.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a"}, + {file = "numpy-1.22.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f"}, + {file = "numpy-1.22.3.zip", hash = "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1478,32 +1474,6 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -typed-ast = [ - {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, - {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, - {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, - {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, - {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, - {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, - {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, - {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, - {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, - {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, -] typing-extensions = [ {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, diff --git a/pyproject.toml b/pyproject.toml index 23d7a90..ac057a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ repository = "https://github.com/iscc/iscc-sdk" keywords=["iscc", "identifier", "media", "content", "hash", "blockchain", "similarity"] [tool.poetry.dependencies] -python = ">=3.7,<4.0" +python = ">=3.8,<4.0" cython = "^0.29" iscc-core = "^0.2" iscc-schema = "^0.3" @@ -21,6 +21,7 @@ platformdirs = "^2.5" jmespath = "^1.0" Pillow = "^9.0" pytaglib = "^1.5" +numpy = "^1.22.3" [tool.poetry.dev-dependencies] pytest = "^7.0" diff --git a/tests/test_mp7.py b/tests/test_mp7.py new file mode 100644 index 0000000..f0e89aa --- /dev/null +++ b/tests/test_mp7.py @@ -0,0 +1,662 @@ +from fractions import Fraction + +from bitarray import bitarray +from iscc_sdk import mp7 +import iscc_sdk as idk + + +def test_calc_byte_to_bit3(): + assert mp7.calc_byte_to_bit3().tolist() == [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 2], + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 2], + [0, 0, 0, 2, 0], + [0, 0, 0, 2, 1], + [0, 0, 0, 2, 2], + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 1], + [0, 0, 1, 0, 2], + [0, 0, 1, 1, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 2], + [0, 0, 1, 2, 0], + [0, 0, 1, 2, 1], + [0, 0, 1, 2, 2], + [0, 0, 2, 0, 0], + [0, 0, 2, 0, 1], + [0, 0, 2, 0, 2], + [0, 0, 2, 1, 0], + [0, 0, 2, 1, 1], + [0, 0, 2, 1, 2], + [0, 0, 2, 2, 0], + [0, 0, 2, 2, 1], + [0, 0, 2, 2, 2], + [0, 1, 0, 0, 0], + [0, 1, 0, 0, 1], + [0, 1, 0, 0, 2], + [0, 1, 0, 1, 0], + [0, 1, 0, 1, 1], + [0, 1, 0, 1, 2], + [0, 1, 0, 2, 0], + [0, 1, 0, 2, 1], + [0, 1, 0, 2, 2], + [0, 1, 1, 0, 0], + [0, 1, 1, 0, 1], + [0, 1, 1, 0, 2], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 1], + [0, 1, 1, 1, 2], + [0, 1, 1, 2, 0], + [0, 1, 1, 2, 1], + [0, 1, 1, 2, 2], + [0, 1, 2, 0, 0], + [0, 1, 2, 0, 1], + [0, 1, 2, 0, 2], + [0, 1, 2, 1, 0], + [0, 1, 2, 1, 1], + [0, 1, 2, 1, 2], + [0, 1, 2, 2, 0], + [0, 1, 2, 2, 1], + [0, 1, 2, 2, 2], + [0, 2, 0, 0, 0], + [0, 2, 0, 0, 1], + [0, 2, 0, 0, 2], + [0, 2, 0, 1, 0], + [0, 2, 0, 1, 1], + [0, 2, 0, 1, 2], + [0, 2, 0, 2, 0], + [0, 2, 0, 2, 1], + [0, 2, 0, 2, 2], + [0, 2, 1, 0, 0], + [0, 2, 1, 0, 1], + [0, 2, 1, 0, 2], + [0, 2, 1, 1, 0], + [0, 2, 1, 1, 1], + [0, 2, 1, 1, 2], + [0, 2, 1, 2, 0], + [0, 2, 1, 2, 1], + [0, 2, 1, 2, 2], + [0, 2, 2, 0, 0], + [0, 2, 2, 0, 1], + [0, 2, 2, 0, 2], + [0, 2, 2, 1, 0], + [0, 2, 2, 1, 1], + [0, 2, 2, 1, 2], + [0, 2, 2, 2, 0], + [0, 2, 2, 2, 1], + [0, 2, 2, 2, 2], + [1, 0, 0, 0, 0], + [1, 0, 0, 0, 1], + [1, 0, 0, 0, 2], + [1, 0, 0, 1, 0], + [1, 0, 0, 1, 1], + [1, 0, 0, 1, 2], + [1, 0, 0, 2, 0], + [1, 0, 0, 2, 1], + [1, 0, 0, 2, 2], + [1, 0, 1, 0, 0], + [1, 0, 1, 0, 1], + [1, 0, 1, 0, 2], + [1, 0, 1, 1, 0], + [1, 0, 1, 1, 1], + [1, 0, 1, 1, 2], + [1, 0, 1, 2, 0], + [1, 0, 1, 2, 1], + [1, 0, 1, 2, 2], + [1, 0, 2, 0, 0], + [1, 0, 2, 0, 1], + [1, 0, 2, 0, 2], + [1, 0, 2, 1, 0], + [1, 0, 2, 1, 1], + [1, 0, 2, 1, 2], + [1, 0, 2, 2, 0], + [1, 0, 2, 2, 1], + [1, 0, 2, 2, 2], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 1], + [1, 1, 0, 0, 2], + [1, 1, 0, 1, 0], + [1, 1, 0, 1, 1], + [1, 1, 0, 1, 2], + [1, 1, 0, 2, 0], + [1, 1, 0, 2, 1], + [1, 1, 0, 2, 2], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 1], + [1, 1, 1, 0, 2], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 2], + [1, 1, 1, 2, 0], + [1, 1, 1, 2, 1], + [1, 1, 1, 2, 2], + [1, 1, 2, 0, 0], + [1, 1, 2, 0, 1], + [1, 1, 2, 0, 2], + [1, 1, 2, 1, 0], + [1, 1, 2, 1, 1], + [1, 1, 2, 1, 2], + [1, 1, 2, 2, 0], + [1, 1, 2, 2, 1], + [1, 1, 2, 2, 2], + [1, 2, 0, 0, 0], + [1, 2, 0, 0, 1], + [1, 2, 0, 0, 2], + [1, 2, 0, 1, 0], + [1, 2, 0, 1, 1], + [1, 2, 0, 1, 2], + [1, 2, 0, 2, 0], + [1, 2, 0, 2, 1], + [1, 2, 0, 2, 2], + [1, 2, 1, 0, 0], + [1, 2, 1, 0, 1], + [1, 2, 1, 0, 2], + [1, 2, 1, 1, 0], + [1, 2, 1, 1, 1], + [1, 2, 1, 1, 2], + [1, 2, 1, 2, 0], + [1, 2, 1, 2, 1], + [1, 2, 1, 2, 2], + [1, 2, 2, 0, 0], + [1, 2, 2, 0, 1], + [1, 2, 2, 0, 2], + [1, 2, 2, 1, 0], + [1, 2, 2, 1, 1], + [1, 2, 2, 1, 2], + [1, 2, 2, 2, 0], + [1, 2, 2, 2, 1], + [1, 2, 2, 2, 2], + [2, 0, 0, 0, 0], + [2, 0, 0, 0, 1], + [2, 0, 0, 0, 2], + [2, 0, 0, 1, 0], + [2, 0, 0, 1, 1], + [2, 0, 0, 1, 2], + [2, 0, 0, 2, 0], + [2, 0, 0, 2, 1], + [2, 0, 0, 2, 2], + [2, 0, 1, 0, 0], + [2, 0, 1, 0, 1], + [2, 0, 1, 0, 2], + [2, 0, 1, 1, 0], + [2, 0, 1, 1, 1], + [2, 0, 1, 1, 2], + [2, 0, 1, 2, 0], + [2, 0, 1, 2, 1], + [2, 0, 1, 2, 2], + [2, 0, 2, 0, 0], + [2, 0, 2, 0, 1], + [2, 0, 2, 0, 2], + [2, 0, 2, 1, 0], + [2, 0, 2, 1, 1], + [2, 0, 2, 1, 2], + [2, 0, 2, 2, 0], + [2, 0, 2, 2, 1], + [2, 0, 2, 2, 2], + [2, 1, 0, 0, 0], + [2, 1, 0, 0, 1], + [2, 1, 0, 0, 2], + [2, 1, 0, 1, 0], + [2, 1, 0, 1, 1], + [2, 1, 0, 1, 2], + [2, 1, 0, 2, 0], + [2, 1, 0, 2, 1], + [2, 1, 0, 2, 2], + [2, 1, 1, 0, 0], + [2, 1, 1, 0, 1], + [2, 1, 1, 0, 2], + [2, 1, 1, 1, 0], + [2, 1, 1, 1, 1], + [2, 1, 1, 1, 2], + [2, 1, 1, 2, 0], + [2, 1, 1, 2, 1], + [2, 1, 1, 2, 2], + [2, 1, 2, 0, 0], + [2, 1, 2, 0, 1], + [2, 1, 2, 0, 2], + [2, 1, 2, 1, 0], + [2, 1, 2, 1, 1], + [2, 1, 2, 1, 2], + [2, 1, 2, 2, 0], + [2, 1, 2, 2, 1], + [2, 1, 2, 2, 2], + [2, 2, 0, 0, 0], + [2, 2, 0, 0, 1], + [2, 2, 0, 0, 2], + [2, 2, 0, 1, 0], + [2, 2, 0, 1, 1], + [2, 2, 0, 1, 2], + [2, 2, 0, 2, 0], + [2, 2, 0, 2, 1], + [2, 2, 0, 2, 2], + [2, 2, 1, 0, 0], + [2, 2, 1, 0, 1], + [2, 2, 1, 0, 2], + [2, 2, 1, 1, 0], + [2, 2, 1, 1, 1], + [2, 2, 1, 1, 2], + [2, 2, 1, 2, 0], + [2, 2, 1, 2, 1], + [2, 2, 1, 2, 2], + [2, 2, 2, 0, 0], + [2, 2, 2, 0, 1], + [2, 2, 2, 0, 2], + [2, 2, 2, 1, 0], + [2, 2, 2, 1, 1], + [2, 2, 2, 1, 2], + [2, 2, 2, 2, 0], + [2, 2, 2, 2, 1], + [2, 2, 2, 2, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 2], + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 2], + [0, 0, 0, 2, 0], + [0, 0, 0, 2, 1], + [0, 0, 0, 2, 2], + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 1], + [0, 0, 1, 0, 2], + [0, 0, 1, 1, 0], + ] + + +def test_pop_bits(): + data = bitarray("1010101010101010") + assert mp7.pop_bits(data, 4, 8) == (170, 12) + + +def test_read_mp7_signature(mp4_file): + sig = idk.video_mp7sig_extract(mp4_file) + result = idk.read_mp7_signature(sig) + frame = result[-1] + assert isinstance(frame, mp7.Frame) + assert frame.confidence == 77 + assert frame.elapsed == Fraction(299, 5) + assert frame.vector.tolist() == [ + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 0, + 0, + 2, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 0, + 2, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 2, + 1, + 0, + 2, + 1, + 1, + 2, + 2, + 1, + 1, + 2, + 1, + 1, + 2, + 0, + 1, + 1, + 0, + 2, + 2, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 2, + 0, + 2, + 1, + 2, + 2, + 2, + 0, + 0, + 0, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 2, + 0, + 1, + 0, + 2, + 0, + 0, + 2, + 2, + 1, + 1, + 0, + 0, + 2, + 2, + 2, + 2, + 1, + 2, + 0, + 0, + 2, + 2, + 0, + 2, + 2, + 2, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 2, + 0, + 1, + 1, + 1, + 1, + 2, + 0, + 0, + 0, + 0, + 0, + 2, + 1, + 2, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 2, + 2, + 2, + 1, + 1, + 0, + 1, + 2, + 2, + 2, + 1, + 0, + 0, + 2, + 2, + 2, + 1, + 1, + 2, + 2, + 2, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 0, + 1, + 2, + 0, + 0, + 2, + 2, + 1, + 0, + 2, + 0, + 0, + 2, + 1, + 0, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 2, + 0, + 0, + 2, + 2, + 2, + 1, + 2, + 1, + 1, + 0, + 1, + 1, + 2, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 2, + 1, + 1, + 0, + 0, + 1, + 2, + 2, + 2, + 0, + 0, + 1, + 2, + 2, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 2, + 2, + 0, + 2, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 2, + 2, + 2, + 0, + 2, + 1, + 0, + 1, + 1, + 2, + 1, + 2, + 0, + 1, + 0, + 2, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 2, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 2, + 0, + 2, + 1, + 0, + 0, + 2, + 2, + 2, + 2, + 2, + 2, + 0, + 1, + 2, + 2, + 0, + 1, + 1, + 0, + 2, + 1, + 0, + 1, + 2, + 0, + 1, + 1, + 1, + 2, + 2, + 1, + 0, + 0, + 2, + 1, + 1, + 2, + 0, + 1, + 0, + 2, + 1, + 0, + 2, + 2, + 2, + 0, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 0, + 2, + 2, + ] diff --git a/tests/test_video.py b/tests/test_video.py index 1633d9b..e105237 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -69,3 +69,13 @@ def test_video_metadata_escaping(mp4_file): def test_video_thumbnail(mp4_file): thumb = idk.video_thumbnail(mp4_file) assert isinstance(thumb, Image.Image) + + +def test_video_mp7sig_extract(mp4_file): + sig = idk.video_mp7sig_extract(mp4_file) + assert sig[-32:].hex() == "9ef43526febb8d3e674975584ad6812ccc144cba28b3e134cd173888449cf51e" + + +def test_video_features_extract(mp4_file): + features = idk.video_features_extract(mp4_file) + assert features[0][:20] == (0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0)