master
Snytkin 6 months ago
commit b0f42c9e8f

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

@ -0,0 +1,32 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="BufCLINotInstalled" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="3.13" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="grpc" />
<item index="1" class="java.lang.String" itemvalue="ts" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N812" />
</list>
</option>
</inspection_tool>
</profile>
</component>

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/VideoCapture.iml" filepath="$PROJECT_DIR$/.idea/VideoCapture.iml" />
</modules>
</component>
</project>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/MotionDetector/motion_detection" vcs="Git" />
<mapping directory="$PROJECT_DIR$/VideoRegister" vcs="Git" />
</component>
</project>

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="a57321da-a595-4a34-b1ef-01654ff4f52a" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/MotionDetector/motion_detection/src/motion_detection/no_opencv/MotionDetector.py" beforeDir="false" afterPath="$PROJECT_DIR$/MotionDetector/motion_detection/src/motion_detection/no_opencv/MotionDetector.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/MotionDetector/motion_detection/src/motion_detection/opencv/MotionDetector.py" beforeDir="false" afterPath="$PROJECT_DIR$/MotionDetector/motion_detection/src/motion_detection/opencv/MotionDetector.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/VideoRegister/backend/Dockerfile" beforeDir="false" afterPath="$PROJECT_DIR$/VideoRegister/backend/Dockerfile" afterDir="false" />
<change beforePath="$PROJECT_DIR$/VideoRegister/backend/app/motion_detector.py" beforeDir="false" afterPath="$PROJECT_DIR$/VideoRegister/backend/app/motion_detector.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/VideoRegister/backend/tests/test_app.py" beforeDir="false" afterPath="$PROJECT_DIR$/VideoRegister/backend/tests/test_app.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/VideoRegister/k8s/backend-deployment.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/VideoRegister/k8s/backend-deployment.yaml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/VideoRegister" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 4
}]]></component>
<component name="ProjectId" id="2mm9K2B9rpFzCJvNfP1FlqTuXgw" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Python tests.pytest for test_app.test_motion_events.executor": "Run",
"Python tests.pytest for test_app.test_start_stream.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "video__register__no__ai",
"last_opened_file_path": "C:/Users/snytk/VideoCapture/Simple_video_register/backend"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\snytk\VideoCapture\Simple_video_register\backend" />
</key>
</component>
<component name="RunManager" selected="Python tests.pytest for test_app.test_start_stream">
<configuration name="pytest for test_app.test_motion_events" type="tests" factoryName="py.test" temporary="true" nameIsGenerated="true">
<module name="VideoCapture" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/VideoRegister/backend/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;test_app.test_motion_events&quot;" />
<option name="_new_targetType" value="&quot;PYTHON&quot;" />
<method v="2" />
</configuration>
<configuration name="pytest for test_app.test_start_stream" type="tests" factoryName="py.test" temporary="true" nameIsGenerated="true">
<module name="VideoCapture" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Simple_video_register/backend/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;test_app.test_start_stream&quot;" />
<option name="_new_targetType" value="&quot;PYTHON&quot;" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Python tests.pytest for test_app.test_start_stream" />
<item itemvalue="Python tests.pytest for test_app.test_motion_events" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-50da183f06c8-d3b881c8e49f-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-233.13135.95" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="a57321da-a595-4a34-b1ef-01654ff4f52a" name="Changes" comment="" />
<created>1727670817034</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1727670817034</updated>
</task>
<servers />
</component>
</project>

@ -0,0 +1 @@
Subproject commit 2ff71b19839d5b0dd14c43d0fd55bd1a37abee38

@ -0,0 +1,15 @@
# Multi-Camera Surveillance Project
## Overview
This project enables you to connect up to 24 cameras, choose the grid size for video feeds dynamically, and manage the camera settings through a Vue.js frontend with a Flask backend.
### How to Run
1. Install Docker and Docker Compose.
2. Run `docker-compose up --build` in the project directory.
3. Access the frontend at `http://localhost:8080`.
4. Access the backend API documentation at `http://localhost:5000/swagger`.
### Features
- Add and remove cameras.
- Dynamic grid size based on the number of cameras.
- Simple motion detection placeholder (to be implemented).

@ -0,0 +1,10 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-b", "0.0.0.0:5000", "app.main:app"]

@ -0,0 +1 @@
# Empty file to initialize the module

@ -0,0 +1,20 @@
from flask import Flask
from flask_restful import Api
from flask_cors import CORS
from app.routes.camera import Camera
from app.routes.detection import Detection
app = Flask(__name__)
CORS(app)
api = Api(app)
# Add routes
api.add_resource(Camera, '/api/camera')
api.add_resource(Detection, '/api/detection')
@app.route('/swagger')
def swagger_ui():
return app.send_static_file('swagger.yaml')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

@ -0,0 +1,25 @@
from flask import request, jsonify
from flask_restful import Resource
class Camera(Resource):
cameras = []
def post(self):
data = request.json
rtsp_url = data.get('rtsp_url')
if len(self.cameras) < 24:
self.cameras.append({'id': len(self.cameras), 'rtsp_url': rtsp_url})
return jsonify({"message": "Camera added", "camera_id": len(self.cameras) - 1}), 200
else:
return jsonify({"message": "Maximum number of cameras reached"}), 400
def delete(self):
data = request.json
camera_id = data.get('camera_id')
if 0 <= camera_id < len(self.cameras):
self.cameras.pop(camera_id)
return jsonify({"message": "Camera removed"}), 200
return jsonify({"message": "Camera ID not found"}), 404
def get(self):
return jsonify({"cameras": self.cameras})

@ -0,0 +1,6 @@
from flask_restful import Resource
class Detection(Resource):
def get(self):
# Placeholder for motion and abandoned object detection
return {"message": "Detection logic not implemented"}, 200

@ -0,0 +1,22 @@
import cv2
class MotionDetector:
def __init__(self):
self.previous_frame = None
def detect_motion(self, frame):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (21, 21), 0)
if self.previous_frame is None:
self.previous_frame = gray
return False
frame_diff = cv2.absdiff(self.previous_frame, gray)
thresh = cv2.threshold(frame_diff, 25, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.dilate(thresh, None, iterations=2)
contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
self.previous_frame = gray
return len(contours) > 0

@ -0,0 +1,12 @@
import cv2
class VideoStream:
def __init__(self, rtsp_url):
self.rtsp_url = rtsp_url
self.stream = cv2.VideoCapture(rtsp_url)
def get_frame(self):
ret, frame = self.stream.read()
if not ret:
return None
return frame

@ -0,0 +1,8 @@
Flask
flask-restful
opencv-python-headless
numpy
flask-cors
gunicorn
sqlalchemy
pymysql

@ -0,0 +1,60 @@
import pytest
from app import app, db, VideoLog, MotionEvent, LeftItemEvent
@pytest.fixture
def client():
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
with app.app_context():
db.drop_all()
def test_start_stream(client):
response = client.post('/video/start_stream/camera1', json={'rtsp_url': 'rtsp://example.com/stream'})
assert response.status_code == 200
assert b'stream started' in response.data
def test_stop_stream(client):
client.post('/video/start_stream/camera1', json={'rtsp_url': 'rtsp://example.com/stream'})
response = client.post('/video/stop_stream/camera1')
assert response.status_code == 200
assert b'stream stopped' in response.data
def test_video_logs(client):
log = VideoLog(camera_id='camera1', event_type='motion', video_path='path/to/video.mp4')
db.session.add(log)
db.session.commit()
response = client.get('/video/video_logs')
assert response.status_code == 200
assert b'camera1' in response.data
def test_motion_events(client):
event = MotionEvent(camera_id='camera1', description='Motion detected', video_path='path/to/video.mp4')
db.session.add(event)
db.session.commit()
response = client.get('/video/motion_events')
assert response.status_code == 200
assert b'camera1' in response.data
def test_left_item_events(client):
event = LeftItemEvent(camera_id='camera1', item_description='Item left', video_path='path/to/video.mp4')
db.session.add(event)
db.session.commit()
response = client.get('/video/left_item_events')
assert response.status_code == 200
assert b'camera1' in response.data

@ -0,0 +1,11 @@
version: '3.7'
services:
frontend:
container_name: frontend
build:
context: ./frontend
dockerfile: Dockerfile-prod
ports:
- '80:80'

@ -0,0 +1,17 @@
version: '3.8'
services:
backend:
build: ./backend
ports:
- "5000:5000"
volumes:
- ./backend:/app
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
volumes:
- './frontend:/src'
- '/src/node_modules'
ports:
- "80:80"

@ -0,0 +1,3 @@
node_modules
.git
.gitignore

@ -0,0 +1,16 @@
# base image
FROM node:12.2.0-alpine
# set working directory
WORKDIR /src
# add `/app/node_modules/.bin` to $PATH
ENV key=/src/node_modules/.bin:$PATH
# install and cache app dependencies
COPY package.json /src/package.json
RUN npm install
RUN npm install @vue/cli@3.7.0 -g
# start app
CMD ["npm", "run", "serve"]

@ -0,0 +1,15 @@
# build environment
FROM node:12.2.0-alpine
WORKDIR /src
ENV key=/src/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install @vue/cli@3.7.0 -g
COPY . /app
RUN npm run build
# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

@ -0,0 +1,18 @@
{
"name": "frontend",
"version": "1.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"vue": "^2.6.11"
},
"devDependencies": {
"@vue/cli-service": "^4.5.0",
"vue-template-compiler": "^2.5.17"
}
}

@ -0,0 +1,83 @@
<template>
<div id="app">
<div class="left-panel">
<Settings :cameras="cameras" @add-camera="addCamera" @remove-camera="removeCamera" />
</div>
<div class="right-panel">
<CameraGrid :cameras="cameras" />
</div>
</div>
</template>
<script>
import axios from 'axios';
import Settings from './components/Settings.vue';
import CameraGrid from './components/CameraGrid.vue';
export default {
components: {
Settings,
CameraGrid,
},
data() {
return {
cameras: [],
};
},
methods: {
addCamera(camera) {
this.cameras.push(camera);
},
removeCamera(index) {
this.cameras.splice(index, 1);
},
},
mounted() {
axios.get('/api/camera')
.then(response => {
this.cameras = response.data.cameras;
})
.catch(error => {
console.error(error);
});
},
};
</script>
<style>
#app {
display: flex;
justify-content: space-between;
padding: 20px;
height: 100vh;
background-color: #f4f4f9;
font-family: Avenir, Helvetica, Arial, sans-serif;
}
.left-panel, .right-panel {
width: 48%;
padding: 20px;
background-color: #ffffff;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.left-panel {
background-color: #e3f2fd; /* Синий оттенок */
}
.right-panel {
background-color: #ffebee; /* Красный оттенок */
}
@media (max-width: 768px) {
#app {
flex-direction: column;
}
.left-panel, .right-panel {
width: 100%;
margin-bottom: 20px;
}
}
</style>

@ -0,0 +1,40 @@
<template>
<div class="camera-grid" :style="gridStyle">
<CameraView v-for="(camera, index) in cameras" :key="index" :rtsp-url="camera.rtsp_url" />
</div>
</template>
<script>
import CameraView from './CameraView.vue';
export default {
components: {
CameraView,
},
props: {
cameras: Array,
},
computed: {
gridStyle() {
const cameraCount = this.cameras.length;
const columns = Math.ceil(Math.sqrt(cameraCount));
return {
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: '10px',
};
},
},
};
</script>
<style scoped>
.camera-grid {
width: 100%;
height: 100%;
background-color: #fafafa;
border-radius: 15px;
padding: 10px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
}
</style>

@ -0,0 +1,31 @@
<template>
<div class="camera-view">
<video ref="video" autoplay muted controls></video>
</div>
</template>
<script>
export default {
props: {
rtspUrl: String,
},
mounted() {
this.startStream();
},
methods: {
startStream() {
const videoElement = this.$refs.video;
videoElement.src = this.rtspUrl; // For simplicity, use direct URL, handled by the backend.
},
},
};
</script>
<style scoped>
.camera-view {
width: 100%;
height: 100%;
background-color: #000;
}
</style>

@ -0,0 +1,71 @@
<template>
<div class="settings">
<h2>Camera Settings</h2>
<div class="input-group">
<label for="camera-url">Camera RTSP URL:</label>
<input type="text" v-model="cameraUrl" placeholder="Enter RTSP URL">
<button @click="addCamera">Add Camera</button>
</div>
<ul>
<li v-for="(camera, index) in cameras" :key="index">
{{ camera.rtsp_url }}
<button @click="removeCamera(index)">Remove</button>
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios';
export default {
props: {
cameras: Array,
},
data() {
return {
cameraUrl: '',
};
},
methods: {
addCamera() {
if (this.cameraUrl) {
axios.post('/api/camera', { rtsp_url: this.cameraUrl })
.then(response => {
this.$emit('add-camera', { id: response.data.camera_id, rtsp_url: this.cameraUrl });
this.cameraUrl = '';
})
.catch(error => {
console.error(error);
});
}
},
removeCamera(index) {
axios.delete('/api/camera', { data: { camera_id: index } })
.then(response => {
this.$emit('remove-camera', index);
})
.catch(error => {
console.error(error);
});
},
},
};
</script>
<style scoped>
.settings {
padding: 10px;
background-color: #ffffff;
border-radius: 15px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
}
.input-group {
margin-bottom: 10px;
}
h2 {
color: #1976d2;
}
</style>

@ -0,0 +1,8 @@
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');

@ -0,0 +1,17 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

@ -0,0 +1,44 @@
name: CI Pipeline
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install Python dependencies
run: |
cd backend
pip install -r requirements.txt
- name: Run backend tests
run: |
cd backend
pytest
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install frontend dependencies
run: |
cd frontend
npm install
- name: Run frontend tests
run: |
cd frontend
npm run test

@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]

@ -0,0 +1,45 @@
from flask import Flask, Response, request, jsonify
from camera import Camera
from database import log_event, get_all_events
import threading
app = Flask(__name__)
# Хранилище камер и событий детекции
cameras = {}
motion_detected = []
@app.route('/add_camera', methods=['POST'])
def add_camera():
camera_id = request.json['id']
if camera_id not in cameras:
cameras[camera_id] = Camera(camera_id)
threading.Thread(target=cameras[camera_id].start_stream, args=(motion_detected,)).start()
return jsonify({"message": "Camera added"}), 200
return jsonify({"message": "Camera already exists"}), 400
@app.route('/remove_camera', methods=['POST'])
def remove_camera():
camera_id = request.json['id']
if camera_id in cameras:
cameras[camera_id].stop_stream()
del cameras[camera_id]
return jsonify({"message": "Camera removed"}), 200
return jsonify({"message": "Camera not found"}), 404
@app.route('/stream/<camera_id>')
def stream(camera_id):
if camera_id in cameras:
return Response(cameras[camera_id].get_frame(), mimetype='multipart/x-mixed-replace; boundary=frame')
return "Camera not found", 404
@app.route('/events', methods=['GET'])
def get_events():
return jsonify(get_all_events()), 200
@app.route('/motion', methods=['GET'])
def motion():
return jsonify(motion_detected), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

@ -0,0 +1,36 @@
import cv2
from motion_detection import detect_motion
from database import log_event
class Camera:
def __init__(self, camera_id, source=0):
self.camera_id = camera_id
self.capture = cv2.VideoCapture(source)
self.is_running = True
def start_stream(self):
background_frame = None
while self.is_running:
ret, frame = self.capture.read()
if not ret:
break
if background_frame is None:
background_frame = frame
moving_objects = detect_motion(frame, background_frame)
if moving_objects:
log_event(self.camera_id, moving_objects)
def get_frame(self):
while self.is_running:
ret, frame = self.capture.read()
if ret:
_, buffer = cv2.imencode('.jpg', frame)
frame = buffer.tobytes()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n')
def stop_stream(self):
self.is_running = False
self.capture.release()

@ -0,0 +1,19 @@
import sqlite3
from datetime import datetime
def log_event(camera_id, objects):
conn = sqlite3.connect('events.db')
c = conn.cursor()
for obj in objects:
c.execute("INSERT INTO events (camera_id, timestamp, object) VALUES (?, ?, ?)",
(camera_id, datetime.now(), str(obj)))
conn.commit()
conn.close()
def get_all_events():
conn = sqlite3.connect('events.db')
c = conn.cursor()
c.execute("SELECT * FROM events")
events = c.fetchall()
conn.close()
return events

@ -0,0 +1,15 @@
import cv2
def detect_motion(frame, background_frame):
diff = cv2.absdiff(background_frame, frame)
gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
_, thresh = cv2.threshold(blur, 20, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
moving_objects = []
for contour in contours:
if cv2.contourArea(contour) > 500:
x, y, w, h = cv2.boundingRect(contour)
moving_objects.append((x, y, w, h))
return moving_objects

@ -0,0 +1,30 @@
version: '3'
services:
backend:
build: ./backend
ports:
- "5000:5000"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
volumes:
- './frontend:/src'
- '/src/node_modules'
ports:
- "80:80"
db:
image: postgres
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3000:3000"

@ -0,0 +1,28 @@
# FROM node:14-alpine
# WORKDIR /app
# COPY . .
# RUN npm install
# RUN npm run build
# EXPOSE 8080
# CMD ["npm", "run", "serve"]
# base image
FROM node:12.2.0-alpine
# set working directory
WORKDIR /src
# add `/app/node_modules/.bin` to $PATH
ENV key=/src/node_modules/.bin:$PATH
# install and cache app dependencies
COPY package.json /src/package.json
RUN npm install
RUN npm install @vue/cli@3.7.0 -g
RUN npm run build
EXPOSE 80
# start app
CMD ["npm", "run", "serve"]

@ -0,0 +1,12 @@
{
"name": "camera-stream",
"version": "1.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"vue": "^2.6.12"
}
}

@ -0,0 +1,60 @@
<template>
<div id="app">
<h1>All Cameras</h1>
<CameraGrid
:cameras="cameras"
:motion-detected-cameras="[]"
@add-camera="handleAddCamera"
@remove-camera="handleRemoveCamera"
/>
<h1>Cameras with Motion Detection</h1>
<CameraGrid
:cameras="motionDetectedCameras"
:motion-detected-cameras="motionDetectedCameras"
@add-camera="handleAddCamera"
@remove-camera="handleRemoveCamera"
/>
</div>
</template>
<script>
import CameraGrid from './components/CameraGrid.vue';
export default {
components: {
CameraGrid
},
data() {
return {
cameras: [],
motionDetectedCameras: []
}
},
methods: {
handleAddCamera(cameraId) {
if (!this.cameras.includes(cameraId)) {
this.cameras.push(cameraId);
}
},
handleRemoveCamera(cameraId) {
this.cameras = this.cameras.filter(id => id !== cameraId);
this.motionDetectedCameras = this.motionDetectedCameras.filter(id => id !== cameraId);
}
}
}
</script>
<style>
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
</style>

@ -0,0 +1,48 @@
<template>
<div class="camera-block" :class="{ 'motion': isMotionDetected }">
<img :src="`/api/stream/${cameraId}`" alt="Camera stream" />
<button class="remove-button" @click="$emit('remove')">x</button>
</div>
</template>
<script>
export default {
props: ['cameraId', 'isMotionDetected']
}
</script>
<style scoped>
.camera-block {
position: relative;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.camera-block img {
width: 100%;
border-radius: 10px;
}
.remove-button {
position: absolute;
top: 10px;
left: 10px;
background-color: red;
color: white;
border: none;
padding: 5px;
border-radius: 50%;
cursor: pointer;
display: none;
}
.camera-block:hover .remove-button {
display: block;
}
.motion {
border: 2px solid green;
}
</style>

@ -0,0 +1,23 @@
<template>
<div>
<input v-model="cameraId" placeholder="Camera ID" />
<button @click="addCamera">Add Camera</button>
</div>
</template>
<script>
export default {
data() {
return {
cameraId: ''
}
},
methods: {
addCamera() {
this.$emit('add-camera', this.cameraId);
this.cameraId = '';
}
}
}
</script>

@ -0,0 +1,57 @@
<template>
<div class="camera-grid">
<!-- Отображение всех камер -->
<CameraBlock
v-for="cameraId in cameras"
:key="cameraId"
:camera-id="cameraId"
:is-motion-detected="motionDetectedCameras.includes(cameraId)"
@remove="removeCamera(cameraId)"
/>
<!-- Блок для добавления новой камеры -->
<div class="add-camera-block">
<button @click="addCamera">+</button>
</div>
</div>
</template>
<script>
import CameraBlock from './CameraBlock.vue';
export default {
components: { CameraBlock },
props: ['cameras', 'motionDetectedCameras'],
methods: {
addCamera() {
this.$emit('add-camera');
},
removeCamera(cameraId) {
this.$emit('remove-camera', cameraId);
}
}
}
</script>
<style scoped>
.camera-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.add-camera-block {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
border: 2px dashed #ccc;
border-radius: 10px;
}
.add-camera-block button {
font-size: 24px;
cursor: pointer;
}
</style>

@ -0,0 +1,13 @@
<template>
<div>
<h2>Camera {{ cameraId }}</h2>
<img :src="`/api/stream/${cameraId}`" alt="Stream" />
</div>
</template>
<script>
export default {
props: ['cameraId'],
}
</script>

@ -0,0 +1,6 @@
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App),
}).$mount('#app')

@ -0,0 +1 @@
Subproject commit 7c0d239e48a11cb620fe18eb10e9750c181a6134
Loading…
Cancel
Save