diff --git a/.DS_Store b/.DS_Store index e5e78613b23e934a047b9c7bd5932dd06a1d227e..7dd6299318eff92ea60bc577e86e374643134dfe 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 335e27633579b4ff3acacfd55986737f1e233e13..ef690c9b96775a43a5d9430ce335df3b62ed52ec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ appstore/static/zamba/train/videos/ process-result.json process-result-dryrun.json -status.json \ No newline at end of file +status.json + +megedetector/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 2856a7502033c46d70fb3c0dad352ec8cb878d06..3037f46ffda698b3fcf7bda51db6fbe921998eed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "trapper"] - path = trapper - url = https://gitlab.com/trapper-project/trapper.git -[submodule "animl-frontend"] - path = animl-frontend - url = https://github.com/tnc-ca-geo/animl-frontend.git +# [submodule "trapper"] +# path = trapper +# url = https://gitlab.com/trapper-project/trapper.git +# [submodule "animl-frontend"] +# path = animl-frontend +# url = https://github.com/tnc-ca-geo/animl-frontend.git diff --git a/Dockerfile b/Dockerfile index 965f1864fb9f6f8555d4192cb6d32061139dbb34..8078425594a090464e53808a5b50c101a3a42e41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,8 @@ RUN apt-get update && apt-get install -y \ gcc \ g++ +RUN apt-get update && apt-get install -y docker.io docker-compose + # Set the working directory in the container WORKDIR /app diff --git a/animl-frontend b/animl-frontend deleted file mode 160000 index 5189331b82cd04851caf7ae4e0645df9ffcca0aa..0000000000000000000000000000000000000000 --- a/animl-frontend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5189331b82cd04851caf7ae4e0645df9ffcca0aa diff --git a/appstore/.DS_Store b/appstore/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f813590c606f8c51ea6d3f3aaba8e70680f98b84 Binary files /dev/null and b/appstore/.DS_Store differ diff --git a/appstore/__init__.py b/appstore/__init__.py index 062a08d14a55ae9e645fc0e1a5894d162a24825f..269808c7750821f2a05c297ae40fb224e5375c26 100644 --- a/appstore/__init__.py +++ b/appstore/__init__.py @@ -2,8 +2,9 @@ from flask import Flask from flask_cors import CORS from sqlalchemy import create_engine from celery import Celery +from appstore.models import Base, AppMetadata -app = Flask(__name__) +app = Flask(__name__, static_folder='static', template_folder='templates') # Enable CORS for all routes CORS(app) @@ -18,11 +19,24 @@ celery.conf.update(app.config) # Database connection engine = create_engine(app.config['DATABASE_URI'], echo=True) connection = engine.connect() +Base.metadata.create_all(engine) # Import and register blueprints -from appstore.routes import main_bp, zamba_bp, trapper_bp, animl_bp +from appstore.routes import main_bp, zamba_bp, trapper_bp, animl_bp, megadetector_bp app.register_blueprint(main_bp) app.register_blueprint(zamba_bp) app.register_blueprint(trapper_bp) -app.register_blueprint(animl_bp) \ No newline at end of file +app.register_blueprint(animl_bp) +app.register_blueprint(megadetector_bp) + +# Import utils after app initialization +from appstore.utils import trapper_metadata, create_app_metadata, animl_metadata +from sqlalchemy import text + +# Create Trapper metadata if it doesn't exist +for app_name in ['Trapper', 'Animl']: + result = connection.execute(text("SELECT * FROM app_metadata WHERE name = :name"), {"name": app_name}).fetchone() + if not result: + app_metadata = globals()[f"{app_name.lower()}_metadata"] + create_app_metadata(app_metadata) \ No newline at end of file diff --git a/appstore/__pycache__/__init__.cpython-39.pyc b/appstore/__pycache__/__init__.cpython-39.pyc index b4e3da8beeaef1739fdfb6a42b0b889b17b292ff..20a3a6886f14a5e5fda062f73cc57b6f559ba09e 100644 Binary files a/appstore/__pycache__/__init__.cpython-39.pyc and b/appstore/__pycache__/__init__.cpython-39.pyc differ diff --git a/appstore/config.py b/appstore/config.py index bb77fe5c75b7bb41ec0f78ffab1cb9049954cdf2..1e376eba92aa152cf85e89e0686d7e7860ea0997 100644 --- a/appstore/config.py +++ b/appstore/config.py @@ -4,14 +4,23 @@ import os SECRET_KEY = 'your_secret_key_here' DEBUG = True -# File upload settings +# Zamba file upload settings UPLOAD_FOLDER = 'static/zamba/media' TRAIN_FOLDER = 'static/zamba/train' TRAIN_VIDEOS_FOLDER = 'static/zamba/train/videos' +# Megadetector file upload settings +MEGADETECTOR_UPLOAD_FOLDER = 'static/megadetector/images' +MEGADETECTOR_MAIN_FOLDER = 'static/megadetector' + # Celery settings CELERY_BROKER_URL = 'redis://localhost:6379/0' CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' # Database settings -DATABASE_URI = 'mysql+pymysql://c22097859:Masterdata822!@csmysql.cs.cf.ac.uk:3306/c22097859_dissertation' \ No newline at end of file +DATABASE_URI = 'mysql+pymysql://c22097859:Masterdata822!@csmysql.cs.cf.ac.uk:3306/c22097859_dissertation' + +# GitLab container registry settings +GITLAB_REGISTRY_URL = 'registry.git.cf.ac.uk' +GITLAB_REGISTRY_USERNAME = 'c22097859' +GITLAB_ACCESS_TOKEN = 'Ye4xAmRLMarzafBBYznx' # READ_WRITE_REGISTRY \ No newline at end of file diff --git a/appstore/models.py b/appstore/models.py new file mode 100644 index 0000000000000000000000000000000000000000..eeb8a3094da4f39412705543a3e45d4b51c550bc --- /dev/null +++ b/appstore/models.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, Integer, String, Text +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class AppMetadata(Base): + __tablename__ = 'app_metadata' + + id = Column(Integer, primary_key=True) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + repository_url = Column(String(255), nullable=False) + docker_compose_file = Column(String(255), nullable=True) + docker_image = Column(String(255), nullable=True) + start_command = Column(Text, nullable=True) + stop_command = Column(Text, nullable=True) + + def __repr__(self): + return f"<AppMetadata(name='{self.name}', id={self.id})>" \ No newline at end of file diff --git a/appstore/routes/__init__.py b/appstore/routes/__init__.py index b139f0d95e234397c553ad935f3eb1a37371da89..2b73ffd86ed6eeb71a948750408febdad193bd3e 100644 --- a/appstore/routes/__init__.py +++ b/appstore/routes/__init__.py @@ -1,4 +1,5 @@ from .main import bp as main_bp from .zamba import bp as zamba_bp from .trapper import bp as trapper_bp -from .animl import bp as animl_bp \ No newline at end of file +from .animl import bp as animl_bp +from .megadetector import bp as megadetector_bp \ No newline at end of file diff --git a/appstore/routes/animl.py b/appstore/routes/animl.py index 26927e6a04381d2ccd3229aabf50103f3626983d..5c69b149d9855ed64bb35ce0d132cb3e31680066 100644 --- a/appstore/routes/animl.py +++ b/appstore/routes/animl.py @@ -1,10 +1,54 @@ from flask import Blueprint, jsonify +from appstore.utils import get_app_metadata, is_container_running, is_container_running_by_name, is_server_ready, is_container_exist +from appstore import app import subprocess +import docker +import time bp = Blueprint('animl', __name__, url_prefix='/animl') @bp.route('/start', methods=['POST']) -def start_animl_detection(): - command = ['npm', 'start'] - process = subprocess.Popen(command, cwd='../animl-frontend', stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - return jsonify({'message': 'Animl started'}), 202 \ No newline at end of file +def start_animl(): + start_animl_endpoint() + + max_retries = 60 + retries = 0 + animl_url = "http://127.0.0.1:5173/" + while retries < max_retries: + if is_container_running_by_name('animl-container') and is_server_ready(animl_url): + return jsonify({'status': 'running', 'url': animl_url}), 200 + time.sleep(5) + retries += 1 + + return jsonify({'error': 'Failed to start Animl', 'status': 'timeout'}), 500 + +def start_animl_endpoint(): + client = docker.from_env() + app_metadata = get_app_metadata('Animl') + + try: + container = client.containers.get('animl-container') + if container.status != 'running': + container.start() + print("Started existing animl-container") + except docker.errors.NotFound: + print("No existing animl-container found, creating new one") + # Login to GitLab container registry + client.login( + username=app.config['GITLAB_REGISTRY_USERNAME'], + password=app.config['GITLAB_ACCESS_TOKEN'], + registry=app.config['GITLAB_REGISTRY_URL'] + ) + + # Pull the latest image + image = client.images.pull(app_metadata['docker_image']) + + # Run the image + container = client.containers.run( + image.id, + name='animl-container', + ports={'5173/tcp': 5173}, + detach=True + ) + + return jsonify({'message': 'Animl started', 'container_id': container.id}), 202 \ No newline at end of file diff --git a/appstore/routes/main.py b/appstore/routes/main.py index f6c614e6734b0a5eaa97334e411af57ce5bde1fa..d68ef7ece58de55ff178850da0b778ecf4652b80 100644 --- a/appstore/routes/main.py +++ b/appstore/routes/main.py @@ -1,5 +1,6 @@ -from flask import Blueprint, render_template, jsonify +from flask import Blueprint, render_template, jsonify, send_from_directory, current_app from appstore import celery +import os bp = Blueprint('main', __name__) @@ -16,4 +17,16 @@ def task_status(task_id): 'status': task.state, 'result': task.result # This could be None if task isn't finished } - return jsonify(data) \ No newline at end of file + return jsonify(data) + +# uncatergorised routes +@bp.route('/Users/<path:filepath>') +def serve_user_files(filepath): + # This route handles the absolute paths in the HTML + if 'static' in filepath: + relative_path = filepath.split('static/')[-1] + return send_from_directory(os.path.join(current_app.root_path, 'static'), relative_path) + elif 'templates' in filepath: + relative_path = filepath.split('templates/')[-1] + return send_from_directory(os.path.join(current_app.root_path, 'templates'), relative_path) + return "File not found", 404 \ No newline at end of file diff --git a/appstore/routes/megadetector.py b/appstore/routes/megadetector.py new file mode 100644 index 0000000000000000000000000000000000000000..82aa7ab54775c5e69bec1456644f73f84e94df44 --- /dev/null +++ b/appstore/routes/megadetector.py @@ -0,0 +1,83 @@ +from flask import Blueprint, request, render_template, jsonify, send_file, send_from_directory, current_app +from appstore import app +from megadetector.detection.run_detector_batch import load_and_run_detector_batch, write_results_to_file +from megadetector.postprocessing.postprocess_batch_results import process_batch_results, PostProcessingOptions +from megadetector.utils import path_utils +import os +import subprocess + +bp = Blueprint('megadetector', __name__, url_prefix='/megadetector') + +@bp.route('/', methods=['GET', 'POST']) +def megadetector(): + if request.method == 'POST': + # image = request.files['image'] + images = request.files.getlist('image') + for image in images: + image.save(os.path.join(os.path.dirname(__file__), '..', app.config['MEGADETECTOR_UPLOAD_FOLDER'], image.filename)) + return render_template('megadetector.html') + else: + return render_template('megadetector.html') + +@bp.route('/run/batch', methods=['POST']) +def megadetector_run_batch(): + # Pick a folder to run MD on recursively, and an output file + image_folder = os.path.join(os.path.dirname(__file__), '..', app.config['MEGADETECTOR_UPLOAD_FOLDER']) + output_file = os.path.join(os.path.dirname(__file__), '..', app.config['MEGADETECTOR_MAIN_FOLDER'], 'output.json') + model = request.form.get('model') + + # Install TensorFlow using pip if the model is MDV4 - may break things now + if model == 'MDV4': + subprocess.check_call([os.sys.executable, '-m', 'pip', 'install', 'tensorflow']) + + image_file_names = path_utils.find_images(image_folder, recursive=True) + + # Run the detector + results = load_and_run_detector_batch(model, image_file_names) + write_results_to_file(results, + output_file, + relative_path_base=image_folder) + + # Postprocess the results into a HTML report + postprocess_results(output_file, image_folder) + + return jsonify({"status": "success", "message": "Batch processing completed"}) + +def postprocess_results(output_file, image_folder): + # Setup options + options = PostProcessingOptions() + options.md_results_file = output_file + options.image_base_dir = image_folder + options.output_dir = os.path.join(os.path.dirname(__file__), '..', 'templates', 'megadetector-results') + + # Run the postprocessing and generate a HTML report + process_batch_results(options) + +@bp.route('/report') +def megadetector_report(): + return render_template('megadetector-results/index.html') + +@bp.route('/detections_animal.html') +def megadetector_detections_animal_report(): + return render_template('megadetector-results/detections_animal.html') + +@bp.route('/detections_animal/<path:filename>') +def serve_detections_animal(filename): + # Serve from the templates directory + return send_from_directory(os.path.join(current_app.root_path, 'templates', 'megadetector-results', 'detections_animal'), filename) + +@bp.route('/static/megadetector/images/<path:filename>') +def serve_megadetector_images(filename): + # Serve from the static directory + return send_from_directory(os.path.join(current_app.root_path, 'static', 'megadetector', 'images'), filename) + +@bp.route('/Users/<path:filepath>') +def serve_user_files(filepath): + # This route handles the absolute paths in the HTML + if 'static' in filepath: + relative_path = filepath.split('static/')[-1] + return send_from_directory(os.path.join(current_app.root_path, 'static'), relative_path) + elif 'templates' in filepath: + relative_path = filepath.split('templates/')[-1] + return send_from_directory(os.path.join(current_app.root_path, 'templates'), relative_path) + return "File not found", 404 \ No newline at end of file diff --git a/appstore/routes/trapper.py b/appstore/routes/trapper.py index 3f99e0cad0742a0b3983af859506dee2600120de..367f4368f2b4a438e303116e833ac65d569b388a 100644 --- a/appstore/routes/trapper.py +++ b/appstore/routes/trapper.py @@ -1,8 +1,11 @@ from flask import Blueprint, jsonify -from appstore.utils import is_container_running, is_trapper_ready +from appstore.utils import is_container_running, is_server_ready, get_app_metadata import subprocess import time import docker +import git +import os +import shutil bp = Blueprint('trapper', __name__, url_prefix='/trapper') @@ -10,11 +13,11 @@ bp = Blueprint('trapper', __name__, url_prefix='/trapper') def start_trapper(): start_trapper_endpoint() - max_retries = 60 # Increased to allow more time + max_retries = 60 retries = 0 trapper_url = "http://0.0.0.0:8000/" while retries < max_retries: - if is_container_running('trapper') and is_trapper_ready(trapper_url): + if is_container_running('trapper') and is_server_ready(trapper_url): return jsonify({'status': 'running', 'url': trapper_url}), 200 time.sleep(5) retries += 1 @@ -22,14 +25,46 @@ def start_trapper(): return jsonify({'error': 'Failed to start Trapper', 'status': 'timeout'}), 500 def start_trapper_endpoint(): - trapper_start_path = '../trapper' - command = ['./start.sh', '-pb', 'dev'] - + app_metadata = get_app_metadata("Trapper") + repo_dir = f"/tmp/trapper" + try: - # Start the process and do not wait for it to complete - process = subprocess.Popen(command, cwd=trapper_start_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + # clone the repository if doesn't exist + print("Checking if repository exists") + if not os.path.exists(repo_dir): + print("Cloning repository") + git.Repo.clone_from(app_metadata["repository_url"], repo_dir) + print("Repository eixsts: ", repo_dir) + os.chdir(repo_dir) + + # Check if .env file exists + # TODO: amend Dockerfile.dev (add -o to line 53) + if not os.path.exists(".env"): + if os.path.exists("trapper.env"): + shutil.copy("trapper.env", ".env") + print("Created .env file from trapper.env") + else: + print("No trapper.env file found") + return False + else: + print(".env file already exists") + + # Run the start command + print("Current directory:", os.getcwd()) + command = app_metadata["start_command"].split() # Split the command into a list + print(f"Executing command: {' '.join(command)}") + result = subprocess.Popen(command, cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + print(f"Command exit code: {result.returncode}") + print(f"Command stdout: {result.stdout}") + print(f"Command stderr: {result.stderr}") + + if result.returncode != 0: + return jsonify({'error': 'Trapper start command failed', 'details': result.stderr}), 500 + return jsonify({'message': 'Trapper start initiated'}), 202 except Exception as e: + print(f"Exception occurred: {str(e)}") return jsonify({'error': 'Failed to start Trapper', 'details': str(e)}), 500 @bp.route('/logs', methods=['GET']) diff --git a/appstore/static/.DS_Store b/appstore/static/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..810f04c794565425909e6e8ae9197d3a3bf5e5a3 Binary files /dev/null and b/appstore/static/.DS_Store differ diff --git a/appstore/static/megadetector/.DS_Store b/appstore/static/megadetector/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..87df1a30da74924da1c3ae450c3fa0b4e6cd0df8 Binary files /dev/null and b/appstore/static/megadetector/.DS_Store differ diff --git a/appstore/static/megadetector/images/.DS_Store b/appstore/static/megadetector/images/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 Binary files /dev/null and b/appstore/static/megadetector/images/.DS_Store differ diff --git a/appstore/static/megadetector/images/nz1.jpg b/appstore/static/megadetector/images/nz1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b210c390b4196518c59c4704741a186efb4d3edb Binary files /dev/null and b/appstore/static/megadetector/images/nz1.jpg differ diff --git a/appstore/static/megadetector/images/nz2.jpg b/appstore/static/megadetector/images/nz2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ca059fa6ec4fad884996f4196a66f4221725823 Binary files /dev/null and b/appstore/static/megadetector/images/nz2.jpg differ diff --git a/appstore/static/megadetector/images/sea_star_sample_image_800.jpg b/appstore/static/megadetector/images/sea_star_sample_image_800.jpg new file mode 100644 index 0000000000000000000000000000000000000000..36446e56d6ce5205e923df5b7e0cc5bab7e176bf Binary files /dev/null and b/appstore/static/megadetector/images/sea_star_sample_image_800.jpg differ diff --git a/appstore/static/megadetector/output.json b/appstore/static/megadetector/output.json new file mode 100644 index 0000000000000000000000000000000000000000..73ac199f6cfb811dbccb935e3c376c92bf726343 --- /dev/null +++ b/appstore/static/megadetector/output.json @@ -0,0 +1,94 @@ +{ + "images": [ + { + "file": "nz1.jpg", + "detections": [ + { + "category": "1", + "conf": 0.0119, + "bbox": [ + 0.5414, + 0.3274, + 0.3871, + 0.467 + ] + }, + { + "category": "1", + "conf": 0.892, + "bbox": [ + 0.3042, + 0.1294, + 0.3585, + 0.7106 + ] + } + ] + }, + { + "file": "nz2.jpg", + "detections": [ + { + "category": "1", + "conf": 0.758, + "bbox": [ + 0.5013, + 0.4493, + 0.2909, + 0.2619 + ] + } + ] + }, + { + "file": "sea_star_sample_image_800.jpg", + "detections": [ + { + "category": "3", + "conf": 0.00617, + "bbox": [ + 0, + 0, + 0.2962, + 0.985 + ] + }, + { + "category": "1", + "conf": 0.0181, + "bbox": [ + 0.001249, + 0, + 0.3062, + 0.9983 + ] + }, + { + "category": "1", + "conf": 0.963, + "bbox": [ + 0.1512, + 0.03499, + 0.6537, + 0.8866 + ] + } + ] + } + ], + "detection_categories": { + "1": "animal", + "2": "person", + "3": "vehicle" + }, + "info": { + "detection_completion_time": "2024-08-16 18:11:21", + "format_version": "1.3", + "detector": "unknown", + "detector_metadata": { + "megadetector_version": "unknown", + "typical_detection_threshold": 0.5, + "conservative_detection_threshold": 0.25 + } + } +} \ No newline at end of file diff --git a/appstore/templates/index.html b/appstore/templates/index.html index 9589da972d5f7b284d1807ffaeb890abaf0e863c..f301de54c03bbdc459d07d85c721bb139b267fc7 100644 --- a/appstore/templates/index.html +++ b/appstore/templates/index.html @@ -4,9 +4,10 @@ <title>Home Page</title> </head> <body> - <h1>Hello, world!</h1> + <h1>Camera Trapper App Store</h1> <button id="animl" onclick="startAniml()">Animl</button> <button id="zamba" onclick="window.location.href='/zamba'">Zamba</button> + <button id="megadetector" onclick="window.location.href='/megadetector'">MegaDetector</button> <button id="trapper" onclick="startTrapper()">Trapper</button> <div id="loading" style="display: none;">Loading Trapper...</div> @@ -19,7 +20,13 @@ } }) .then(response => response.json()) - .then(data => console.log(data)) + .then(data => { + if (data.status === 'running') { + window.location.href = data.url; + } else { + console.error('Failed to start Animl:', data.error); + } + }) .catch(error => console.error('Error:', error)); } function startTrapper() { diff --git a/appstore/templates/megadetector-results/detections_animal.html b/appstore/templates/megadetector-results/detections_animal.html new file mode 100644 index 0000000000000000000000000000000000000000..0f81481f92740e5bc9e150acfa84c95ef4d1ce5a --- /dev/null +++ b/appstore/templates/megadetector-results/detections_animal.html @@ -0,0 +1,8 @@ +<html><body> +<h1>DETECTIONS_ANIMAL</h1><p style="font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5"><b>Result type</b>: detections_animal, <b>Image</b>: nz1.jpg, <b>Max conf</b>: 0.892</p> +<a href="/Users/jesslam/Desktop/CMT403_Dissertation/AppStore/appstore/routes/../static/megadetector/images/nz1.jpg"><img src="detections_animal/detections_animal_nz1.jpg" style="margin:0px;margin-top:5px;margin-bottom:5px;"> +</a><br/><p style="font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5"><b>Result type</b>: detections_animal, <b>Image</b>: nz2.jpg, <b>Max conf</b>: 0.758</p> +<a href="/Users/jesslam/Desktop/CMT403_Dissertation/AppStore/appstore/routes/../static/megadetector/images/nz2.jpg"><img src="detections_animal/detections_animal_nz2.jpg" style="margin:0px;margin-top:5px;margin-bottom:5px;"> +</a><br/><p style="font-family:verdana,arial,calibri;font-size:80%;text-align:left;margin-top:20;margin-bottom:5"><b>Result type</b>: detections_animal, <b>Image</b>: sea_star_sample_image_800.jpg, <b>Max conf</b>: 0.963</p> +<a href="/Users/jesslam/Desktop/CMT403_Dissertation/AppStore/appstore/routes/../static/megadetector/images/sea_star_sample_image_800.jpg"><img src="detections_animal/detections_animal_sea_star_sample_image_800.jpg" style="margin:0px;margin-top:5px;margin-bottom:5px;"> +</a></body></html> diff --git a/appstore/templates/megadetector-results/detections_animal/detections_animal_nz1.jpg b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b740f8d80b95d1f5e87124eab11e510c06a9a3a1 Binary files /dev/null and b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz1.jpg differ diff --git a/appstore/templates/megadetector-results/detections_animal/detections_animal_nz2.jpg b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8013ef481012e510023171e37ed3c49ef0bd6658 Binary files /dev/null and b/appstore/templates/megadetector-results/detections_animal/detections_animal_nz2.jpg differ diff --git a/appstore/templates/megadetector-results/detections_animal/detections_animal_sea_star_sample_image_800.jpg b/appstore/templates/megadetector-results/detections_animal/detections_animal_sea_star_sample_image_800.jpg new file mode 100644 index 0000000000000000000000000000000000000000..788d7bec6a3b3cc6fe4af35591ba55c52359cf8e Binary files /dev/null and b/appstore/templates/megadetector-results/detections_animal/detections_animal_sea_star_sample_image_800.jpg differ diff --git a/appstore/templates/megadetector-results/index.html b/appstore/templates/megadetector-results/index.html new file mode 100644 index 0000000000000000000000000000000000000000..faa7343b353c4709dd5b7e3f94d561d3050cab8c --- /dev/null +++ b/appstore/templates/megadetector-results/index.html @@ -0,0 +1,26 @@ +<html> +<head> + <style type="text/css"> + a { text-decoration: none; } + body { font-family: segoe ui, calibri, "trebuchet ms", verdana, arial, sans-serif; } + div.contentdiv { margin-left: 20px; } + </style> + </head> +<body> + + <h2>Visualization of results for output.json</h2> + + <p>A sample of 3 images (of 3 total) (0 failures), annotated with detections above confidence 50.00%.</p> + + + <div class="contentdiv"> + <p>Model version: unknown</p> + </div> + + <h3>Sample images</h3> + + <div class="contentdiv"> +<a href="detections_animal.html">Detections: animal</a> (3, 100.0%)<br/> +Non-detections (0, 0.0%)<br/> +</div> +</body></html> \ No newline at end of file diff --git a/appstore/templates/megadetector.html b/appstore/templates/megadetector.html new file mode 100644 index 0000000000000000000000000000000000000000..ba66f88bbd6ea42220b13077ef644a9d6176c3cb --- /dev/null +++ b/appstore/templates/megadetector.html @@ -0,0 +1,63 @@ +<DOCTYPE html> + <html> + <head> + <title>MegaDetector</title> + <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> + </head> + <body> + <h1>MegaDetector</h1> + <p>MegaDetector is an AI model that identifies animals, people, and vehicles in camera trap images (which also makes it useful for eliminating blank images).</p> + + <form action="/megadetector" method="post" enctype="multipart/form-data"> + <label for="image">Upload an image:</label> + <input type="file" name="image" id="image" accept="image/*" multiple> + <br> + <input type="submit" value="Upload"> + </form> + <br> + <form action="/megadetector/run/batch" method="post" enctype="multipart/form-data" id="batchForm"> + <label for="model">Select a model:</label> + <select name="model" id="model"> + <option value="MDV5A" selected>MDv5a</option> + <option value="MDV5B">MDv5b</option> + <option value="MDV4">MDv4</option> + </select> + <br> + <div id="warning" style="color: red; font-weight: bold; display: none;"> + <p>Warning: MDv4 is not recommended, please only select this if you have a really good reason to do so.</p> + </div> + <input type="submit" value="Run Batch"> + </form> + + <button onclick="window.location.href='/megadetector/report'">View Report</button> + + <script> + document.getElementById('model').addEventListener('change', function() { + const modelFileInput = document.getElementById('warning'); + if (this.value === 'MDV4') { + modelFileInput.style.display = 'block'; + } else { + modelFileInput.style.display = 'none'; + } + }); + + document.getElementById('batchForm').addEventListener('submit', function(e) { + e.preventDefault(); + const formData = new FormData(this); + + fetch('/megadetector/run/batch', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + console.log(data); + }) + .catch(error => { + console.error('Error:', error); + }); + }); + </script> + </body> + </html> +</DOCTYPE> \ No newline at end of file diff --git a/appstore/utils.py b/appstore/utils.py index 7217df801134e7ab7b5772c465f0616752028760..352952959301a7fe27c9b0393adc12fedd7acb07 100644 --- a/appstore/utils.py +++ b/appstore/utils.py @@ -4,9 +4,75 @@ from appstore import celery import subprocess import json import os +import git import pandas as pd from sqlalchemy import text from appstore import connection, engine +from appstore.models import AppMetadata + +# variables +trapper_metadata = AppMetadata( + name="Trapper", + description="Trapper Expert - core web application for camera trap data management", + repository_url="https://gitlab.com/trapper-project/trapper.git", + docker_compose_file="docker-compose.yml", + start_command="./start.sh -pb dev", + stop_command="./start.sh prod stop" +) +animl_metadata = AppMetadata( + name="Animl", + description="Animl - core web application for animal detection", + repository_url="https://github.com/tnc-ca-geo/animl-frontend.git", + docker_image="registry.git.cf.ac.uk/c22097859/c22097859_cmt403_dissertation/animl:latest", + start_command="docker run -p 5173:5173 animl-container animl", + stop_command="docker stop animl-container" +) + +# functions +def create_app_metadata(app_metadata): + with engine.connect() as connection: + connection.execute(AppMetadata.__table__.insert().values( + name=app_metadata.name, + description=app_metadata.description, + repository_url=app_metadata.repository_url, + docker_compose_file=app_metadata.docker_compose_file, + docker_image=app_metadata.docker_image, + start_command=app_metadata.start_command, + stop_command=app_metadata.stop_command + )) + +def get_app_metadata(app_name): + with engine.connect() as connection: + result = connection.execute(text("SELECT * FROM app_metadata WHERE name = :name"), {"name": app_name}).fetchone() + return { + "name": result[1], + "description": result[2], + "repository_url": result[3], + "docker_compose_file": result[4], + "docker_image": result[5], + "start_command": result[6], + "stop_command": result[7] + } + +def is_container_exist(container_name): + client = docker.from_env() + try: + containers = client.containers.list(filters={'name': container_name}) + return any(container.status == 'running' for container in containers) + except Exception as e: + print(f"Error checking container status: {e}") + return False + +def is_container_running_by_name(container_name): + client = docker.from_env() + try: + container = client.containers.get(container_name) + return container.status == 'running' + except docker.errors.NotFound: + return False + except Exception as e: + print(f"Error checking container status: {e}") + return False def is_container_running(service_name): client = docker.from_env() @@ -17,13 +83,14 @@ def is_container_running(service_name): print(f"Error checking container status: {e}") return False -def is_trapper_ready(url, timeout=5): +def is_server_ready(url, timeout=5): try: response = requests.get(url, timeout=timeout) return response.status_code == 200 except requests.RequestException: return False +# celery tasks @celery.task(name='train_zamba_task') def train_zamba_task(model, dryRun, labels, data_dir='appstore/static/zamba/train/videos'): command = ['zamba', 'train', '--data-dir', data_dir, '--labels', labels, '--model', model, '-y'] diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index f07ad548eda2f1af6f7fe30d20ef6560ec392e0e..0000000000000000000000000000000000000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,13 +0,0 @@ -version: '1' -services: - app: - image: appstore - ports: - - 5001:5000 - volumes: - - ./app:/app - environment: - - ENV_VARIABLE=value - trapper: - image: registry.gitlab.com/trapper-project/trapper:latest - \ No newline at end of file diff --git a/trapper b/trapper deleted file mode 160000 index 767818867054984c448f3e33fba92177d2248ff5..0000000000000000000000000000000000000000 --- a/trapper +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 767818867054984c448f3e33fba92177d2248ff5