{ "cells": [ { "cell_type": "code", "execution_count": 2, "id": "40ea9d5a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "โœ… Updated app.py created successfully with portable U2Net and session check!\n" ] } ], "source": [ "# %% [markdown]\n", "# # Step 6: Create Flask Application with Updated Features\n", "# ## Using Air Temperature (user input) and Soil Temperature (mean from training)\n", "\n", "from flask import redirect, url_for, flash # ADDED flash import\n", "# Add these to your imports\n", "from sklearn.preprocessing import StandardScaler, MinMaxScaler\n", "from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash # ADDED flash\n", "\n", "# %%\n", "# Write the Flask app to a file\n", "flask_app_code = '''\n", "import os\n", "import numpy as np\n", "import tensorflow as tf\n", "from tensorflow.keras import backend as K\n", "from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash\n", "from werkzeug.utils import secure_filename\n", "from sklearn.preprocessing import StandardScaler, MinMaxScaler\n", "\n", "import cv2\n", "from datetime import datetime\n", "import pickle\n", "import uuid\n", "import time\n", "import glob\n", "import shutil\n", "from PIL import Image\n", "import json\n", "import sys\n", "import traceback\n", "\n", "# Import utilities\n", "from utils.background_removal import BackgroundRemover\n", "from utils.edge_detection import EdgeDetector\n", "from utils.preprocessing import ImagePreprocessor\n", "\n", "# ============= DEFINE CUSTOM ACTIVATION FUNCTIONS =============\n", "def mish_activation(x):\n", " \"\"\"\n", " Mish activation function: x * tanh(softplus(x))\n", " \"\"\"\n", " return x * tf.math.tanh(tf.math.softplus(x))\n", "\n", "def swish_activation(x):\n", " \"\"\"\n", " Swish activation function: x * sigmoid(x)\n", " \"\"\"\n", " return x * tf.nn.sigmoid(x)\n", "\n", "# Register custom activations\n", "tf.keras.utils.get_custom_objects()['mish_activation'] = mish_activation\n", "tf.keras.utils.get_custom_objects()['swish_activation'] = swish_activation\n", "# ==============================================================\n", "\n", "# Define custom metrics functions (same as during training)\n", "def rmse(y_true, y_pred):\n", " \"\"\"Root Mean Square Error\"\"\"\n", " return K.sqrt(K.mean(K.square(y_pred - y_true)))\n", "\n", "def r2(y_true, y_pred):\n", " \"\"\"R-squared (Coefficient of determination)\"\"\"\n", " SS_res = K.sum(K.square(y_true - y_pred))\n", " SS_tot = K.sum(K.square(y_true - K.mean(y_true)))\n", " return 1 - SS_res/(SS_tot + K.epsilon())\n", "\n", "def mae(y_true, y_pred):\n", " \"\"\"Mean Absolute Error\"\"\"\n", " return K.mean(K.abs(y_pred - y_true))\n", "\n", "app = Flask(__name__)\n", "app.secret_key = 'nitrosense-secret-key-2024'\n", "app.config['SESSION_TYPE'] = 'filesystem'\n", "app.config['SESSION_PERMANENT'] = False\n", "app.config['SESSION_USE_SIGNER'] = True\n", "app.config['SESSION_COOKIE_NAME'] = 'nitrosense_session'\n", "app.config['SESSION_COOKIE_SECURE'] = False # Set to False for HTTP\n", "app.config['SESSION_COOKIE_HTTPONLY'] = True\n", "app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n", "\n", "# Configuration\n", "app.config['UPLOAD_FOLDER'] = 'static/uploads'\n", "app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024\n", "app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}\n", "\n", "# Initialize models and utilities\n", "print(\"=\"*50)\n", "print(\"Loading NitroSense AI Application\")\n", "print(\"=\"*50)\n", "\n", "# ============= LOAD SOIL TEMPERATURE MEAN ONLY (Air temp comes from user) =============\n", "print(\"\\\\n๐Ÿ“Š Loading soil temperature mean from training data...\")\n", "try:\n", " with open('utils/temperature_means.pkl', 'rb') as f:\n", " temp_means = pickle.load(f)\n", " SOIL_TEMP_MEAN = temp_means.get('soil_temp_mean', 29.8) # Only soil temperature mean\n", " print(f\"โœ… Using Soil Temperature mean: {SOIL_TEMP_MEAN}ยฐC\")\n", " print(f\"โ„น๏ธ Air Temperature will be provided by user input\")\n", "except Exception as e:\n", " print(f\"โš ๏ธ Could not load temperature means: {e}\")\n", " print(\" Using default soil temperature:\")\n", " SOIL_TEMP_MEAN = 29.8 # Default fallback\n", " print(f\" Soil Temp: {SOIL_TEMP_MEAN}ยฐC\")\n", "# ====================================================================================\n", "\n", "# Load U2Net model (portable version)\n", "print(\"\\\\n๐Ÿ“ฆ Initializing Background Remover...\")\n", "try:\n", " # The BackgroundRemover class now handles path finding automatically\n", " # It will look for weights in u2net/weights/u2net.pth\n", " background_remover = BackgroundRemover()\n", " print(\"โœ… BackgroundRemover initialized (portable version)\")\n", " print(\" U2Net source will auto-download if missing\")\n", " print(\" Weights should be at: u2net/weights/u2net.pth\")\n", "except Exception as e:\n", " print(f\"โš ๏ธ Error initializing BackgroundRemover: {e}\")\n", " print(\" Will use fallback mode (no background removal)\")\n", " background_remover = None\n", "\n", "# Load edge detector\n", "print(\"\\\\n๐Ÿ“ฆ Initializing Edge Detector...\")\n", "try:\n", " edge_detector = EdgeDetector()\n", " print(\"โœ… EdgeDetector initialized\")\n", "except Exception as e:\n", " print(f\"โš ๏ธ Error: {e}\")\n", " edge_detector = None\n", "\n", "# Load preprocessor\n", "print(\"\\\\n๐Ÿ“ฆ Initializing Image Preprocessor...\")\n", "try:\n", " preprocessor = ImagePreprocessor(target_size=(224, 224)) # 224x224 for DenseNet121\n", " print(\"โœ… ImagePreprocessor initialized\")\n", "except Exception as e:\n", " print(f\"โš ๏ธ Error: {e}\")\n", " preprocessor = None\n", "\n", "# Load your trained model with custom metrics\n", "print(\"\\\\n๐Ÿ“ฆ Loading Model...\")\n", "model_path = os.path.join('models', 'Pyramid_fusion_densenet121_model.h5')\n", "model = None\n", "\n", "if os.path.exists(model_path):\n", " try:\n", " # Custom objects for loading\n", " custom_objects = {\n", " 'mse': tf.keras.losses.MeanSquaredError(),\n", " 'MSE': tf.keras.losses.MeanSquaredError(),\n", " 'mean_squared_error': tf.keras.losses.MeanSquaredError(),\n", " 'mae': mae,\n", " 'MAE': mae,\n", " 'mean_absolute_error': mae,\n", " 'rmse': rmse,\n", " 'RMSE': rmse,\n", " 'root_mean_squared_error': rmse,\n", " 'r2': r2,\n", " 'R2': r2,\n", " 'r_squared': r2,\n", " 'R_squared': r2,\n", " # Add custom activation functions\n", " 'mish_activation': mish_activation,\n", " 'swish_activation': swish_activation,\n", " }\n", " \n", " model = tf.keras.models.load_model(\n", " model_path, \n", " custom_objects=custom_objects,\n", " compile=False\n", " )\n", " print(\"โœ… Model loaded successfully!\")\n", " \n", " # Verify model inputs\n", " print(\"\\\\n๐Ÿ“Š Model input structure:\")\n", " for i, input_layer in enumerate(model.inputs):\n", " print(f\" Input {i+1}: {input_layer.name} - Shape: {input_layer.shape}\")\n", " \n", " # Recompile\n", " model.compile(\n", " optimizer='adam',\n", " loss='mse',\n", " metrics=[mae, rmse, r2]\n", " )\n", " print(\"โœ… Model recompiled with custom metrics\")\n", " \n", " except Exception as e:\n", " print(f\"โš ๏ธ Error loading model: {e}\")\n", " model = None\n", "else:\n", " print(f\"โŒ Model file not found at: {model_path}\")\n", "\n", "# ============= Load ALL scalers =============\n", "print(\"\\\\n๐Ÿ“ฆ Loading All Scalers...\")\n", "try:\n", " # Load temperature scaler (for Avg_Temp and Soil)\n", " continuous_scaler_path = 'utils/scalers/continuous_scaler.pkl'\n", " if os.path.exists(continuous_scaler_path):\n", " continuous_scaler = pickle.load(open(continuous_scaler_path, 'rb'))\n", " print(\"โœ… continuous_scaler loaded successfully\")\n", " else:\n", " continuous_scaler = None\n", " print(\"โš ๏ธ continuous_scaler not found\")\n", " \n", " # Load days scaler\n", " days_scaler_path = 'utils/scalers/days_scaler.pkl'\n", " if os.path.exists(days_scaler_path):\n", " days_scaler = pickle.load(open(days_scaler_path, 'rb'))\n", " print(\"โœ… days_scaler loaded successfully\")\n", " else:\n", " days_scaler = None\n", " print(\"โš ๏ธ days_scaler not found\")\n", " \n", " # Load nitrogen mapping (for display only)\n", " nitrogen_map_path = 'utils/scalers/nitrogen_map.pkl'\n", " if os.path.exists(nitrogen_map_path):\n", " nitrogen_map = pickle.load(open(nitrogen_map_path, 'rb'))\n", " print(\"โœ… nitrogen_map loaded successfully\")\n", " else:\n", " nitrogen_map = {0:0, 30:1, 60:2, 90:3, 120:4, 150:5, 180:6, 210:7}\n", " print(\"โš ๏ธ nitrogen_map not found, using default mapping\")\n", " \n", " # Load target scaler\n", " scaler_y_path = 'utils/scalers/scaler_y.pkl'\n", " if os.path.exists(scaler_y_path):\n", " scaler_y = pickle.load(open(scaler_y_path, 'rb'))\n", " print(f\"โœ… scaler_y loaded successfully!\")\n", " print(f\" Target scaler mean: {scaler_y.mean_[0]:.4f}\")\n", " print(f\" Target scaler scale: {scaler_y.scale_[0]:.4f}\")\n", " else:\n", " scaler_y = None\n", " print(\"โš ๏ธ scaler_y file not found\")\n", "except Exception as e:\n", " print(f\"โš ๏ธ Error loading scalers: {e}\")\n", " continuous_scaler = None\n", " days_scaler = None\n", " nitrogen_map = {0:0, 30:1, 60:2, 90:3, 120:4, 150:5, 180:6, 210:7}\n", " scaler_y = None\n", "# ====================================================================\n", "\n", "print(\"\\\\n\" + \"=\"*50)\n", "print(\"โœ… Application initialization complete!\")\n", "print(\"=\"*50)\n", "\n", "def cleanup_old_images(max_age_hours=24):\n", " \"\"\"Delete images older than max_age_hours\"\"\"\n", " try:\n", " upload_folder = app.config['UPLOAD_FOLDER']\n", " if not os.path.exists(upload_folder):\n", " return\n", " current_time = time.time()\n", " for filename in os.listdir(upload_folder):\n", " filepath = os.path.join(upload_folder, filename)\n", " if os.path.isfile(filepath):\n", " file_age = current_time - os.path.getctime(filepath)\n", " if file_age > max_age_hours * 3600: # Convert hours to seconds\n", " os.remove(filepath)\n", " print(f\"๐Ÿงน Deleted old image: {filename}\")\n", " except Exception as e:\n", " print(f\"โš ๏ธ Error cleaning up images: {e}\")\n", "\n", "def allowed_file(filename):\n", " return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']\n", "\n", "def calculate_days(sowing_date, capture_date):\n", " \"\"\"Calculate number of days between sowing and capture\"\"\"\n", " date_format = \"%Y-%m-%d\"\n", " sowing = datetime.strptime(sowing_date, date_format)\n", " capture = datetime.strptime(capture_date, date_format)\n", " days = (capture - sowing).days\n", " return max(days, 0) # Ensure non-negative\n", "\n", "def map_fertilizer_to_category(fertilizer_value):\n", " \"\"\"\n", " Map continuous fertilizer value (0-210) to categorical bin (0-7)\n", " Based on the training preprocessing logic\n", " \"\"\"\n", " nitrogen_bins = [0, 30, 60, 90, 120, 150, 180, 210]\n", " nitrogen_map = {0:0, 30:1, 60:2, 90:3, 120:4, 150:5, 180:6, 210:7}\n", " \n", " # Find closest bin\n", " closest_bin = min(nitrogen_bins, key=lambda x: abs(x - fertilizer_value))\n", " return nitrogen_map[closest_bin]\n", "\n", "def classify_nitrogen_level(nitrogen_value):\n", " \"\"\"\n", " Classify nitrogen content into categories\n", " nitrogen_value is in original scale (2-4.95%)\n", " \"\"\"\n", " if nitrogen_value < 3.0:\n", " return {\n", " 'category': 'Deficient',\n", " 'message': 'โš ๏ธ Nitrogen Deficient - Fertilizer Recommended',\n", " 'color': 'warning',\n", " 'action': 'Apply nitrogen fertilizer'\n", " }\n", " elif nitrogen_value <= 4.0:\n", " return {\n", " 'category': 'Sufficient',\n", " 'message': 'โœ… Nitrogen Sufficient - No Fertilizer Needed',\n", " 'color': 'success',\n", " 'action': 'Maintain current practices'\n", " }\n", " else:\n", " return {\n", " 'category': 'Excess',\n", " 'message': 'โš ๏ธ Nitrogen Excess - Reduce Fertilizer Application for the next crop',\n", " 'color': 'danger',\n", " 'action': 'Reduce or skip nitrogen application'\n", " }\n", "\n", "@app.route('/')\n", "def index():\n", " return render_template('index.html')\n", " \n", "@app.route('/estimation')\n", "def estimation():\n", " \"\"\"Estimation page route - redirects to home if accessed directly without proper session\"\"\"\n", " # Check if this is a direct access without an uploaded image\n", " # Also check URL parameters for image filename (from capture page)\n", " image_param = request.args.get('image')\n", " \n", " if image_param:\n", " # If coming from capture page with image parameter\n", " session['current_image'] = image_param\n", " session.modified = True\n", " return render_template('estimation.html')\n", " \n", " # Check if there's an image in session (uploaded from file or capture)\n", " if 'current_image' not in session:\n", " # This appears to be a direct access - redirect to home page\n", " # Show a flash message explaining why\n", " flash('Please upload or capture a wheat leaf image first before estimation.', 'info')\n", " return redirect(url_for('index'))\n", " \n", " return render_template('estimation.html')\n", " \n", "@app.route('/capture')\n", "def capture():\n", " return render_template('capture.html')\n", "\n", "@app.route('/upload', methods=['POST'])\n", "def upload_file():\n", " try:\n", " if 'image' not in request.files:\n", " return jsonify({'error': 'No image uploaded'}), 400\n", " \n", " file = request.files['image']\n", " print(f\"๐Ÿ“ค Upload - File received: {file.filename}\")\n", " \n", " if file.filename == '':\n", " return jsonify({'error': 'No image selected'}), 400\n", " \n", " if file and allowed_file(file.filename):\n", " # Ensure upload folder exists\n", " os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)\n", " \n", " # Generate filename\n", " filename = str(uuid.uuid4()) + '_' + secure_filename(file.filename)\n", " filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)\n", " \n", " # Save file\n", " file.save(filepath)\n", " print(f\"๐Ÿ’พ File saved to: {filepath}\")\n", " \n", " # Verify file was saved and is readable\n", " if os.path.exists(filepath):\n", " file_size = os.path.getsize(filepath)\n", " print(f\"๐Ÿ“ File size: {file_size} bytes\")\n", " \n", " if file_size > 0:\n", " # Try to read with OpenCV\n", " test_img = cv2.imread(filepath)\n", " if test_img is not None:\n", " print(f\"โœ… Image verified with OpenCV: {test_img.shape}\")\n", " # Store in session immediately\n", " session['current_image'] = filename\n", " session.modified = True\n", " print(f\"โœ… Stored in session: {session.get('current_image')}\")\n", " \n", " return jsonify({\n", " 'success': True,\n", " 'filename': filename,\n", " 'image_url': f'/static/uploads/{filename}'\n", " })\n", " else:\n", " # Try with PIL as fallback\n", " try:\n", " pil_img = Image.open(filepath)\n", " print(f\"โœ… PIL can read image: {pil_img.format}, {pil_img.size}\")\n", " # Convert to RGB and save as JPEG\n", " rgb_img = pil_img.convert('RGB')\n", " rgb_img.save(filepath, 'JPEG', quality=95)\n", " print(f\"โœ… Image converted to JPEG format\")\n", " \n", " # Test again with OpenCV\n", " test_img = cv2.imread(filepath)\n", " if test_img is not None:\n", " print(f\"โœ… OpenCV can now read the converted image\")\n", " session['current_image'] = filename\n", " session.modified = True\n", " \n", " return jsonify({\n", " 'success': True,\n", " 'filename': filename,\n", " 'image_url': f'/static/uploads/{filename}'\n", " })\n", " else:\n", " print(f\"โŒ OpenCV still cannot read the image after conversion\")\n", " os.remove(filepath)\n", " return jsonify({'error': 'Unsupported image format'}), 400\n", " except Exception as pil_error:\n", " print(f\"โŒ PIL also failed: {pil_error}\")\n", " os.remove(filepath)\n", " return jsonify({'error': 'Corrupted image file'}), 400\n", " else:\n", " print(f\"โŒ File is empty\")\n", " os.remove(filepath)\n", " return jsonify({'error': 'Empty file'}), 400\n", " else:\n", " print(f\"โŒ Failed to save file\")\n", " return jsonify({'error': 'Failed to save file'}), 400\n", " \n", " return jsonify({'error': 'Invalid file type'}), 400\n", " \n", " except Exception as e:\n", " print(f\"โŒ Upload error: {e}\")\n", " traceback.print_exc()\n", " return jsonify({'error': str(e)}), 500\n", "\n", "@app.route('/predict', methods=['POST'])\n", "def predict():\n", " # Clean up old images (older than 24 hours)\n", " cleanup_old_images(24)\n", " \n", " try:\n", " # Handle both JSON and form data\n", " if request.is_json:\n", " data = request.get_json()\n", " else:\n", " data = request.form\n", " \n", " print(f\"๐Ÿ“จ Form data received: {data}\")\n", " \n", " # ============= GET CONTINUOUS FERTILIZER VALUE =============\n", " try:\n", " fertilizer_amount = float(data.get('fertilizer'))\n", " # Validate range\n", " if fertilizer_amount < 0 or fertilizer_amount > 210:\n", " return jsonify({'error': 'Fertilizer amount must be between 0 and 210 kg/ha'}), 400\n", " except (TypeError, ValueError):\n", " return jsonify({'error': 'Please provide a valid fertilizer amount between 0-210 kg/ha'}), 400\n", " # ===========================================================\n", " \n", " # Get dates and calculate days\n", " sowing_date = data.get('sowingDate')\n", " capture_date = data.get('captureDate')\n", " days = calculate_days(sowing_date, capture_date)\n", " \n", " # Validate days range\n", " if days < 60 or days > 120:\n", " return jsonify({'error': f'Days must be between 60-120. Current: {days}'}), 400\n", " \n", " # Get air temperature from user input\n", " try:\n", " air_temp = float(data.get('airTemp'))\n", " except (TypeError, ValueError) as e:\n", " return jsonify({'error': 'Please provide valid air temperature value'}), 400\n", " \n", " # Use soil temperature mean from training (loaded during initialization)\n", " soil_temp = SOIL_TEMP_MEAN\n", " \n", " print(f\"\\\\n๐Ÿ“ Input parameters:\")\n", " print(f\" Fertilizer: {fertilizer_amount} kg/ha\")\n", " print(f\" Days after sowing: {days}\")\n", " print(f\" Air Temperature: {air_temp}ยฐC (user input)\")\n", " print(f\" Soil Temperature: {soil_temp}ยฐC (mean from training)\")\n", " \n", " # ============= MAP FERTILIZER TO CATEGORICAL =============\n", " nitrogen_category = map_fertilizer_to_category(fertilizer_amount)\n", " print(f\" Fertilizer {fertilizer_amount} kg/ha โ†’ Category {nitrogen_category}\")\n", " # ===========================================================\n", " \n", " # Check if all scalers are available\n", " if any(v is None for v in [continuous_scaler, days_scaler, scaler_y]):\n", " missing = []\n", " if continuous_scaler is None: missing.append(\"continuous_scaler\")\n", " if days_scaler is None: missing.append(\"days_scaler\")\n", " if scaler_y is None: missing.append(\"scaler_y\")\n", " return jsonify({'error': f'Scalers not loaded: {\", \".join(missing)}'}), 500\n", " \n", " # ============= APPLY SAME PREPROCESSING AS TRAINING =============\n", " # Create raw features with user input air temp and mean soil temp\n", " temperature_features = np.array([[air_temp, soil_temp]], dtype=np.float32)\n", " days_array = np.array([[days]], dtype=np.float32)\n", " \n", " # Step 1: Scale temperature features (using StandardScaler from training)\n", " temperature_scaled = continuous_scaler.transform(temperature_features)\n", " \n", " # Step 2: Map nitrogen level to categorical (0-7)\n", " nitrogen_categorical = np.array([[nitrogen_category]], dtype=np.float32)\n", " \n", " # Step 3: Scale days (using MinMaxScaler from training)\n", " days_scaled = days_scaler.transform(days_array)\n", " \n", " # Step 4: Combine all features in the SAME ORDER as training\n", " tabular_processed = np.concatenate([\n", " temperature_scaled, # [air_temp_scaled, soil_temp_scaled]\n", " nitrogen_categorical, # [nitrogen_category]\n", " days_scaled # [days_scaled]\n", " ], axis=1).astype('float32')\n", " \n", " print(f\"๐Ÿ“Š Processed features shape: {tabular_processed.shape}\")\n", " print(f\"๐Ÿ“Š Processed features: {tabular_processed[0]}\")\n", " # ================================================================\n", " \n", " # Get image - try multiple methods\n", " filename = None\n", " \n", " # Method 1: Try to get from session\n", " filename = session.get('current_image')\n", " print(f\"๐Ÿ” Method 1 - Filename from session: {filename}\")\n", " \n", " # Method 2: If not in session, check if image was uploaded in this request\n", " if not filename and 'image' in request.files:\n", " file = request.files['image']\n", " print(f\"๐Ÿ” Method 2 - File from request: {file.filename}\")\n", " if file and allowed_file(file.filename):\n", " filename = str(uuid.uuid4()) + '_' + secure_filename(file.filename)\n", " filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)\n", " file.save(filepath)\n", " session['current_image'] = filename\n", " session.modified = True\n", " print(f\"โœ… Method 2 - Saved new image: {filename}\")\n", " \n", " # Method 3: Check if filename was passed in form data\n", " if not filename:\n", " filename = data.get('filename') or data.get('uploaded_filename')\n", " print(f\"๐Ÿ” Method 3 - Filename from form: {filename}\")\n", " if filename:\n", " test_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)\n", " if os.path.exists(test_path):\n", " session['current_image'] = filename\n", " session.modified = True\n", " print(f\"โœ… Method 3 - Validated filename from form\")\n", " else:\n", " print(f\"โŒ Method 3 - File does not exist: {test_path}\")\n", " filename = None\n", " \n", " if not filename:\n", " print(\"โŒ No filename found from any method\")\n", " return jsonify({'error': 'No image found'}), 400\n", " \n", " image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)\n", " print(f\"๐Ÿ” Final image path: {image_path}\")\n", " print(f\"๐Ÿ” File exists: {os.path.exists(image_path)}\")\n", " \n", " if not os.path.exists(image_path):\n", " return jsonify({'error': f'Image file not found: {filename}'}), 400\n", " \n", " file_size = os.path.getsize(image_path)\n", " print(f\"๐Ÿ“ File size: {file_size} bytes\")\n", " \n", " if file_size == 0:\n", " return jsonify({'error': 'Image file is empty'}), 400\n", " \n", " # Read image\n", " image = cv2.imread(image_path)\n", " if image is None:\n", " # Try PIL as fallback\n", " try:\n", " pil_img = Image.open(image_path)\n", " if pil_img.mode != 'RGB':\n", " pil_img = pil_img.convert('RGB')\n", " image = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)\n", " print(f\"โœ… PIL fallback succeeded\")\n", " except Exception as e:\n", " print(f\"โŒ All image reading methods failed: {e}\")\n", " return jsonify({'error': 'Could not read image'}), 400\n", " \n", " image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n", " \n", " # Background removal (now using portable version)\n", " if background_remover is not None:\n", " try:\n", " # The portable BackgroundRemover handles resizing internally\n", " bg_removed, mask = background_remover.remove_background(\n", " image_rgb, \n", " target_size=(224, 224), # Match your model's expected input\n", " max_size=800 # Memory optimization\n", " )\n", " print(f\"โœ… Background removal completed\")\n", " if mask is not None:\n", " print(f\" Mask shape: {mask.shape}\")\n", " except Exception as e:\n", " print(f\"โš ๏ธ Background removal error: {e}\")\n", " bg_removed = cv2.resize(image_rgb, (224, 224))\n", " else:\n", " bg_removed = cv2.resize(image_rgb, (224, 224))\n", " \n", " # Edge detection\n", " if edge_detector is not None:\n", " try:\n", " edge_image, edges = edge_detector.detect_edges(bg_removed)\n", " except Exception as e:\n", " print(f\"โš ๏ธ Edge detection error: {e}\")\n", " edge_image = bg_removed\n", " else:\n", " edge_image = bg_removed\n", " \n", " # Final preprocessing\n", " if preprocessor is not None:\n", " processed_image = preprocessor.preprocess_for_model(edge_image)\n", " else:\n", " processed_image = cv2.resize(edge_image, (224, 224)).astype(np.float32) / 255.0\n", " \n", " # ============= ENHANCED PREDICTION WITH DEBUGGING =============\n", " # Make prediction\n", " nitrogen_content = 3.62 # Default fallback\n", " prediction_successful = False\n", " \n", " if model is not None:\n", " try:\n", " image_input = np.expand_dims(processed_image, axis=0)\n", " feature_input = np.expand_dims(tabular_processed[0], axis=0)\n", " \n", " print(f\"๐Ÿ”„ Model input shapes: Image={image_input.shape}, Features={feature_input.shape}\")\n", " \n", " prediction = model.predict([image_input, feature_input], verbose=0)\n", " raw_prediction = prediction[0][0]\n", " print(f\"๐Ÿ” Raw model output (normalized): {raw_prediction:.6f}\")\n", " \n", " # Denormalize using scaler_y\n", " if scaler_y is not None:\n", " nitrogen_content = scaler_y.inverse_transform(prediction)[0][0]\n", " print(f\"๐Ÿ” After inverse_transform: {nitrogen_content:.4f}%\")\n", " prediction_successful = True\n", " \n", " # Validate range\n", " if nitrogen_content < 1.0 or nitrogen_content > 6.0:\n", " print(f\"โš ๏ธ Warning: Predicted nitrogen {nitrogen_content:.2f}% is outside expected range\")\n", " nitrogen_content = 3.62\n", " prediction_successful = False\n", " else:\n", " nitrogen_content = raw_prediction\n", " prediction_successful = True\n", " \n", " except Exception as e:\n", " print(f\"โŒ Prediction error: {e}\")\n", " traceback.print_exc()\n", " nitrogen_content = 3.62\n", " else:\n", " print(\"โš ๏ธ Model not loaded, using default value\")\n", " nitrogen_content = 3.62\n", " \n", " print(f\"โœ… Final nitrogen content: {nitrogen_content:.2f}%\")\n", " # ===========================================================================\n", " \n", " # Classify\n", " classification = classify_nitrogen_level(nitrogen_content)\n", " \n", " # Save intermediate images (optional - you can remove if not needed)\n", " def save_intermediate_image(img, prefix):\n", " try:\n", " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", " img_filename = f\"{prefix}_{timestamp}_{uuid.uuid4().hex[:8]}.jpg\"\n", " img_path = os.path.join(app.config['UPLOAD_FOLDER'], img_filename)\n", " \n", " if isinstance(img, np.ndarray):\n", " if len(img.shape) == 3 and img.shape[2] == 3:\n", " img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)\n", " cv2.imwrite(img_path, img_bgr)\n", " else:\n", " cv2.imwrite(img_path, img)\n", " \n", " return f'/static/uploads/{img_filename}'\n", " return None\n", " except:\n", " return None\n", " \n", " # Save original image\n", " original_filename = f\"original_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg\"\n", " original_path = os.path.join(app.config['UPLOAD_FOLDER'], original_filename)\n", " \n", " if os.path.exists(image_path):\n", " shutil.copy2(image_path, original_path)\n", " original_url = f'/static/uploads/{original_filename}'\n", " else:\n", " original_url = f'/static/uploads/{filename}'\n", " \n", " timestamp = int(time.time())\n", " \n", " bg_removed_url = save_intermediate_image(bg_removed, 'bg_removed')\n", " edge_detected_url = save_intermediate_image(edge_image, 'edge_detected')\n", " \n", " intermediate_images = {\n", " 'original': f\"{original_url}?v={timestamp}\",\n", " 'bg_removed': f\"{bg_removed_url}?v={timestamp}\" if bg_removed_url else None,\n", " 'edge_detected': f\"{edge_detected_url}?v={timestamp}\" if edge_detected_url else None\n", " }\n", " \n", " # Store in session and redirect\n", " session['prediction_result'] = {\n", " 'nitrogen_content': round(float(nitrogen_content), 2),\n", " 'fertilizer_applied': fertilizer_amount,\n", " 'days': days,\n", " 'classification': classification,\n", " 'images': intermediate_images,\n", " 'prediction_successful': prediction_successful\n", " }\n", " session.modified = True\n", " \n", " return redirect(url_for('result'))\n", " \n", " except Exception as e:\n", " print(f\"โŒ Prediction route error: {e}\")\n", " traceback.print_exc()\n", " return jsonify({'error': str(e)}), 500\n", "\n", "@app.route('/result')\n", "def result():\n", " \"\"\"Result page route\"\"\"\n", " prediction_result = session.get('prediction_result', None)\n", " \n", " if prediction_result is None:\n", " return redirect(url_for('estimation'))\n", " \n", " return render_template('result.html', result=prediction_result)\n", " \n", "# ==================== NAVIGATION ROUTES ====================\n", "@app.route('/about')\n", "def about():\n", " return render_template('about.html')\n", "\n", "@app.route('/how-it-works')\n", "def how_it_works():\n", " return render_template('how-it-works.html')\n", "\n", "@app.route('/contact')\n", "def contact():\n", " return render_template('contact.html')\n", "\n", "@app.route('/team')\n", "def team():\n", " return render_template('team.html')\n", "# ==================== END OF ROUTES ====================\n", "\n", "if __name__ == '__main__':\n", " os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)\n", " print(f\"๐Ÿ“ Upload folder ready: {app.config['UPLOAD_FOLDER']}\")\n", " print(f\"๐Ÿ“ U2Net weights should be at: u2net/weights/u2net.pth\")\n", " print(f\"\\\\n๐Ÿš€ Starting NitroSense AI application...\")\n", " print(f\" Access at: http://localhost:5000\")\n", " print(f\" Press CTRL+C to quit\\\\n\")\n", " \n", " # Run the app with proper settings\n", " try:\n", " app.run(\n", " debug=True, \n", " host='0.0.0.0', \n", " port=5000,\n", " use_reloader=False # Disable reloader to avoid watchdog issues\n", " )\n", " except SystemExit:\n", " print(\"\\\\n๐Ÿ‘‹ Application stopped normally\")\n", " except Exception as e:\n", " print(f\"\\\\nโŒ Error running application: {e}\")\n", " traceback.print_exc()\n", "'''\n", "\n", "# Write to file\n", "with open('app.py', 'w', encoding='utf-8') as f:\n", " f.write(flask_app_code)\n", "\n", "print(\"โœ… Updated app.py created successfully with portable U2Net and session check!\")" ] }, { "cell_type": "code", "execution_count": 2, "id": "bc618b1e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "==================================================\n", "SCALER_Y VERIFICATION\n", "==================================================\n", "Scaler_y type: \n", "Mean: [3.62491874]\n", "Scale: [0.72524264]\n", "n_features_in_: 1\n", "\n", "๐Ÿ“Š Test denormalization:\n", "----------------------------------------\n", "Normalized 0.00 -> Original 3.62% (should be around 2%)\n", "Normalized 0.50 -> Original 3.99% (should be around 3.5%)\n", "Normalized 1.00 -> Original 4.35% (should be around 5%)\n", "\n", "๐Ÿ“ˆ Expected nitrogen range: [2.17%, 5.08%]\n" ] } ], "source": [ "# %% [markdown]\n", "# ### Verify scaler_y is working correctly\n", "\n", "# %%\n", "import pickle\n", "import numpy as np\n", "\n", "try:\n", " with open('utils/scalers/scaler_y.pkl', 'rb') as f:\n", " scaler_y = pickle.load(f)\n", " \n", " print(\"=\"*50)\n", " print(\"SCALER_Y VERIFICATION\")\n", " print(\"=\"*50)\n", " print(f\"Scaler_y type: {type(scaler_y)}\")\n", " print(f\"Mean: {scaler_y.mean_}\")\n", " print(f\"Scale: {scaler_y.scale_}\")\n", " print(f\"n_features_in_: {scaler_y.n_features_in_}\")\n", " \n", " # Test denormalization\n", " test_normalized = np.array([[0.0], [0.5], [1.0]])\n", " test_original = scaler_y.inverse_transform(test_normalized)\n", " \n", " print(\"\\n๐Ÿ“Š Test denormalization:\")\n", " print(\"-\" * 40)\n", " print(f\"Normalized 0.00 -> Original {test_original[0][0]:.2f}% (should be around 2%)\")\n", " print(f\"Normalized 0.50 -> Original {test_original[1][0]:.2f}% (should be around 3.5%)\")\n", " print(f\"Normalized 1.00 -> Original {test_original[2][0]:.2f}% (should be around 5%)\")\n", " \n", " # Expected range based on your training data\n", " expected_min = scaler_y.mean_[0] - 2*scaler_y.scale_[0]\n", " expected_max = scaler_y.mean_[0] + 2*scaler_y.scale_[0]\n", " print(f\"\\n๐Ÿ“ˆ Expected nitrogen range: [{expected_min:.2f}%, {expected_max:.2f}%]\")\n", " \n", "except Exception as e:\n", " print(f\"โŒ Error loading scaler_y: {e}\")" ] }, { "cell_type": "code", "execution_count": 5, "id": "4f9add22", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Upload folder exists: True\n", "Upload folder writable: True\n" ] } ], "source": [ "import requests\n", "import os\n", "\n", "# Test if the upload folder exists and is writable\n", "upload_folder = 'static/uploads'\n", "os.makedirs(upload_folder, exist_ok=True)\n", "print(f\"Upload folder exists: {os.path.exists(upload_folder)}\")\n", "print(f\"Upload folder writable: {os.access(upload_folder, os.W_OK)}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "b6706560", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.5" } }, "nbformat": 4, "nbformat_minor": 5 }