Sara-Adjo commited on
Commit
a18e884
·
verified ·
1 Parent(s): 8f56b8f

Upload 9 files

Browse files
Files changed (10) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +18 -0
  3. app.py +103 -0
  4. data_loader.py +140 -0
  5. model_pytorch.py +90 -0
  6. model_tensorflow.py +70 -0
  7. predict.py +141 -0
  8. requirements.txt +8 -0
  9. sara_model.keras +3 -0
  10. sara_model.pth +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ sara_model.keras filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+
7
+ RUN pip install --no-cache-dir flask pillow numpy gunicorn
8
+ RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
9
+ RUN pip install --no-cache-dir tensorflow-cpu
10
+
11
+
12
+ COPY . .
13
+
14
+
15
+ EXPOSE 7860
16
+
17
+
18
+ CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:7860", "--timeout", "120"]
app.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ from pathlib import Path
5
+ from flask import (Flask, request, render_template,jsonify, redirect, url_for)
6
+ from werkzeug.utils import secure_filename
7
+ from PIL import Image
8
+ from predict import predict_pytorch, predict_tensorflow
9
+
10
+
11
+
12
+ app = Flask(__name__)
13
+
14
+
15
+ app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024 # 5 MB upload limit
16
+ ALLOWED_EXT = {"png", "jpg", "jpeg", "webp", "bmp"}
17
+
18
+ PYTORCH_MODEL_PATH = os.getenv("PYTORCH_MODEL_PATH", "sara_model.pth")
19
+ TF_MODEL_PATH = os.getenv("TF_MODEL_PATH", "sara_model.keras")
20
+
21
+ CLASS_ICONS = {
22
+ "buildings",
23
+ "forest",
24
+ "glacier",
25
+ "mountain",
26
+ "sea",
27
+ "street",
28
+ }
29
+
30
+
31
+ def allowed_file(filename: str) -> bool:
32
+ return ("." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXT)
33
+
34
+
35
+ def image_to_b64(img: Image.Image, fmt: str = "JPEG") -> str:
36
+ buf = io.BytesIO()
37
+ img.convert("RGB").save(buf, format=fmt)
38
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
39
+
40
+
41
+ # Routes
42
+ @app.route("/", methods=["GET"])
43
+ def index():
44
+ return render_template("index.html")
45
+
46
+
47
+ @app.route("/predict", methods=["POST"])
48
+ def predict():
49
+ model_choice = request.form.get("model", "pytorch")
50
+ file = request.files.get("image")
51
+
52
+ if not file or file.filename == "":
53
+ return render_template("index.html", error="Please upload an image file."), 400
54
+
55
+ if not allowed_file(file.filename):
56
+ return render_template("index.html", error="Unsupported file type. " "Use JPG, PNG, WEBP or BMP."), 400
57
+
58
+
59
+ img_bytes = file.read()
60
+ pil_img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
61
+
62
+ tmp_path = Path("tmp_upload.jpg")
63
+ pil_img.save(tmp_path, format="JPEG")
64
+
65
+ try:
66
+ if model_choice == "pytorch":
67
+ result = predict_pytorch(str(tmp_path),model_path=PYTORCH_MODEL_PATH)
68
+ else:
69
+ result = predict_tensorflow(str(tmp_path),model_path=TF_MODEL_PATH)
70
+ except FileNotFoundError as e:
71
+ tmp_path.unlink(missing_ok=True)
72
+ return render_template("index.html",error=f"Model file not found: {e}. " "Train a model first."), 500
73
+ except Exception as e:
74
+ tmp_path.unlink(missing_ok=True)
75
+ return render_template("index.html",error=f"Inference error: {e}"), 500
76
+ finally:
77
+ tmp_path.unlink(missing_ok=True)
78
+
79
+ img_b64 = image_to_b64(pil_img)
80
+ probs = result["all_probabilities"]
81
+
82
+ # Sort by confidence descending for the bar chart
83
+ sorted_probs = sorted(probs.items(), key=lambda x: -x[1])
84
+
85
+ return render_template(
86
+ "index.html",
87
+ result = result,
88
+ model_used = model_choice,
89
+ img_b64 = img_b64,
90
+ sorted_probs = sorted_probs,
91
+ class_icons = CLASS_ICONS,
92
+ )
93
+
94
+
95
+ @app.route("/health")
96
+ def health():
97
+ return jsonify({"status": "ok"}), 200
98
+
99
+
100
+
101
+ if __name__ == "__main__":
102
+ port = int(os.getenv("PORT", 7860))
103
+ app.run(host="0.0.0.0", port=port, debug=False)
data_loader.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ from pathlib import Path
4
+ import torch
5
+ from torch.utils.data import DataLoader, random_split
6
+ from torchvision import datasets, transforms
7
+ import tensorflow as tf
8
+
9
+
10
+
11
+ IMAGE_SIZE = (150, 150)
12
+ BATCH_SIZE = 32
13
+ VAL_SPLIT = 0.2
14
+ SEED = 42
15
+
16
+ CLASS_NAMES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]
17
+
18
+
19
+
20
+
21
+ # PyTorch Data Pipeline
22
+ def get_pytorch_loaders(train_dir: str, test_dir: str):
23
+ mean = [0.485, 0.456, 0.406]
24
+ std = [0.229, 0.224, 0.225]
25
+
26
+
27
+ train_transform = transforms.Compose([
28
+ transforms.Resize(IMAGE_SIZE),
29
+ transforms.RandomHorizontalFlip(p=0.5),
30
+ transforms.RandomRotation(degrees=15),
31
+ transforms.RandomResizedCrop(IMAGE_SIZE,scale=(0.8, 1.0)),
32
+ transforms.ColorJitter(brightness=0.2,contrast=0.2),
33
+ transforms.ToTensor(),
34
+ transforms.Normalize(mean, std),
35
+ ])
36
+
37
+ eval_transform = transforms.Compose([
38
+ transforms.Resize(IMAGE_SIZE),
39
+ transforms.ToTensor(),
40
+ transforms.Normalize(mean, std),
41
+ ])
42
+
43
+ full_train = datasets.ImageFolder(root=train_dir,transform=train_transform)
44
+
45
+ # Split into train / validation
46
+ n_val = int(len(full_train) * VAL_SPLIT)
47
+ n_train = len(full_train) - n_val
48
+ train_ds, val_ds = random_split(
49
+ full_train, [n_train, n_val],
50
+ generator=torch.Generator().manual_seed(SEED)
51
+ )
52
+
53
+
54
+ val_ds.dataset = datasets.ImageFolder(root=train_dir,transform=eval_transform)
55
+ test_ds = datasets.ImageFolder(root=test_dir, transform=eval_transform)
56
+
57
+
58
+ train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)
59
+ val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)
60
+ test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)
61
+
62
+ print(f"[PyTorch] Train: {n_train} | Val: {n_val} | Test: {len(test_ds)}")
63
+ return train_loader, val_loader, test_loader, full_train.classes
64
+
65
+
66
+
67
+
68
+
69
+ # TensorFlow Data Pipeline
70
+ def get_tensorflow_datasets(train_dir: str, test_dir: str):
71
+ img_h, img_w = IMAGE_SIZE
72
+
73
+ raw_train = tf.keras.utils.image_dataset_from_directory(
74
+ train_dir,
75
+ validation_split=VAL_SPLIT,
76
+ subset="training",
77
+ seed=SEED,
78
+ image_size=IMAGE_SIZE,
79
+ batch_size=BATCH_SIZE,
80
+ label_mode="categorical",
81
+ )
82
+
83
+ raw_val = tf.keras.utils.image_dataset_from_directory(
84
+ train_dir,
85
+ validation_split=VAL_SPLIT,
86
+ subset="validation",
87
+ seed=SEED,
88
+ image_size=IMAGE_SIZE,
89
+ batch_size=BATCH_SIZE,
90
+ label_mode="categorical",
91
+ )
92
+
93
+ raw_test = tf.keras.utils.image_dataset_from_directory(
94
+ test_dir,
95
+ image_size=IMAGE_SIZE,
96
+ batch_size=BATCH_SIZE,
97
+ label_mode="categorical",
98
+ shuffle=False,
99
+ )
100
+
101
+
102
+ class_names = raw_train.class_names
103
+
104
+
105
+ normalization = tf.keras.layers.Rescaling(1.0 / 255)
106
+ augmentation = tf.keras.Sequential([
107
+ tf.keras.layers.RandomFlip("horizontal"),
108
+ tf.keras.layers.RandomRotation(0.1),
109
+ tf.keras.layers.RandomZoom(0.2),
110
+ tf.keras.layers.RandomContrast(0.1),
111
+ ])
112
+
113
+ def preprocess_train(images, labels):
114
+ images = normalization(images)
115
+ images = augmentation(images, training=True)
116
+ return images, labels
117
+
118
+ def preprocess_eval(images, labels):
119
+ images = normalization(images)
120
+ return images, labels
121
+
122
+ AUTOTUNE = tf.data.AUTOTUNE
123
+
124
+ train_ds = (raw_train
125
+ .map(preprocess_train, num_parallel_calls=AUTOTUNE)
126
+ .cache()
127
+ .shuffle(1000)
128
+ .prefetch(AUTOTUNE))
129
+
130
+ val_ds = (raw_val
131
+ .map(preprocess_eval, num_parallel_calls=AUTOTUNE)
132
+ .cache()
133
+ .prefetch(AUTOTUNE))
134
+
135
+ test_ds = (raw_test
136
+ .map(preprocess_eval, num_parallel_calls=AUTOTUNE)
137
+ .prefetch(AUTOTUNE))
138
+
139
+ print(f"[TensorFlow] Classes: {class_names}")
140
+ return train_ds, val_ds, test_ds, class_names
model_pytorch.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ model_pytorch.py
3
+ Architecture summary:
4
+ Block 1 : Conv(32) → BN → ReLU → Conv(32) → BN → ReLU → MaxPool → Dropout
5
+ Block 2 : Conv(64) → BN → ReLU → Conv(64) → BN → ReLU → MaxPool → Dropout
6
+ Block 3 : Conv(128) → BN → ReLU → Conv(128) → BN → ReLU → MaxPool → Dropout
7
+ Head : Flatten → FC(256) → BN → ReLU → Dropout → FC(6) → Softmax
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+
14
+
15
+ class ConvBlock(nn.Module):
16
+ """
17
+ Reusable double-convolution block.
18
+ Two Conv2d layers → Batch Norm → ReLU → MaxPool → Dropout.
19
+ """
20
+ def __init__(self, in_channels: int, out_channels: int, dropout_rate: float = 0.25):
21
+ super().__init__()
22
+
23
+ self.block = nn.Sequential(
24
+ nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
25
+ nn.BatchNorm2d(out_channels),
26
+ nn.ReLU(inplace=True),
27
+
28
+ nn.Conv2d(out_channels, out_channels,kernel_size=3, padding=1, bias=False),
29
+ nn.BatchNorm2d(out_channels),
30
+ nn.ReLU(inplace=True),
31
+
32
+ nn.MaxPool2d(kernel_size=2, stride=2),
33
+
34
+ nn.Dropout2d(p=dropout_rate),
35
+ )
36
+
37
+ def forward(self, x):
38
+ return self.block(x)
39
+
40
+
41
+ class SaraCNN(nn.Module):
42
+ def __init__(self, num_classes: int = 6):
43
+ super().__init__()
44
+
45
+ self.features = nn.Sequential(
46
+ ConvBlock(3, 32, dropout_rate=0.25),
47
+ ConvBlock(32, 64, dropout_rate=0.25),
48
+ ConvBlock(64, 128, dropout_rate=0.25),
49
+ )
50
+
51
+ self.classifier = nn.Sequential(
52
+ nn.Flatten(),
53
+
54
+ nn.Linear(128 * 18 * 18, 256),
55
+ nn.BatchNorm1d(256),
56
+ nn.ReLU(inplace=True),
57
+ nn.Dropout(p=0.5),
58
+
59
+ nn.Linear(256, num_classes),
60
+ )
61
+
62
+
63
+ self._init_weights()
64
+
65
+ def _init_weights(self):
66
+ for m in self.modules():
67
+ if isinstance(m, nn.Conv2d):
68
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
69
+ elif isinstance(m, nn.Linear):
70
+ nn.init.xavier_uniform_(m.weight)
71
+ nn.init.zeros_(m.bias)
72
+ elif isinstance(m, (nn.BatchNorm2d, nn.BatchNorm1d)):
73
+ nn.init.ones_(m.weight)
74
+ nn.init.zeros_(m.bias)
75
+
76
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
77
+ x = self.features(x)
78
+ x = self.classifier(x)
79
+ return x
80
+
81
+
82
+
83
+ if __name__ == "__main__":
84
+ model = SaraCNN(num_classes=6)
85
+ dummy = torch.randn(4, 3, 150, 150)
86
+ out = model(dummy)
87
+ print("SaraCNN output shape:", out.shape)
88
+
89
+ total = sum(p.numel() for p in model.parameters() if p.requires_grad)
90
+ print(f"Trainable parameters: {total:,}")
model_tensorflow.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Architecture summary:
3
+ Stem : Conv(32, 3×3) → BN → ReLU
4
+ Stage 1: SepConv(64) → BN → ReLU → MaxPool → Dropout
5
+ Stage 2: SepConv(128) → BN → ReLU → MaxPool → Dropout
6
+ Stage 3: SepConv(256) → BN → ReLU → MaxPool → Dropout
7
+ Head : GlobalAvgPool → Dense(128) → BN → ReLU → Dropout → Dense(6, softmax)
8
+
9
+ """
10
+
11
+ import tensorflow as tf
12
+ from tensorflow.keras import layers, Model
13
+
14
+
15
+ def separable_block(x, filters: int, dropout_rate: float = 0.25):
16
+ x = layers.SeparableConv2D(
17
+ filters, kernel_size=3, padding="same", use_bias=False)(x)
18
+ x = layers.BatchNormalization()(x)
19
+ x = layers.Activation("relu")(x)
20
+
21
+ x = layers.SeparableConv2D(
22
+ filters, kernel_size=3, padding="same", use_bias=False)(x)
23
+ x = layers.BatchNormalization()(x)
24
+ x = layers.Activation("relu")(x)
25
+
26
+ x = layers.MaxPooling2D(pool_size=2)(x)
27
+
28
+ x = layers.SpatialDropout2D(rate=dropout_rate)(x)
29
+
30
+ return x
31
+
32
+
33
+ def build_sara_tf_model(input_shape=(150, 150, 3), num_classes: int = 6) -> Model:
34
+
35
+ inputs = tf.keras.Input(shape=input_shape, name="image_input")
36
+
37
+ x = layers.Conv2D(32, kernel_size=3, padding="same",
38
+ use_bias=False, name="stem_conv")(inputs)
39
+ x = layers.BatchNormalization(name="stem_bn")(x)
40
+ x = layers.Activation("relu", name="stem_relu")(x)
41
+
42
+ x = separable_block(x, filters=64, dropout_rate=0.25)
43
+ x = separable_block(x, filters=128, dropout_rate=0.25)
44
+ x = separable_block(x, filters=256, dropout_rate=0.30)
45
+
46
+
47
+ x = layers.GlobalAveragePooling2D(name="gap")(x)
48
+
49
+ x = layers.Dense(128, use_bias=False, name="fc1")(x)
50
+ x = layers.BatchNormalization(name="fc1_bn")(x)
51
+ x = layers.Activation("relu", name="fc1_relu")(x)
52
+ x = layers.Dropout(0.5, name="fc1_drop")(x)
53
+
54
+ outputs = layers.Dense(num_classes, activation="softmax",
55
+ name="predictions")(x)
56
+
57
+ model = Model(inputs=inputs, outputs=outputs, name="SaraCNN_TF")
58
+
59
+ model.compile(
60
+ optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
61
+ loss="categorical_crossentropy",
62
+ metrics=["accuracy"],
63
+ )
64
+
65
+ return model
66
+
67
+
68
+ if __name__ == "__main__":
69
+ model = build_sara_tf_model()
70
+ model.summary()
predict.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import numpy as np
3
+ from pathlib import Path
4
+ from PIL import Image
5
+ import torch
6
+ from torchvision import transforms
7
+ from model_pytorch import SaraCNN
8
+ import tensorflow as tf
9
+
10
+
11
+ CLASS_NAMES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]
12
+
13
+ IMAGE_SIZE = (150, 150)
14
+ MEAN = [0.485, 0.456, 0.406]
15
+ STD = [0.229, 0.224, 0.225]
16
+
17
+
18
+
19
+
20
+ # PyTorch Prediction
21
+ def predict_pytorch(image_path: str,
22
+ model_path: str = "sara_model.pth") -> dict:
23
+ """
24
+ Load the PyTorch checkpoint and return class probabilities.
25
+
26
+ Args:
27
+ image_path : path to the input image
28
+ model_path : path to the saved .pth file
29
+
30
+ Returns:
31
+ dict with keys: predicted_class, confidence, all_probabilities
32
+ """
33
+
34
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
35
+
36
+ checkpoint = torch.load(model_path, map_location=device)
37
+ class_names = checkpoint.get("class_names", CLASS_NAMES)
38
+ num_classes = len(class_names)
39
+
40
+ model = SaraCNN(num_classes=num_classes)
41
+ model.load_state_dict(checkpoint["model_state"])
42
+ model.to(device)
43
+ model.eval()
44
+
45
+ transform = transforms.Compose([
46
+ transforms.Resize(IMAGE_SIZE),
47
+ transforms.ToTensor(),
48
+ transforms.Normalize(MEAN, STD),
49
+ ])
50
+
51
+ img = Image.open(image_path).convert("RGB")
52
+ tensor = transform(img).unsqueeze(0).to(device)
53
+
54
+ with torch.no_grad():
55
+ logits = model(tensor)
56
+ probs = torch.softmax(logits, dim=1).squeeze().cpu().numpy()
57
+
58
+ idx = int(np.argmax(probs))
59
+ pred_class = class_names[idx]
60
+ confidence = float(probs[idx])
61
+
62
+ return {
63
+ "predicted_class": pred_class,
64
+ "confidence": round(confidence * 100, 2),
65
+ "all_probabilities": {
66
+ cls: round(float(p) * 100, 2)
67
+ for cls, p in zip(class_names, probs)
68
+ },
69
+ }
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+ # TensorFlow Prediction
78
+ def predict_tensorflow(image_path: str,
79
+ model_path: str = "sara_model.keras") -> dict:
80
+ """
81
+ Load the Keras model and return class probabilities.
82
+
83
+ Args:
84
+ image_path : path to the input image
85
+ model_path : path to the saved .keras file
86
+
87
+ Returns:
88
+ dict with keys: predicted_class, confidence, all_probabilities
89
+ """
90
+
91
+ model = tf.keras.models.load_model(model_path)
92
+
93
+ img = tf.keras.utils.load_img(image_path, target_size=IMAGE_SIZE)
94
+ arr = tf.keras.utils.img_to_array(img)
95
+ arr = arr / 255.0
96
+ arr = np.expand_dims(arr, axis=0)
97
+
98
+
99
+ probs = model.predict(arr, verbose=0)[0]
100
+ idx = int(np.argmax(probs))
101
+ pred_class = CLASS_NAMES[idx]
102
+ confidence = float(probs[idx])
103
+
104
+ return {
105
+ "predicted_class": pred_class,
106
+ "confidence": round(confidence * 100, 2),
107
+ "all_probabilities": {
108
+ cls: round(float(p) * 100, 2)
109
+ for cls, p in zip(CLASS_NAMES, probs)
110
+ },
111
+ }
112
+
113
+
114
+
115
+
116
+
117
+ # CLI Entry Point
118
+ def parse_args():
119
+ p = argparse.ArgumentParser(description="Predict image class")
120
+ p.add_argument("--model", required=True, choices=["pytorch", "tensorflow"])
121
+ p.add_argument("--image", required=True, help="Path to the image file")
122
+ p.add_argument("--model_path", default=None, help="Override default model file path")
123
+ return p.parse_args()
124
+
125
+
126
+ if __name__ == "__main__":
127
+ args = parse_args()
128
+
129
+ if args.model == "pytorch":
130
+ path = args.model_path or "sara_model.pth"
131
+ result = predict_pytorch(args.image, model_path=path)
132
+ else:
133
+ path = args.model_path or "sara_model.keras"
134
+ result = predict_tensorflow(args.image, model_path=path)
135
+
136
+ print(f"\n Predicted class : {result['predicted_class']}")
137
+ print(f" Confidence : {result['confidence']}%")
138
+ print("\n All probabilities:")
139
+ for cls, prob in sorted(result["all_probabilities"].items(), key=lambda x: -x[1]):
140
+ bar = " " * int(prob / 5)
141
+ print(f"{cls:<12} {prob:6.2f}% {bar}")
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ tensorflow>=2.14.0
2
+ Pillow>=10.0.0
3
+ numpy>=1.24.0
4
+ Flask>=3.0.0
5
+ Werkzeug>=3.0.0
6
+ gunicorn>=21.2.0
7
+ tqdm>=4.66.0
8
+ matplotlib>=3.7.0
sara_model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d30ece385de64716fd13700050611ebe94eff9217deeb1e54c2b0877e6964bef
3
+ size 2214460
sara_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:485b7717b80e6d2506458f57323a5bfaa9b70e8994bb2cbcba17ea3dfe67761b
3
+ size 130921990