diff --git a/Models/model_host.py b/Models/model_host.py index 3d3fb2e..5359eca 100644 --- a/Models/model_host.py +++ b/Models/model_host.py @@ -1,6 +1,7 @@ import keras from keras.src.legacy.preprocessing.image import ImageDataGenerator -from flask import Flask, render_template, send_file, request +from flask import Flask, render_template, send_file, request, jsonify +import traceback import numpy as np import tensorflow import sys @@ -12,6 +13,11 @@ import uuid #init flaskapp app = Flask(__name__) +@app.errorhandler(Exception) +def handle_exception(e): + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + # use this on image passed in to the API def save_image(imgData): filename=str(uuid.uuid4())+'.jpg' @@ -69,11 +75,11 @@ def predict_vgg16(): else : threshold_output = np.where(predictions > 0.5, 1, 0) response=str(predictions)+'-->'+str(threshold_output) + print('/predict_vgg16:'+response) return response @app.route('/predict_resnet50', methods=['GET','POST']) def predict_resnet50(): - print('/predict_resnet50') test_image=request.get_data() test_image = PIL.Image.open(io.BytesIO(test_image)) test_image = test_image.convert('L') @@ -86,13 +92,13 @@ def predict_resnet50(): else : threshold_output = np.where(predictions > 0.5, 1, 0) response=str(predictions)+'-->'+str(threshold_output) + print('/predict_resnet50:'+response) return response # New model updated in October 2024 # To Test : curl -X POST --data-binary @ADT_11_0_0_Training_BollingerBand_20191129_270d.jpg http://127.0.0.1:5000/predict_resnet50_20241024_270 @app.route('/predict_resnet50_20241024_270', methods=['GET','POST']) def predict_resnet50_20241024_270(): - print('/predict_resnet50_20241024_270') test_image=request.get_data() test_image = PIL.Image.open(io.BytesIO(test_image)) test_image = test_image.convert('L') @@ -105,29 +111,36 @@ def predict_resnet50_20241024_270(): else : threshold_output = np.where(predictions > 0.5, 1, 0) response=str(predictions)+'-->'+str(threshold_output) + print('/predict_resnet50_20241024_270:'+response) return response -# This version expects the image to be of the form (x,x,3). -# @app.route('/predict_resnet50B', methods=['GET','POST']) -# def predict_resnet50B(): -# print('/predict_resnet50B') -# test_image=request.get_data() -# save_image(test_image) -# test_image = PIL.Image.open(io.BytesIO(test_image)) -# test_array=keras.preprocessing.image.img_to_array(test_image) -# batch_test_array=np.array([test_array]) -# predictions=resnet50b_model.predict(batch_test_array) -# if type(predictions) == list: -# average_prediction = sum(predictions)/len(predictions) -# threshold_output = np.where(average_prediction > 0.5, 1, 0) -# else : -# threshold_output = np.where(predictions > 0.5, 1, 0) -# response=str(predictions)+'-->'+str(threshold_output) -# return response +@tensorflow.function(reduce_retracing=True) +def infer_convnext(x): + return convnext_model(x, training=False) + +# New model updated in February 2026 ConvNexT) +@app.route('/predict_convnext', methods=['GET','POST']) +def predict_convnext(): + test_image=request.get_data() + test_image = PIL.Image.open(io.BytesIO(test_image)) + # Convert grayscale → RGB + test_image = test_image.convert('RGB') + # Resize to match training size + if test_image.size != (224, 224): + test_image = test_image.resize((224, 224)) + test_array=keras.preprocessing.image.img_to_array(test_image) + batch_test_array = np.expand_dims(test_array, axis=0) +# predictions=convnext_model.predict(batch_test_array) +# predictions = convnext_model(batch_test_array, training=False) + predictions = infer_convnext(batch_test_array) + threshold_output = np.where(predictions > 0.5, 1, 0) + response=str(predictions)+'-->'+str(threshold_output) + print('/predict_convnext:'+response) + return response + @app.route('/predict_lenet5', methods=['GET','POST']) def predict_lenet5(): - print('/predict_lenet5') test_image=request.get_data() test_image = PIL.Image.open(io.BytesIO(test_image)) test_image = test_image.convert('L') @@ -140,6 +153,7 @@ def predict_lenet5(): else : threshold_output = np.where(predictions > 0.5, 1, 0) response=str(predictions)+'-->'+str(threshold_output) + print('/predict_lenet5:'+response) return response # This method is used to process an image through PIL and send it back to the client. The client can then used this processed image as part of the training data @@ -172,9 +186,10 @@ if __name__ == '__main__': print('Loading {model_name}'.format(model_name=resnet50_20241024_270_model_name)) resnet50_20241024_270_model = keras.models.load_model(resnet50_20241024_270_model_name) - # resnet50b_model_name='../Weights/resnet50B.h5' - # print('Loading {model_name}'.format(model_name=resnet50b_model_name)) - # resnet50b_model = keras.models.load_model(resnet50b_model_name) + convnext_model_name='../Weights/convnext_20260228_90.h5.keras' + convnext_model_name=os.path.realpath(path + '/' + convnext_model_name) + print('Loading {model_name}'.format(model_name=convnext_model_name)) + convnext_model=keras.models.load_model(convnext_model_name); vgg16_model_name='../Weights/vggnet16.h5.keras' vgg16_model_name = os.path.realpath(path + '/' + vgg16_model_name) diff --git a/Models/model_sk_convnext_v1.py b/Models/model_sk_convnext_v1.py new file mode 100644 index 0000000..6b1637f --- /dev/null +++ b/Models/model_sk_convnext_v1.py @@ -0,0 +1,256 @@ +import tensorflow as tf +from tensorflow.keras import layers, models, regularizers +from tensorflow.keras.optimizers import Adam +from tensorflow.keras.callbacks import TensorBoard, ModelCheckpoint, EarlyStopping, ReduceLROnPlateau +from tensorflow.keras.applications import ConvNeXtTiny +from tensorflow.keras.applications.convnext import preprocess_input +from time import time +import matplotlib.pyplot as plt + +# This is the new 2026 version +# This model was trained with 13,185 images +# See CNNImageProcessor solution for create the test images for training this model + + + +# Figure out if we are training in CPU or GPU +print("GPUs:", tf.config.list_physical_devices('GPU')) +# ----------------------- +# ConvNeXt-Tiny Base Model +# ----------------------- +modelname='convnext_20260228_90.h5.keras' + +# convneXt was pretrained with 224 but our image data is 128 so we upscale our images to match the 224 requirements of the model +actualImageDimension=224 +convneXtImageDimension=224 + +# Tensorboard +log_dir = f'logs/convnext_{int(time())}' +tensorboard = TensorBoard(log_dir=log_dir) + +# ----------------------- +# Configuration +# ----------------------- +shuffle_count=3000 +dataset_path = 'C:\\boneyard\\DeepLearning\\data' +image_size = (actualImageDimension, actualImageDimension) +batch_size = 16 # try 16 was 32 +image_size=(actualImageDimension, actualImageDimension) + + +# ----------------------- +# Dataset Loading +# ----------------------- + +train_ds = tf.keras.preprocessing.image_dataset_from_directory( + dataset_path, + label_mode="binary", + subset="training", + validation_split=0.2, + image_size=image_size, + color_mode='rgb', # IMPORTANT for grayscale datasets + batch_size=batch_size, + seed=50 +) + +val_ds = tf.keras.preprocessing.image_dataset_from_directory( + dataset_path, + label_mode="binary", + subset="validation", + validation_split=0.2, + image_size=image_size, + color_mode='rgb', + batch_size=batch_size, + seed=50 +) + +# ----------------------- +# Data Augmentation +# ----------------------- + +# data_augmentation = tf.keras.Sequential([ +# layers.RandomFlip("horizontal"), +# layers.RandomRotation(0.1) +# ]) +#data_augmentation = tf.keras.Sequential([ +# layers.RandomFlip("horizontal"), +# layers.RandomRotation(0.1), +# layers.RandomRotation(0.1, fill_mode="nearest"), +# layers.RandomZoom(0.1) +#]) + + +# def preprocess_train(x, y): +# x = data_augmentation(x, training=True) +# return x, y + +def preprocess_val(x, y): + return x, y + +val_ds = ( + val_ds + .prefetch(tf.data.AUTOTUNE) +) + +train_ds = ( + train_ds + .shuffle(3000) + .prefetch(tf.data.AUTOTUNE) +) + + +# for images, labels in train_ds.take(1): + +# plt.figure(figsize=(10,10)) + +# for i in range(12): +# ax = plt.subplot(3,4,i+1) +# plt.imshow(images[i].numpy().astype("uint8")) +# plt.title(int(labels[i].numpy())) +# plt.axis("off") + +# plt.tight_layout() +# plt.show() + + + + +# ----------------------- +# ConvNeXt-Tiny Base Model +# ----------------------- + +base_model = ConvNeXtTiny( + weights='imagenet', + include_top=False, + input_shape=(convneXtImageDimension, convneXtImageDimension, 3) +) + +base_model.trainable = False # Freeze for initial training + +# ----------------------- +# Build Full Model (Preprocessing Inside Model) +# ----------------------- +inputs = tf.keras.Input(shape=(actualImageDimension, actualImageDimension, 3)) + + + +x = preprocess_input(inputs) +x = base_model(x) +# Dense Head +# x = layers.GlobalAveragePooling2D()(x) +# x = layers.BatchNormalization()(x) +# x = layers.Dense(512, activation="relu")(x) +# x = layers.Dropout(0.3)(x) +# x = layers.Dense(128, activation="relu")(x) + +x = layers.GlobalAveragePooling2D()(x) +x = layers.BatchNormalization()(x) +x = layers.Dense(256, activation="relu")(x) +x = layers.Dropout(0.4)(x) + +# End Dense Head +outputs = layers.Dense(1, activation="sigmoid")(x) +model = tf.keras.Model(inputs, outputs) + + +model.compile( + optimizer=Adam(learning_rate=1e-4), + loss='binary_crossentropy', + metrics=['accuracy'] +) + +model.summary() + +# ----------------------- +# Callbacks +# ----------------------- + + +early_stopping = EarlyStopping( + monitor='val_loss', + patience=15, + restore_best_weights=True, + verbose=1 +) + +checkpointer = ModelCheckpoint( + filepath=modelname, + monitor='val_accuracy', + save_best_only=True, + verbose=1 +) + +lr_scheduler = ReduceLROnPlateau( + monitor='val_loss', + factor=0.5, + patience=5, + min_lr=1e-6, + verbose=1 +) + +# ----------------------- +# Initial Training +# ----------------------- + +history = model.fit( + train_ds, + epochs=50, + validation_data=val_ds, + callbacks=[tensorboard, lr_scheduler, early_stopping, checkpointer] +) + +# ----------------------- +# Fine-Tuning +# ----------------------- + +base_model.trainable = True + +# Freeze early layers (recommended) +for layer in base_model.layers[:-40]: + layer.trainable = False + +model.compile( + optimizer=Adam(1e-5), + loss='binary_crossentropy', + metrics=['accuracy'] +) + +history_fine = model.fit( + train_ds, + epochs=50, + validation_data=val_ds, + callbacks=[tensorboard, lr_scheduler, early_stopping, checkpointer] +) + + +# ----------------------- +# Plot Results +# ----------------------- + +def plot_history(hist, title_prefix=""): + plt.figure() + plt.plot(hist.history['accuracy'], label='Train Accuracy') + plt.plot(hist.history['val_accuracy'], label='Val Accuracy') + plt.title(f'{title_prefix} Accuracy') + plt.xlabel('Epochs') + plt.ylabel('Accuracy') + plt.legend() + plt.show() + + plt.figure() + plt.plot(hist.history['loss'], label='Train Loss') + plt.plot(hist.history['val_loss'], label='Val Loss') + plt.title(f'{title_prefix} Loss') + plt.xlabel('Epochs') + plt.ylabel('Loss') + plt.legend() + plt.show() + +plot_history(history, "Initial Training") +plot_history(history_fine, "Fine-Tuning") + +# ----------------------- +# Save Final Model +# ----------------------- + +#model.save(modelname) \ No newline at end of file diff --git a/Models/verify_model_sk_convnext_v1.py b/Models/verify_model_sk_convnext_v1.py new file mode 100644 index 0000000..3459dd2 --- /dev/null +++ b/Models/verify_model_sk_convnext_v1.py @@ -0,0 +1,105 @@ +import os +import numpy as np +import matplotlib +matplotlib.use('Agg') # Headless backend +import matplotlib.pyplot as plt +from PIL import Image +from tensorflow.keras.preprocessing.image import ImageDataGenerator +from tensorflow.keras.models import load_model +from tensorflow.keras.applications.convnext import preprocess_input +from sklearn.metrics import confusion_matrix, accuracy_score, ConfusionMatrixDisplay + +# ------------------------------ +# This is the verification for convnext_20260228_90.h5.keras +# We are currently at 99% accuracy in the predictions with the data +# ------------------------------ + + +# ---------------------------- +# Paths +# ---------------------------- +# MODEL_PATH = "/home/pi/CNN/Weights/convnext_20260228_90.h5.keras" +# TEST_DIR = "/home/pi/DeepLearning/Data" # test images structured by class +# OUTPUT_DIR = "/home/pi/CNN/Models/evaluation_outputs" + +MODEL_PATH = "c:\boneyard\DeepLearning\CNN\Weights\convnext_20260228_90.h5.keras" +TEST_DIR = "c:\boneyard\DeepLearning\CNN\Data" # test images structured by class +OUTPUT_DIR = "c:\boneyard\DeepLearning\CNN\Models\evaluation_outputs" + +os.makedirs(OUTPUT_DIR, exist_ok=True) + +# ---------------------------- +# Suppress TensorFlow warnings +# ---------------------------- +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # only errors + +# ---------------------------- +# Test Data Generator +# ---------------------------- +test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input) + +test_generator = test_datagen.flow_from_directory( + TEST_DIR, + target_size=(224, 224), + batch_size=32, + class_mode='binary', + shuffle=False +) + +# ---------------------------- +# Load Model +# ---------------------------- +model = load_model(MODEL_PATH) +print("Model loaded successfully.") + +# ---------------------------- +# Predictions +# ---------------------------- +y_true = test_generator.classes +y_pred_probs = model.predict(test_generator, verbose=1) +y_pred = (y_pred_probs > 0.5).astype(int).reshape(-1) + +# ---------------------------- +# Accuracy +# ---------------------------- +accuracy = accuracy_score(y_true, y_pred) +print(f"Test Accuracy: {accuracy*100:.2f}%") + +# ---------------------------- +# Confusion Matrix +# ---------------------------- +cm = confusion_matrix(y_true, y_pred) +disp = ConfusionMatrixDisplay( + confusion_matrix=cm, + display_labels=list(test_generator.class_indices.keys()) +) +disp.plot(cmap=plt.cm.Blues) +plt.title("Confusion Matrix") +conf_matrix_path = os.path.join(OUTPUT_DIR, "confusion_matrix.png") +plt.savefig(conf_matrix_path, dpi=300) +plt.close() +print(f"Confusion matrix saved to: {conf_matrix_path}") + +# ---------------------------- +# Misclassified Images +# ---------------------------- +misclassified_idx = np.where(y_true != y_pred)[0] +print(f"Number of misclassified images: {len(misclassified_idx)}") + +MAX_IMAGES = 20 # Number of misclassified images to save +for i, idx in enumerate(misclassified_idx[:MAX_IMAGES]): + img_path = test_generator.filepaths[idx] + + # Open image with Pillow + img = Image.open(img_path).convert('RGB') + + plt.imshow(np.array(img)) + true_label = list(test_generator.class_indices.keys())[y_true[idx]] + pred_label = list(test_generator.class_indices.keys())[y_pred[idx]] + plt.title(f"True: {true_label}, Pred: {pred_label}") + plt.axis('off') + + save_path = os.path.join(OUTPUT_DIR, f"misclassified_{i+1}.png") + plt.savefig(save_path, dpi=200) + plt.close() + print(f"Saved misclassified image: {save_path}") \ No newline at end of file diff --git a/Weights/convnext_20260228_90.h5.keras b/Weights/convnext_20260228_90.h5.keras new file mode 100644 index 0000000..8286e06 Binary files /dev/null and b/Weights/convnext_20260228_90.h5.keras differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d41eb3f Binary files /dev/null and b/requirements.txt differ