upload
Browse files- .firebase/hosting.cHVibGlj.cache +2 -0
- .firebaserc +5 -0
- .gcloudignore +17 -0
- __pycache__/main.cpython-39.pyc +0 -0
- firebase-debug.log +90 -0
- firebase.json +10 -0
- main.py +278 -0
- package-lock.json +6 -0
- pd4_model.pth +3 -0
- public/404.html +33 -0
- public/index.html +123 -0
- requirements.txt +5 -0
.firebase/hosting.cHVibGlj.cache
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
index.html,1741591499789,a164687b08f5e91f6befe297edcde59d6005c1d8ff9a124bba47226efd0adf1c
|
| 2 |
+
404.html,1741591499640,762bf484ba67404bd1a3b181546ea28d60dfddf18e9dd4795d8d25bcf3c1a890
|
.firebaserc
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"projects": {
|
| 3 |
+
"default": "skincancerscreening-a9c4d"
|
| 4 |
+
}
|
| 5 |
+
}
|
.gcloudignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file specifies files that are *not* uploaded to Google Cloud
|
| 2 |
+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
|
| 3 |
+
# "#!include" directives (which insert the entries of the given .gitignore-style
|
| 4 |
+
# file at that point).
|
| 5 |
+
#
|
| 6 |
+
# For more information, run:
|
| 7 |
+
# $ gcloud topic gcloudignore
|
| 8 |
+
#
|
| 9 |
+
.gcloudignore
|
| 10 |
+
# If you would like to upload your .git directory, .gitignore file or files
|
| 11 |
+
# from your .gitignore file, remove the corresponding line
|
| 12 |
+
# below:
|
| 13 |
+
.git
|
| 14 |
+
.gitignore
|
| 15 |
+
|
| 16 |
+
node_modules
|
| 17 |
+
#!include:.gitignore
|
__pycache__/main.cpython-39.pyc
ADDED
|
Binary file (10.7 kB). View file
|
|
|
firebase-debug.log
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[debug] [2025-03-10T08:17:30.321Z] ----------------------------------------------------------------------
|
| 2 |
+
[debug] [2025-03-10T08:17:30.325Z] Command: C:\Program Files\nodejs\node.exe C:\Users\phaml\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js serve --only hosting
|
| 3 |
+
[debug] [2025-03-10T08:17:30.325Z] CLI Version: 13.33.0
|
| 4 |
+
[debug] [2025-03-10T08:17:30.325Z] Platform: win32
|
| 5 |
+
[debug] [2025-03-10T08:17:30.326Z] Node Version: v20.17.0
|
| 6 |
+
[debug] [2025-03-10T08:17:30.326Z] Time: Mon Mar 10 2025 15:17:30 GMT+0700 (Indochina Time)
|
| 7 |
+
[debug] [2025-03-10T08:17:30.326Z] ----------------------------------------------------------------------
|
| 8 |
+
[debug]
|
| 9 |
+
[debug] [2025-03-10T08:17:30.987Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
| 10 |
+
[debug] [2025-03-10T08:17:30.987Z] > authorizing via signed-in user (phamlethanhnhan0707@gmail.com)
|
| 11 |
+
[debug] [2025-03-10T08:17:30.987Z] [iam] checking project skincancerscreening-a9c4d for permissions ["firebase.projects.get"]
|
| 12 |
+
[debug] [2025-03-10T08:17:30.989Z] Checked if tokens are valid: false, expires at: 1741594954977
|
| 13 |
+
[debug] [2025-03-10T08:17:30.989Z] Checked if tokens are valid: false, expires at: 1741594954977
|
| 14 |
+
[debug] [2025-03-10T08:17:30.989Z] > refreshing access token with scopes: []
|
| 15 |
+
[debug] [2025-03-10T08:17:30.992Z] >>> [apiv2][query] POST https://www.googleapis.com/oauth2/v3/token [none]
|
| 16 |
+
[debug] [2025-03-10T08:17:30.992Z] >>> [apiv2][body] POST https://www.googleapis.com/oauth2/v3/token [omitted]
|
| 17 |
+
[debug] [2025-03-10T08:17:31.169Z] <<< [apiv2][status] POST https://www.googleapis.com/oauth2/v3/token 200
|
| 18 |
+
[debug] [2025-03-10T08:17:31.170Z] <<< [apiv2][body] POST https://www.googleapis.com/oauth2/v3/token [omitted]
|
| 19 |
+
[debug] [2025-03-10T08:17:31.222Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d:testIamPermissions [none]
|
| 20 |
+
[debug] [2025-03-10T08:17:31.222Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d:testIamPermissions x-goog-quota-user=projects/skincancerscreening-a9c4d
|
| 21 |
+
[debug] [2025-03-10T08:17:31.223Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d:testIamPermissions {"permissions":["firebase.projects.get"]}
|
| 22 |
+
[debug] [2025-03-10T08:17:32.345Z] <<< [apiv2][status] POST https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d:testIamPermissions 200
|
| 23 |
+
[debug] [2025-03-10T08:17:32.346Z] <<< [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d:testIamPermissions {"permissions":["firebase.projects.get"]}
|
| 24 |
+
[debug] [2025-03-10T08:17:32.347Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 25 |
+
[debug] [2025-03-10T08:17:32.347Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 26 |
+
[debug] [2025-03-10T08:17:32.347Z] >>> [apiv2][query] GET https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d [none]
|
| 27 |
+
[debug] [2025-03-10T08:17:32.609Z] <<< [apiv2][status] GET https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d 200
|
| 28 |
+
[debug] [2025-03-10T08:17:32.609Z] <<< [apiv2][body] GET https://cloudresourcemanager.googleapis.com/v1/projects/skincancerscreening-a9c4d {"projectNumber":"830343088745","projectId":"skincancerscreening-a9c4d","lifecycleState":"ACTIVE","name":"skincancerscreening","labels":{"firebase":"enabled","firebase-core":"disabled"},"createTime":"2025-03-10T07:20:38.909685Z"}
|
| 29 |
+
[debug] [2025-03-10T08:17:32.612Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 30 |
+
[debug] [2025-03-10T08:17:32.612Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 31 |
+
[debug] [2025-03-10T08:17:32.613Z] >>> [apiv2][query] GET https://firebasehosting.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d/sites
|
| 32 |
+
[debug] [2025-03-10T08:17:33.344Z] <<< [apiv2][status] GET https://firebasehosting.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d/sites 200
|
| 33 |
+
[debug] [2025-03-10T08:17:33.345Z] <<< [apiv2][body] GET https://firebasehosting.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d/sites {"sites":[{"name":"projects/skincancerscreening-a9c4d/sites/skincancerscreening-a9c4d","defaultUrl":"https://skincancerscreening-a9c4d.web.app","type":"DEFAULT_SITE"}]}
|
| 34 |
+
[debug] [2025-03-10T08:17:33.345Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 35 |
+
[debug] [2025-03-10T08:17:33.346Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 36 |
+
[debug] [2025-03-10T08:17:33.346Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d/webApps/-/config [none]
|
| 37 |
+
[debug] [2025-03-10T08:17:33.869Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d/webApps/-/config 200
|
| 38 |
+
[debug] [2025-03-10T08:17:33.869Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d/webApps/-/config {"projectId":"skincancerscreening-a9c4d","storageBucket":"skincancerscreening-a9c4d.firebasestorage.app","apiKey":"AIzaSyBmT9PuKj5MnWE5mD1ivG4e09ENfb_ZObM","authDomain":"skincancerscreening-a9c4d.firebaseapp.com","messagingSenderId":"830343088745"}
|
| 39 |
+
[debug] [2025-03-10T08:17:33.909Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 40 |
+
[debug] [2025-03-10T08:17:33.909Z] Checked if tokens are valid: true, expires at: 1741598250170
|
| 41 |
+
[debug] [2025-03-10T08:17:33.910Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d [none]
|
| 42 |
+
[debug] [2025-03-10T08:17:35.294Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d 200
|
| 43 |
+
[debug] [2025-03-10T08:17:35.294Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/skincancerscreening-a9c4d {"projectId":"skincancerscreening-a9c4d","projectNumber":"830343088745","displayName":"skincancerscreening","name":"projects/skincancerscreening-a9c4d","resources":{"hostingSite":"skincancerscreening-a9c4d"},"state":"ACTIVE","etag":"1_b7dc9c78-635e-464c-a390-955983ce6ca1"}
|
| 44 |
+
[info] i hosting[skincancerscreening-a9c4d]: Serving hosting files from: public {"metadata":{"emulator":{"name":"hosting"},"message":"Serving hosting files from: \u001b[1mpublic\u001b[22m"}}
|
| 45 |
+
[info] + hosting[skincancerscreening-a9c4d]: Local server: http://localhost:5000 {"metadata":{"emulator":{"name":"hosting"},"message":"Local server: \u001b[4m\u001b[1mhttp://localhost:5000\u001b[22m\u001b[24m"}}
|
| 46 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET / HTTP/1.1" 200 - "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET / HTTP/1.1\" 200 - \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 47 |
+
[debug] [2025-03-10T08:18:29.064Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-app-compat.js [none]
|
| 48 |
+
[debug] [2025-03-10T08:18:29.073Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-auth-compat.js [none]
|
| 49 |
+
[debug] [2025-03-10T08:18:29.148Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-database-compat.js [none]
|
| 50 |
+
[debug] [2025-03-10T08:18:29.155Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-firestore-compat.js [none]
|
| 51 |
+
[debug] [2025-03-10T08:18:29.161Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-functions-compat.js [none]
|
| 52 |
+
[debug] [2025-03-10T08:18:29.166Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-messaging-compat.js [none]
|
| 53 |
+
[debug] [2025-03-10T08:18:29.244Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-app-compat.js 200
|
| 54 |
+
[debug] [2025-03-10T08:18:29.244Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-app-compat.js [stream]
|
| 55 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-app-compat.js HTTP/1.1" 200 10177 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-app-compat.js HTTP/1.1\" 200 10177 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 56 |
+
[debug] [2025-03-10T08:18:29.261Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-auth-compat.js 200
|
| 57 |
+
[debug] [2025-03-10T08:18:29.261Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-auth-compat.js [stream]
|
| 58 |
+
[debug] [2025-03-10T08:18:29.267Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-storage-compat.js [none]
|
| 59 |
+
[debug] [2025-03-10T08:18:29.287Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-database-compat.js 200
|
| 60 |
+
[debug] [2025-03-10T08:18:29.287Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-database-compat.js [stream]
|
| 61 |
+
[debug] [2025-03-10T08:18:29.293Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-messaging-compat.js 200
|
| 62 |
+
[debug] [2025-03-10T08:18:29.293Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-messaging-compat.js [stream]
|
| 63 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-messaging-compat.js HTTP/1.1" 200 9968 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-messaging-compat.js HTTP/1.1\" 200 9968 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 64 |
+
[debug] [2025-03-10T08:18:29.303Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-analytics-compat.js [none]
|
| 65 |
+
[debug] [2025-03-10T08:18:29.304Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-functions-compat.js 200
|
| 66 |
+
[debug] [2025-03-10T08:18:29.304Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-functions-compat.js [stream]
|
| 67 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-functions-compat.js HTTP/1.1" 200 3974 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-functions-compat.js HTTP/1.1\" 200 3974 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 68 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-auth-compat.js HTTP/1.1" 200 40259 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-auth-compat.js HTTP/1.1\" 200 40259 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 69 |
+
[debug] [2025-03-10T08:18:29.311Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-storage-compat.js 200
|
| 70 |
+
[debug] [2025-03-10T08:18:29.311Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-storage-compat.js [stream]
|
| 71 |
+
[debug] [2025-03-10T08:18:29.315Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-remote-config-compat.js [none]
|
| 72 |
+
[debug] [2025-03-10T08:18:29.319Z] >>> [apiv2][query] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-performance-compat.js [none]
|
| 73 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-storage-compat.js HTTP/1.1" 200 13023 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-storage-compat.js HTTP/1.1\" 200 13023 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 74 |
+
[debug] [2025-03-10T08:18:29.324Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-firestore-compat.js 200
|
| 75 |
+
[debug] [2025-03-10T08:18:29.324Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-firestore-compat.js [stream]
|
| 76 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/init.js?useEmulator=true HTTP/1.1" 200 - "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/init.js?useEmulator=true HTTP/1.1\" 200 - \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 77 |
+
[debug] [2025-03-10T08:18:29.340Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-analytics-compat.js 200
|
| 78 |
+
[debug] [2025-03-10T08:18:29.340Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-analytics-compat.js [stream]
|
| 79 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-analytics-compat.js HTTP/1.1" 200 9132 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-analytics-compat.js HTTP/1.1\" 200 9132 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 80 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-database-compat.js HTTP/1.1" 200 48150 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-database-compat.js HTTP/1.1\" 200 48150 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 81 |
+
[debug] [2025-03-10T08:18:29.359Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-remote-config-compat.js 200
|
| 82 |
+
[debug] [2025-03-10T08:18:29.359Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-remote-config-compat.js [stream]
|
| 83 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-remote-config-compat.js HTTP/1.1" 200 9172 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-remote-config-compat.js HTTP/1.1\" 200 9172 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 84 |
+
[debug] [2025-03-10T08:18:29.366Z] <<< [apiv2][status] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-performance-compat.js 200
|
| 85 |
+
[debug] [2025-03-10T08:18:29.366Z] <<< [apiv2][body] GET https://www.gstatic.com/firebasejs/11.4.0/firebase-performance-compat.js [stream]
|
| 86 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-performance-compat.js HTTP/1.1" 200 13495 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-performance-compat.js HTTP/1.1\" 200 13495 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 87 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /__/firebase/11.4.0/firebase-firestore-compat.js HTTP/1.1" 200 102688 "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /__/firebase/11.4.0/firebase-firestore-compat.js HTTP/1.1\" 200 102688 \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 88 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:18:29 +0000] "GET /favicon.ico HTTP/1.1" 404 - "http://localhost:5000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:18:29 +0000] \"GET /favicon.ico HTTP/1.1\" 404 - \"http://localhost:5000/\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 89 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:22:07 +0000] "GET / HTTP/1.1" 200 - "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:22:07 +0000] \"GET / HTTP/1.1\" 200 - \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
| 90 |
+
[info] i hosting: ::1 - - [10/Mar/2025:08:25:59 +0000] "GET / HTTP/1.1" 200 - "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" {"metadata":{"emulator":{"name":"hosting"},"message":"::1 - - [10/Mar/2025:08:25:59 +0000] \"GET / HTTP/1.1\" 200 - \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36\""}}
|
firebase.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"hosting": {
|
| 3 |
+
"public": "public",
|
| 4 |
+
"ignore": [
|
| 5 |
+
"firebase.json",
|
| 6 |
+
"**/.*",
|
| 7 |
+
"**/node_modules/**"
|
| 8 |
+
]
|
| 9 |
+
}
|
| 10 |
+
}
|
main.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import io
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torchvision import transforms, models
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
from flask import jsonify, request
|
| 11 |
+
import functions_framework
|
| 12 |
+
|
| 13 |
+
# Set device (Cloud Functions generally use CPU, but GPU will be used if available)
|
| 14 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 15 |
+
|
| 16 |
+
###############################################################################
|
| 17 |
+
# Utility Functions for Image Preprocessing (Optional)
|
| 18 |
+
###############################################################################
|
| 19 |
+
def remove_hair_and_markings(img_bgr, kernel_size=17):
|
| 20 |
+
"""Remove thin hair and markings using morphological operations."""
|
| 21 |
+
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
|
| 22 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
|
| 23 |
+
blackhat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel)
|
| 24 |
+
_, mask = cv2.threshold(blackhat, 10, 255, cv2.THRESH_BINARY)
|
| 25 |
+
inpainted = cv2.inpaint(img_bgr, mask, 3, cv2.INPAINT_TELEA)
|
| 26 |
+
return inpainted
|
| 27 |
+
|
| 28 |
+
def normalize_color(img_bgr):
|
| 29 |
+
"""Per-channel normalization: subtract mean and divide by std for each channel."""
|
| 30 |
+
img_float = img_bgr.astype(np.float32)
|
| 31 |
+
b, g, r = cv2.split(img_float)
|
| 32 |
+
for channel in (b, g, r):
|
| 33 |
+
mean_val = np.mean(channel)
|
| 34 |
+
std_val = np.std(channel) + 1e-8
|
| 35 |
+
channel[:] = (channel - mean_val) / std_val
|
| 36 |
+
normalized = cv2.merge([b, g, r])
|
| 37 |
+
return normalized
|
| 38 |
+
|
| 39 |
+
def mmwf_filter(gray_image, window_size=3, noise_variance=0.01):
|
| 40 |
+
"""Apply the Median–Modified Wiener Filter (MMWF) on a grayscale image."""
|
| 41 |
+
if gray_image.dtype != np.float32:
|
| 42 |
+
gray_image = gray_image.astype(np.float32)
|
| 43 |
+
pad_size = window_size // 2
|
| 44 |
+
padded = np.pad(gray_image, pad_size, mode='reflect')
|
| 45 |
+
filtered = np.zeros_like(gray_image, dtype=np.float32)
|
| 46 |
+
rows, cols = gray_image.shape
|
| 47 |
+
for i in range(rows):
|
| 48 |
+
for j in range(cols):
|
| 49 |
+
local_patch = padded[i:i+window_size, j:j+window_size]
|
| 50 |
+
mu_m = np.median(local_patch)
|
| 51 |
+
sigma_sq = local_patch.var()
|
| 52 |
+
a_val = gray_image[i, j]
|
| 53 |
+
if sigma_sq < 1e-12:
|
| 54 |
+
filtered[i, j] = mu_m
|
| 55 |
+
else:
|
| 56 |
+
filtered[i, j] = mu_m + ((sigma_sq - noise_variance) / sigma_sq) * (a_val - mu_m)
|
| 57 |
+
return filtered
|
| 58 |
+
|
| 59 |
+
def mmwf_filter_color(img_bgr, window_size=3, noise_variance=0.01):
|
| 60 |
+
"""Apply MMWF to each channel of a BGR image."""
|
| 61 |
+
if img_bgr.dtype != np.float32:
|
| 62 |
+
img_bgr = img_bgr.astype(np.float32)
|
| 63 |
+
b, g, r = cv2.split(img_bgr)
|
| 64 |
+
b_denoised = mmwf_filter(b, window_size, noise_variance)
|
| 65 |
+
g_denoised = mmwf_filter(g, window_size, noise_variance)
|
| 66 |
+
r_denoised = mmwf_filter(r, window_size, noise_variance)
|
| 67 |
+
denoised_bgr = cv2.merge([b_denoised, g_denoised, r_denoised])
|
| 68 |
+
return denoised_bgr
|
| 69 |
+
|
| 70 |
+
def preprocess_image(image):
|
| 71 |
+
"""
|
| 72 |
+
Optionally, apply preprocessing steps:
|
| 73 |
+
1. Convert from PIL RGB to NumPy BGR.
|
| 74 |
+
2. Remove hair/markings.
|
| 75 |
+
3. Normalize color.
|
| 76 |
+
4. Apply MMWF filter.
|
| 77 |
+
5. Convert back to PIL RGB.
|
| 78 |
+
"""
|
| 79 |
+
image_np = np.array(image)
|
| 80 |
+
image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
|
| 81 |
+
image_bgr = remove_hair_and_markings(image_bgr, kernel_size=17)
|
| 82 |
+
image_bgr = normalize_color(image_bgr)
|
| 83 |
+
image_bgr = cv2.normalize(image_bgr, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX).astype(np.uint8)
|
| 84 |
+
# image_bgr = mmwf_filter_color(image_bgr, window_size=3, noise_variance=0.01)
|
| 85 |
+
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
| 86 |
+
return Image.fromarray(image_rgb)
|
| 87 |
+
|
| 88 |
+
###############################################################################
|
| 89 |
+
# Helper Modules for the Classifier
|
| 90 |
+
###############################################################################
|
| 91 |
+
def create_classification_head(input_dim, num_classes):
|
| 92 |
+
return nn.Sequential(
|
| 93 |
+
nn.Linear(input_dim, 512),
|
| 94 |
+
nn.ReLU(),
|
| 95 |
+
nn.Dropout(0.5),
|
| 96 |
+
nn.Linear(512, num_classes)
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
class MetadataEncoder(nn.Module):
|
| 100 |
+
def __init__(self, input_size, output_size):
|
| 101 |
+
super(MetadataEncoder, self).__init__()
|
| 102 |
+
self.encoder = nn.Sequential(
|
| 103 |
+
nn.Conv1d(1, 16, kernel_size=3, padding=1),
|
| 104 |
+
nn.BatchNorm1d(16),
|
| 105 |
+
nn.ReLU(),
|
| 106 |
+
nn.Conv1d(16, 32, kernel_size=3, padding=1),
|
| 107 |
+
nn.BatchNorm1d(32),
|
| 108 |
+
nn.ReLU(),
|
| 109 |
+
nn.Flatten(),
|
| 110 |
+
nn.Linear(32 * input_size, output_size),
|
| 111 |
+
nn.ReLU()
|
| 112 |
+
)
|
| 113 |
+
def forward(self, x):
|
| 114 |
+
x = x.unsqueeze(1)
|
| 115 |
+
return self.encoder(x)
|
| 116 |
+
|
| 117 |
+
class GraphAttentionLayer(nn.Module):
|
| 118 |
+
def __init__(self, in_features, out_features, dropout=0.0, alpha=0.2):
|
| 119 |
+
super(GraphAttentionLayer, self).__init__()
|
| 120 |
+
self.W = nn.Linear(in_features, out_features, bias=False)
|
| 121 |
+
self.a = nn.Parameter(torch.empty(2 * out_features, 1))
|
| 122 |
+
nn.init.xavier_uniform_(self.W.weight.data, gain=1.414)
|
| 123 |
+
nn.init.xavier_uniform_(self.a.data, gain=1.414)
|
| 124 |
+
self.leakyrelu = nn.LeakyReLU(alpha)
|
| 125 |
+
self.dropout = nn.Dropout(dropout)
|
| 126 |
+
def forward(self, h, adj=None):
|
| 127 |
+
Wh = self.W(h)
|
| 128 |
+
batch_size, N, _ = Wh.size()
|
| 129 |
+
Wh_i = Wh.unsqueeze(2).repeat(1, 1, N, 1)
|
| 130 |
+
Wh_j = Wh.unsqueeze(1).repeat(1, N, 1, 1)
|
| 131 |
+
e = self.leakyrelu(torch.matmul(torch.cat([Wh_i, Wh_j], dim=-1), self.a).squeeze(-1))
|
| 132 |
+
attention = F.softmax(e, dim=-1)
|
| 133 |
+
attention = self.dropout(attention)
|
| 134 |
+
h_prime = torch.matmul(attention, Wh)
|
| 135 |
+
return h_prime, attention
|
| 136 |
+
|
| 137 |
+
class MultiHeadGraphAttentionLayer(nn.Module):
|
| 138 |
+
def __init__(self, in_features, out_features, num_heads=4, dropout=0.0, alpha=0.2):
|
| 139 |
+
super(MultiHeadGraphAttentionLayer, self).__init__()
|
| 140 |
+
self.num_heads = num_heads
|
| 141 |
+
self.heads = nn.ModuleList([
|
| 142 |
+
GraphAttentionLayer(in_features, out_features, dropout, alpha)
|
| 143 |
+
for _ in range(num_heads)
|
| 144 |
+
])
|
| 145 |
+
self.linear = nn.Linear(num_heads * out_features, out_features)
|
| 146 |
+
def forward(self, h):
|
| 147 |
+
head_outputs = [head(h)[0] for head in self.heads]
|
| 148 |
+
h_concat = torch.cat(head_outputs, dim=-1)
|
| 149 |
+
return self.linear(h_concat)
|
| 150 |
+
|
| 151 |
+
class EnhancedGraphFusion(nn.Module):
|
| 152 |
+
def __init__(self, in_features, out_features, num_heads=4, num_layers=2, dropout=0.0, alpha=0.2):
|
| 153 |
+
super(EnhancedGraphFusion, self).__init__()
|
| 154 |
+
self.global_init = nn.Parameter(torch.zeros(in_features))
|
| 155 |
+
self.layers = nn.ModuleList([
|
| 156 |
+
MultiHeadGraphAttentionLayer(in_features, in_features, num_heads, dropout, alpha)
|
| 157 |
+
for _ in range(num_layers)
|
| 158 |
+
])
|
| 159 |
+
self.norm = nn.LayerNorm(in_features)
|
| 160 |
+
self.proj = nn.Linear(in_features, out_features)
|
| 161 |
+
def forward(self, image_nodes, metadata_feat):
|
| 162 |
+
batch_size = image_nodes.size(0)
|
| 163 |
+
metadata_node = metadata_feat.unsqueeze(1)
|
| 164 |
+
global_node = self.global_init.unsqueeze(0).expand(batch_size, -1).unsqueeze(1)
|
| 165 |
+
h = torch.cat([image_nodes, metadata_node, global_node], dim=1)
|
| 166 |
+
for layer in self.layers:
|
| 167 |
+
residual = h
|
| 168 |
+
h = layer(h)
|
| 169 |
+
h = F.elu(h)
|
| 170 |
+
h = self.norm(h + residual)
|
| 171 |
+
fused = h[:, -1, :]
|
| 172 |
+
return self.proj(fused)
|
| 173 |
+
|
| 174 |
+
###############################################################################
|
| 175 |
+
# DenseNet201-based Classifier for Skin Lesion Classification
|
| 176 |
+
###############################################################################
|
| 177 |
+
class DenseNet201Classifier(nn.Module):
|
| 178 |
+
def __init__(self, num_classes=6, metadata_input_size=3, metadata_output_size=768):
|
| 179 |
+
super(DenseNet201Classifier, self).__init__()
|
| 180 |
+
self.densenet = models.densenet201(pretrained=True)
|
| 181 |
+
self.num_features = self.densenet.classifier.in_features
|
| 182 |
+
self.img_proj = nn.Linear(self.num_features, metadata_output_size)
|
| 183 |
+
self.metadata_encoder = MetadataEncoder(metadata_input_size, metadata_output_size)
|
| 184 |
+
self.enhanced_graph_fusion = EnhancedGraphFusion(
|
| 185 |
+
in_features=metadata_output_size,
|
| 186 |
+
out_features=metadata_output_size,
|
| 187 |
+
num_heads=4,
|
| 188 |
+
num_layers=2,
|
| 189 |
+
dropout=0.1,
|
| 190 |
+
alpha=0.2
|
| 191 |
+
)
|
| 192 |
+
self.head = create_classification_head(metadata_output_size, num_classes)
|
| 193 |
+
def forward_feature_map(self, x):
|
| 194 |
+
features = self.densenet.features(x)
|
| 195 |
+
return F.relu(features, inplace=True)
|
| 196 |
+
def forward(self, x, metadata):
|
| 197 |
+
fmap = self.forward_feature_map(x)
|
| 198 |
+
batch_size, C, H, W = fmap.shape
|
| 199 |
+
image_nodes = fmap.view(batch_size, C, H * W).transpose(1, 2)
|
| 200 |
+
image_nodes = self.img_proj(image_nodes)
|
| 201 |
+
metadata_features = self.metadata_encoder(metadata)
|
| 202 |
+
fused_features = self.enhanced_graph_fusion(image_nodes, metadata_features)
|
| 203 |
+
return self.head(fused_features)
|
| 204 |
+
|
| 205 |
+
###############################################################################
|
| 206 |
+
# Inference Pipeline Setup
|
| 207 |
+
###############################################################################
|
| 208 |
+
# Define image transformation for inference
|
| 209 |
+
transform = transforms.Compose([
|
| 210 |
+
transforms.Resize((224, 224)),
|
| 211 |
+
transforms.ToTensor(),
|
| 212 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406],
|
| 213 |
+
std=[0.229, 0.224, 0.225])
|
| 214 |
+
])
|
| 215 |
+
|
| 216 |
+
def load_model(model_path='pd4_model.pth'):
|
| 217 |
+
"""Load the pre-trained DenseNet201Classifier model."""
|
| 218 |
+
num_classes = 6
|
| 219 |
+
metadata_input_size = 3 # Expecting: age, gender, skin_cancer_history
|
| 220 |
+
model = DenseNet201Classifier(num_classes=num_classes,
|
| 221 |
+
metadata_input_size=metadata_input_size,
|
| 222 |
+
metadata_output_size=768)
|
| 223 |
+
state_dict = torch.load(model_path, map_location='cpu')
|
| 224 |
+
model.load_state_dict(state_dict)
|
| 225 |
+
model.to(device)
|
| 226 |
+
model.eval()
|
| 227 |
+
return model
|
| 228 |
+
|
| 229 |
+
# Load the model once at function startup
|
| 230 |
+
model = load_model()
|
| 231 |
+
|
| 232 |
+
###############################################################################
|
| 233 |
+
# Cloud Function HTTP Endpoint (Using functions_framework)
|
| 234 |
+
###############################################################################
|
| 235 |
+
@functions_framework.http
|
| 236 |
+
def predict(request):
|
| 237 |
+
"""
|
| 238 |
+
HTTP-triggered Cloud Function that accepts a POST request with an image file.
|
| 239 |
+
Optionally, you can uncomment the preprocessing step if needed.
|
| 240 |
+
Returns the predicted class and confidence as JSON.
|
| 241 |
+
"""
|
| 242 |
+
if request.method != 'POST':
|
| 243 |
+
return jsonify({'error': 'Only POST method is supported.'}), 405
|
| 244 |
+
|
| 245 |
+
if 'file' not in request.files:
|
| 246 |
+
return jsonify({'error': 'No file provided.'}), 400
|
| 247 |
+
|
| 248 |
+
file = request.files['file']
|
| 249 |
+
try:
|
| 250 |
+
image = Image.open(file.stream).convert('RGB')
|
| 251 |
+
except Exception as e:
|
| 252 |
+
return jsonify({'error': 'Invalid image file.'}), 400
|
| 253 |
+
|
| 254 |
+
# Optionally apply advanced preprocessing:
|
| 255 |
+
image = preprocess_image(image)
|
| 256 |
+
|
| 257 |
+
image_tensor = transform(image).unsqueeze(0).to(device)
|
| 258 |
+
# Use default metadata values (e.g., zeros for [age, gender, skin_cancer_history])
|
| 259 |
+
default_metadata = torch.zeros((1, 3), dtype=torch.float).to(device)
|
| 260 |
+
|
| 261 |
+
with torch.no_grad():
|
| 262 |
+
outputs = model(image_tensor, default_metadata)
|
| 263 |
+
probabilities = F.softmax(outputs, dim=1)
|
| 264 |
+
confidence, predicted = torch.max(probabilities, dim=1)
|
| 265 |
+
|
| 266 |
+
result = {
|
| 267 |
+
'predicted_class': predicted.item(),
|
| 268 |
+
'confidence': confidence.item()
|
| 269 |
+
}
|
| 270 |
+
return jsonify(result)
|
| 271 |
+
|
| 272 |
+
if __name__ == '__main__':
|
| 273 |
+
import os
|
| 274 |
+
port = int(os.environ.get("PORT", 8080))
|
| 275 |
+
# The functions_framework creates a Flask app that exposes your target function.
|
| 276 |
+
from functions_framework import create_app
|
| 277 |
+
app = create_app(target="predict")
|
| 278 |
+
app.run(host="0.0.0.0", port=port, debug=True)
|
package-lock.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "skincancerscreening",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {}
|
| 6 |
+
}
|
pd4_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd2aacb12d67299548ad2905a9f11f7bf4ae1d1a4a41b37aa06169a1e9f8820c
|
| 3 |
+
size 129438302
|
public/404.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Page Not Found</title>
|
| 7 |
+
|
| 8 |
+
<style media="screen">
|
| 9 |
+
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
| 10 |
+
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
|
| 11 |
+
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
|
| 12 |
+
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
| 13 |
+
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
| 14 |
+
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
| 15 |
+
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
| 16 |
+
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
| 17 |
+
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
| 18 |
+
@media (max-width: 600px) {
|
| 19 |
+
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
| 20 |
+
body { border-top: 16px solid #ffa100; }
|
| 21 |
+
}
|
| 22 |
+
</style>
|
| 23 |
+
</head>
|
| 24 |
+
<body>
|
| 25 |
+
<div id="message">
|
| 26 |
+
<h2>404</h2>
|
| 27 |
+
<h1>Page Not Found</h1>
|
| 28 |
+
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
|
| 29 |
+
<h3>Why am I seeing this?</h3>
|
| 30 |
+
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
|
| 31 |
+
</div>
|
| 32 |
+
</body>
|
| 33 |
+
</html>
|
public/index.html
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>Skin Lesion Screening</title>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<style>
|
| 7 |
+
body {
|
| 8 |
+
font-family: Arial, sans-serif;
|
| 9 |
+
margin: 40px;
|
| 10 |
+
background-color: #f9f9f9;
|
| 11 |
+
}
|
| 12 |
+
.container {
|
| 13 |
+
max-width: 500px;
|
| 14 |
+
margin: auto;
|
| 15 |
+
background: #fff;
|
| 16 |
+
padding: 20px;
|
| 17 |
+
border-radius: 8px;
|
| 18 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 19 |
+
text-align: center;
|
| 20 |
+
}
|
| 21 |
+
input, select, button {
|
| 22 |
+
width: 100%;
|
| 23 |
+
padding: 10px;
|
| 24 |
+
margin: 10px 0;
|
| 25 |
+
font-size: 1em;
|
| 26 |
+
}
|
| 27 |
+
button {
|
| 28 |
+
background-color: #007BFF;
|
| 29 |
+
border: none;
|
| 30 |
+
color: white;
|
| 31 |
+
cursor: pointer;
|
| 32 |
+
border-radius: 4px;
|
| 33 |
+
}
|
| 34 |
+
button:hover {
|
| 35 |
+
background-color: #0056b3;
|
| 36 |
+
}
|
| 37 |
+
.result {
|
| 38 |
+
margin-top: 20px;
|
| 39 |
+
font-size: 1.2em;
|
| 40 |
+
color: #333;
|
| 41 |
+
white-space: pre-line;
|
| 42 |
+
}
|
| 43 |
+
</style>
|
| 44 |
+
</head>
|
| 45 |
+
<body>
|
| 46 |
+
<div class="container">
|
| 47 |
+
<h1>Skin Lesion Screening</h1>
|
| 48 |
+
<form id="upload-form">
|
| 49 |
+
<!-- Image File Input -->
|
| 50 |
+
<label for="file">Upload Image:</label>
|
| 51 |
+
<input type="file" id="file" accept="image/*" capture="camera" required>
|
| 52 |
+
|
| 53 |
+
<!-- Metadata: Age -->
|
| 54 |
+
<label for="age">Age:</label>
|
| 55 |
+
<input type="number" id="age" placeholder="Enter your age" required>
|
| 56 |
+
|
| 57 |
+
<!-- Metadata: Gender -->
|
| 58 |
+
<label for="gender">Gender:</label>
|
| 59 |
+
<select id="gender" required>
|
| 60 |
+
<option value="" disabled selected>Select your gender</option>
|
| 61 |
+
<option value="0">Male</option>
|
| 62 |
+
<option value="1">Female</option>
|
| 63 |
+
<option value="2">Other</option>
|
| 64 |
+
</select>
|
| 65 |
+
|
| 66 |
+
<!-- Metadata: Skin Cancer History -->
|
| 67 |
+
<label for="cancerHistory">History of Skin Cancer:</label>
|
| 68 |
+
<select id="cancerHistory" required>
|
| 69 |
+
<option value="" disabled selected>Do you have a history of skin cancer?</option>
|
| 70 |
+
<option value="0">No</option>
|
| 71 |
+
<option value="1">Yes</option>
|
| 72 |
+
</select>
|
| 73 |
+
|
| 74 |
+
<button type="submit">Analyze</button>
|
| 75 |
+
</form>
|
| 76 |
+
<div id="result" class="result"></div>
|
| 77 |
+
</div>
|
| 78 |
+
<script>
|
| 79 |
+
document.getElementById('upload-form').addEventListener('submit', async function(e) {
|
| 80 |
+
e.preventDefault();
|
| 81 |
+
|
| 82 |
+
// Get the inputs
|
| 83 |
+
const fileInput = document.getElementById('file');
|
| 84 |
+
const ageInput = document.getElementById('age');
|
| 85 |
+
const genderInput = document.getElementById('gender');
|
| 86 |
+
const cancerHistoryInput = document.getElementById('cancerHistory');
|
| 87 |
+
|
| 88 |
+
if (!fileInput.files.length) {
|
| 89 |
+
alert("Please select an image file.");
|
| 90 |
+
return;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Create a FormData object and append file and metadata
|
| 94 |
+
const formData = new FormData();
|
| 95 |
+
formData.append('file', fileInput.files[0]);
|
| 96 |
+
formData.append('age', ageInput.value);
|
| 97 |
+
formData.append('gender', genderInput.value);
|
| 98 |
+
formData.append('cancer_history', cancerHistoryInput.value);
|
| 99 |
+
|
| 100 |
+
// Replace this URL with your actual Cloud Function endpoint URL
|
| 101 |
+
const functionUrl = 'https://us-central1-skincancerscreening.cloudfunctions.net/predict';
|
| 102 |
+
|
| 103 |
+
// Send POST request to the backend
|
| 104 |
+
try {
|
| 105 |
+
const response = await fetch(functionUrl, {
|
| 106 |
+
method: 'POST',
|
| 107 |
+
body: formData
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
if (!response.ok) {
|
| 111 |
+
throw new Error("Network response was not ok");
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const data = await response.json();
|
| 115 |
+
document.getElementById('result').innerText =
|
| 116 |
+
'Predicted Class: ' + data.predicted_class + '\nConfidence: ' + (data.confidence * 100).toFixed(2) + '%';
|
| 117 |
+
} catch (error) {
|
| 118 |
+
document.getElementById('result').innerText = 'Error: ' + error;
|
| 119 |
+
}
|
| 120 |
+
});
|
| 121 |
+
</script>
|
| 122 |
+
</body>
|
| 123 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
functions-framework==3.1.0
|
| 2 |
+
torch==1.13.1
|
| 3 |
+
torchvision==0.14.1
|
| 4 |
+
Pillow==9.4.0
|
| 5 |
+
opencv-python-headless==4.7.0.72
|