| |
| |
| |
| import os |
| import io |
| import cv2 |
| import torch |
| import numpy as np |
| import requests |
| import json |
| import base64 |
| from datetime import datetime, timedelta |
| from flask import Flask, request, jsonify, send_from_directory |
| from flask_cors import CORS |
| from PIL import Image |
| from pymongo import MongoClient |
| import segmentation_models_pytorch as smp |
| import albumentations as A |
| from albumentations.pytorch.transforms import ToTensorV2 |
|
|
| |
| |
| |
| app = Flask(__name__, static_folder='static/assets', template_folder='templates') |
| CORS(app) |
|
|
| |
| MONGO_URI = os.getenv("MONGO_URI") |
| WEATHER_API_KEY = os.getenv("WEATHER_API_KEY") |
| MAPBOX_ACCESS_TOKEN = os.getenv("MAPBOX_ACCESS_TOKEN") |
| RAPIDAPI_KEY = os.getenv("RAPIDAPI_KEY") |
| RAPIDAPI_HOST = 'vessels1.p.rapidapi.com' |
|
|
| |
| mongo_client = None |
| VesselCache = None |
| VesselHistory = None |
|
|
| if MONGO_URI: |
| try: |
| mongo_client = MongoClient(MONGO_URI) |
| db = mongo_client.get_default_database() |
| VesselCache = db['vesselcaches'] |
| VesselHistory = db['vesselhistories'] |
| |
| VesselCache.create_index("cachedAt", expireAfterSeconds=86400) |
| print("Connected to MongoDB") |
| except Exception as e: |
| print(f"MongoDB Connection Failed: {e}") |
|
|
| |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| model = None |
| best_threshold = 0.65 |
|
|
| |
| |
| |
| def load_model(): |
| global model, best_threshold |
| net = smp.DeepLabV3Plus( |
| encoder_name="resnet50", |
| encoder_weights="imagenet", |
| in_channels=3, |
| classes=1 |
| ).to(device) |
|
|
| |
| checkpoint_files = ["deeplabv3p_best.pth", "spillguard_enhanced_final.pth"] |
| for ckpt_file in checkpoint_files: |
| if os.path.exists(ckpt_file): |
| try: |
| print(f"Loading model from {ckpt_file}...") |
| checkpoint = torch.load(ckpt_file, map_location=device) |
| if "model_state_dict" in checkpoint: |
| net.load_state_dict(checkpoint["model_state_dict"]) |
| best_threshold = float(checkpoint.get("best_threshold", best_threshold)) |
| else: |
| net.load_state_dict(checkpoint, strict=False) |
| |
| net.eval() |
| model = net |
| print(f"Model loaded. Threshold: {best_threshold}") |
| return |
| except Exception as e: |
| print(f"Failed to load {ckpt_file}: {e}") |
|
|
| print("Warning: No trained model found.") |
| net.eval() |
| model = net |
|
|
| load_model() |
|
|
| def preprocess_image(image): |
| IMG_SIZE = 512 |
| if isinstance(image, Image.Image): |
| image = image.convert("RGB") |
| img_array = np.array(image) |
| else: |
| img_array = image |
|
|
| if img_array.ndim == 3: |
| img_gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) |
| else: |
| img_gray = img_array |
|
|
| img_gray = img_gray.astype(np.float32) |
| if img_gray.max() > 1.0: img_gray /= 255.0 |
|
|
| img_3ch = np.stack([img_gray, img_gray, img_gray], axis=2) |
|
|
| transform = A.Compose([ |
| A.LongestMaxSize(max_size=IMG_SIZE, interpolation=cv2.INTER_CUBIC), |
| A.PadIfNeeded(IMG_SIZE, IMG_SIZE, border_mode=cv2.BORDER_REFLECT_101), |
| A.ToFloat(max_value=1.0), |
| ToTensorV2(), |
| ]) |
| transformed = transform(image=img_3ch) |
| return transformed["image"].unsqueeze(0).to(device), img_gray |
|
|
| |
| |
| |
| NAME_TO_MMSI = { |
| 'EVER GIVEN': '353136000', |
| 'COMPASS': '244110352', |
| 'EVERGREEN': '353136000' |
| } |
|
|
| def fetch_weather(lat, lon): |
| try: |
| url = "https://api.openweathermap.org/data/2.5/weather" |
| params = { |
| "lat": lat, "lon": lon, "appid": WEATHER_API_KEY, "units": "metric" |
| } |
| resp = requests.get(url, params=params) |
| return resp.json() |
| except Exception as e: |
| print(f"Weather error: {e}") |
| return {} |
|
|
| def fetch_satellite_image(lat, lon): |
| try: |
| url = f"https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static/{lon},{lat},17/1000x600?access_token={MAPBOX_ACCESS_TOKEN}" |
| resp = requests.get(url) |
| return resp.content |
| except Exception as e: |
| print(f"Mapbox error: {e}") |
| return None |
|
|
| def run_ml_inference(image_bytes): |
| try: |
| image = Image.open(io.BytesIO(image_bytes)) |
| ow, oh = image.size |
| img_tensor, _ = preprocess_image(image) |
| |
| with torch.no_grad(): |
| logits = model(img_tensor) |
| prob = torch.sigmoid(logits)[0, 0].cpu().numpy() |
| |
| prob_resized = cv2.resize(prob, (ow, oh), interpolation=cv2.INTER_LINEAR) |
| mask = (prob_resized >= best_threshold).astype(np.uint8) |
| |
| |
| total_px = mask.size |
| oil_px = int(mask.sum()) |
| oil_pct = (oil_px / total_px) * 100 if total_px > 0 else 0 |
| is_spill = oil_pct > 1.0 |
|
|
| |
| orig_arr = np.array(image.convert("RGB")) |
| gray_bg = cv2.cvtColor(orig_arr, cv2.COLOR_RGB2GRAY) |
| gray_bg_rgb = cv2.cvtColor(gray_bg, cv2.COLOR_GRAY2RGB) |
| overlay = gray_bg_rgb.copy() |
| overlay[mask == 1] = [255, 0, 0] |
| blended = cv2.addWeighted(gray_bg_rgb, 0.7, overlay, 0.3, 0) |
| |
| pil_res = Image.fromarray(blended) |
| buf = io.BytesIO() |
| pil_res.save(buf, format="PNG") |
| res_b64 = base64.b64encode(buf.getvalue()).decode('utf-8') |
| |
| return { |
| 'is_spill': is_spill, |
| 'oil_percentage': oil_pct, |
| 'confidence': float(prob_resized.max()), |
| 'analysisImage': res_b64 |
| } |
| except Exception as e: |
| print(f"ML Error: {e}") |
| return {'error': str(e)} |
|
|
| @app.route('/api/vessel-position', methods=['GET']) |
| def get_vessel_position(): |
| query = request.args.get('name') |
| if not query: return jsonify({'error': 'Name/MMSI required'}), 400 |
|
|
| mmsi = query if query.isdigit() and len(query) == 9 else NAME_TO_MMSI.get(query.upper()) |
| if not mmsi: return jsonify({'error': 'Vessel not found'}), 404 |
|
|
| |
| if VesselCache is not None: |
| one_day_ago = datetime.utcnow() - timedelta(days=1) |
| cached = VesselCache.find_one({"mmsi": mmsi, "cachedAt": {"$gte": one_day_ago}}, sort=[("cachedAt", -1)]) |
| |
| if cached: |
| print("Cache HIT") |
| sat_img = fetch_satellite_image(cached['latitude'], cached['longitude']) |
| ml_data = run_ml_inference(sat_img) if sat_img else None |
| |
| resp_data = { |
| "name": cached['name'], "mmsi": mmsi, "imo": cached['imo'], |
| "latitude": cached['latitude'], "longitude": cached['longitude'], |
| "speed": 0, "course": cached['cog'], "timestamp": cached['timestamp'], |
| "weather": cached.get('weather'), "weatherDescription": cached.get('weatherdescription'), |
| "temperature": cached.get('temperature'), "windspeed": cached.get('windspeed'), |
| "humidity": cached.get('humidity'), "satelliteImage": base64.b64encode(sat_img).decode('utf-8') if sat_img else None, |
| "oilSpillData": ml_data |
| } |
| return jsonify(resp_data) |
|
|
| |
| print("Cache MISS - Fetching RapidAPI") |
| try: |
| api_url = f"https://vessels1.p.rapidapi.com/vessels/{mmsi}" |
| headers = {"x-rapidapi-key": RAPIDAPI_KEY, "x-rapidapi-host": RAPIDAPI_HOST} |
| |
| |
| |
| |
| resp = requests.get(api_url, headers=headers) |
| if resp.status_code != 200: |
| raise Exception(f"RapidAPI Error: {resp.status_code}") |
| |
| data = resp.json().get('data', {}) |
| |
| vessel_data = { |
| "mmsi": str(data.get('mmsi', mmsi)), |
| "name": data.get('name', 'Unknown'), |
| "imo": data.get('imo', 0), |
| "latitude": float(data.get('last_position', {}).get('latitude', 0)), |
| "longitude": float(data.get('last_position', {}).get('longitude', 0)), |
| "course": float(data.get('last_position', {}).get('course', 0)), |
| "timestamp": datetime.utcnow() |
| } |
|
|
| |
| w_data = fetch_weather(vessel_data['latitude'], vessel_data['longitude']) |
| |
| |
| sat_img = fetch_satellite_image(vessel_data['latitude'], vessel_data['longitude']) |
| ml_data = run_ml_inference(sat_img) if sat_img else None |
|
|
| |
| if VesselCache is not None: |
| cache_entry = { |
| "mmsi": mmsi, "name": vessel_data['name'], "imo": vessel_data['imo'], |
| "latitude": vessel_data['latitude'], "longitude": vessel_data['longitude'], |
| "cog": vessel_data['course'], "timestamp": vessel_data['timestamp'], |
| "cachedAt": datetime.utcnow(), |
| "weather": w_data.get('weather', [{}])[0].get('main'), |
| "weatherdescription": w_data.get('weather', [{}])[0].get('description'), |
| "temperature": w_data.get('main', {}).get('temp'), |
| "windspeed": w_data.get('wind', {}).get('speed'), |
| "humidity": w_data.get('main', {}).get('humidity') |
| } |
| VesselCache.insert_one(cache_entry) |
|
|
| return jsonify({ |
| **vessel_data, |
| "weather": w_data.get('weather', [{}])[0].get('main'), |
| "weatherDescription": w_data.get('weather', [{}])[0].get('description'), |
| "temperature": w_data.get('main', {}).get('temp'), |
| "windspeed": w_data.get('wind', {}).get('speed'), |
| "humidity": w_data.get('main', {}).get('humidity'), |
| "satelliteImage": base64.b64encode(sat_img).decode('utf-8') if sat_img else None, |
| "oilSpillData": ml_data |
| }) |
|
|
| except Exception as e: |
| print(f"API Fetch Error: {e}") |
| return jsonify({'error': 'Failed to fetch data'}), 500 |
|
|
| |
| |
| |
| @app.route('/predict', methods=['POST', 'OPTIONS']) |
| def predict(): |
| if request.method == 'OPTIONS': |
| return jsonify({}), 200 |
|
|
| if model is None: |
| return jsonify({'error': 'Model not loaded'}), 500 |
|
|
| try: |
| data = request.json |
| if 'image' not in data: |
| return jsonify({'error': 'No image provided'}), 400 |
|
|
| |
| image_data = base64.b64decode(data['image']) |
| image = Image.open(io.BytesIO(image_data)) |
| |
| |
| ow, oh = image.size |
|
|
| |
| img_tensor, _ = preprocess_image(image) |
| with torch.no_grad(): |
| logits = model(img_tensor) |
| prob = torch.sigmoid(logits)[0, 0].cpu().numpy() |
|
|
| |
| prob_resized = cv2.resize(prob, (ow, oh), interpolation=cv2.INTER_LINEAR) |
| |
| |
| mask = (prob_resized >= best_threshold).astype(np.uint8) |
| |
| |
| total_px = mask.size |
| oil_px = int(mask.sum()) |
| oil_pct = (oil_px / total_px) * 100 if total_px > 0 else 0 |
| |
| is_spill = oil_pct > 1.0 |
|
|
| |
| |
| orig_arr = np.array(image.convert("RGB")) |
| |
| |
| gray_bg = cv2.cvtColor(orig_arr, cv2.COLOR_RGB2GRAY) |
| gray_bg_rgb = cv2.cvtColor(gray_bg, cv2.COLOR_GRAY2RGB) |
| |
| |
| overlay = gray_bg_rgb.copy() |
| overlay[mask == 1] = [255, 0, 0] |
| |
| |
| blended = cv2.addWeighted(gray_bg_rgb, 0.7, overlay, 0.3, 0) |
| |
| |
| pil_result = Image.fromarray(blended) |
| buf = io.BytesIO() |
| pil_result.save(buf, format="PNG") |
| result_b64 = base64.b64encode(buf.getvalue()).decode('utf-8') |
|
|
| return jsonify({ |
| 'is_spill': is_spill, |
| 'oil_percentage': oil_pct, |
| 'confidence': float(prob_resized.max()), |
| 'annotated_image': result_b64, |
| 'details': { |
| 'threshold': best_threshold, |
| 'oil_pixels': oil_px, |
| 'total_pixels': total_px |
| } |
| }) |
|
|
| except Exception as e: |
| print(f"Error processing request: {e}") |
| return jsonify({'error': str(e)}), 500 |
|
|
| |
| |
| |
| @app.route('/') |
| def index(): |
| return send_from_directory('templates', 'index.html') |
|
|
| @app.route('/<path:path>') |
| def serve_static(path): |
| |
| if path.startswith('predict'): |
| return jsonify({'error': 'Route mismatch in static handler'}), 500 |
|
|
| |
| if os.path.exists(os.path.join('static/assets', path)): |
| return send_from_directory('static/assets', path) |
| return send_from_directory('templates', path) |
|
|
| if __name__ == '__main__': |
| print("Starting Flask ML Service (Routes Fixed V3)...") |
| app.run(host='0.0.0.0', port=7860, debug=False) |
|
|