├── .cruft.json ├── .flake8 ├── .github └── workflows │ └── publish_PYPI_each_tag.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── example_app ├── requirements.txt └── streamlit_app.py ├── pyproject.toml ├── requirements.txt ├── setup.py └── src └── camera_input_live ├── __init__.py └── frontend ├── index.html ├── main.js ├── streamlit-component-lib.js └── style.css /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/blackary/cookiecutter-streamlit-component/", 3 | "commit": "c972054b7b0890a1aa1c02e792bedc71c525ea87", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "author_name": "Zachary Blackwood", 8 | "author_email": "zachary@streamlit.io", 9 | "project_name": "Streamlit Camera Input Live", 10 | "package_name": "streamlit-camera-input-live", 11 | "import_name": "camera_input_live", 12 | "description": "Alternative version of st.camera_input which returns the webcam images live, without any button press needed", 13 | "deployment_via_github_actions": "y", 14 | "working_with_dataframes": "n", 15 | "open_source_license": "MIT license", 16 | "_template": "https://github.com/blackary/cookiecutter-streamlit-component/" 17 | } 18 | }, 19 | "directory": null 20 | } 21 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | select = 4 | E # pep8 errors 5 | F # pyflakes errors 6 | W # pep8 warnings 7 | B # flake8-bugbear warnings 8 | ignore = 9 | E501 # "Line lengths are recommended to be no greater than 79 characters" 10 | E203 # "Whitespace before ':'": conflicts with black 11 | W503 # "line break before binary operator": conflicts with black 12 | exclude = 13 | .git 14 | .vscode 15 | .pytest_cache 16 | .mypy_cache 17 | .venv 18 | .env 19 | .direnv 20 | per-file-ignores = -------------------------------------------------------------------------------- /.github/workflows/publish_PYPI_each_tag.yml: -------------------------------------------------------------------------------- 1 | 2 | # This workflows will upload a Python Package using Twine when a release is created 3 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 4 | 5 | name: Upload Python Package 6 | 7 | on: 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: "3.x" 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | pwd 31 | python setup.py sdist bdist_wheel 32 | twine upload dist/* 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # OSX useful to ignore 7 | *.DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | # C extensions 31 | *.so 32 | 33 | # Distribution / packaging 34 | .Python 35 | env/ 36 | venv/ 37 | build/ 38 | develop-eggs/ 39 | dist/ 40 | downloads/ 41 | eggs/ 42 | .eggs/ 43 | lib/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | 52 | # PyInstaller 53 | # Usually these files are written by a python script from a template 54 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 55 | *.manifest 56 | *.spec 57 | 58 | # Installer logs 59 | pip-log.txt 60 | pip-delete-this-directory.txt 61 | 62 | # Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *,cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Django stuff: 79 | *.log 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # IntelliJ Idea family of suites 85 | .idea 86 | *.iml 87 | ## File-based project format: 88 | *.ipr 89 | *.iws 90 | ## mpeltonen/sbt-idea plugin 91 | .idea_modules/ 92 | 93 | # PyBuilder 94 | target/ 95 | 96 | # Cookiecutter 97 | output/ 98 | python_boilerplate/ 99 | cookiecutter-pypackage-env/ 100 | 101 | # IDE settings 102 | .vscode/ 103 | 104 | # direnv 105 | .envrc 106 | .direnv/ 107 | 108 | # streamlit 109 | .streamlit/secrets.toml 110 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.8.3 4 | hooks: 5 | - id: ruff 6 | args: 7 | - --fix 8 | - id: ruff-format 9 | - repo: https://github.com/pre-commit/mirrors-mypy 10 | rev: v1.13.0 11 | hooks: 12 | - id: mypy 13 | args: 14 | - --ignore-missing-imports 15 | - --follow-imports=silent 16 | - repo: https://github.com/pre-commit/pre-commit-hooks 17 | rev: v5.0.0 # Use the ref you want to point at 18 | hooks: 19 | - id: trailing-whitespace 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022, Zachary Blackwood 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 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/camera_input_live/frontend * 2 | include README.md 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit-camera-input-live 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/streamlit-camera-input-live.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/streamlit-camera-input-live/) 4 | [![PyPI downloads](https://img.shields.io/pypi/dm/streamlit-camera-input-live.svg)](https://pypistats.org/packages/streamlit-camera-input-live) 5 | [![GitHub](https://img.shields.io/github/license/blackary/streamlit-camera-input-live.svg)](LICENSE) 6 | [![Code style: Black](https://img.shields.io/badge/code%20style-Black-000000.svg)](https://github.com/psf/black) 7 | 8 | Alternative version of st.camera_input which returns the webcam images live, without any button press needed 9 | 10 | [![Open in Streamlit](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://camera.streamlitapp.com) 11 | 12 | ## Installation instructions 13 | 14 | ```sh 15 | pip install streamlit-camera-input-live 16 | ``` 17 | 18 | ## Usage instructions 19 | 20 | ```python 21 | import streamlit as st 22 | 23 | from camera_input_live import camera_input_live 24 | 25 | image = camera_input_live() 26 | 27 | if image: 28 | st.image(image) 29 | ``` 30 | -------------------------------------------------------------------------------- /example_app/requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit-camera-input-live 2 | opencv-python-headless -------------------------------------------------------------------------------- /example_app/streamlit_app.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import streamlit as st 4 | 5 | from camera_input_live import camera_input_live 6 | 7 | "# Streamlit camera input live Demo" 8 | "## Try holding a qr code in front of your webcam" 9 | 10 | image = camera_input_live() 11 | 12 | if image is not None: 13 | st.image(image) 14 | bytes_data = image.getvalue() 15 | cv2_img = cv2.imdecode(np.frombuffer(bytes_data, np.uint8), cv2.IMREAD_COLOR) 16 | 17 | detector = cv2.QRCodeDetector() 18 | 19 | data, bbox, straight_qrcode = detector.detectAndDecode(cv2_img) 20 | 21 | if data: 22 | st.write("# Found QR code") 23 | st.write(data) 24 | with st.expander("Show details"): 25 | st.write("BBox:", bbox) 26 | st.write("Straight QR code:", straight_qrcode) 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | exclude = [ 3 | ".git", 4 | ".vscode", 5 | ".pytest_cache", 6 | ".bamboo", 7 | ".tecton", 8 | ".mypy_cache", 9 | ".env", 10 | ] 11 | ignore = [ 12 | "B008", 13 | "ISC001", 14 | "E501", 15 | "W191" 16 | ] 17 | line-length = 88 18 | select = [ 19 | "B", # https://pypi.org/project/flake8-bugbear/ 20 | "E", # https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes 21 | "F", # https://flake8.pycqa.org/en/latest/user/error-codes.html 22 | "W", # https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes 23 | "I", # https://pycqa.github.io/isort/ 24 | "N", # https://github.com/PyCQA/pep8-naming 25 | "C4", # https://github.com/adamchainz/flake8-comprehensions 26 | "EXE", # https://pypi.org/project/flake8-executable/ 27 | "ISC", # https://github.com/flake8-implicit-str-concat/flake8-implicit-str-concat 28 | "ICN", # https://pypi.org/project/flake8-import-conventions/ 29 | "PIE", # https://pypi.org/project/flake8-pie/ 30 | "PT", # https://github.com/m-burst/flake8-pytest-style 31 | "RET", # https://pypi.org/project/flake8-return/ 32 | "SIM", # https://pypi.org/project/flake8-simplify/ 33 | "ERA", # https://pypi.org/project/flake8-eradicate/ 34 | "PLC", # https://beta.ruff.rs/docs/rules/#convention-plc 35 | "RUF", # https://beta.ruff.rs/docs/rules/#ruff-specific-rules-ruf 36 | "ARG", # https://beta.ruff.rs/docs/rules/#flake8-unused-arguments-arg 37 | ] 38 | 39 | [tool.ruff.per-file-ignores] 40 | "__init__.py" = ["F401"] 41 | 42 | [tool.mypy] 43 | files = [ 44 | "**/*.py", 45 | ] 46 | follow_imports = "silent" 47 | ignore_missing_imports = true 48 | scripts_are_modules = true 49 | python_version = 3.9 50 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import setuptools 4 | 5 | this_directory = Path(__file__).parent 6 | long_description = (this_directory / "README.md").read_text() 7 | 8 | setuptools.setup( 9 | name="streamlit-camera-input-live", 10 | version="0.2.0", 11 | author="Zachary Blackwood", 12 | author_email="zachary@streamlit.io", 13 | description="Alternative version of st.camera_input which returns the webcam images live, without any button press needed", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | packages=setuptools.find_packages(where="src"), 17 | package_dir={"": "src"}, 18 | include_package_data=True, 19 | classifiers=[], 20 | python_requires=">=3.7", 21 | install_requires=["streamlit>=1.2", "jinja2"], 22 | url="https://github.com/blackary/streamlit-camera-input-live", 23 | ) 24 | -------------------------------------------------------------------------------- /src/camera_input_live/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | import streamlit as st 7 | import streamlit.components.v1 as components 8 | 9 | # Tell streamlit that there is a component called camera_input_live, 10 | # and that the code to display that component is in the "frontend" folder 11 | frontend_dir = (Path(__file__).parent / "frontend").absolute() 12 | _component_func = components.declare_component( 13 | "camera_input_live", path=str(frontend_dir) 14 | ) 15 | 16 | 17 | def camera_input_live( 18 | debounce: int = 1000, 19 | height: int = 530, 20 | width: int = 704, 21 | key: Optional[str] = None, 22 | show_controls: bool = True, 23 | start_label: str = "Start capturing", 24 | stop_label: str = "Pause capturing", 25 | ) -> Optional[BytesIO]: 26 | """ 27 | Add a descriptive docstring 28 | """ 29 | b64_data: Optional[str] = _component_func( 30 | height=height, 31 | width=width, 32 | debounce=debounce, 33 | showControls=show_controls, 34 | startLabel=start_label, 35 | stopLabel=stop_label, 36 | key=key, 37 | ) 38 | 39 | if b64_data is None: 40 | return None 41 | 42 | raw_data = b64_data.split(",")[1] # Strip the data: type prefix 43 | 44 | return BytesIO(base64.b64decode(raw_data)) 45 | 46 | 47 | def main(): 48 | st.write("## Example") 49 | 50 | image = camera_input_live(show_controls=True) 51 | 52 | if image is not None: 53 | st.image(image) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /src/camera_input_live/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | streamlit-camera-input-live 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/camera_input_live/frontend/main.js: -------------------------------------------------------------------------------- 1 | // The `Streamlit` object exists because our html file includes 2 | // `streamlit-component-lib.js`. 3 | // If you get an error about "Streamlit" not being defined, that 4 | // means you're missing that file. 5 | 6 | function sendValue(value) { 7 | Streamlit.setComponentValue(value) 8 | } 9 | 10 | 11 | 12 | /** 13 | * The component's render function. This will be called immediately after 14 | * the component is initially loaded, and then again every time the 15 | * component gets new data from Python. 16 | */ 17 | function onRender(event) { 18 | // Only run the render code the first time the component is loaded. 19 | if (!window.rendered) { 20 | // You most likely want to get the data passed in like this 21 | const {height, width, debounce, showControls, startLabel, stopLabel} = event.detail.args 22 | 23 | if (showControls) { 24 | Streamlit.setFrameHeight(45) 25 | } 26 | 27 | if (isNaN(height)) { 28 | height = width / (4/3); 29 | } 30 | 31 | let video = document.getElementById('video'); 32 | let canvas = document.getElementById('canvas'); 33 | let button = document.getElementById('button'); 34 | 35 | let stopped = false; 36 | 37 | video.setAttribute('width', width); 38 | video.setAttribute('height', height); 39 | canvas.setAttribute('width', width); 40 | canvas.setAttribute('height', height); 41 | 42 | function takepicture() { 43 | if (stopped) { 44 | return; 45 | } 46 | let context = canvas.getContext('2d'); 47 | canvas.width = width; 48 | canvas.height = height; 49 | context.drawImage(video, 0, 0, width, height); 50 | 51 | var data = canvas.toDataURL('image/png'); 52 | sendValue(data); 53 | } 54 | 55 | 56 | function stopVideo() { 57 | video.pause(); 58 | video.srcObject.getTracks()[0].stop(); 59 | stopped = true; 60 | } 61 | 62 | function startVideo() { 63 | navigator.mediaDevices.getUserMedia({ video: true }) 64 | .then(function(stream) { 65 | video.srcObject = stream; 66 | video.play(); 67 | }) 68 | .catch(function(err) { 69 | console.log("An error occurred: " + err); 70 | }); 71 | } 72 | 73 | function toggleVideo() { 74 | if (stopped) { 75 | startVideo(); 76 | stopped = false; 77 | } else { 78 | stopVideo(); 79 | stopped = true; 80 | } 81 | // Toggle the button text 82 | button.textContent = stopped ? startLabel : stopLabel; 83 | } 84 | 85 | if (navigator.mediaDevices.getUserMedia) { 86 | navigator.mediaDevices 87 | .getUserMedia({ video: true }) 88 | .then(function (stream) { 89 | video.srcObject = stream; 90 | }) 91 | .catch(function (error) { 92 | console.log("Something went wrong!"); 93 | console.error(error); 94 | }); 95 | } 96 | 97 | button.addEventListener('click', toggleVideo); 98 | button.textContent = stopped ? startLabel : stopLabel; 99 | 100 | takepicture(); 101 | setInterval(takepicture, debounce); 102 | window.rendered = true 103 | } 104 | } 105 | 106 | // Render the component whenever python send a "render event" 107 | Streamlit.events.addEventListener(Streamlit.RENDER_EVENT, onRender) 108 | // Tell Streamlit that the component is ready to receive events 109 | Streamlit.setComponentReady() 110 | // Don't actually need to display anything, so set the height to 0 111 | Streamlit.setFrameHeight(0) 112 | -------------------------------------------------------------------------------- /src/camera_input_live/frontend/streamlit-component-lib.js: -------------------------------------------------------------------------------- 1 | 2 | // Borrowed minimalistic Streamlit API from Thiago 3 | // https://discuss.streamlit.io/t/code-snippet-create-components-without-any-frontend-tooling-no-react-babel-webpack-etc/13064 4 | function sendMessageToStreamlitClient(type, data) { 5 | console.log(type, data) 6 | const outData = Object.assign({ 7 | isStreamlitMessage: true, 8 | type: type, 9 | }, data); 10 | window.parent.postMessage(outData, "*"); 11 | } 12 | 13 | const Streamlit = { 14 | setComponentReady: function() { 15 | sendMessageToStreamlitClient("streamlit:componentReady", {apiVersion: 1}); 16 | }, 17 | setFrameHeight: function(height) { 18 | sendMessageToStreamlitClient("streamlit:setFrameHeight", {height: height}); 19 | }, 20 | setComponentValue: function(value) { 21 | sendMessageToStreamlitClient("streamlit:setComponentValue", {value: value}); 22 | }, 23 | RENDER_EVENT: "streamlit:render", 24 | events: { 25 | addEventListener: function(type, callback) { 26 | window.addEventListener("message", function(event) { 27 | if (event.data.type === type) { 28 | event.detail = event.data 29 | callback(event); 30 | } 31 | }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/camera_input_live/frontend/style.css: -------------------------------------------------------------------------------- 1 | button { 2 | text-size-adjust: 100%; 3 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 4 | -webkit-font-smoothing: auto; 5 | box-sizing: border-box; 6 | font-size: inherit; 7 | font-family: inherit; 8 | overflow: visible; 9 | text-transform: none; 10 | appearance: button; 11 | display: inline-flex; 12 | -webkit-box-align: center; 13 | align-items: center; 14 | -webkit-box-pack: center; 15 | justify-content: center; 16 | font-weight: 400; 17 | padding: 0.25rem 0.75rem; 18 | border-radius: 0.25rem; 19 | margin: 0px; 20 | line-height: 1.6; 21 | color: inherit; 22 | width: auto; 23 | user-select: none; 24 | background-color: rgb(255, 255, 255); 25 | border: 1px solid rgba(49, 51, 63, 0.2); 26 | cursor: pointer; 27 | position: absolute; 28 | left: 0; 29 | } 30 | 31 | button:hover { 32 | border-color: rgb(255, 75, 75); 33 | color: rgb(255, 75, 75); 34 | } 35 | button:active { 36 | color: rgb(255, 255, 255); 37 | border-color: rgb(255, 75, 75); 38 | background-color: rgb(255, 75, 75); 39 | } 40 | 41 | .padding { 42 | height: 100px; 43 | } 44 | --------------------------------------------------------------------------------