amansingh9612 commited on
Commit
f2db646
·
verified ·
1 Parent(s): 11d608c

Upload 9 files

Browse files
Files changed (9) hide show
  1. .gitattributes +36 -35
  2. .gitignore +5 -0
  3. Dockerfile +32 -0
  4. README.md +11 -0
  5. app.js +94 -0
  6. app.py +498 -0
  7. model.py +492 -0
  8. requirements.txt +6 -0
  9. users.db +3 -0
.gitattributes CHANGED
@@ -1,35 +1,36 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz 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
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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
+ users.db filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ my/
2
+ __pycache__/
3
+ *.db
4
+ users.db
5
+ .vscode/
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies if any are needed for SQLite, Pillow, or other libraries
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy the requirements file and install dependencies
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Hugging Face Spaces requires running as a non-root user
15
+ RUN useradd -m -u 1000 user
16
+
17
+ # Copy the rest of the application files
18
+ COPY --chown=user:user . /app
19
+
20
+ # Set permissions so the app can create and write to the SQLite database
21
+ # We change permissions of the app directory because SQLite needs to create journal files alongside the .db file
22
+ RUN chmod 777 /app
23
+ RUN touch /app/users.db && chmod 666 /app/users.db
24
+
25
+ # Switch to the non-root user
26
+ USER user
27
+
28
+ # Expose port 7860 as required by Hugging Face Spaces
29
+ EXPOSE 7860
30
+
31
+ # Run the app
32
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Struct Scan AI
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: Struct Scan AI is an intelligent structural inspection platf
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.js ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ window.api = async function(url, options = {}) {
2
+ // Use mock token for now since backend doesn't properly implement JWT auth,
3
+ // but it does use Flask session which is automatic via cookies.
4
+ const headers = { ...options.headers };
5
+ if (!(options.body instanceof FormData)) {
6
+ headers["Content-Type"] = "application/json";
7
+ }
8
+ const token = localStorage.getItem("token");
9
+ if (token) headers["Authorization"] = "Bearer " + token;
10
+
11
+ const res = await fetch(url, { ...options, headers });
12
+
13
+ // Handle empty responses
14
+ if (res.status === 204) return null;
15
+
16
+ const text = await res.text();
17
+ let data = {};
18
+ if (text) {
19
+ try {
20
+ data = JSON.parse(text);
21
+ } catch (e) {
22
+ if (text.includes("<!DOCTYPE html>") || text.includes("<html")) {
23
+ data = { error: "Server error or Flask is not running on this URL. Please ensure you are running `python app.py` and accessing it via http://127.0.0.1:5000." };
24
+ } else {
25
+ data = { error: "Invalid response format from server" };
26
+ }
27
+ }
28
+ }
29
+
30
+ if (!res.ok) {
31
+ const error = new Error(data.error || "An error occurred");
32
+ error.status = res.status;
33
+ throw error;
34
+ }
35
+ return data;
36
+ };
37
+
38
+ window.Auth = {
39
+ setSession: (token, user) => {
40
+ localStorage.setItem("token", token);
41
+ localStorage.setItem("user", JSON.stringify(user));
42
+ },
43
+ getUser: () => {
44
+ const u = localStorage.getItem("user");
45
+ return u ? JSON.parse(u) : null;
46
+ },
47
+ getToken: () => localStorage.getItem("token"),
48
+ clear: () => {
49
+ localStorage.removeItem("token");
50
+ localStorage.removeItem("user");
51
+ },
52
+ requireAuth: () => {
53
+ if (!localStorage.getItem("token")) {
54
+ window.location.href = "/login";
55
+ return false;
56
+ }
57
+ return true;
58
+ },
59
+ redirectIfAuthed: () => {
60
+ if (localStorage.getItem("token")) {
61
+ window.location.href = "/dashboard";
62
+ }
63
+ }
64
+ };
65
+
66
+ window.toast = (msg, type = "info") => {
67
+ const t = document.createElement("div");
68
+ t.className = "toast " + type;
69
+ t.textContent = msg;
70
+ document.body.appendChild(t);
71
+
72
+ // Trigger reflow
73
+ t.offsetHeight;
74
+ t.classList.add("show");
75
+
76
+ setTimeout(() => {
77
+ t.classList.remove("show");
78
+ setTimeout(() => t.remove(), 300);
79
+ }, 3000);
80
+ };
81
+
82
+ window.initials = (name) => {
83
+ if (!name) return "U";
84
+ return name.split(" ").map(n => n[0]).join("").substring(0, 2).toUpperCase();
85
+ };
86
+
87
+ window.initReveal = () => {
88
+ const reveals = document.querySelectorAll('.reveal');
89
+ reveals.forEach((r, i) => {
90
+ setTimeout(() => {
91
+ r.classList.add('in');
92
+ }, i * 100);
93
+ });
94
+ };
app.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import base64
3
+ import random
4
+ import io
5
+ from functools import wraps
6
+ from flask import Flask, request, jsonify, session, render_template, send_from_directory
7
+ from werkzeug.security import generate_password_hash, check_password_hash
8
+
9
+ app = Flask(__name__)
10
+ app.secret_key = 'super_secret_structscan_key'
11
+ DB_PATH = 'users.db'
12
+
13
+ # ==============================================================================
14
+ # Database Initialization & Management
15
+ # ==============================================================================
16
+ def init_db():
17
+ conn = sqlite3.connect(DB_PATH)
18
+ c = conn.cursor()
19
+ c.execute('''
20
+ CREATE TABLE IF NOT EXISTS users (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ name TEXT NOT NULL,
23
+ email TEXT UNIQUE NOT NULL,
24
+ password TEXT NOT NULL
25
+ )
26
+ ''')
27
+ c.execute('''
28
+ CREATE TABLE IF NOT EXISTS projects (
29
+ id TEXT PRIMARY KEY,
30
+ user_id INTEGER,
31
+ name TEXT,
32
+ structure_type TEXT,
33
+ age_years TEXT,
34
+ location TEXT,
35
+ material_brand TEXT,
36
+ material_amount TEXT,
37
+ material_composition TEXT,
38
+ inspection_zone TEXT,
39
+ notes TEXT
40
+ )
41
+ ''')
42
+ c.execute('''
43
+ CREATE TABLE IF NOT EXISTS analyses (
44
+ project_id TEXT PRIMARY KEY,
45
+ risk_level TEXT,
46
+ risk_desc TEXT,
47
+ primary_defect TEXT,
48
+ health INTEGER,
49
+ image_b64 TEXT,
50
+ boxes_json TEXT,
51
+ probabilities_json TEXT,
52
+ specs_json TEXT,
53
+ report_text TEXT
54
+ )
55
+ ''')
56
+ conn.commit()
57
+ conn.close()
58
+
59
+ init_db()
60
+
61
+ def login_required_api(f):
62
+ @wraps(f)
63
+ def decorated_function(*args, **kwargs):
64
+ if 'user_id' not in session:
65
+ return jsonify({"error": "Unauthorized. Please log in."}), 401
66
+ return f(*args, **kwargs)
67
+ return decorated_function
68
+
69
+ # ==============================================================================
70
+ # Page Routes
71
+ # ==============================================================================
72
+ @app.route("/")
73
+ def index():
74
+ return render_template("index.html")
75
+
76
+ @app.route("/login")
77
+ def login():
78
+ return render_template("login.html")
79
+
80
+ @app.route("/signup")
81
+ def signup():
82
+ return render_template("signup.html")
83
+
84
+ @app.route("/dashboard")
85
+ def dashboard():
86
+ return render_template("dashboard.html")
87
+
88
+ @app.route("/app.js")
89
+ def serve_app_js():
90
+ return send_from_directory(".", "app.js")
91
+
92
+ # ==============================================================================
93
+ # Auth API
94
+ # ==============================================================================
95
+ @app.route("/api/auth/register", methods=["POST"])
96
+ def api_register():
97
+ data = request.get_json() or {}
98
+ name = data.get("name", "").strip()
99
+ email = data.get("email", "").strip()
100
+ password = data.get("password", "")
101
+
102
+ if not name or not email or len(password) < 6:
103
+ return jsonify({"error": "Invalid registrations input criteria parameters."}), 400
104
+
105
+ if " " in name:
106
+ return jsonify({"error": "Spaces are not allowed in the username."}), 400
107
+
108
+ if " " in password:
109
+ return jsonify({"error": "Spaces are not allowed in the password."}), 400
110
+
111
+ hashed_pw = generate_password_hash(password)
112
+ try:
113
+ conn = sqlite3.connect(DB_PATH)
114
+ c = conn.cursor()
115
+ c.execute("INSERT INTO users (name, email, password) VALUES (?, ?, ?)", (name, email, hashed_pw))
116
+ user_id = c.lastrowid
117
+ conn.commit()
118
+ conn.close()
119
+
120
+ session['user_id'] = user_id
121
+ session['username'] = name
122
+ return jsonify({
123
+ "token": f"mock_token_{user_id}",
124
+ "user": {"id": user_id, "name": name, "email": email}
125
+ }), 201
126
+ except sqlite3.IntegrityError:
127
+ return jsonify({"error": "Account registration email already coordinates inside user files."}), 400
128
+
129
+ @app.route("/api/auth/login", methods=["POST"])
130
+ def api_login():
131
+ data = request.get_json() or {}
132
+ email = data.get("email", "").strip()
133
+ password = data.get("password", "")
134
+
135
+ if " " in password:
136
+ return jsonify({"error": "Spaces are not allowed in the password."}), 400
137
+
138
+ conn = sqlite3.connect(DB_PATH)
139
+ c = conn.cursor()
140
+ c.execute("SELECT id, name, password FROM users WHERE email = ?", (email,))
141
+ row = c.fetchone()
142
+ conn.close()
143
+
144
+ if row and check_password_hash(row[2], password):
145
+ session['user_id'] = row[0]
146
+ session['username'] = row[1]
147
+ return jsonify({
148
+ "token": f"mock_token_{row[0]}",
149
+ "user": {"id": row[0], "name": row[1], "email": email}
150
+ }), 200
151
+ return jsonify({"error": "Invalid account email or credential authorization signature verification failure."}), 401
152
+
153
+ @app.route("/api/auth/profile", methods=["PUT"])
154
+ @login_required_api
155
+ def update_profile():
156
+ data = request.get_json() or {}
157
+ name = data.get("name", "").strip()
158
+ password = data.get("password", "")
159
+ user_id = session.get("user_id")
160
+
161
+ if not name:
162
+ return jsonify({"error": "Name cannot be empty."}), 400
163
+ if " " in name:
164
+ return jsonify({"error": "Spaces are not allowed in the username."}), 400
165
+
166
+ conn = sqlite3.connect(DB_PATH)
167
+ c = conn.cursor()
168
+
169
+ if password:
170
+ if " " in password:
171
+ conn.close()
172
+ return jsonify({"error": "Spaces are not allowed in the password."}), 400
173
+ if len(password) < 6:
174
+ conn.close()
175
+ return jsonify({"error": "Password must be at least 6 characters."}), 400
176
+ hashed_pw = generate_password_hash(password)
177
+ c.execute("UPDATE users SET name = ?, password = ? WHERE id = ?", (name, hashed_pw, user_id))
178
+ else:
179
+ c.execute("UPDATE users SET name = ? WHERE id = ?", (name, user_id))
180
+
181
+ conn.commit()
182
+ conn.close()
183
+
184
+ session['username'] = name
185
+ return jsonify({"success": True, "name": name}), 200
186
+
187
+ # ==============================================================================
188
+ # Projects API
189
+ # ==============================================================================
190
+ @app.route("/api/projects", methods=["GET"])
191
+ @login_required_api
192
+ def get_projects():
193
+ user_id = session['user_id']
194
+ conn = sqlite3.connect(DB_PATH)
195
+ conn.row_factory = sqlite3.Row
196
+ c = conn.cursor()
197
+ c.execute("""
198
+ SELECT p.*, a.risk_level, a.risk_desc, a.primary_defect, a.health,
199
+ a.image_b64, a.boxes_json, a.probabilities_json, a.specs_json, a.report_text
200
+ FROM projects p
201
+ LEFT JOIN analyses a ON p.id = a.project_id
202
+ WHERE p.user_id = ?
203
+ """, (user_id,))
204
+ rows = c.fetchall()
205
+ conn.close()
206
+
207
+ import json
208
+ project_list = []
209
+ for row in rows:
210
+ project_dict = {
211
+ "id": row["id"],
212
+ "name": row["name"],
213
+ "structure_type": row["structure_type"],
214
+ "age_years": row["age_years"],
215
+ "location": row["location"],
216
+ "material_brand": row["material_brand"],
217
+ "material_amount": row["material_amount"],
218
+ "material_composition": row["material_composition"],
219
+ "inspection_zone": row["inspection_zone"],
220
+ "notes": row["notes"],
221
+ "last_analysis": None
222
+ }
223
+ if row["health"] is not None:
224
+ project_dict["last_analysis"] = {
225
+ "risk_level": row["risk_level"],
226
+ "risk_desc": row["risk_desc"],
227
+ "primary_defect": row["primary_defect"],
228
+ "health": row["health"],
229
+ "image_b64": row["image_b64"],
230
+ "boxes": json.loads(row["boxes_json"]) if row["boxes_json"] else [],
231
+ "probabilities": json.loads(row["probabilities_json"]) if row["probabilities_json"] else [],
232
+ "specs": json.loads(row["specs_json"]) if row["specs_json"] else {},
233
+ "report": row["report_text"]
234
+ }
235
+ project_list.append(project_dict)
236
+
237
+ return jsonify(project_list), 200
238
+
239
+ @app.route("/api/projects", methods=["POST"])
240
+ @login_required_api
241
+ def create_project():
242
+ data = request.get_json() or {}
243
+ name = data.get("name", "").strip()
244
+ loc = data.get("location", "").strip()
245
+ brand = data.get("material_brand", "").strip()
246
+ amount = data.get("material_amount", "").strip()
247
+
248
+ if not name or not loc or not brand or not amount:
249
+ return jsonify({"error": "Missing essential structure data fields configuration bounds."}), 400
250
+
251
+ project_id = f"proj_{int(random.random() * 1000000)}"
252
+ user_id = session['user_id']
253
+
254
+ conn = sqlite3.connect(DB_PATH)
255
+ c = conn.cursor()
256
+ c.execute("""
257
+ INSERT INTO projects (id, user_id, name, structure_type, age_years, location,
258
+ material_brand, material_amount, material_composition, inspection_zone, notes)
259
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
260
+ """, (project_id, user_id, name, data.get("structure_type"), data.get("age_years"), loc,
261
+ brand, amount, data.get("material_composition"), data.get("inspection_zone"), data.get("notes")))
262
+ conn.commit()
263
+ conn.close()
264
+
265
+ return jsonify({
266
+ "id": project_id,
267
+ "name": name,
268
+ "structure_type": data.get("structure_type"),
269
+ "age_years": data.get("age_years"),
270
+ "location": loc,
271
+ "material_brand": brand,
272
+ "material_amount": amount,
273
+ "material_composition": data.get("material_composition"),
274
+ "inspection_zone": data.get("inspection_zone"),
275
+ "notes": data.get("notes"),
276
+ "last_analysis": None
277
+ }), 201
278
+
279
+ @app.route("/api/projects/<project_id>/analyze", methods=["POST"])
280
+ @login_required_api
281
+ def analyze_project(project_id):
282
+ if "file" not in request.files:
283
+ return jsonify({"error": "No image resource payload submitted inside structural file paths."}), 400
284
+
285
+ file = request.files["file"]
286
+ img_bytes = file.read()
287
+
288
+ # Fetch project details to get the grade
289
+ conn = sqlite3.connect(DB_PATH)
290
+ conn.row_factory = sqlite3.Row
291
+ c = conn.cursor()
292
+ c.execute("SELECT material_composition, material_brand FROM projects WHERE id = ?", (project_id,))
293
+ proj = c.fetchone()
294
+
295
+ grade = proj["material_composition"] if proj and proj["material_composition"] else "OPC 43"
296
+ brand = proj["material_brand"] if proj and proj["material_brand"] else "Unknown"
297
+
298
+ lookup_grade = grade
299
+ if grade and "OPC 33" in grade: lookup_grade = "OPC 33"
300
+ elif grade and "OPC 43" in grade: lookup_grade = "OPC 43"
301
+ elif grade and "OPC 53" in grade: lookup_grade = "OPC 53"
302
+ elif grade and "PPC" in grade: lookup_grade = "PPC"
303
+ elif grade and "PSC" in grade: lookup_grade = "PSC"
304
+ else: lookup_grade = "OPC 43"
305
+
306
+ cement_info = CEMENT_DATA.get(lookup_grade, CEMENT_DATA["OPC 43"])
307
+
308
+ cement_str = f"\n\n**CEMENT STRENGTH ANALYSIS**\n"
309
+ cement_str += f"Material Selected: {brand} ({grade})\n"
310
+ cement_str += f"28-Day Compressive Strength: {cement_info['strength']} MPa [{cement_info['category']}]\n"
311
+ cement_str += f"Recommended Applications: {', '.join(cement_info['applications'])}\n"
312
+ cement_str += f"Engineering Remarks: {cement_info['remark']}"
313
+
314
+ # Simulate dynamic AI model responses
315
+ defects = [
316
+ {"name": "Concrete Cracking", "risk": "medium", "desc": "Surface micro-fractures tracking stress vectors.", "min_h": 65, "max_h": 85},
317
+ {"name": "Spalling & Delamination", "risk": "high", "desc": "Severe localized concrete spalling exposing rebar.", "min_h": 40, "max_h": 60},
318
+ {"name": "Efflorescence / Water Seepage", "risk": "low", "desc": "Minor salt deposits due to water ingress.", "min_h": 80, "max_h": 90},
319
+ {"name": "Structural Deformation", "risk": "high", "desc": "Abnormal deflection or structural bowing detected.", "min_h": 30, "max_h": 50},
320
+ {"name": "Healthy Surface", "risk": "low", "desc": "No major structural anomalies detected.", "min_h": 92, "max_h": 100}
321
+ ]
322
+
323
+ import random
324
+ import hashlib
325
+ from PIL import Image, ImageFilter, ImageStat
326
+ import io
327
+
328
+ # Create deterministic seed based on image contents
329
+ img_hash = hashlib.md5(img_bytes).hexdigest()
330
+ random.seed(img_hash)
331
+
332
+ defect = random.choice(defects)
333
+
334
+ try:
335
+ pil_img = Image.open(io.BytesIO(img_bytes)).convert("L")
336
+ edges = pil_img.filter(ImageFilter.FIND_EDGES)
337
+ stat = ImageStat.Stat(edges)
338
+ edge_intensity = stat.mean[0]
339
+
340
+ if edge_intensity > 20:
341
+ defect_name = random.choice(["Concrete Cracking", "Spalling & Delamination", "Structural Deformation"])
342
+ defect = next(d for d in defects if d["name"] == defect_name)
343
+ elif edge_intensity < 8:
344
+ defect = next(d for d in defects if d["name"] == "Healthy Surface")
345
+ except Exception:
346
+ pass
347
+ health_score = random.randint(defect["min_h"], defect["max_h"])
348
+ confidence = round(random.uniform(75.0, 98.9), 1)
349
+
350
+ boxes = []
351
+ if defect["name"] != "Healthy Surface":
352
+ num_boxes = random.randint(2, 6)
353
+ for _ in range(num_boxes):
354
+ bx = random.randint(5, 70)
355
+ by = random.randint(5, 70)
356
+ bw = random.randint(15, min(40, 95 - bx))
357
+ bh = random.randint(15, min(40, 95 - by))
358
+ boxes.append({
359
+ "x": bx, "y": by, "w": bw, "h": bh,
360
+ "label": defect["name"],
361
+ "confidence": round(random.uniform(max(50.0, confidence - 15.0), confidence), 1)
362
+ })
363
+
364
+ needs_demolish = "Yes" if defect["risk"] == "high" and health_score < 45 else "No"
365
+ if defect["name"] == "Healthy Surface":
366
+ cost_inr = 0
367
+ elif defect["risk"] == "low":
368
+ cost_inr = random.randint(5000, 25000)
369
+ elif defect["risk"] == "medium":
370
+ cost_inr = random.randint(30000, 100000)
371
+ else:
372
+ cost_inr = random.randint(150000, 1000000)
373
+
374
+ encoded_source = f"data:image/jpeg;base64,{base64.b64encode(img_bytes).decode()}"
375
+ report_data = {
376
+ "risk_level": defect["risk"],
377
+ "risk_desc": defect["desc"],
378
+ "primary_defect": defect["name"],
379
+ "health": health_score,
380
+ "image_b64": encoded_source,
381
+ "boxes": boxes,
382
+ "probabilities": [
383
+ {"label": defect["name"], "prob": confidence, "severity": defect["risk"]},
384
+ {"label": "Secondary Anomaly", "prob": round(random.uniform(5.0, 25.0), 1), "severity": "low"}
385
+ ],
386
+ "specs": {
387
+ "edge_density": f"{round(random.uniform(0.1, 0.6), 3)} px⁻¹",
388
+ "luminance": f"{random.randint(90, 180)} cd/m²",
389
+ "rgb": [str(random.randint(90, 150)), str(random.randint(90, 150)), str(random.randint(90, 150))],
390
+ "model": "StructScan Core (Deterministic Simulation)",
391
+ "demolish": needs_demolish,
392
+ "cost": f"₹ {cost_inr:,}"
393
+ },
394
+ "report": f"**STRUCTURAL DIAGNOSTIC REVIEWS**\nDiagnostic pass complete. {defect['desc']}" + cement_str
395
+ }
396
+
397
+ # Reset seed so we don't affect global random state
398
+ random.seed()
399
+ import json
400
+ conn = sqlite3.connect(DB_PATH)
401
+ c = conn.cursor()
402
+ c.execute("""
403
+ INSERT OR REPLACE INTO analyses
404
+ (project_id, risk_level, risk_desc, primary_defect, health, image_b64, boxes_json, probabilities_json, specs_json, report_text)
405
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
406
+ """, (
407
+ project_id,
408
+ report_data["risk_level"],
409
+ report_data["risk_desc"],
410
+ report_data["primary_defect"],
411
+ report_data["health"],
412
+ report_data["image_b64"],
413
+ json.dumps(report_data["boxes"]),
414
+ json.dumps(report_data["probabilities"]),
415
+ json.dumps(report_data["specs"]),
416
+ report_data["report"]
417
+ ))
418
+ conn.commit()
419
+ conn.close()
420
+
421
+ return jsonify(report_data), 200
422
+
423
+ # ==============================================================================
424
+ # Cement Strength API
425
+ # ==============================================================================
426
+ CEMENT_DATA = {
427
+ "OPC 33": {
428
+ "strength": "33",
429
+ "category": "Standard Strength",
430
+ "applications": ["Plastering", "Masonry Work", "Residential Construction"],
431
+ "remark": "OPC 33 grade cement provides adequate baseline compressive strength."
432
+ },
433
+ "OPC 43": {
434
+ "strength": "43",
435
+ "category": "Medium-High Strength",
436
+ "applications": ["RCC Structures", "Slabs", "Beams", "Columns"],
437
+ "remark": "OPC 43 grade delivers a robust 43 MPa compressive strength after 28 days."
438
+ },
439
+ "OPC 53": {
440
+ "strength": "53",
441
+ "category": "High Strength",
442
+ "applications": ["High-Rise Buildings", "Bridges", "Industrial Structures", "Heavy Load Bearing Elements"],
443
+ "remark": "OPC 53 grade achieves rapid and high compressive strength."
444
+ },
445
+ "PPC": {
446
+ "strength": "33-53",
447
+ "category": "Durable Concrete",
448
+ "applications": ["Dams", "Marine Structures", "Mass Concreting", "Long-Life Construction"],
449
+ "remark": "Portland Pozzolana Cement (PPC) offers superior resistance to sulfate attacks."
450
+ },
451
+ "PSC": {
452
+ "strength": "33-53",
453
+ "category": "High Durability Concrete",
454
+ "applications": ["Coastal Structures", "Foundations", "Sewage Treatment Plants", "Aggressive Environmental Conditions"],
455
+ "remark": "Portland Slag Cement (PSC) features excellent durability against chloride."
456
+ }
457
+ }
458
+
459
+ @app.route('/api/cement/strength', methods=['POST'])
460
+ def cement_strength():
461
+ try:
462
+ data = request.get_json()
463
+ if not data:
464
+ return jsonify({"error": "Invalid request payload."}), 400
465
+
466
+ brand = data.get('brand', '').strip()
467
+ grade = data.get('grade', '').strip()
468
+
469
+ if not brand or not grade:
470
+ return jsonify({"error": "Both brand and grade must be provided."}), 400
471
+
472
+ lookup_grade = grade
473
+ if "OPC 33" in grade: lookup_grade = "OPC 33"
474
+ elif "OPC 43" in grade: lookup_grade = "OPC 43"
475
+ elif "OPC 53" in grade: lookup_grade = "OPC 53"
476
+ elif "PPC" in grade: lookup_grade = "PPC"
477
+ elif "PSC" in grade: lookup_grade = "PSC"
478
+
479
+ if lookup_grade not in CEMENT_DATA:
480
+ return jsonify({"error": "Invalid cement grade selected."}), 400
481
+
482
+ result = CEMENT_DATA[lookup_grade]
483
+
484
+ response_data = {
485
+ "brand": brand,
486
+ "grade": grade,
487
+ "strength": result["strength"],
488
+ "category": result["category"],
489
+ "applications": result["applications"],
490
+ "remark": result["remark"]
491
+ }
492
+ return jsonify(response_data), 200
493
+ except Exception as e:
494
+ print(f"Error processing cement request: {e}")
495
+ return jsonify({"error": "An internal processing error occurred."}), 500
496
+
497
+ if __name__ == '__main__':
498
+ app.run(host='0.0.0.0', port=7860)
model.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import base64
3
+ import random
4
+ from PIL import Image, ImageFilter, ImageStat, ImageEnhance, ImageDraw
5
+
6
+ # Try to import PyTorch, fallback if unavailable
7
+ try:
8
+ import torch
9
+ import torch.nn as nn
10
+ HAS_TORCH = True
11
+ except ImportError:
12
+ HAS_TORCH = False
13
+
14
+ if HAS_TORCH:
15
+ class AdvancedStructuralSHMNet(nn.Module):
16
+ """
17
+ A PyTorch CNN classifier that extracts features from input tensors
18
+ and predicts logits for 12 structural defect classes and 5 severity levels.
19
+ """
20
+ def __init__(self, num_defect_classes=12):
21
+ super().__init__()
22
+ self.backbone = nn.Sequential(
23
+ nn.Conv2d(3, 16, 3, padding=1),
24
+ nn.BatchNorm2d(16),
25
+ nn.ReLU(),
26
+ nn.MaxPool2d(2, 2),
27
+
28
+ nn.Conv2d(16, 32, 3, padding=1),
29
+ nn.BatchNorm2d(32),
30
+ nn.ReLU(),
31
+ nn.MaxPool2d(2, 2),
32
+
33
+ nn.Conv2d(32, 64, 3, padding=1),
34
+ nn.BatchNorm2d(64),
35
+ nn.ReLU(),
36
+ nn.AdaptiveAvgPool2d((1, 1))
37
+ )
38
+ self.defect_fc = nn.Linear(64, num_defect_classes)
39
+ self.severity_fc = nn.Linear(64, 5) # 5 severity levels
40
+
41
+ def forward(self, x):
42
+ features = self.backbone(x)
43
+ features = torch.flatten(features, 1)
44
+ defect_logits = self.defect_fc(features)
45
+ severity_logits = self.severity_fc(features)
46
+ return {
47
+ "defect_logits": defect_logits,
48
+ "severity_logits": severity_logits
49
+ }
50
+ else:
51
+ AdvancedStructuralSHMNet = None
52
+
53
+
54
+ class StructuralDecisionEngine:
55
+ """
56
+ Expert decision engine that integrates neural network predictions,
57
+ image features (edges, color), and metadata to generate structural
58
+ diagnostics, recommendations, localized bounding boxes, and heatmaps.
59
+ """
60
+ def __init__(self):
61
+ self.defect_classes = [
62
+ "Longitudinal Crack",
63
+ "Transverse Crack",
64
+ "Fatigue / Grid Crack",
65
+ "Spalling / Delamination",
66
+ "Concrete Efflorescence",
67
+ "Rebar Exposure & Corrosion",
68
+ "Honeycomb / Voiding",
69
+ "Settlement / Subsidence Crack",
70
+ "Moisture / Water Seepage",
71
+ "Joint Failure / Gap Expansion",
72
+ "Surface Erosion / Abrasion",
73
+ "Biological Growth / Vegetation"
74
+ ]
75
+
76
+ self.severity_levels = [
77
+ "Negligible (Severity 1)",
78
+ "Low / Minor (Severity 2)",
79
+ "Moderate / Medium (Severity 3)",
80
+ "High / Severe (Severity 4)",
81
+ "Critical / Extreme (Severity 5)"
82
+ ]
83
+
84
+ def _extract_visual_features(self, img_bytes: bytes) -> dict:
85
+ """Analyze image characteristics dynamically using PIL."""
86
+ try:
87
+ img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
88
+ width, height = img.size
89
+
90
+ # Color distributions
91
+ stat = ImageStat.Stat(img)
92
+ mean_r, mean_g, mean_b = stat.mean
93
+
94
+ # Edge density / texture analyzer
95
+ gray = img.convert("L")
96
+ edges = gray.filter(ImageFilter.FIND_EDGES)
97
+ edge_stat = ImageStat.Stat(edges)
98
+ edge_intensity = edge_stat.mean[0] # Average brightness of edge image
99
+
100
+ return {
101
+ "edge_intensity": edge_intensity,
102
+ "mean_r": mean_r,
103
+ "mean_g": mean_g,
104
+ "mean_b": mean_b,
105
+ "width": width,
106
+ "height": height
107
+ }
108
+ except Exception as e:
109
+ print(f"Error in visual feature extraction: {e}")
110
+ return {
111
+ "edge_intensity": 12.0,
112
+ "mean_r": 128.0,
113
+ "mean_g": 128.0,
114
+ "mean_b": 128.0,
115
+ "width": 800,
116
+ "height": 600
117
+ }
118
+
119
+ def _generate_defect_heatmap(self, img_bytes: bytes) -> str:
120
+ """Generate a realistic blended defect heatmap overlay (simulated Grad-CAM)."""
121
+ try:
122
+ orig = Image.open(io.BytesIO(img_bytes)).convert("RGB")
123
+ w, h = orig.size
124
+
125
+ # Resize for performance
126
+ scale_w = min(600, w)
127
+ scale_h = int(h * (scale_w / w))
128
+ img = orig.resize((scale_w, scale_h), Image.Resampling.LANCZOS)
129
+
130
+ # Get edges
131
+ gray = img.convert("L")
132
+ edges = gray.filter(ImageFilter.FIND_EDGES)
133
+
134
+ # Dilate and blur to make it look like a smooth neural activation map
135
+ heatmap_mask = edges.filter(ImageFilter.MaxFilter(5))
136
+ heatmap_mask = heatmap_mask.filter(ImageFilter.GaussianBlur(radius=15))
137
+
138
+ # Sharp heatmap mask for core defects
139
+ strong_edges = edges.filter(ImageFilter.MaxFilter(3))
140
+ strong_edges = strong_edges.filter(ImageFilter.GaussianBlur(radius=5))
141
+
142
+ # Overlays
143
+ red_overlay = Image.new("RGB", (scale_w, scale_h), (247, 129, 102)) # Theme Accent
144
+ yellow_overlay = Image.new("RGB", (scale_w, scale_h), (255, 166, 87)) # Theme Warn
145
+
146
+ # Base heatmap
147
+ heatmap = Image.new("RGB", (scale_w, scale_h), (13, 17, 40)) # Dark blueish base
148
+
149
+ # Composite colors
150
+ heatmap = Image.composite(yellow_overlay, heatmap, heatmap_mask)
151
+ heatmap = Image.composite(red_overlay, heatmap, strong_edges)
152
+
153
+ # Enhance
154
+ heatmap = ImageEnhance.Contrast(heatmap).enhance(1.4)
155
+
156
+ # Blend back with original image (45% opacity)
157
+ blended = Image.blend(img, heatmap, 0.45)
158
+
159
+ # Convert to base64
160
+ buf = io.BytesIO()
161
+ blended.save(buf, format="JPEG", quality=85)
162
+ return base64.b64encode(buf.getvalue()).decode()
163
+ except Exception as e:
164
+ print(f"Error generating heatmap: {e}")
165
+ return ""
166
+
167
+ def _detect_defect_boxes(self, img_bytes: bytes, edge_intensity: float) -> list:
168
+ """Find coordinates of high-texture regions to build real defect bounding boxes."""
169
+ try:
170
+ img = Image.open(io.BytesIO(img_bytes)).convert("L")
171
+ w, h = img.size
172
+
173
+ # Use grid-based thresholding
174
+ gw, gh = 8, 8
175
+ img_resized = img.resize((gw, gh))
176
+ edges = img_resized.filter(ImageFilter.FIND_EDGES)
177
+ pixels = list(edges.getdata())
178
+
179
+ # Determine threshold based on average edge intensity
180
+ threshold = max(12.0, edge_intensity * 0.8)
181
+
182
+ active_cells = []
183
+ for y in range(gh):
184
+ for x in range(gw):
185
+ idx = y * gw + x
186
+ val = pixels[idx]
187
+ if val > threshold:
188
+ active_cells.append((x, y, val))
189
+
190
+ # BFS clustering
191
+ visited = set()
192
+ clusters = []
193
+ for x, y, val in active_cells:
194
+ if (x, y) in visited:
195
+ continue
196
+ queue = [(x, y)]
197
+ cluster = []
198
+ while queue:
199
+ cx, cy = queue.pop(0)
200
+ if (cx, cy) in visited:
201
+ continue
202
+ visited.add((cx, cy))
203
+ cluster.append((cx, cy))
204
+ for nx in [cx-1, cx, cx+1]:
205
+ for ny in [cy-1, cy, cy+1]:
206
+ if 0 <= nx < gw and 0 <= ny < gh:
207
+ n_idx = ny * gw + nx
208
+ if pixels[n_idx] > threshold and (nx, ny) not in visited:
209
+ queue.append((nx, ny))
210
+ clusters.append(cluster)
211
+
212
+ boxes = []
213
+ # Map of possible defects depending on sequential clusters
214
+ possible_defects = [
215
+ ("Longitudinal Crack", "Linear cracking running parallel to structural axis. Indicates bending stress or shrinkage."),
216
+ ("Concrete Spalling", "Chipping/fracturing of concrete cover exposing inner layers. Suggests rebar oxidation expansion."),
217
+ ("Rebar Corrosion", "Visible oxidation of steel reinforcement. Highly critical due to loss of tensile strength."),
218
+ ("Moisture Seepage", "Dampness/water filtration through pores. Accelerates concrete carbonation and structural decay."),
219
+ ("Efflorescence", "Salt deposits left after water evaporation. Indicates persistent internal moisture transport.")
220
+ ]
221
+
222
+ for idx, cluster in enumerate(clusters[:4]): # limit to max 4 defect boxes
223
+ min_x = min(c[0] for c in cluster)
224
+ max_x = max(c[0] for c in cluster)
225
+ min_y = min(c[1] for c in cluster)
226
+ max_y = max(c[1] for c in cluster)
227
+
228
+ # Convert to percentages
229
+ x1 = max(0, min_x * 12.5 - 2)
230
+ y1 = max(0, min_y * 12.5 - 2)
231
+ x2 = min(100, (max_x + 1) * 12.5 + 2)
232
+ y2 = min(100, (max_y + 1) * 12.5 + 2)
233
+
234
+ def_name, def_desc = possible_defects[idx % len(possible_defects)]
235
+ conf = float(min(98.4, 65.0 + (sum(pixels[c[1]*gw + c[0]] for c in cluster) / len(cluster)) * 1.2))
236
+
237
+ boxes.append({
238
+ "id": f"defect_{idx}",
239
+ "class": def_name,
240
+ "description": def_desc,
241
+ "confidence": round(conf, 1),
242
+ "box": [round(x1, 1), round(y1, 1), round(x2, 1), round(y2, 1)]
243
+ })
244
+
245
+ return boxes
246
+ except Exception as e:
247
+ print(f"Error in bounding box detection: {e}")
248
+ return []
249
+
250
+ def process_inference(self, outputs: dict, meta: dict, img_bytes: bytes = None) -> tuple:
251
+ """
252
+ Process logits, image features, and metadata to generate the final
253
+ detailed Inspection Report text and a dictionary of analytical metrics.
254
+ """
255
+ # Parse logits from PyTorch output
256
+ defect_logits = outputs.get("defect_logits")
257
+ severity_logits = outputs.get("severity_logits")
258
+
259
+ # Softmax to get probabilities (simulate if torch doesn't have logits)
260
+ if HAS_TORCH and isinstance(defect_logits, torch.Tensor):
261
+ defect_probs = torch.softmax(defect_logits, dim=-1).squeeze().tolist()
262
+ severity_probs = torch.softmax(severity_logits, dim=-1).squeeze().tolist()
263
+ else:
264
+ defect_probs = [0.08] * 12
265
+ severity_probs = [0.2] * 5
266
+
267
+ # Extract visual details from image if available
268
+ vis = self._extract_visual_features(img_bytes) if img_bytes else {
269
+ "edge_intensity": 10.0, "mean_r": 128, "mean_g": 128, "mean_b": 128, "width": 800, "height": 600
270
+ }
271
+
272
+ edge_intensity = vis["edge_intensity"]
273
+
274
+ # Bias defect probabilities based on visual characteristics and metadata
275
+ # 1. Biological Growth (driven by Green color bias)
276
+ g_ratio = vis["mean_g"] / max(1.0, vis["mean_r"] + vis["mean_b"])
277
+ if g_ratio > 0.55:
278
+ defect_probs[11] += 0.40 # Biological Growth
279
+
280
+ # 2. Rebar Corrosion (driven by Red/Brown color bias)
281
+ r_ratio = vis["mean_r"] / max(1.0, vis["mean_g"] + vis["mean_b"])
282
+ if r_ratio > 0.58:
283
+ defect_probs[5] += 0.40 # Rebar Corrosion / Rust
284
+
285
+ # 3. Moisture / Seepage (driven by overall dark/blue levels)
286
+ if vis["mean_b"] > 140 and vis["mean_r"] < 100:
287
+ defect_probs[8] += 0.35 # Moisture Seepage
288
+
289
+ # 4. Crack categories (driven by high edge intensity)
290
+ if edge_intensity > 25.0:
291
+ defect_probs[0] += 0.25 # Longitudinal Crack
292
+ defect_probs[1] += 0.25 # Transverse Crack
293
+ defect_probs[2] += 0.20 # Fatigue Crack
294
+ defect_probs[3] += 0.15 # Spalling
295
+
296
+ # Normalize probabilities
297
+ def_sum = sum(defect_probs)
298
+ defect_probs = [p / def_sum for p in defect_probs]
299
+
300
+ # Determine highest probability defect
301
+ max_defect_idx = defect_probs.index(max(defect_probs))
302
+ detected_defect = self.defect_classes[max_defect_idx]
303
+
304
+ # Compute dynamic health score (starts at 100, drops based on edge intensity & defect severity)
305
+ # Higher edge intensity -> lower health score. Heavy corrosion/cracking -> lower health score.
306
+ severity_score = sum(i * p for i, p in enumerate(severity_probs)) # 0 to 4
307
+ health_penalty = (edge_intensity * 1.5) + (severity_score * 12.0)
308
+
309
+ # Add metadata-based age penalty (older structures have slightly lower base health)
310
+ age_str = meta.get("age", "").lower()
311
+ age_years = 0
312
+ for word in age_str.split():
313
+ if word.isdigit():
314
+ age_years = int(word)
315
+ break
316
+ if age_years > 20:
317
+ health_penalty += min(15.0, age_years * 0.25)
318
+
319
+ health_score = max(5.0, min(100.0, 100.0 - health_penalty))
320
+
321
+ # Risk assessment level based on health score
322
+ if health_score < 40.0:
323
+ risk_level = "Critical"
324
+ verdict = "UNSAFE - High structural hazard. Immediate stabilization required."
325
+ is_critical_alert = True
326
+ elif health_score < 65.0:
327
+ risk_level = "High"
328
+ verdict = "POTENTIALLY HAZARDOUS - Significant deterioration. Restrict load limit."
329
+ is_critical_alert = False
330
+ elif health_score < 85.0:
331
+ risk_level = "Medium"
332
+ verdict = "STABLE WITH DEFECTS - Preventive maintenance and repair needed."
333
+ is_critical_alert = False
334
+ else:
335
+ risk_level = "Low"
336
+ verdict = "STRUCTURALLY SOUND - Negligible anomalies. Maintain standard monitoring."
337
+ is_critical_alert = False
338
+
339
+ # Build list of dynamic defect breakdown for UI
340
+ defect_breakdown = []
341
+ for idx, p in enumerate(defect_probs):
342
+ if p > 0.05: # Report anything above 5% confidence
343
+ defect_breakdown.append({
344
+ "name": self.defect_classes[idx],
345
+ "confidence": round(p * 100.0, 1)
346
+ })
347
+ defect_breakdown = sorted(defect_breakdown, key=lambda x: x["confidence"], reverse=True)
348
+
349
+ # Get heatmap and boxes
350
+ heatmap_b64 = self._generate_defect_heatmap(img_bytes) if img_bytes else ""
351
+ bounding_boxes = self._detect_defect_boxes(img_bytes, edge_intensity) if img_bytes else []
352
+
353
+ # Match detected boxes with classes or populate them if empty
354
+ if not bounding_boxes:
355
+ # Fallback boxes if none detected
356
+ bounding_boxes = [{
357
+ "id": "defect_0",
358
+ "class": detected_defect,
359
+ "description": "Primary structural anomaly detected in the high-contrast surface regions.",
360
+ "confidence": round(defect_probs[max_defect_idx] * 100, 1),
361
+ "box": [25.0, 30.0, 75.0, 70.0]
362
+ }]
363
+
364
+ # Set primary defect name
365
+ primary_defect = bounding_boxes[0]["class"]
366
+ primary_confidence = bounding_boxes[0]["confidence"]
367
+
368
+ # Generate the structured Report text for the frontend parser
369
+ lines = []
370
+ if is_critical_alert:
371
+ lines.append("CRITICAL STRUCTURAL WARNING")
372
+ lines.append("===========================")
373
+ lines.append("HIGH RISK: EMERGENCY INTERVENTION STRONGLY ADVISED.")
374
+ lines.append("")
375
+
376
+ # Executive Summary
377
+ lines.append("Executive Summary")
378
+ lines.append("-----------------")
379
+ lines.append(f"During visual inspection of the {meta.get('location')}, anomalies were detected. The primary defect identified is {primary_defect} with an estimated model confidence of {primary_confidence}%. Overall, the structure is rated at {round(health_score, 1)}/100 on the Structural Health Index, placing it in a {risk_level.upper()} risk category. {verdict}")
380
+ lines.append("")
381
+
382
+ # Structure Overview
383
+ lines.append("Structure Overview")
384
+ lines.append("------------------")
385
+ lines.append(f"Structure Type: {meta.get('type')}")
386
+ lines.append(f"Material Type: {meta.get('material')}")
387
+ lines.append(f"Estimated Age: {meta.get('age')}")
388
+ lines.append(f"Inspection Zone: {meta.get('location')}")
389
+ lines.append("")
390
+
391
+ # Detected Defects
392
+ lines.append("Detected Defects")
393
+ lines.append("----------------")
394
+ for db in defect_breakdown[:3]:
395
+ lines.append(f"{db['name']}: {db['confidence']}% Confidence")
396
+ lines.append("")
397
+
398
+ # Root Cause Analysis
399
+ lines.append("Root Cause Analysis")
400
+ lines.append("-------------------")
401
+ if primary_defect == "Longitudinal Crack" or primary_defect == "Transverse Crack" or primary_defect == "Fatigue / Grid Crack":
402
+ lines.append("Crack propagation is likely driven by thermal stress fatigue, excessive load cycles, or drying shrinkage of the concrete matrix.")
403
+ elif primary_defect == "Concrete Spalling":
404
+ lines.append("Spalling occurs due to internal tensile stress, typically generated by the volumetric expansion of corroding steel reinforcement.")
405
+ elif primary_defect == "Rebar Exposure & Corrosion":
406
+ lines.append("Carbonation or chloride ingress has compromised the concrete alkaline passivation layer, resulting in rapid steel reinforcement oxidation.")
407
+ elif primary_defect == "Moisture / Water Seepage" or primary_defect == "Concrete Efflorescence":
408
+ lines.append("Hydrostatic pressure or poor drainage interfaces are forcing water through capillaries, carrying soluble salts that deposit on the outer face.")
409
+ else:
410
+ lines.append("Surface anomalies are driven by environmental erosion, material degradation over time, or dynamic loading variations.")
411
+ lines.append("")
412
+
413
+ # Structural Risk Assessment
414
+ lines.append("Structural Risk Assessment")
415
+ lines.append("--------------------------")
416
+ lines.append(f"Risk Rating: {risk_level}")
417
+ lines.append(f"Health Score: {round(health_score, 1)} / 100")
418
+ lines.append(f"Structural Integrity Degradation: {round(100.0 - health_score, 1)}%")
419
+ lines.append(f"Load Bearing Reduction Required: {'Yes' if health_score < 60.0 else 'No'}")
420
+ lines.append("")
421
+
422
+ # Recoverability Assessment
423
+ lines.append("Recoverability Assessment")
424
+ lines.append("-------------------------")
425
+ if health_score < 30.0:
426
+ lines.append("Repair Difficulty: High (Structural reinforcement required)")
427
+ lines.append("Demolition Recommended: Yes (High risk of progressive collapse)")
428
+ elif health_score < 60.0:
429
+ lines.append("Repair Difficulty: Moderate (Specialized shoring and grouting required)")
430
+ lines.append("Demolition Recommended: No")
431
+ else:
432
+ lines.append("Repair Difficulty: Low (Standard patch repairs and waterproofing)")
433
+ lines.append("Demolition Recommended: No")
434
+ lines.append("")
435
+
436
+ # Recommended Repairs
437
+ lines.append("Recommended Repairs")
438
+ lines.append("-------------------")
439
+ if primary_defect == "Longitudinal Crack" or primary_defect == "Transverse Crack" or primary_defect == "Fatigue / Grid Crack":
440
+ lines.append("1. Epoxy resin pressure injection to seal structural cracks.")
441
+ lines.append("2. Carbon fiber reinforced polymer (CFRP) wrapping to restore tensile load transfer.")
442
+ elif primary_defect == "Concrete Spalling":
443
+ lines.append("1. Remove loose concrete down to sound aggregate.")
444
+ lines.append("2. Clean rust from steel rebar, apply anti-corrosive coating, and patch with polymer-modified repair mortar.")
445
+ elif primary_defect == "Rebar Exposure & Corrosion":
446
+ lines.append("1. Sandblast exposed steel bars to SA 2.5 finish.")
447
+ lines.append("2. Install sacrificial zinc anodes to control galvanic corrosion, then rebuild section.")
448
+ elif primary_defect == "Moisture / Water Seepage" or primary_defect == "Concrete Efflorescence":
449
+ lines.append("1. Inject polyurethane expansion grout to seal leakage pathways.")
450
+ lines.append("2. Apply crystalline silane/siloxane water-repellent coating to external faces.")
451
+ else:
452
+ lines.append("1. Localized surface cleaning and patch repairs.")
453
+ lines.append("2. Re-apply protective sealants.")
454
+ lines.append("")
455
+
456
+ # Urgent Actions
457
+ lines.append("Urgent Actions")
458
+ lines.append("--------------")
459
+ if health_score < 40.0:
460
+ lines.append("1. EVACUATE / RESTRICT AREA: Suspend heavy vehicle/load movement immediately.")
461
+ lines.append("2. SHORING: Install immediate emergency structural props.")
462
+ lines.append("3. DETAILED INVESTIGATION: Schedule a full core-drilling and ultrasonic inspection.")
463
+ elif health_score < 65.0:
464
+ lines.append("1. SHORING: Recommend temporary structural support under damaged sections.")
465
+ lines.append("2. DETAILED INVESTIGATION: Perform non-destructive testing (NDT) within 7 days.")
466
+ else:
467
+ lines.append("1. MONITORING: Review crack widths every 6 months.")
468
+ lines.append("2. GENERAL REPAIR: Seal cracks during upcoming routine maintenance cycle.")
469
+ lines.append("")
470
+
471
+ # Final Verdict
472
+ lines.append("Final Verdict")
473
+ lines.append("-------------")
474
+ lines.append(f"Verdict: {verdict}")
475
+
476
+ report_text = "\n".join(lines)
477
+
478
+ # Prepare JSON analytics payload
479
+ analysis_data = {
480
+ "health_score": round(health_score, 1),
481
+ "risk_level": risk_level,
482
+ "defects": defect_breakdown[:4],
483
+ "bounding_boxes": bounding_boxes,
484
+ "edge_intensity": round(edge_intensity, 2),
485
+ "color_balance": {
486
+ "r": round(vis["mean_r"], 1),
487
+ "g": round(vis["mean_g"], 1),
488
+ "b": round(vis["mean_b"], 1)
489
+ }
490
+ }
491
+
492
+ return report_text, analysis_data, heatmap_b64
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask>=3.0
2
+ torch
3
+ torchvision
4
+ pillow
5
+ gunicorn
6
+
users.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f81e255e0f7f8df0381362207dece48aaf0603490029f553ba6a29704ef79312
3
+ size 2514944