Add README with file type guidelines and snake_case naming rule; update .gitignore typo
This commit is contained in:
2073
workspace/sprint_1_2/Design_Material/API_docs/API_CONTRACT_DRAFT.md
Normal file
2073
workspace/sprint_1_2/Design_Material/API_docs/API_CONTRACT_DRAFT.md
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
File diff suppressed because it is too large
Load Diff
BIN
workspace/sprint_1_2/Design_Material/CI_CD_docs/ci_cd_flow.png
Normal file
BIN
workspace/sprint_1_2/Design_Material/CI_CD_docs/ci_cd_flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
@@ -0,0 +1,818 @@
|
||||
## PlantUML Deliverable 1 — Logical ER / Schema Diagram
|
||||
|
||||
Recommended design file:
|
||||
|
||||
```text
|
||||
PILOT_PROJECT/workspace/sprint_1_2/Design_Material/DATA-INGESTION/RELATIONAL_DB_SCHEMA_SPEC.md
|
||||
```
|
||||
|
||||
Use this ER/schema diagram as the primary schema diagram.
|
||||
|
||||
```plantuml
|
||||
@startuml "VKIST_Relational_DB__ER_Schema"
|
||||
left to right direction
|
||||
skinparam linetype ortho
|
||||
|
||||
entity "clinician_users" as clinician_users {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
display_name : text
|
||||
role : enum <<not null>>
|
||||
created_at : timestamp
|
||||
updated_at : timestamp
|
||||
}
|
||||
|
||||
entity "patient_cases" as patient_cases {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
patient_hash : text <<unique>>
|
||||
case_hash : text <<unique>>
|
||||
metadata_json : json
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "diagnostic_sessions" as diagnostic_sessions {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
patient_case_id : uuid <<FK>>
|
||||
clinician_user_id : uuid <<FK>>
|
||||
status : enum <<not null>>
|
||||
review_decision_id : uuid <<FK nullable>>
|
||||
created_at : timestamp
|
||||
updated_at : timestamp
|
||||
}
|
||||
|
||||
entity "image_assets" as image_assets {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
session_id : uuid <<FK>>
|
||||
asset_type : enum <<not null>>
|
||||
bucket_name : text
|
||||
object_key : text <<uuid-only>>
|
||||
sha256_hash : text <<not null>>
|
||||
size_bytes : bigint
|
||||
content_type : text
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "scan_frames" as scan_frames {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
diagnostic_session_id : uuid <<FK>>
|
||||
source_asset_id : uuid <<FK>>
|
||||
extracted_asset_id : uuid <<FK nullable>>
|
||||
frame_index : int <<default 0>>
|
||||
width : int
|
||||
height : int
|
||||
modality : enum
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "calibrations" as calibrations {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
scan_frame_id : uuid <<FK unique>>
|
||||
pixel_spacing_x_mm : decimal
|
||||
pixel_spacing_y_mm : decimal
|
||||
source : enum
|
||||
confidence : decimal nullable
|
||||
}
|
||||
|
||||
entity "analysis_jobs" as analysis_jobs {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
diagnostic_session_id : uuid <<FK>>
|
||||
scan_frame_id : uuid <<FK unique>>
|
||||
model_registry_entry_id : uuid <<FK nullable>>
|
||||
status : enum <<not null>>
|
||||
trace_id : uuid
|
||||
idempotency_key : text
|
||||
created_at : timestamp
|
||||
started_at : timestamp nullable
|
||||
completed_at : timestamp nullable
|
||||
failed_at : timestamp nullable
|
||||
}
|
||||
|
||||
entity "pipeline_steps" as pipeline_steps {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
analysis_job_id : uuid <<FK>>
|
||||
step_name : text
|
||||
status : enum
|
||||
started_at : timestamp nullable
|
||||
completed_at : timestamp nullable
|
||||
error_code : text nullable
|
||||
error_message : text nullable
|
||||
metadata_json : json
|
||||
}
|
||||
|
||||
entity "model_registry" as model_registry {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
task_name : enum <<not null>>
|
||||
model_name : text <<not null>>
|
||||
version : text <<not null>>
|
||||
default : boolean
|
||||
enabled : boolean
|
||||
input_spec_json : json
|
||||
output_spec_json : json
|
||||
}
|
||||
|
||||
entity "model_artifacts" as model_artifacts {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
model_registry_entry_id : uuid <<FK>>
|
||||
artifact_uri : text <<not null>>
|
||||
sha256_hash : text <<not null>>
|
||||
format : text
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "preprocessed_images" as preprocessed_images {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
scan_frame_id : uuid <<FK>>
|
||||
analysis_job_id : uuid <<FK>>
|
||||
artifact_id : uuid <<FK nullable>>
|
||||
resize_w : int
|
||||
resize_h : int
|
||||
normalization : text
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "angle_predictions" as angle_predictions {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
analysis_job_id : uuid <<FK unique>>
|
||||
predicted_class : enum
|
||||
confidence : decimal
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "inflammation_predictions" as inflammation_predictions {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
analysis_job_id : uuid <<FK unique>>
|
||||
detected : boolean
|
||||
confidence : decimal
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "segmentation_masks" as segmentation_masks {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
analysis_job_id : uuid <<FK unique>>
|
||||
artifact_id : uuid <<FK>>
|
||||
class_labels_json : json
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "measurements" as measurements {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
analysis_job_id : uuid <<FK>>
|
||||
target_class : text
|
||||
value_mm : decimal
|
||||
pixel_count : bigint nullable
|
||||
confidence : decimal nullable
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "synovitis_grades" as synovitis_grades {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
analysis_job_id : uuid <<FK unique>>
|
||||
suggested_grade : int <<0..3>>
|
||||
label : text
|
||||
confidence : decimal
|
||||
basis_json : json
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "review_decisions" as review_decisions {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
diagnostic_session_id : uuid <<FK unique>>
|
||||
analysis_job_id : uuid <<FK>>
|
||||
clinician_user_id : uuid <<FK>>
|
||||
status : enum <<not null>>
|
||||
clinician_grade : int nullable <<0..3>>
|
||||
comment : text nullable
|
||||
digital_signature_ref : text nullable
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
entity "audit_ledger_entries" as audit_ledger_entries {
|
||||
* id : uuid <<PK>>
|
||||
--
|
||||
session_id : uuid <<FK>>
|
||||
job_id : uuid <<FK nullable>>
|
||||
event_type : text
|
||||
previous_hash : text nullable
|
||||
audit_hash : text <<not null>>
|
||||
hash_inputs_json : json
|
||||
created_at : timestamp
|
||||
}
|
||||
|
||||
clinician_users ||--o{ diagnostic_sessions : creates
|
||||
patient_cases ||--o{ diagnostic_sessions : owns
|
||||
diagnostic_sessions ||--o{ image_assets : owns
|
||||
diagnostic_sessions ||--o{ scan_frames : contains
|
||||
image_assets ||--o{ scan_frames : source_asset
|
||||
image_assets ||--o{ scan_frames : extracted_asset
|
||||
scan_frames ||--o| calibrations : has
|
||||
diagnostic_sessions ||--o{ analysis_jobs : runs
|
||||
analysis_jobs ||--o{ pipeline_steps : executes
|
||||
analysis_jobs }o--|| model_registry : uses
|
||||
model_registry ||--o{ model_artifacts : publishes
|
||||
analysis_jobs ||--o{ preprocessed_images : transforms
|
||||
analysis_jobs ||--o| angle_predictions : produces
|
||||
analysis_jobs ||--o| inflammation_predictions : produces
|
||||
analysis_jobs ||--o| segmentation_masks : produces
|
||||
analysis_jobs ||--o{ measurements : produces
|
||||
analysis_jobs ||--o| synovitis_grades : produces
|
||||
diagnostic_sessions ||--o| review_decisions : finalizes
|
||||
analysis_jobs ||--o| review_decisions : supports
|
||||
clinician_users ||--o{ review_decisions : signs
|
||||
diagnostic_sessions ||--o{ audit_ledger_entries : appends
|
||||
analysis_jobs ||--o{ audit_ledger_entries : appends
|
||||
|
||||
note right of image_assets
|
||||
ArtifactReference is generated from image_assets + object store metadata.
|
||||
end note
|
||||
|
||||
note bottom of audit_ledger_entries
|
||||
Append-only. PostgreSQL triggers block UPDATE/DELETE.
|
||||
end note
|
||||
@enduml
|
||||
```
|
||||
|
||||
## PlantUML Deliverable 2 — Chen Notation ER Diagram
|
||||
|
||||
Use this as the Chen-notation ER diagram. It emphasizes entities, relationships, and cardinality. Attribute ovals show key attributes only to keep the diagram readable.
|
||||
|
||||
```plantuml
|
||||
@startchen "VKIST_Relational_DB__Chen_Notation"
|
||||
skinparam linetype ortho
|
||||
|
||||
' Entities with their nested attributes
|
||||
entity ClinicianUser {
|
||||
id <<key>>
|
||||
display_name
|
||||
role
|
||||
}
|
||||
|
||||
entity PatientCase {
|
||||
id <<key>>
|
||||
patient_hash
|
||||
case_hash
|
||||
}
|
||||
|
||||
entity DiagnosticSession {
|
||||
id <<key>>
|
||||
status
|
||||
created_at
|
||||
}
|
||||
|
||||
entity ImageAsset {
|
||||
id <<key>>
|
||||
asset_type
|
||||
sha256_hash
|
||||
object_key
|
||||
}
|
||||
|
||||
entity ScanFrame {
|
||||
id <<key>>
|
||||
frame_index
|
||||
width
|
||||
height
|
||||
}
|
||||
|
||||
entity Calibration {
|
||||
id <<key>>
|
||||
pixel_spacing
|
||||
source
|
||||
confidence
|
||||
}
|
||||
|
||||
entity AnalysisJob {
|
||||
id <<key>>
|
||||
status
|
||||
trace_id
|
||||
}
|
||||
|
||||
entity PipelineStep {
|
||||
id <<key>>
|
||||
step_name
|
||||
status
|
||||
}
|
||||
|
||||
entity ModelRegistryEntry {
|
||||
id <<key>>
|
||||
task_name
|
||||
model_name
|
||||
version
|
||||
}
|
||||
|
||||
entity ModelArtifact {
|
||||
id <<key>>
|
||||
artifact_uri
|
||||
sha256_hash
|
||||
}
|
||||
|
||||
entity AnglePrediction {
|
||||
id <<key>>
|
||||
predicted_class
|
||||
confidence
|
||||
}
|
||||
|
||||
entity InflammationPrediction {
|
||||
id <<key>>
|
||||
detected
|
||||
confidence
|
||||
}
|
||||
|
||||
entity SegmentationMask {
|
||||
id <<key>>
|
||||
artifact_id
|
||||
class_labels
|
||||
}
|
||||
|
||||
entity Measurement {
|
||||
id <<key>>
|
||||
target_class
|
||||
value_mm
|
||||
}
|
||||
|
||||
entity SynovitisGrade {
|
||||
id <<key>>
|
||||
suggested_grade
|
||||
confidence
|
||||
}
|
||||
|
||||
entity ReviewDecision {
|
||||
id <<key>>
|
||||
status
|
||||
clinician_grade
|
||||
}
|
||||
|
||||
entity AuditLedgerEntry {
|
||||
id <<key>>
|
||||
event_type
|
||||
audit_hash
|
||||
}
|
||||
|
||||
' Relationships
|
||||
relationship creates {
|
||||
}
|
||||
|
||||
relationship owns {
|
||||
}
|
||||
|
||||
relationship stores {
|
||||
}
|
||||
|
||||
relationship contains {
|
||||
}
|
||||
|
||||
relationship calibrates {
|
||||
}
|
||||
|
||||
relationship runs {
|
||||
}
|
||||
|
||||
relationship executes {
|
||||
}
|
||||
|
||||
relationship uses {
|
||||
}
|
||||
|
||||
relationship publishes {
|
||||
}
|
||||
|
||||
relationship produces_angle {
|
||||
}
|
||||
|
||||
relationship produces_inflam {
|
||||
}
|
||||
|
||||
relationship produces_mask {
|
||||
}
|
||||
|
||||
relationship measures {
|
||||
}
|
||||
|
||||
relationship grades {
|
||||
}
|
||||
|
||||
relationship user_signs {
|
||||
}
|
||||
|
||||
relationship session_signs {
|
||||
}
|
||||
|
||||
relationship job_signs {
|
||||
}
|
||||
|
||||
relationship session_appends {
|
||||
}
|
||||
|
||||
relationship job_appends {
|
||||
}
|
||||
|
||||
' Connectivity & Participation Mapping
|
||||
creates -1- ClinicianUser
|
||||
creates -N- DiagnosticSession
|
||||
|
||||
owns -1- PatientCase
|
||||
owns -N- DiagnosticSession
|
||||
|
||||
stores -1- DiagnosticSession
|
||||
stores -N- ImageAsset
|
||||
|
||||
contains -1- DiagnosticSession
|
||||
contains -N- ScanFrame
|
||||
|
||||
calibrates -1- ScanFrame
|
||||
calibrates -(0,1)- Calibration
|
||||
|
||||
runs -1- DiagnosticSession
|
||||
runs -N- AnalysisJob
|
||||
|
||||
executes -1- AnalysisJob
|
||||
executes -N- PipelineStep
|
||||
|
||||
uses -N- AnalysisJob
|
||||
uses -1- ModelRegistryEntry
|
||||
|
||||
publishes -1- ModelRegistryEntry
|
||||
publishes -N- ModelArtifact
|
||||
|
||||
produces_angle -1- AnalysisJob
|
||||
produces_angle -(0,1)- AnglePrediction
|
||||
|
||||
produces_inflam -1- AnalysisJob
|
||||
produces_inflam -(0,1)- InflammationPrediction
|
||||
|
||||
produces_mask -1- AnalysisJob
|
||||
produces_mask -(0,1)- SegmentationMask
|
||||
|
||||
measures -1- AnalysisJob
|
||||
measures -N- Measurement
|
||||
|
||||
grades -1- AnalysisJob
|
||||
grades -(0,1)- SynovitisGrade
|
||||
|
||||
user_signs -1- ClinicianUser
|
||||
user_signs -N- ReviewDecision
|
||||
|
||||
session_signs -1- DiagnosticSession
|
||||
session_signs -(0,1)- ReviewDecision
|
||||
|
||||
job_signs -1- AnalysisJob
|
||||
job_signs -(0,1)- ReviewDecision
|
||||
|
||||
session_appends -1- DiagnosticSession
|
||||
session_appends -N- AuditLedgerEntry
|
||||
|
||||
job_appends -1- AnalysisJob
|
||||
job_appends -N- AuditLedgerEntry
|
||||
|
||||
@endchen
|
||||
```
|
||||
|
||||
* Refractor & Splited version for easy to visualizing
|
||||
### Part 1: Core Clinical & Diagnostic Subsystem
|
||||
```
|
||||
@startchen "VKIST_Clinical_Diagnostic_Subsystem"
|
||||
skinparam linetype ortho
|
||||
|
||||
entity ClinicianUser {
|
||||
id <<key>>
|
||||
display_name
|
||||
role
|
||||
}
|
||||
|
||||
entity PatientCase {
|
||||
id <<key>>
|
||||
patient_hash
|
||||
case_hash
|
||||
}
|
||||
|
||||
entity DiagnosticSession {
|
||||
id <<key>>
|
||||
status
|
||||
created_at
|
||||
}
|
||||
|
||||
entity ImageAsset {
|
||||
id <<key>>
|
||||
asset_type
|
||||
sha256_hash
|
||||
object_key
|
||||
}
|
||||
|
||||
entity ScanFrame {
|
||||
id <<key>>
|
||||
frame_index
|
||||
width
|
||||
height
|
||||
}
|
||||
|
||||
entity Calibration {
|
||||
id <<key>>
|
||||
pixel_spacing
|
||||
source
|
||||
confidence
|
||||
}
|
||||
|
||||
entity ReviewDecision {
|
||||
id <<key>>
|
||||
status
|
||||
clinician_grade
|
||||
}
|
||||
|
||||
relationship Creates_Session {
|
||||
}
|
||||
relationship Owns_Session {
|
||||
}
|
||||
relationship Stores_Asset {
|
||||
}
|
||||
relationship Contains_Frame {
|
||||
}
|
||||
relationship Calibrates_Frame {
|
||||
}
|
||||
relationship User_Signs {
|
||||
}
|
||||
relationship Session_Signs {
|
||||
}
|
||||
|
||||
ClinicianUser -1- Creates_Session
|
||||
Creates_Session -N- DiagnosticSession
|
||||
|
||||
PatientCase -1- Owns_Session
|
||||
Owns_Session -N- DiagnosticSession
|
||||
|
||||
DiagnosticSession -1- Stores_Asset
|
||||
Stores_Asset -N- ImageAsset
|
||||
|
||||
DiagnosticSession -1- Contains_Frame
|
||||
Contains_Frame -N- ScanFrame
|
||||
|
||||
ScanFrame -1- Calibrates_Frame
|
||||
Calibrates_Frame -(0,1)- Calibration
|
||||
|
||||
ClinicianUser -1- User_Signs
|
||||
User_Signs -N- ReviewDecision
|
||||
|
||||
DiagnosticSession -1- Session_Signs
|
||||
Session_Signs -(0,1)- ReviewDecision
|
||||
|
||||
@endchen
|
||||
```
|
||||
### Part 2: AI/ML Pipeline & Audit Subsystem
|
||||
```
|
||||
@startchen "VKIST_ML_Pipeline_Audit_Subsystem"
|
||||
skinparam linetype ortho
|
||||
|
||||
entity DiagnosticSession {
|
||||
id <<key>>
|
||||
}
|
||||
|
||||
entity ReviewDecision {
|
||||
id <<key>>
|
||||
}
|
||||
|
||||
entity AnalysisJob {
|
||||
id <<key>>
|
||||
status
|
||||
trace_id
|
||||
}
|
||||
|
||||
entity PipelineStep {
|
||||
id <<key>>
|
||||
step_name
|
||||
status
|
||||
}
|
||||
|
||||
entity ModelRegistryEntry {
|
||||
id <<key>>
|
||||
task_name
|
||||
model_name
|
||||
version
|
||||
}
|
||||
|
||||
entity ModelArtifact {
|
||||
id <<key>>
|
||||
artifact_uri
|
||||
sha256_hash
|
||||
}
|
||||
|
||||
entity AnglePrediction {
|
||||
id <<key>>
|
||||
predicted_class
|
||||
confidence
|
||||
}
|
||||
|
||||
entity InflammationPrediction {
|
||||
id <<key>>
|
||||
detected
|
||||
confidence
|
||||
}
|
||||
|
||||
entity SegmentationMask {
|
||||
id <<key>>
|
||||
artifact_id
|
||||
class_labels
|
||||
}
|
||||
|
||||
entity Measurement {
|
||||
id <<key>>
|
||||
target_class
|
||||
value_mm
|
||||
}
|
||||
|
||||
entity SynovitisGrade {
|
||||
id <<key>>
|
||||
suggested_grade
|
||||
confidence
|
||||
}
|
||||
|
||||
entity AuditLedgerEntry {
|
||||
id <<key>>
|
||||
event_type
|
||||
audit_hash
|
||||
}
|
||||
|
||||
relationship Runs_Job {
|
||||
}
|
||||
relationship Executes_Step {
|
||||
}
|
||||
relationship Uses_Model {
|
||||
}
|
||||
relationship Publishes_Artifact {
|
||||
}
|
||||
relationship Produces_Angle {
|
||||
}
|
||||
relationship Produces_Inflam {
|
||||
}
|
||||
relationship Produces_Mask {
|
||||
}
|
||||
relationship Measures_Metrics {
|
||||
}
|
||||
relationship Grades_Synovitis {
|
||||
}
|
||||
relationship Job_Signs {
|
||||
}
|
||||
relationship Session_Appends {
|
||||
}
|
||||
relationship Job_Appends {
|
||||
}
|
||||
|
||||
DiagnosticSession -1- Runs_Job
|
||||
Runs_Job -N- AnalysisJob
|
||||
|
||||
AnalysisJob -1- Executes_Step
|
||||
Executes_Step -N- PipelineStep
|
||||
|
||||
AnalysisJob -N- Uses_Model
|
||||
Uses_Model -1- ModelRegistryEntry
|
||||
|
||||
ModelRegistryEntry -1- Publishes_Artifact
|
||||
Publishes_Artifact -N- ModelArtifact
|
||||
|
||||
AnalysisJob -1- Produces_Angle
|
||||
Produces_Angle -(0,1)- AnglePrediction
|
||||
|
||||
AnalysisJob -1- Produces_Inflam
|
||||
Produces_Inflam -(0,1)- InflammationPrediction
|
||||
|
||||
AnalysisJob -1- Produces_Mask
|
||||
Produces_Mask -(0,1)- SegmentationMask
|
||||
|
||||
AnalysisJob -1- Measures_Metrics
|
||||
Measures_Metrics -N- Measurement
|
||||
|
||||
AnalysisJob -1- Grades_Synovitis
|
||||
Grades_Synovitis -(0,1)- SynovitisGrade
|
||||
|
||||
AnalysisJob -1- Job_Signs
|
||||
Job_Signs -(0,1)- ReviewDecision
|
||||
|
||||
DiagnosticSession -1- Session_Appends
|
||||
Session_Appends -N- AuditLedgerEntry
|
||||
|
||||
AnalysisJob -1- Job_Appends
|
||||
Job_Appends -N- AuditLedgerEntry
|
||||
|
||||
@endchen
|
||||
```
|
||||
|
||||
|
||||
## Migration / Schema Plan
|
||||
|
||||
### Step 1 — Logical schema freeze
|
||||
|
||||
Create the design spec first. Include:
|
||||
|
||||
- ER/schema PlantUML,
|
||||
- Chen PlantUML,
|
||||
- table list,
|
||||
- FK map,
|
||||
- indexes,
|
||||
- constraints,
|
||||
- NFR/UC traceability.
|
||||
|
||||
### Step 2 — SQLite Sprint 1_2 migrations
|
||||
|
||||
Implement minimal SQLite migrations for:
|
||||
|
||||
- `clinician_users`
|
||||
- `patient_cases`
|
||||
- `diagnostic_sessions`
|
||||
- `image_assets`
|
||||
- `scan_frames`
|
||||
- `calibrations`
|
||||
- `analysis_jobs`
|
||||
- `pipeline_steps`
|
||||
- `model_registry`
|
||||
- `model_artifacts`
|
||||
- `preprocessed_images`
|
||||
- `angle_predictions`
|
||||
- `inflammation_predictions`
|
||||
- `segmentation_masks`
|
||||
- `measurements`
|
||||
- `synovitis_grades`
|
||||
- `review_decisions`
|
||||
- `audit_ledger_entries`
|
||||
|
||||
SQLite constraints:
|
||||
|
||||
- UUIDs stored as `TEXT`.
|
||||
- `CHECK` constraints for grade ranges, enum-like statuses, and non-empty hashes.
|
||||
- Foreign keys enabled.
|
||||
- `audit_ledger_entries` treated as append-only at application layer.
|
||||
|
||||
### Step 3 — PostgreSQL target migrations
|
||||
|
||||
Add PostgreSQL migrations with:
|
||||
|
||||
- `uuid` type,
|
||||
- `jsonb`,
|
||||
- `CHECK` constraints,
|
||||
- `NOT NULL` FKs,
|
||||
- indexes for FK and query paths,
|
||||
- append-only triggers on `audit_ledger_entries`,
|
||||
- RLS policies where required,
|
||||
- optional `postgis` schema readiness for future spatial markers.
|
||||
|
||||
### Step 4 — Object storage contract
|
||||
|
||||
Add object storage metadata contract:
|
||||
|
||||
```text
|
||||
image_assets.object_key
|
||||
image_assets.bucket_name
|
||||
image_assets.sha256_hash
|
||||
image_assets.content_type
|
||||
image_assets.size_bytes
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- UUID-only object keys.
|
||||
- No PHI in object keys.
|
||||
- Store binary payloads in object storage.
|
||||
- Store only metadata and safe references in DB.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Confirm whether Sprint 1_2 uses SQLite only or PostgreSQL-ready migrations from the start.
|
||||
2. Write `PILOT_PROJECT/workspace/sprint_1_2/Design_Material/DATA-INGESTION/RELATIONAL_DB_SCHEMA_SPEC.md`.
|
||||
3. Add ER/schema PlantUML.
|
||||
4. Add Chen notation PlantUML.
|
||||
5. Add table-by-table schema with types, constraints, indexes, and FK rules.
|
||||
6. Add NFR/UC traceability matrix.
|
||||
7. Add open decisions and migration plan.
|
||||
8. After design approval, implement migrations and repositories.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
1. `patients` table: Sprint 1_2 uses `patient_cases` with hashes only; FR-25 may need a fuller PHI-bearing `patients` table under strict compliance.
|
||||
2. `review_decisions`: single latest decision table vs append-only decision revisions.
|
||||
3. `artifact_references`: generated value only vs physical table for presigned URL cache.
|
||||
4. `preprocessed_images`: keep as metadata table or fold into `image_assets` with `asset_type = preprocessed`.
|
||||
5. `pipeline_steps`: include per-model VRAM/runtime metrics in Sprint 1_2 or add later.
|
||||
6. `review_decisions.digital_signature_ref`: store only a reference hash/credential ID, never private keys.
|
||||
7. Q4 anomaly telemetry: DB metadata only, with raw tensors/masks in isolated object storage.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] ER/schema PlantUML covers all OOP domain objects from Sprint 1_2.
|
||||
- [ ] Chen notation PlantUML covers core entities, relationships, and cardinalities.
|
||||
- [ ] Schema includes all required tables from `OOP_DATA_ENGINEERING_SPEC.md`.
|
||||
- [ ] FK map supports upload, analysis, review, audit, and finalization flows.
|
||||
- [ ] Constraints enforce grade ranges, status values, hash presence, and human sign-off.
|
||||
- [ ] NFR traceability covers privacy, immutability, air-gap, explainability, and latency requirements.
|
||||
- [ ] Object storage boundary is explicit: DB stores refs/checksums, not binary payloads.
|
||||
- [ ] Design supports SQLite PoC and PostgreSQL/PostGIS target migration path.
|
||||
@@ -0,0 +1,361 @@
|
||||
# Data Ingestion Pipeline ETL First Draft Plan — VKIST MSK Sprint 1_2
|
||||
|
||||
## Plan Status
|
||||
- Source plan file requested: `.kilo/plans/data-ingestion-pipeline-etl-first-draft-plan.md`
|
||||
- Requested source plan file was missing in workspace.
|
||||
- This file is now the implementation source of truth for the Data Ingestion ETL plan.
|
||||
- Primary ingestion design source: `PILOT_PROJECT/workspace/sprint_1_2/Design_Material/DATA-INGESTION/data-ingestion-pipeline-etl-first-draft-plan.md`
|
||||
- OOP/data-concept source of truth: `PILOT_PROJECT/workspace/sprint_1_2/Design_Material/data_engineering/OOP_DATA_ENGINEERING_SPEC.md`
|
||||
|
||||
---
|
||||
## Goal
|
||||
Design and implement a small-first, incrementally scalable Data Ingestion ETL pipeline for VKIST MSK Sprint 1_2.
|
||||
|
||||
The ingestion design must match the OOP data and concept model in `OOP_DATA_ENGINEERING_SPEC.md`, especially:
|
||||
- Core domain objects
|
||||
- Runtime agents/services
|
||||
- Workflow
|
||||
- SQLite metadata model
|
||||
- Cloud object storage layout
|
||||
- API contract objects
|
||||
- Validation rules
|
||||
- Sprint 1_2 acceptance criteria
|
||||
|
||||
Target path:
|
||||
```text
|
||||
DICOM/image upload → secure artifact storage → frame extraction → preprocessing → vision inference → structured result objects → SQLite metadata → S3-compatible artifact refs → browser mask preview
|
||||
```
|
||||
|
||||
---
|
||||
## Source Context
|
||||
### Ingestion Design Context
|
||||
Use `PILOT_PROJECT/workspace/sprint_1_2/Design_Material/DATA-INGESTION/data-ingestion-pipeline-etl-first-draft-plan.md` as the current ingestion design baseline.
|
||||
|
||||
Key ingestion design points:
|
||||
- ETL phases: `Extract`, `Transform`, `Load`
|
||||
- Sprint 1_2 PoC stack: FastAPI + SQLite + S3-compatible object store
|
||||
- Production upgrade path: FastAPI → Redis queue → worker pool → Triton → PostgreSQL/PostGIS + S3 + observability
|
||||
- No PHI in filenames, object keys, URLs, logs, telemetry, or response paths
|
||||
- UUID-based object keys and artifact references
|
||||
- Mock and real PyTorch/Triton-compatible inference adapters
|
||||
- Stable API responses with `session_id`, `job_id`, `trace_id`, and `audit_hash`
|
||||
|
||||
### OOP Data Engineering Context
|
||||
Use `PILOT_PROJECT/workspace/sprint_1_2/Design_Material/data_engineering/OOP_DATA_ENGINEERING_SPEC.md` as the data/concept source of truth.
|
||||
|
||||
Sprint 1_2 scope:
|
||||
```text
|
||||
DICOM/image upload → frame extraction → preprocessing → vision inference → structured result objects → SQLite metadata → cloud object storage artifact refs → browser mask preview
|
||||
```
|
||||
|
||||
Sprint 1_2 includes:
|
||||
- Single-frame DICOM upload
|
||||
- Standard image upload fallback
|
||||
- SQLite for structured metadata
|
||||
- Cloud object service for binary artifacts
|
||||
- FastAPI API contracts
|
||||
- PyTorch-compatible inference adapters
|
||||
- Structured OOP domain model for sessions, frames, jobs, predictions, masks, measurements, and audit records
|
||||
|
||||
Sprint 1_2 excludes:
|
||||
- Multi-frame DICOM series
|
||||
- Triton runtime
|
||||
- GraphRAG
|
||||
- EMR sync
|
||||
- Socratic safety agents
|
||||
- Full PWA workspace
|
||||
- Collaboration/annotations
|
||||
|
||||
---
|
||||
## OOP Boundary
|
||||
```text
|
||||
Domain objects = clinical and analysis facts.
|
||||
Agents/services = runtime workers that transform facts.
|
||||
Repositories = SQLite persistence adapters.
|
||||
Artifact stores = S3-compatible binary storage adapters.
|
||||
Orchestrators = coordinate use cases and workflow state.
|
||||
```
|
||||
|
||||
---
|
||||
## Required Domain Objects
|
||||
The Data Ingestion design must produce, persist, or reference these OOP objects from `OOP_DATA_ENGINEERING_SPEC.md`.
|
||||
|
||||
### Clinical/session objects
|
||||
- `ClinicianUser`
|
||||
- `PatientCase`
|
||||
- `DiagnosticSession`
|
||||
- `ReviewDecision`
|
||||
- `AuditLedgerEntry`
|
||||
|
||||
### Ingestion/artifact objects
|
||||
- `ImageAsset`
|
||||
- `ScanFrame`
|
||||
- `Calibration`
|
||||
- `ArtifactReference`
|
||||
|
||||
### Analysis/job objects
|
||||
- `AnalysisJob`
|
||||
- `PipelineStep`
|
||||
- `ModelRegistryEntry`
|
||||
- `ModelArtifact`
|
||||
- `PreprocessedImage`
|
||||
|
||||
### Prediction/result objects
|
||||
- `AnglePrediction`
|
||||
- `InflammationPrediction`
|
||||
- `SegmentationMask`
|
||||
- `Measurement`
|
||||
- `SynovitisGrade`
|
||||
|
||||
---
|
||||
## Required Runtime Agents / Services
|
||||
The ingestion implementation must align with these runtime roles from `OOP_DATA_ENGINEERING_SPEC.md`.
|
||||
|
||||
- `DICOMIngestAgent`
|
||||
- `accept_upload(file) -> ImageAsset`
|
||||
- `extract_frame(image_asset) -> ScanFrame`
|
||||
- `extract_calibration(image_asset) -> Calibration`
|
||||
|
||||
- `ImageUploadIngestAgent`
|
||||
- `accept_upload(file) -> ImageAsset`
|
||||
- `build_scan_frame(image_asset) -> ScanFrame`
|
||||
|
||||
- `FramePreprocessor`
|
||||
- CLAHE, resize, normalization, tensor preparation
|
||||
|
||||
- `AngleValidatorAgent`
|
||||
- Predicts `AnglePrediction`
|
||||
- Validates supported angle branch
|
||||
|
||||
- `ROICropperAgent`
|
||||
- Optional PoC/no-op first
|
||||
|
||||
- `VisionPipelineAgent`
|
||||
- Coordinates `run(session_id, frame_id) -> AnalysisJob`
|
||||
|
||||
- `InferenceRunner`
|
||||
- Loads and runs PyTorch-compatible adapters
|
||||
|
||||
- `MeasurementAgent`
|
||||
- Converts mask pixels to mm using `Calibration`
|
||||
|
||||
- `SeverityScorerAgent`
|
||||
- Maps measurements to `SynovitisGrade`
|
||||
|
||||
- `ModelRegistryAgent`
|
||||
- Selects approved model names and versions
|
||||
|
||||
- `ArtifactStoreAgent`
|
||||
- Stores raw and derived artifacts
|
||||
- Returns `ImageAsset` / `ArtifactReference`
|
||||
|
||||
- `LedgerWriterAgent`
|
||||
- Appends immutable `AuditLedgerEntry`
|
||||
|
||||
---
|
||||
## ETL Design Aligned to OOP Objects
|
||||
|
||||
### Phase 1 — Extract
|
||||
Purpose:
|
||||
Accept upload, quarantine, hash, store raw artifact, and create canonical ingestion metadata.
|
||||
|
||||
Inputs:
|
||||
- `POST /api/v1/sessions/{session_id}/frames`
|
||||
- Single-frame DICOM or standard image upload
|
||||
|
||||
Components:
|
||||
- `UploadController`
|
||||
- `QuarantineStorage`
|
||||
- `HashingService`
|
||||
- `ArtifactStoreAgent`
|
||||
- `DICOMIngestAgent`
|
||||
- `ImageUploadIngestAgent`
|
||||
- `LedgerWriterAgent`
|
||||
|
||||
OOP outputs:
|
||||
- `ImageAsset`
|
||||
- `ScanFrame`
|
||||
- `Calibration`
|
||||
- `AuditLedgerEntry`
|
||||
|
||||
Validation:
|
||||
- Accept single-frame DICOM
|
||||
- Extract pixel array
|
||||
- Extract pixel spacing if available
|
||||
- Accept common image formats
|
||||
- Generate synthetic/fallback calibration for standard images
|
||||
- Reject unreadable DICOM
|
||||
- No PHI in filenames, object keys, URLs, logs, telemetry, or response paths
|
||||
|
||||
### Phase 2 — Transform
|
||||
Purpose:
|
||||
Convert the extracted frame into clinical analysis facts.
|
||||
|
||||
Components:
|
||||
- `AnalysisJobOrchestrator`
|
||||
- `FramePreprocessor`
|
||||
- `AngleValidatorAgent`
|
||||
- `ROICropperAgent`
|
||||
- `InferenceRunner`
|
||||
- `MeasurementAgent`
|
||||
- `SeverityScorerAgent`
|
||||
- `SafetyRouter`
|
||||
|
||||
OOP outputs:
|
||||
- `AnalysisJob`
|
||||
- `PipelineStep`
|
||||
- `PreprocessedImage`
|
||||
- `AnglePrediction`
|
||||
- `InflammationPrediction`
|
||||
- `SegmentationMask`
|
||||
- `Measurement`
|
||||
- `SynovitisGrade`
|
||||
- `AuditLedgerEntry`
|
||||
|
||||
Validation:
|
||||
- Every result links to `analysis_job_id`
|
||||
- Mask artifact links to `artifact_id`
|
||||
- Measurements use calibration when available
|
||||
- Grade range is `0..3`
|
||||
- Unsupported angle, low confidence, missing calibration, or artifact correction flags are preserved
|
||||
|
||||
### Phase 3 — Load
|
||||
Purpose:
|
||||
Persist structured facts and return safe artifact references.
|
||||
|
||||
Components:
|
||||
- `SQLiteMetadataRepository`
|
||||
- `ArtifactReferenceBuilder`
|
||||
- `AnalysisResultAssembler`
|
||||
- `AuditHashBuilder`
|
||||
- `ObservabilityEmitter`
|
||||
|
||||
OOP outputs:
|
||||
- Stable API response
|
||||
- Browser mask preview refs
|
||||
- Immutable audit chain
|
||||
|
||||
Validation:
|
||||
- Every structured object has UUID primary key
|
||||
- Every analysis result links to `session_id` and `job_id`
|
||||
- Every audit entry has `audit_hash`
|
||||
- Audit hash inputs include:
|
||||
- `session_id`
|
||||
- `job_id`
|
||||
- model versions
|
||||
- prediction IDs
|
||||
- artifact hashes
|
||||
- review decision when present
|
||||
|
||||
---
|
||||
## SQLite Metadata Alignment
|
||||
Use the Sprint 1_2 SQLite model from `OOP_DATA_ENGINEERING_SPEC.md:1058`.
|
||||
|
||||
Required tables:
|
||||
- `diagnostic_sessions`
|
||||
- `image_assets`
|
||||
- `scan_frames`
|
||||
- `calibrations`
|
||||
- `analysis_jobs`
|
||||
- `pipeline_steps`
|
||||
- `model_registry`
|
||||
- `model_artifacts`
|
||||
- `angle_predictions`
|
||||
- `inflammation_predictions`
|
||||
- `segmentation_masks`
|
||||
- `measurements`
|
||||
- `synovitis_grades`
|
||||
- `review_decisions`
|
||||
- `audit_ledger_entries`
|
||||
|
||||
Additive PoC columns allowed by ingestion design:
|
||||
- `analysis_jobs.trace_id`
|
||||
- `analysis_jobs.idempotency_key`
|
||||
- `pipeline_steps.error_code`
|
||||
- `audit_ledger_entries.previous_hash`
|
||||
|
||||
Do not add PHI-bearing columns or original filenames as required fields.
|
||||
|
||||
---
|
||||
## Object Storage Alignment
|
||||
Use the UUID-only storage layout from `OOP_DATA_ENGINEERING_SPEC.md:1240`.
|
||||
|
||||
```text
|
||||
s3://vkist-poc/
|
||||
sessions/
|
||||
{session_id}/
|
||||
source/
|
||||
{asset_id}.dcm
|
||||
{asset_id}.png
|
||||
frames/
|
||||
{frame_id}.png
|
||||
masks/
|
||||
{mask_id}.png
|
||||
preprocessed/
|
||||
{preprocessed_id}.npy
|
||||
audit/
|
||||
{audit_id}.json
|
||||
```
|
||||
|
||||
Ingestion design may include these additional UUID-keyed artifact prefixes:
|
||||
```text
|
||||
preprocessed/
|
||||
overlays/
|
||||
outputs/
|
||||
```
|
||||
|
||||
Rules:
|
||||
- No patient name in object key
|
||||
- No raw patient ID in object key
|
||||
- No timestamp-only object key
|
||||
- Use UUID for all keys
|
||||
- Store SHA-256 hash for every artifact
|
||||
- Store metadata in SQLite
|
||||
- SQLite/Postgres stores refs only, not binary image payloads
|
||||
|
||||
---
|
||||
## API Contract Alignment
|
||||
Use the OOP API contract objects from `OOP_DATA_ENGINEERING_SPEC.md:1274`.
|
||||
|
||||
Required Sprint 1_2 endpoints:
|
||||
```text
|
||||
POST /api/v1/sessions
|
||||
GET /api/v1/sessions/{session_id}
|
||||
POST /api/v1/sessions/{session_id}/frames
|
||||
POST /api/v1/analysis-jobs
|
||||
GET /api/v1/analysis-jobs/{job_id}
|
||||
GET /api/v1/analysis-jobs/{job_id}/steps
|
||||
PATCH /api/v1/sessions/{session_id}/review
|
||||
```
|
||||
|
||||
Create Session Request must include:
|
||||
```json
|
||||
{
|
||||
"patient_hash": "sha256_patient_hash",
|
||||
"case_hash": "sha256_case_hash",
|
||||
"clinician_user_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Upload Frame Request:
|
||||
```text
|
||||
POST /api/v1/sessions/{session_id}/frames
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: .dcm or image
|
||||
```
|
||||
|
||||
Upload Frame Response must expose:
|
||||
- `frame_id`
|
||||
- `source_asset`
|
||||
- `calibration`
|
||||
|
||||
Analysis Job Response must expose stable objects:
|
||||
- `job_id`
|
||||
- `status`
|
||||
- `angle`
|
||||
- `inflammation`
|
||||
- `segmentation_mask`
|
||||
- `measurements`
|
||||
- `synovitis_grade`
|
||||
- safe artifact references
|
||||
@@ -0,0 +1,186 @@
|
||||
BELOW is the consolidated structural baseline engineering reference document tracking all architectural constraints, system interactions, and discovered sub-components for the **FR-25 Synovitis Grading Engine**.
|
||||
|
||||
---
|
||||
|
||||
# Context_FR_25_UC.md
|
||||
|
||||
## 1. Context, Mission Boundary & Core Rationale
|
||||
|
||||
### 1.1 Scope Identification
|
||||
|
||||
* **Target Core Functional Requirement:** `FR-25` (ULTS-Đánh giá và phân cấp mức độ viêm màng hoạt dịch / Synovitis Grading Engine).
|
||||
* **Primary System Boundary:** The workspace acts as a strict secure diagnostic execution wrapper. In order to manage systemic liability and avoid black-box compliance failures, the advanced multi-agent checking modules (**LLM Explainer**, **BERT Hallucination Detector**, and **RAG-Referee**) are restricted from handling generic application operations. They are encapsulated entirely as **Internal Layered Workspace Subsystems** dedicated exclusively to validating human-AI coordination logs for `FR-25`.
|
||||
|
||||
### 1.2 Engineering Value Optimization
|
||||
|
||||
* **The Clinical Chasm:** In high-volume Vietnamese public clinics, specialists face intensive shift demands often exceeding 100 scans daily. Basic AI setups risk inducing automated checklist fatigue or introducing catastrophic blindness loops if human clinicians simply blind-concur with machine estimations.
|
||||
* **The System Solution:** By engineering clear internal state boundaries separating standard CRUD processing from multi-tier validation agents, the system systematically handles four concurrent behavioral quadrants. It forces explicit human verification loops during disagreements, cross-references findings against un-biased clinical knowledge nodes, and maintains diagnostic speed metrics without degrading data correctness limits.
|
||||
|
||||
---
|
||||
|
||||
## 2. Structural Actor Profile Mapping
|
||||
|
||||
| Actor Name | Canonical Identifier | Role Scope & Operational Profile within FR-25 Boundary |
|
||||
| --- | --- | --- |
|
||||
| **Diagnostic Radiologist** | `Rad (UP5)` | **Primary Human Actor:** National-level clinical expert (Specialist II / Professor, age 40–60+). Holds ultimate medical accountability; validates and signs off pixel segmentations, metrics, and grading summaries. |
|
||||
| **Hospital EMR System** | `EMR` | **External System Actor:** Recipient database server. Receives finalized JSON structures and signed clinical data logs over localized network pipes post human validation. |
|
||||
| **VKIST Vision Grader Engine** | `Grader` | **External System Actor:** Foundation deep-learning array (ConvNeXt/MedSAM). Ingests raw frame parameters and produces pixel segmentations, thickness markers (mm), and classification tensors. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Discovered Use Cases (4-Quadrant Framework)
|
||||
|
||||
### 3.1 Core Image Data Intake & Workflow Baselines
|
||||
|
||||
* **`UC-48376` (Load Patient Scan Session):** Ingests incoming image frames, maps spatial structures, and activates local session states.
|
||||
* **`UC-47988` (Review Suggested Synovitis Grade):** Renders the initial classification summary panel (Grades 0-3) alongside color-coded segmentation overlaps.
|
||||
* **`UC-92006` (Finalize & Sign Electronic Record):** Seals the session data via localized human cryptographic signing steps before dispatching payload JSON logs to the hospital storage sinks.
|
||||
|
||||
### 3.2 Quadrant 1: True Agreement Flow (AI Correct / Doctor Correct)
|
||||
|
||||
* **`UC-25776` (Generate GradCAM & CoT Explanation Panel):** The internal LLM Explainer checks raw pixel masks and maps multi-modal prompt matrices to output clear, human-scannable rationales.
|
||||
* **`UC-02423` (Log High-Trust Concur Block):** Encapsulates the corresponding Chain-of-Thought logs confirming human-machine consensus into a structured block to verify system state traceability.
|
||||
|
||||
### 3.3 Quadrant 2: Automation Override Risk Loop (AI Correct / Doctor Oversight)
|
||||
|
||||
* **`UC-22159` (Trigger Conversational Circuit Breaker):** Intercepts standard workspace finalization pathways if high mouse click adjustments or text conflict markers indicate user friction or ambiguity.
|
||||
* **`UC-55146` (Facilitate Socratic Reasoning Dialogue):** Initiates an inline workspace chat panel, prompting the specialist to evaluate spatial discrepancies or vascular metrics versus the machine's model inputs.
|
||||
* **`UC-74821` (Monitor Drift via BERT Sub-Layer):** Scans active conversation tokens continuously to detect illogical claims or contextual drift during active discussion.
|
||||
* **`UC-65473` (Arbitrate Evidence via RAG-Referee):** Intervenes if human-machine disputes reach an impasse, bypassing active chat logs to pull verified medical guidelines directly from fixed reference sources.
|
||||
|
||||
### 3.4 Quadrant 3: Clinician Subservience Risk Loop (AI Hallucinates / Doctor Correct)
|
||||
|
||||
* **`UC-25637` (Expose Pixel-Level Activation Logic):** Displays granular layer activations and weight scores when a clinician actively contests a machine grade suggestion.
|
||||
* **`UC-60739` (Isolate Visual Noise/Artifacts):** Provides on-screen cursor brushes for the specialist to isolate and mask out clutter variables like acoustic shadowing or bone scattering.
|
||||
* **`UC-62864` (Commit Validated Ground-Truth Record):** Re-runs data logs through the verification referee, updating final reports to show human superiority while saving the masked framework for subsequent model training runs.
|
||||
|
||||
### 3.5 Quadrant 4: Double Blind Failure Loop (AI Faulty / Doctor Biased)
|
||||
|
||||
* **`UC-35956` (Activate Clinical Investigation Mode):** Transitions the user interface environment instantly to a strict manual tracking orientation when low vision confidence values align with zero-match RAG search responses.
|
||||
* **`UC-47796` (Execute Structured Morphology Annotation):** Displays a standardized template forcing manual plotting of novel structural modifications or unrecognized lesion variations.
|
||||
* **`UC-01580` (Serialize Session to Telemetry Queue):** Packages unencrypted image tensors, coordinate indices, and clinical commentary blocks into localized storage pipelines, bypassing standard EMR charts to flag data directly for software engineering team review.
|
||||
|
||||
---
|
||||
|
||||
## 4. Master PlantUML System Compilation
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
' Settings & Aesthetic Optimization
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
' External Actors
|
||||
actor "Diagnostic Radiologist (UP5)" as Rad
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
actor "VKIST Vision Grader Engine" as Grader << System >>
|
||||
|
||||
' System Boundary
|
||||
rectangle "VKIST MSK Workspace (FR-25: Synovitis Grading Scope)" {
|
||||
|
||||
' Core Viewing & Data Intake Pipelines
|
||||
usecase "UC-48376\nLoad Patient Scan Session" as UC_Load
|
||||
usecase "UC-47988\nReview Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "UC-92006\nFinalize & Sign Electronic Record" as UC_Finalize
|
||||
|
||||
' Sub-Boundary for the Internal Cognitive/Multi-Agent Subsystems
|
||||
rectangle "Internal Cognitive & Validation Stack" {
|
||||
|
||||
' Q1: True Agreement Use Cases
|
||||
usecase "UC-25776\nGenerate GradCAM & CoT Explanation Panel" as UC_Q1_Explain
|
||||
usecase "UC-02423\nLog High-Trust Concur Block" as UC_Q1_Log
|
||||
|
||||
' Q2: Automation Override Risk Use Cases
|
||||
usecase "UC-22159\nTrigger Conversational Circuit Breaker" as UC_Q2_Intercept
|
||||
usecase "UC-55146\nFacilitate Socratic Reasoning Dialogue" as UC_Q2_Socratic
|
||||
usecase "UC-74821\nMonitor Drift via BERT Sub-Layer" as UC_Q2_BERT
|
||||
usecase "UC-65473\nArbitrate Evidence via RAG-Referee" as UC_Q2_Arbiter
|
||||
|
||||
' Q3: Clinician Subservience Risk Use Cases
|
||||
usecase "UC-25637\nExpose Pixel-Level Activation Logic" as UC_Q3_Expose
|
||||
usecase "UC-60739\nIsolate Visual Noise/Artifacts" as UC_Q3_Isolate
|
||||
usecase "UC-62864\nCommit Validated Ground-Truth Record" as UC_Q3_Commit
|
||||
|
||||
' Q4: Double Blind Failure Use Cases
|
||||
usecase "UC-35956\nActivate Clinical Investigation Mode" as UC_Q4_Escalate
|
||||
usecase "UC-47796\nExecute Structured Morphology Annotation" as UC_Q4_Annotate
|
||||
usecase "UC-01580\nSerialize Session to Telemetry Queue" as UC_Q4_Queue
|
||||
}
|
||||
}
|
||||
|
||||
' Human-System Interactions
|
||||
Rad --> UC_Load
|
||||
Rad --> UC_Review
|
||||
Rad --> UC_Finalize
|
||||
|
||||
' Q2, Q3 & Q4 Interaction Entrypoints
|
||||
Rad --> UC_Q2_Socratic : Argue observations
|
||||
Rad --> UC_Q3_Isolate : Tag artifacts
|
||||
Rad --> UC_Q4_Annotate : Document manual findings
|
||||
|
||||
' Machine-to-Machine Pipelines
|
||||
Grader --> UC_Load : Feeds vision tensors & initial scores
|
||||
|
||||
' Internal Use Case Associations & Extends
|
||||
UC_Load ..> UC_Q1_Explain : <<include>>
|
||||
UC_Q1_Explain ..> UC_Q1_Log : <<include>>
|
||||
|
||||
' Q2 Loop Connections
|
||||
UC_Review <.. UC_Q2_Intercept : <<extend>> (If clinician friction detected)
|
||||
UC_Q2_Intercept ..> UC_Q2_Socratic : <<include>>
|
||||
UC_Q2_Socratic ..> UC_Q2_BERT : <<include>>
|
||||
UC_Q2_BERT ..> UC_Q2_Arbiter : <<extend>> (If impasse or semantic drift caught)
|
||||
|
||||
' Q3 Loop Connections
|
||||
UC_Review <.. UC_Q3_Expose : <<extend>> (If clinician contests AI score)
|
||||
UC_Q3_Expose ..> UC_Q3_Isolate : <<include>>
|
||||
UC_Q3_Isolate ..> UC_Q3_Commit : <<include>>
|
||||
|
||||
' Q4 Loop Connections
|
||||
UC_Review <.. UC_Q4_Escalate : <<extend>> (If low confidence & empty RAG)
|
||||
UC_Q4_Escalate ..> UC_Q4_Annotate : <<include>>
|
||||
UC_Q4_Annotate ..> UC_Q4_Queue : <<include>>
|
||||
|
||||
' Final Hand-off Synchronization
|
||||
UC_Finalize ..> EMR : Sync standardized structural JSON data
|
||||
@enduml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Blueprint Cross-Traceability Matrices
|
||||
|
||||
### 5.1 Scenario-to-Agent Mapping Tracking Matrix
|
||||
|
||||
This structural trace links the user behavior scenarios directly back to the active internal validation elements processing the loop.
|
||||
|
||||
```
|
||||
+---------------------------+-----------------------+-------------------------+-------------------------+
|
||||
| Interaction Scenario | Core Vision Component | Dialogue Safety Layer | Arbitration Safety Node |
|
||||
+---------------------------+-----------------------+-------------------------+-------------------------+
|
||||
| Q1: True Agreement | VKIST Vision Grader | LLM Explainer (CoT Log) | RAG-Referee (Clear) |
|
||||
| Q2: Automation Override | VKIST Vision Grader | Socratic Circuit Breaker| RAG-Referee (Active) |
|
||||
| Q3: Clinician Subservience| Feature Map Vis | Objective Critic Dialog | RAG-Referee (Active) |
|
||||
| Q4: Double Blind Edge Case| Anomaly State Ingest | Exploratory Morphology | Telemetry Retrain Queue |
|
||||
+---------------------------+-----------------------+-------------------------+-------------------------+
|
||||
|
||||
```
|
||||
|
||||
### 5.2 Functional Requirements Validation Trace
|
||||
|
||||
* **ULTS-FR-25 Criteria Trace 01:** The system must process initial classifications using pixel-percentage markers. Checked by: `UC_Load` $\rightarrow$ `UC_Review`.
|
||||
* **ULTS-FR-25 Criteria Trace 02:** System designs must enforce a circuit breaker step if user actions indicate diagnostic mismatch or context drift. Checked by: `UC_Q2_Intercept` $\rightarrow$ `UC_Q2_BERT`.
|
||||
* **ULTS-FR-25 Criteria Trace 03:** Sessions documenting unmapped anatomical variants must bypass standard hospital charts and stream records directly to optimization sinks. Checked by: `UC_Q4_Escalate` $\rightarrow$ `UC_Q4_Queue`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Downstream UIX & Specification Target Anchors
|
||||
|
||||
This baseline file establishes structural anchor configurations for subsequent product development phases:
|
||||
|
||||
1. **Phase 4 (Workspace Dashboard Wireframing):** Wireframe templates must reserve split-screen display blocks: one side hosting the medical canvas with artifact isolation tools (`UC_Q3_Isolate`), and an inline section mapping socratic messaging interactions (`UC_Q2_Socratic`).
|
||||
2. **Use Case Specification Drafting:** Individual use case descriptions can reference this anchor block to maintain absolute consistency regarding precondition bounds, primary exception vectors, and hand-off synchronization parameters (`EMR`).
|
||||
|
||||
---
|
||||
|
||||
*This engineering reference document accurately captures the system boundaries finalized during the requirement discovery sprint.*
|
||||
@@ -0,0 +1,180 @@
|
||||
|
||||
### Insight-Note - @Đạt Trần Tiến (Daves Tran)
|
||||
|
||||
→ Additionally we shall develop the app follow this manner: PWA - progressive web app
|
||||
|
||||
- UP-6 insight (suggest the insight for converting toward FRs)
|
||||
- Insight - to FR-draft 1
|
||||
- Core Insight 1: The Information Bottleneck
|
||||
- Formulated Pain Point
|
||||
- **User Vulnerability:** Vietnamese Physiotherapists struggle to accurately target internal tissue pathologies during physical therapy sessions.
|
||||
- **Root Cause:** Diagnosing medical doctors rarely or poorly pass digital DICOM imaging streams down the operational chain, providing only brief, low-context paper or text-based prescription sheets.
|
||||
- **Negative Impact:** Clinicians are forced to execute high-intensity therapeutic modalities semi-blindly over estimated surface anatomy, drastically reducing treatment precision and patient efficacy.
|
||||
- Target Structural Keywords
|
||||
- `{information-clarification & context clarification}`
|
||||
- `{from the prescribe → detect where to treat}`
|
||||
- Functional Requirements Blueprint
|
||||
- **The Ingestion & Context Clarification Pipeline:** The platform must feature an integrated Optical Character Recognition (OCR) scanner tool optimized for mobile tablet arrays. When a PT receives a text-only sheet, the system scans the raw text, extracts localized shorthand clinical terms, and cross-references them with centralized clinical guidelines to generate immediate context clarification regarding structural contraindications.
|
||||
- **Automated Spatial Target Generation ("Prescribe → Guide on Detect the Region of Treatment"):**
|
||||
- Your database architecture must map abstract text instructions to specific coordinate layers. When a text prescription is parsed (e.g., *"Ultrasound therapy, left shoulder"*), the system can:
|
||||
- automatically triggers a default, rotatable 3D musculoskeletal target field, highlighting the structural depth and biological tissue boundaries where the treatment must physically occur.
|
||||
- Generating suggestion guidance on how to interpret the prescrbe under PT knowledge-base + suggest the certain treatment methodology for referential value
|
||||
- Core Insight 2: The Cross-Domain Literacy Gap
|
||||
- Formulated Pain Point
|
||||
- **User Vulnerability:** Vietnamese Physiotherapists struggle to accurately interpret raw diagnostic findings and seamlessly coordinate with prescribing medical specialists.
|
||||
- **Root Cause:** Their foundational training is rooted entirely in kinetic biomechanics, which completely mismatches the physician’s world of complex radiological pixel data. This structural literacy gap is severely compounded by a massive 84% foreign language deficit and zero academic background in evaluating clinical statistics or research methodologies.
|
||||
- **Negative Impact:** The communication mismatch creates high inter-departmental silos, forces a reliance on subjective clinical guesswork, and increases the risk of deploying highly ineffective or contraindicated treatment tracks.
|
||||
- Target Structural Keywords
|
||||
- `{Cross-Domain Knowledge Translator Between Clinicians & Specialist}`
|
||||
- `{Educating Channel / Communication medium}`
|
||||
- **Functional Requirements Blueprint**
|
||||
- **Cross-Domain Knowledge Translation Canvas:** Your frontend rendering engine must not display a raw, uninterpreted grayscale DICOM viewport. The system must feature a visual abstraction layer that automatically translates multi-slice pixel matrices and annotations into an interactive 3D musculoskeletal medium. Complex radiological data points are remapped into intuitive anatomical indicators (e.g., muscle layer depth maps, color-coded inflammation zones) that line up natively with the PT's biomechanical domain knowledge.
|
||||
- **Asynchronous Bidirectional Communication Medium:** Because local medical regulations strictly bar technicians from editing primary physician diagnosis records, the collaborative workspace must provide an isolated "Clinical Observation & Progress Flagging" backchannel. The PT interacts with the workspace by pinning tactile assessment data directly onto the shared 3D scan canvas, which compiles an objective, standardized data summary that can be pushed seamlessly to the physician's review dashboard.
|
||||
- **The Visual Patient-Educating Channel:** To address the locomotive health education requirement, the workspace must feature a decoupled "Patient View" mode switch. This toggle filters out dense clinical indicators and transforms the active 3D biomechanical model into a simplified, clear educational medium. The therapist utilizes this channel to visually demonstrate joint mechanics and injury zones directly to the patient, replacing blind compliance with objective spatial understanding.
|
||||
|
||||
###
|
||||
|
||||
- **Insight to FR draft 2**
|
||||
|
||||
To ensure our engineering team does not waste resources rebuilding duplicate features, we need to carefully cross-reference this **Physiotherapist (PT) Blueprint** with our previous **Rheumatologist & Orthopedic Surgeon Workspace (UP-7)**.
|
||||
|
||||
Below is a structural overlap analysis, followed by the refined, non-overlapping Functional Requirements written strictly from the PT’s unique clinical point of view.
|
||||
|
||||
#### Part 1: Overlap & Delta Analysis (UP-7 vs. PT Workspace)
|
||||
|
||||
Our primary goal is to ensure the PT requirements capture **functional differences**, not just terminology differences.
|
||||
|
||||
### 🏢 What is Overlapped (Shared Infrastructure)
|
||||
|
||||
- **The Asynchronous Communication Medium vs. UP-7 FR-02 (Voice Memo/JSON Timeline):** Both requirements solve the same technical problem: asynchronous communication via metadata layers over a shared canvas. They should be unified into a single backend thread module.
|
||||
- **The Visual Patient-Educating Channel vs. UP-7 FR-04 & FR-05 (Adaptive 3D & Sandbox Link):** The code needed to switch to a simplified 3D viewport or hand over a view to a patient is identical to the underlying architecture we designed for the doctor's profile.
|
||||
|
||||
### ⚡ What is Unique to the PT Context (The Real Deltas)
|
||||
|
||||
- **Upstream Data Ingestion:** Doctors *create* the DICOM and write text prescriptions. PTs *receive* the raw text and have to parse it. The OCR ingestion pipeline is $100\%$ unique to the PT workflow.
|
||||
- **Text-to-3D Coordinate Projection:** Doctors map 2D DICOM coordinates to 3D. PTs need to map **abstract text descriptions** (e.g., *"Supraspinatus tendinitis"*) to 3D coordinates on a model when the raw DICOM is missing.
|
||||
- **Biomechanics vs. Radiology:** Doctors look for raw structural or inflammatory data. PTs look for kinematic impact (e.g., muscle depth layers, range-of-motion constraints, therapy execution depths).
|
||||
|
||||
### Part 2: Refined, Non-Overlapping PT Functional Requirements
|
||||
|
||||
#### FR-PT-01: Client-Side WebML Prescription Parser (TensorFlow.js)
|
||||
|
||||
- **PT Point of View:** "Patients walk in with a crumpled piece of text-based paper from the doctor. I need to instantly parse what treatment to give them without sending their medical notes to an insecure external cloud server."
|
||||
- **Technical & Demography Pivot:** Instead of heavy, costly cloud-based OCR APIs, this runs entirely in the mobile web browser. To keep sensitive health data strictly private under **Decree 13/2023/ND-CP**, no images leave the phone.
|
||||
- **Functional Scope:**
|
||||
- The PWA (progressive web-app) must use a lightweight, browser-optimized `MobileNetV2` text-segmentation model via TensorFlow.js to scan text via the phone's native camera stream (`getUserMedia`).
|
||||
- The script must parse Vietnamese localized shorthand clinical terms directly on-device to extract the targeted joint zone, prescribed therapy modality (e.g., Ultrasound, Shockwave), and frequency.
|
||||
- **Instant Safety Warning:** The client-side logic immediately processes the extracted words against a lightweight dictionary to trigger a prominent warning panel if any structural contraindications exist (e.g., severe osteoporosis warnings for manual manipulation).
|
||||
|
||||
#### FR-PT-02: Zero-GPU Text-to-3D Target Mapping Framework
|
||||
|
||||
- **PT Point of View:** "When the doctor provides zero digital X-ray files, I have to guess how deep the tissue pathology is based on their text notes. I need a clear visual guide on my phone, even if it's an old device."
|
||||
- **Technical & Demography Pivot:** Running a live, mathematically intense 3D translation matrix inside a mobile browser on a legacy, weak-GPU phone will drop frame rates and crash. We implement a **Hybrid Dual-Engine** directly in the PWA.
|
||||
- **Functional Scope:**
|
||||
- **High-Spec Profile (Three.js):** If the browser passes a WebGL capabilities test, the abstract text tokens parsed in FR-PT-01 are mapped to spatial coordinates on an interactive 3D bone/muscle model.
|
||||
- **Low-Spec Profile (CPU-Bound Sprite Animator):** If the device's GPU is flagged as outdated, the system halts Three.js execution. It fetches a pre-rendered 36-frame flat image sequence (a 360-degree turntable shot of the musculoskeletal target). The phone's CPU simply switches the index of the visible 2D image based on the PT's swipe gestures, creating a zero-GPU "3D rotation" effect.
|
||||
- The system dynamically draws an SVG vector shape highlight over the target area to indicate precisely which tissue layer depth (superficial skin vs. deep muscle belly) the therapy must focus on.
|
||||
|
||||
#### FR-PT-03: Biomechanical Kinetic Overlay & Muscle Depth Mapping
|
||||
|
||||
- **PT Point of View:** "Doctors care about bone structural alignment. I care about the kinetic soft-tissue layers, muscle fibers, and where exactly to place my physical therapy machine probes."
|
||||
- **Technical & Demography Pivot:** Since we cannot use native OS GPU layers or native haptic engines in a standard PWA context, we rely on pure CSS layout composition to avoid interface lag.
|
||||
- **Functional Scope:**
|
||||
- The canvas view must provide a dedicated **Kinetic Overlay Toggle**. This maps a colorful, transparent cross-section depth map ($1\text{cm}$ to $5\text{cm}$ color coding) directly over the target treatment region.
|
||||
- Rather than relying on heavy WebGL shader calculations, the app dynamically appends lightweight, text-based HTML `<svg>` paths detailing muscle cross-sections on top of the base image viewport.
|
||||
- This allows the PT to visualize the estimated soft-tissue depth to calculate the correct angle of approach when placing ultrasound transducers or laser therapy devices on the patient's body.
|
||||
|
||||
#### FR-PT-04: Isolated Data-Segregated Observation & Progress Tracking Track
|
||||
|
||||
- **PT Point of View:** "Legally, I cannot change the primary doctor’s medical diagnosis. But as I work with the patient daily, I need to track their range-of-motion improvements and flag pain triggers that the doctor should see before their next follow-up."
|
||||
- **Technical & Demography Pivot:** To satisfy both local medical regulations and the strict access control requirements of **Decree 13/2023/ND-CP**, data tracks must be isolated.
|
||||
- **Functional Scope:**
|
||||
- The system implements strict role-based data encapsulation. The PT frontend interface operates on a **Read-Only** schema regarding the doctor's primary diagnostic charts or annotations.
|
||||
- The PWA provides a distinct **Kinetic Tracking Channel**. The PT can tap the screen to place "Kinetic Progress Pins" onto a separate workspace layer.
|
||||
- Each pin logs localized, chronological metrics: Range of Motion (ROM) angles, subjective pain indices ($1\text{--}10$), and local tissue behavior notes. This timeline sub-packet is serialized as text data and pushed directly to the physician's tracking panel without mutating the primary medical file.
|
||||
|
||||
#### FR-PT-05: DOM-Mirrored Kinetic Demonstration (Patient Education Mode)
|
||||
|
||||
- **PT Point of View:** "When doing manual therapy, poor patients often resist or tense up because they don't understand their joint mechanics. I need to show them exactly why they are hurting using their own legacy phones, without the app stuttering."
|
||||
- **Technical & Demography Pivot:** Emulating dynamic bone impingements in real-time WebGL on a patient's low-end phone causes extreme overheating. We move the animation burden from the GPU to simple DOM manipulation.
|
||||
- **Functional Scope:**
|
||||
- The PWA features a dedicated "Patient Demonstration Mode" with a clean, low-density layout interface.
|
||||
- The view presents a split layout: a static anatomical image on one side, and a simple html Range-of-Motion slider on the other.
|
||||
- As the PT moves the slider to match the patient’s physical arm or leg position, the app cycles through a lightweight array of cached 2D illustrations. This visual progression dynamically demonstrates joint flexion and extension, color-coding the exact soft tissues that are tightening or impinging. This gives the patient an immediate visual understanding of their pain using $0\%$ GPU processing power.
|
||||
|
||||
#### Low-Cost PWA Technical Architecture Overview
|
||||
|
||||

|
||||
|
||||
#### Engineering Architecture Stack for MVP:
|
||||
|
||||
- **Frontend Core:** React.js configured as an installable PWA with a robust Service Worker for aggressive caching, optimizing performance for patchy hospital Wi-Fi networks.
|
||||
- **Data Encryption:** WebCrypto API on the client side to securely wrap sensitive patient details before syncing with a local Vietnamese host (e.g., Viettel IDC) to ensure compliance with Decree 13.
|
||||
- **Graphics Delivery:** Hybrid WebGL-to-DOM rendering logic, ensuring that poor patients with legacy CPU-strong/GPU-weak phones receive the exact same educational data via lightweight 2D frame-switching.
|
||||
|
||||
- UP-7 insight (suggest the insight for converting toward FRs)
|
||||
- Some keywords & insight on **Rheumatologist & Orthopedic Surgeon** from the user-profile
|
||||
- Who they are: senior specialist that `ultimate consumers of the diagnostic imaging data and the primary architects of the patient's comprehensive treatment plan.` → they are the must character in the patient’s journey & patient shall have to interact with them & they shall interact with multiple patients
|
||||
- They have to work with multiple information on user’s profile & pathologies → synthesize to have a picture of patients & yeild the treatment journey & treatment strategy & handle the legal & ethical & patient’s safetiness & outcome
|
||||
- Understand the clinical implication + patient’s psychology + ready to explain & spend-time to explain with patient & debunk misinformation - but while they still have tight-time constrain
|
||||
- They may not known surely what patient’s are doing before reach toward them → harder to form the patient’s pathologies and harder to form diagnosing-picture & put them under a broken interaction & challenging case where patient may come with compilcations that exacerbating the conditions
|
||||
- They less-interest in shallow ML algorithm for identify the fracture where they can do , but interest more on consultative & deep while fast analysis & predictive (like explain the root-cause & the pathologies pattern & the potential adjustment - covering edge_case of miss-diagnosing)
|
||||
- They also interest with `a massive potential force multiplier for patient education, provided it **visually translates complex DICOM data into beautifully simple formats the patient can instantly understand**, thereby saving precious consultation minutes for more personalized discussion on treatment plan. (while doctors are assure the model are faithful & accurate)`
|
||||
- These clincians `need the platform to automatically aggregate highly fragmented patient data (historical X-rays, recent MRIs, scattered lab results) into a single, unified, rapid-consumption dashboard to exponentially expedite their clinical decision-making process.`
|
||||
- From Insight toward FR
|
||||
|
||||
#### FR-01: Haptic-Assisted Touch Viewport & Edge-Snapping Magnifier
|
||||
|
||||
- **User Insight:** Rheumatologists and Orthopedic Surgeons require pixel-level accuracy to calculate structural joint metrics (e.g., Cobb angles, joint space narrowing). On a compact 6-inch mobile screen, human fingers naturally obscure these fine anatomical landmarks.
|
||||
- **Engineering Feasibility:** High (9/10). Utilizes native iOS/Android low-latency graphics canvas and built-in vibration APIs.
|
||||
- **Functional Scope:**
|
||||
- **Touch Viewport:** Renders a single DICOM viewport optimized for native multi-touch gestures (pinch-to-zoom, two-finger pan, single-finger window/level adjustments).
|
||||
- **Floating Lens:** Activating an annotation tool and pressing down must trigger a floating, high-magnification "lens" widget positioned $150\text{px}$ vertically above the touch point to bypass finger obstruction.
|
||||
- **Edge-Snapping:** Incorporates a lightweight, client-side edge-detection algorithm (Canny/Hough transform). When drawing, the crosshairs automatically snap to the nearest high-contrast bone boundary, confirmed by a localized native haptic pulse.
|
||||
|
||||
#### FR-02: Asynchronous Voice-Over Annotation & Timeline Canvas
|
||||
|
||||
- **User Insight:** Clinicians navigate highly fragmented, fast-paced hospital rounds and surgical schedules in Vietnamese facilities. Coordinating live, simultaneous multi-user phone calls is functionally unfeasible, yet basic text messaging lacks spatial anatomical context.
|
||||
- **Engineering Feasibility:** High (8/10). Replaces complex real-time WebSockets with an offline-first, asynchronous data synchronization model suited for patchy hospital Wi-Fi networks.
|
||||
- **Functional Scope:**
|
||||
- **Metadata Recorder:** Captures microphone input while recording all viewport coordinate state changes ($X, Y$ positions, zoom vectors, panning arrays, and hand-drawn vectors) instead of recording a heavy video file.
|
||||
- **Interactive Playback:** Compiles these telemetry inputs into a serialized JSON timeline file. When the receiving doctor opens the case memo, the app replays the sender’s exact viewport transformations and vector drawing steps synchronized with the audio track.
|
||||
- **Threaded Replays:** Supports nested, asynchronous audio/canvas replies directly inside the specific case file workspace.
|
||||
|
||||
#### FR-03: Progressive Disclosure Sheets & Native Workflow Alerts
|
||||
|
||||
- **User Insight:** Displaying automated medical guidelines (e.g., Kellgren-Lawrence grading profiles) alongside a high-fidelity image on a mobile screen causes severe cognitive overload.
|
||||
- **Engineering Feasibility:** High (9/10). Uses highly optimized, native OS interface layouts and lightweight push services (FCM/APNS) that run efficiently on both flagship and mid-tier Android devices.
|
||||
- **Functional Scope:**
|
||||
- **Single-Focus Layout:** Dedicates $100\%$ of the background viewport strictly to the active DICOM canvas.
|
||||
- **Progressive Bottom-Sheet:** Confines automated clinical telemetry, AI-driven objective calculations, and reference guidelines within an expandable native Bottom-Sheet component. Supports three swipe states: $25\%$ peek view, $60\%$ detailed data view, and $100\%$ full-screen deep dive.
|
||||
- **Deep-Linked Push Notifications:** Triggers a native OS push alert when a case update occurs, deep-linking the receiving doctor directly into the specific case viewport configuration state.
|
||||
|
||||
#### FR-04: Hardware-Adaptive 3D Musculoskeletal Synchronization Engine
|
||||
|
||||
- **User Insight:** Patients struggle to interpret abstract 2D X-ray slices, which directly hinders their locomotive health literacy. The platform must connect 2D pathologies to an intuitive 3D visualization without crashing the devices of low-income or rural patients utilizing legacy phone chipsets.
|
||||
- **Engineering Feasibility:** High (8/10). Dual-Engine Framework. The system executes a client-side feature detection script probing WebGL and GPU vendor extensions (filtering out low-end units like Mali-T, Adreno 3xx, or PowerVR Rogue architectures).
|
||||
- **Functional Scope:**
|
||||
- **Profile A (High-Performance WebGL Engine):** Maps 2D DICOM coordinates onto a standardized, lightweight 3D skeletal mesh running on an embedded cross-platform WebGL/Three.js engine. Tapping a pathology highlights the isolated 3D node in real time.
|
||||
- **Profile B (CPU-Bound Sprite-Sheet Fallback):** Bypasses WebGL entirely if weak GPU flags are triggered. The system downloads a pre-compiled, 36-frame turntable "sprite sheet" of the model (pre-rendered at $10^\circ$ increments on the cloud backend). The client phone uses its stable CPU to index and switch the visible frame based on the patient's swipe gestures, creating a zero-GPU 3D rotation effect.
|
||||
|
||||
#### FR-05: Sensitive Data Sandbox & Patient Share Portal (Decree 13/2023/ND-CP Compliant)
|
||||
|
||||
- **User Insight:** Patients need clean, accessible, jargon-free medical information on their personal devices post-consultation without dealing with complex medical document readers or risking leakages of their sensitive health histories.
|
||||
- **Engineering Feasibility:** Moderate (8/10). Strictly aligned with **Vietnam's Decree 13/2023/ND-CP** requirements for processing sensitive personal data (health status and clinical records).
|
||||
- **Functional Scope:**
|
||||
- **Explicit Consent Check:** The portal cannot render or process the patient packet until a native, explicit "Opt-In Consent" click-action is logged and cryptographically recorded from the patient, conforming to Decree 13 legal consent parameters.
|
||||
- **Data Sanitization & Server-Side Flattening:** Strips away raw DICOM arrays, institutional metadata, and backend telemetry. If a `LOW_GPU` profile is active, the local cloud server flattens all clinician vector overlays directly into a compressed static JPEG to offload rendering tasks from weak patient browsers.
|
||||
- **Decentralized Local AES-256 Encryption:** In accordance with Decree 13 mandates for technical protection measures, the app must encrypt the patient's packet using localized AES-256 bit keys.
|
||||
- **Time-Expiring Tokenized Access:** Generates a unique QR code or deep link mapped to a one-time token with a $14\text{-day}$ Time-To-Live (TTL). When scanned over standard local 4G, it loads an adaptive dashboard in $\le 2.0\text{ seconds}$ hosting only the simplified locomotive health summaries and clinician voice memos.
|
||||
|
||||
### Updated Technical Blueprint & Architecture Summary
|
||||
|
||||

|
||||
|
||||
### Updated Recommended Target MVP Tech Stack
|
||||
|
||||
- **Cross-Platform Interface:** Flutter (Dart) or React Native (TypeScript) to optimize code reusability across Vietnam's divided OS market.
|
||||
- **DICOM Framework:** OFFIS DCMTK compiled to native C++ mobile binaries via platform channels.
|
||||
- **3D & Hybrid Visualization Subsystem:** An inline WebView context using vanilla JavaScript. The script evaluates graphics-pipe telemetry and dynamically switches rendering branches between Three.js (WebGL) and optimized CSS/DOM-level image switching (Sprite-Sheet).
|
||||
- **Data Compliance Module:** Client-side encryption using localized cryptographic plugins. All production cloud infrastructure, file caches, and backend web databases must be hosted on local cloud server providers (e.g., Viettel IDC, VNPT, or local AWS/GCP regions) to fully satisfy local data protection audit requirements under Decree 13.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Q1: True Agreement
|
||||
(AI Correct / Doctor Correct)
|
||||
|
||||
Layered Three-Tier ML Stack Performance Impact (Your Proposed Design): Explainable Baseline Sync: The VKIST Grader computes the numerical matrices & the GradCAM. The LLM Explainer parses the raw segmentation parameters + GradCAM and automatically generates an interactive diagnostic draft chat panel & LLM based on the GradCAM + RAG-knowledge + the raw-ultrasound to explain the VKIST-grader. The RAG-Referee confirms zero clinical guidelines variance, and logs a high-trust concur structural block. <note both LLM have to record back the Chain-of-Though for explain why the LLM’s agree & allow the result)
|
||||
|
||||
```planuml
|
||||
@startuml
|
||||
' Settings
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
' Actors
|
||||
actor "Diagnostic Radiologist (UP5)" as Rad
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
' System Boundary
|
||||
rectangle "VKIST MSK Workspace - Q1: True Agreement Flow" {
|
||||
|
||||
usecase "Ingest Diagnostic Ultrasound" as UC_Load
|
||||
|
||||
rectangle "Pipeline: Vision & Reasoning" {
|
||||
usecase "Compute Matrices & GradCAM (VKIST Grader)" as UC_Vision
|
||||
usecase "Parse Features & Draft Explanation (LLM Explainer)" as UC_Explain
|
||||
usecase "Log Chain-of-Thought (CoT)" as UC_CoT
|
||||
}
|
||||
|
||||
rectangle "Audit: RAG-Referee" {
|
||||
usecase "Verify Clinical Guideline Alignment" as UC_Referee
|
||||
usecase "Cache Concurrence Structural Block" as UC_Log
|
||||
}
|
||||
|
||||
rectangle "Clinical Finalization" {
|
||||
usecase "Review & Confirm Diagnosis" as UC_Review
|
||||
usecase "Sign & Commit Record" as UC_Finalize
|
||||
}
|
||||
|
||||
usecase "Synchronize EMR Ledger" as UC_Sync
|
||||
}
|
||||
|
||||
' Interaction Paths
|
||||
Rad --> UC_Load
|
||||
UC_Load ..> UC_Vision : <<include>>
|
||||
UC_Vision --> UC_Explain : Provide Tensors & GradCAM
|
||||
UC_Explain ..> UC_CoT : <<include>> (Persist Reasoning Path)
|
||||
|
||||
' Independent Verification Gate
|
||||
UC_Explain ..> UC_Referee : <<include>>
|
||||
UC_Referee ..> UC_Log : <<include>> (High-Trust Block)
|
||||
|
||||
' Final User Confirmation
|
||||
Rad --> UC_Review
|
||||
UC_Log --> UC_Review : Show "High-Trust Concurrence"
|
||||
Rad --> UC_Finalize
|
||||
UC_Finalize ..> UC_Sync : <<include>>
|
||||
UC_Sync --> EMR : POST Validated JSON Record
|
||||
@enduml
|
||||
```
|
||||
|
||||
/image.png)
|
||||
@@ -0,0 +1,67 @@
|
||||
# Q2: Automation Override Risk
|
||||
(AI Correct / Doctor Oversights / Confuse)
|
||||
|
||||
Layered Three-Tier ML Stack Performance Impact (Your Proposed Design): The Conversational Circuit Breaker triggers when a clinician disagrees / confuse / uncertain with the system's diagnostic grade, halting the workflow to launch an interactive Socratic dialogue that bridges the gap between human intuition and machine inference. In this mode, the system (LLM-explainer) shall synthesize raw VKIST-ML vision tensors, GradCAM activation heatmaps, and evidence retrieved via RAG into a collaborative analysis session, forcing the clinician to articulate their reasoning against the machine's spatial and vascular observations. To ensure diagnostic integrity, a BERT-based hallucination detector continuously monitors the chat for semantic drift or illogical premises; if the conversation reaches an impasse or the system detects potential contextual hallucination, the RAG-Referee intervenes as an unbiased arbiter. This referee bypasses the conversational history to provide definitive, evidence-based source material from clinical guidelines (such as ESSR) directly tied to the raw imaging metrics, resolving the ambiguity through objective, verifiable medical evidence rather than subjective negotiation.
|
||||
|
||||
```planuml
|
||||
@startuml
|
||||
skinparam linetype polyline
|
||||
skinparam packageStyle rectangle
|
||||
skinparam rectangle {
|
||||
BackgroundColor #fefefe
|
||||
BorderColor #555555
|
||||
}
|
||||
|
||||
' Actors
|
||||
actor "Radiologist (UP5)" as Rad
|
||||
actor "Internal Consultor (LLM Chat)" as Cons <<System>>
|
||||
actor "Hospital EMR" as EMR <<System>>
|
||||
|
||||
rectangle "VKIST MSK Workspace - Q2 Architecture" {
|
||||
|
||||
usecase "Trigger Circuit Breaker Panel" as UC2_Trigger
|
||||
|
||||
rectangle "Socratic Workspace UI Panel" {
|
||||
usecase "Lock Main Diagnostic Flow" as UC2_Halt
|
||||
usecase "Engage in Socratic Discussion" as UC2_Socratic
|
||||
usecase "Display Visual GradCAM Overlay" as UC2_Synth
|
||||
}
|
||||
|
||||
rectangle "Verification & Arbitration Kernel" {
|
||||
usecase "Audit Chat Token Drift (BERT)" as UC2_BERT
|
||||
usecase "Execute RAG-Referee Check" as UC2_Referee
|
||||
usecase "Query Immutable Guideline Base" as UC2_RAG_Fetch
|
||||
}
|
||||
|
||||
rectangle "Clinical Resolution Gate" {
|
||||
usecase "Review Referee Verdict Card" as UC2_Review
|
||||
usecase "Commit Signed Diagnosis" as UC2_Finalize
|
||||
}
|
||||
|
||||
usecase "EMR Ledger Sync" as UC2_Sync
|
||||
}
|
||||
|
||||
' Core Interaction Flow
|
||||
Rad --> UC2_Trigger : Disagreement/Uncertainty
|
||||
UC2_Trigger ..> UC2_Halt : <<include>>
|
||||
UC2_Halt ..> UC2_Synth : <<include>>
|
||||
|
||||
' Dynamic Chat Loop between Doctor and Internal Consultor LLM
|
||||
Rad --> UC2_Socratic
|
||||
Cons --> UC2_Socratic : Drive Pathologic Inquiry Dialogue
|
||||
|
||||
' Asynchronous Automated Verification Channel
|
||||
UC2_Socratic ..> UC2_BERT : Stream Conversation Tokens
|
||||
UC2_BERT ..> UC2_Referee : <<extend>> (Triggered on Impasse / Chat Hallucination)
|
||||
UC2_Referee ..> UC2_RAG_Fetch : <<include>>
|
||||
UC2_RAG_Fetch ..> UC2_Review : Inject Ground-Truth Evidence
|
||||
|
||||
' Finalization Steps
|
||||
Rad --> UC2_Review
|
||||
Rad --> UC2_Finalize
|
||||
UC2_Finalize ..> UC2_Sync : <<include>>
|
||||
UC2_Sync --> EMR : POST Validated JSON Payload
|
||||
@enduml
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,69 @@
|
||||
# Q3: Clinician Subservience Risk
|
||||
(AI Hallucinates / Doctor Correct)
|
||||
|
||||
Layered Three-Tier ML Stack Performance Impact (Your Proposed Design): The Objective Critic Loop initiates when a clinician contests an automated diagnostic grade, triggering an interactive Socratic consultation that bridges human intuition with machine inference via the VKIST-ML vision stack. During this loop, the LLM Explainer renders a GradCAM-anchored reasoning draft that visualizes the specific pixel-level feature activation logic, enabling the clinician to identify and isolate artifacts—such as motion tremors—that may have induced a system hallucination. To ensure diagnostic integrity, a BERT-based detector continuously monitors the dialogue for semantic drift, and if the interaction reaches an impasse or context hallucination is detected, the RAG-Referee intervenes as an unbiased, independent arbiter. By cross-verifying the clinician’s assertion and the model’s reasoning against raw imaging tensors and immutable, source-cited clinical guidelines (e.g., ESSR/OMERACT standards), the Referee resolves diagnostic ambiguity with objective evidence, ultimately committing the validated session as an annotated ground-truth record for targeted system reinforcement.
|
||||
|
||||
```planuml
|
||||
@startuml
|
||||
' Layout optimizations to secure compact rendering and prevent image fragmentation
|
||||
skinparam linetype polyline
|
||||
skinparam packageStyle rectangle
|
||||
skinparam rectangle {
|
||||
BackgroundColor #fefefe
|
||||
BorderColor #555555
|
||||
}
|
||||
|
||||
' Actors strictly mapped to match your canonical architectural definitions
|
||||
actor "Radiologist (UP5)" as Rad
|
||||
actor "Internal Consultor (LLM Chat)" as Cons <<System>>
|
||||
actor "System Maintainer" as Maint <<System>>
|
||||
|
||||
rectangle "VKIST MSK Workspace - Q4 Architecture" {
|
||||
|
||||
usecase "Evaluate Epistemic Uncertainty Gate" as UC4_Trigger
|
||||
|
||||
rectangle "Socratic Workspace UI Panel" {
|
||||
usecase "Shift to Clinical Investigation Mode" as UC4_Halt
|
||||
usecase "Engage in Socratic Discussion" as UC4_Socratic
|
||||
usecase "Render Manual Checklist Canvas" as UC4_Synth
|
||||
}
|
||||
|
||||
rectangle "Verification & Arbitration Kernel" {
|
||||
usecase "Audit Chat Token Drift (BERT)" as UC4_BERT
|
||||
usecase "Execute RAG-Referee Check" as UC4_Referee
|
||||
usecase "Return Null-Match Signal" as UC4_RAG_Fetch
|
||||
}
|
||||
|
||||
rectangle "Clinical Resolution Gate" {
|
||||
usecase "Document Novel Morphological Features" as UC4_Review
|
||||
usecase "Authorize Serialized Anomaly Package" as UC4_Finalize
|
||||
}
|
||||
|
||||
usecase "Asynchronous Telemetry Queue Sync" as UC4_Sync
|
||||
}
|
||||
|
||||
' Initial Data Intake and Uncertainty Routing Paths
|
||||
Rad --> UC4_Trigger : Feed OOD Image Tensors
|
||||
UC4_Trigger ..> UC4_RAG_Fetch : <<include>> (Triggers Empty Vector Result)
|
||||
UC4_RAG_Fetch ..> UC4_Halt : <<extend>> (On Zero-Match Guidelines + Low Conf Tensors)
|
||||
UC4_Halt ..> UC4_Synth : <<include>>
|
||||
|
||||
' Direct Socratic Analysis Run-time Workspace
|
||||
Rad --> UC4_Socratic
|
||||
Cons --> UC4_Socratic : Drive Exploratory Morphology Dialogue
|
||||
|
||||
' Live Guardrail and Exception Evaluation Paths
|
||||
UC4_Socratic ..> UC4_BERT : Stream Conversation Tokens
|
||||
UC4_BERT ..> UC4_Referee : <<include>> (Validates Logical Framing Stability)
|
||||
|
||||
' Manual Audit, Documenting Anomaly and Consent Finalization
|
||||
Rad --> UC4_Review : Acknowledge Guideline Limitation
|
||||
Rad --> UC4_Finalize : Provide Native Opt-In Telemetry Consent
|
||||
|
||||
' Async Data Serialization Sink to System Maintainer Ledger
|
||||
UC4_Finalize ..> UC4_Sync : <<include>>
|
||||
UC4_Sync --> Maint : POST Encrypted Tensors & Logs for Model Retraining
|
||||
@enduml
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,69 @@
|
||||
# Q4: Double Blind Failure dues to edge-case
|
||||
(AI Faulty / Doctor Biased)
|
||||
|
||||
Layered Three-Tier ML Stack Performance Impact (Your Proposed Design): Anomaly Escalation Protocol: In instances where both the diagnostic system and the clinician encounter an edge-case—or "unknown-unknown"—that lacks precedent in the current RAG knowledge base, the system initiates the Anomaly Escalation Protocol. The LLM Explainer detects this "epistemic uncertainty" (via low vision-stack confidence and empty RAG retrieval results) and shifts the interface from "Diagnostic Support" to "Clinical Investigation Mode." Instead of attempting to force a Grade-based diagnosis, the Internal Consultor guides the clinician to document the unique morphological features through a structured annotation protocol, facilitating a Socratic investigation into the anomaly. The system transparently acknowledges the limitation, explicitly stating that current clinical guidelines do not cover this specific presentation, and prompts the clinician to manually document findings. With the clinician’s consent, the workspace commits this session as a "Novel Research Case," automatically serializing the raw imaging tensors, clinician observations, and artifact logs to a secure telemetry queue, flagging the data for system maintainers to perform targeted model retraining and protocol refinement.
|
||||
|
||||
```planuml
|
||||
@startuml
|
||||
' Layout optimizations to secure compact rendering and prevent image fragmentation
|
||||
skinparam linetype polyline
|
||||
skinparam packageStyle rectangle
|
||||
skinparam rectangle {
|
||||
BackgroundColor #fefefe
|
||||
BorderColor #555555
|
||||
}
|
||||
|
||||
' Actors strictly mapped to match your canonical architectural definitions
|
||||
actor "Radiologist (UP5)" as Rad
|
||||
actor "Internal Consultor (LLM Chat)" as Cons <<System>>
|
||||
actor "System Maintainer" as Maint <<System>>
|
||||
|
||||
rectangle "VKIST MSK Workspace - Q4 Architecture" {
|
||||
|
||||
usecase "Evaluate Epistemic Uncertainty Gate" as UC4_Trigger
|
||||
|
||||
rectangle "Socratic Workspace UI Panel" {
|
||||
usecase "Shift to Clinical Investigation Mode" as UC4_Halt
|
||||
usecase "Engage in Socratic Discussion" as UC4_Socratic
|
||||
usecase "Render Manual Checklist Canvas" as UC4_Synth
|
||||
}
|
||||
|
||||
rectangle "Verification & Arbitration Kernel" {
|
||||
usecase "Audit Chat Token Drift (BERT)" as UC4_BERT
|
||||
usecase "Execute RAG-Referee Check" as UC4_Referee
|
||||
usecase "Return Null-Match Signal" as UC4_RAG_Fetch
|
||||
}
|
||||
|
||||
rectangle "Clinical Resolution Gate" {
|
||||
usecase "Document Novel Morphological Features" as UC4_Review
|
||||
usecase "Authorize Serialized Anomaly Package" as UC4_Finalize
|
||||
}
|
||||
|
||||
usecase "Asynchronous Telemetry Queue Sync" as UC4_Sync
|
||||
}
|
||||
|
||||
' Initial Data Intake and Uncertainty Routing Paths
|
||||
Rad --> UC4_Trigger : Feed OOD Image Tensors
|
||||
UC4_Trigger ..> UC4_RAG_Fetch : <<include>> (Triggers Empty Vector Result)
|
||||
UC4_RAG_Fetch ..> UC4_Halt : <<extend>> (On Zero-Match Guidelines + Low Conf Tensors)
|
||||
UC4_Halt ..> UC4_Synth : <<include>>
|
||||
|
||||
' Direct Socratic Analysis Run-time Workspace
|
||||
Rad --> UC4_Socratic
|
||||
Cons --> UC4_Socratic : Drive Exploratory Morphology Dialogue
|
||||
|
||||
' Live Guardrail and Exception Evaluation Paths
|
||||
UC4_Socratic ..> UC4_BERT : Stream Conversation Tokens
|
||||
UC4_BERT ..> UC4_Referee : <<include>> (Validates Logical Framing Stability)
|
||||
|
||||
' Manual Audit, Documenting Anomaly and Consent Finalization
|
||||
Rad --> UC4_Review : Acknowledge Guideline Limitation
|
||||
Rad --> UC4_Finalize : Provide Native Opt-In Telemetry Consent
|
||||
|
||||
' Async Data Serialization Sink to System Maintainer Ledger
|
||||
UC4_Finalize ..> UC4_Sync : <<include>>
|
||||
UC4_Sync --> Maint : POST Encrypted Tensors & Logs for Model Retraining
|
||||
@enduml
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,946 @@
|
||||
|
||||
---
|
||||
|
||||
# PART 1: Core Viewing & Data Intake Pipelines
|
||||
|
||||
## 1. UC-48376: Load Patient Scan Session
|
||||
|
||||
### Notion Properties Input Panel
|
||||
|
||||
```text
|
||||
* Name [Verb + Noun]: Load Patient Scan Session
|
||||
* Actor: Diagnostic Radiologist (Rad), VKIST Vision Grader Engine (Grader)
|
||||
* Goal: Ingest raw ultrasound frame arrays and initialize the diagnostic session state.
|
||||
* Interaction: System-to-System / User-to-System
|
||||
* Stimulus: User opens an unreviewed patient file, or the workspace catches an active DICOM stream hook.
|
||||
* SysResponse: Confirmation that raw frame arrays are mapped, spatial calibrations are set, and the local session state is active.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Load Patient Scan Session' defines a User-to-System / System-to-System interaction where the Diagnostic Radiologist (Rad) and VKIST Vision Grader Engine (Grader) aim to Ingest raw ultrasound frame arrays and initialize the diagnostic session state. This workflow is triggered when User opens an unreviewed patient file, or the workspace catches an active DICOM stream hook, causing the system to respond by providing Confirmation that raw frame arrays are mapped, spatial calibrations are set, and the local session state is active."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Load Patient Scan Session
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Local workspace application is authenticated and has secure socket access to the local image buffer.
|
||||
* DICOM/raw frame data payload is uncorrupted and readable.
|
||||
* **Postconditions (Success State):**
|
||||
* Core frame parameters are loaded into memory with spatial scale calibrations preserved.
|
||||
* Background parsing pipeline registers the unique session hash and prepares the context matrix for downstream agents.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **Diagnostic Radiologist** selects a patient case file from the workspace worklist interface.
|
||||
2. **VKIST Vision Grader Engine** feeds raw ultrasound image tensors, spatial calibrations, and foundational frame telemetry metadata into the workspace memory layer.
|
||||
3. **System** extracts pixel dimensions and constructs localized rendering viewports.
|
||||
4. **System** includes `UC_Q1_Explain` in the background to spin up explanation prompt matrices.
|
||||
5. **System** displays the fully loaded image frame in the workspace canvas, preparing the viewport for immediate review.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Exception Flow A: Corrupted Image Frame Payload**
|
||||
* At step [2], if the payload data fails format validation or structural check headers, the system halts execution, logs a data corruption fault code, and alerts the user with an "Unable to Parse Scan Session" dialog box.
|
||||
* **Exception Flow B: Resolution / Calibration Mismatch**
|
||||
* At step [3], if spatial aspect ratios or metadata pixel matrices lack the standardized calibration tags required by the vision engine, the workspace falls back to a safe default scale flag and displays a non-blocking diagnostic accuracy warning icon.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
actor "VKIST Vision Grader Engine" as Grader << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-48376\nLoad Patient Scan Session" as UC_Core
|
||||
usecase "UC-25776\nGenerate GradCAM & CoT Explanation Panel" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
Grader --> UC_Core
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. UC-47988: Review Suggested Synovitis Grade (0-3)
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Review Suggested Synovitis Grade (0-3)
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Evaluate the ML engine's proposed synovitis classification and structural overlays.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: The workspace completes localized UI construction and displays the diagnostic panel.
|
||||
* SysResponse: Display of classification metrics (Grades 0-3), color-coded overlays, and active risk-extension hooks.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Review Suggested Synovitis Grade (0-3)' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Evaluate the ML engine's proposed synovitis classification and structural overlays. This workflow is triggered when The workspace completes localized UI construction and displays the diagnostic panel, causing the system to respond by providing Display of classification metrics (Grades 0-3), color-coded overlays, and active risk-extension hooks."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Review Suggested Synovitis Grade (0-3)
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Image frames and raw ML prediction tensors (segmentation masks, classification weights) are fully loaded in memory via `Load Patient Scan Session`.
|
||||
* **Postconditions (Success State):**
|
||||
* System records human gaze/interaction initialization flags.
|
||||
* System keeps exception-based extend vectors armed (`UC_Q2_Intercept`, `UC_Q3_Expose`, `UC_Q4_Escalate`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** presents the active ultrasound canvas with interactive, toggleable, color-coded segmentation mask overlays.
|
||||
2. **System** displays the vision engine's suggested synovitis grading estimation (Grade 0, 1, 2, or 3) alongside structural pixel-percentage distribution metrics.
|
||||
3. **Diagnostic Radiologist** inspects the spatial distribution of the synovial hypertrophy markers and reads the inline text panels.
|
||||
4. **Diagnostic Radiologist** approves the visual data metrics without requesting alterations or triggering corrective dialogue paths.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Extension Flow A: Clinician Friction / Disagreement Caught**
|
||||
* At step [3], if mouse click frequencies suggest hesitation or manual adjustments cross a conflict delta threshold, the execution path triggers `UC_Q2_Intercept` to prevent blind override errors.
|
||||
* **Extension Flow B: Expert Contests Automated Grade**
|
||||
* At step [3], if the clinician explicitly changes the classification dropdown away from the ML-proposed score, the workspace extends to `UC_Q3_Expose` to display the machine activation weights.
|
||||
* **Extension Flow C: Anomaly / Confidence Failure Detected**
|
||||
* At step [1], if the deep-learning array returned a classification confidence metric below safety bounds paired with blank knowledge base lookups, the interface branches into `UC_Q4_Escalate`.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-47988\nReview Suggested Synovitis Grade (0-3)" as UC_Core
|
||||
usecase "UC-22159\nTrigger Conversational Circuit Breaker" as UC_Q2
|
||||
usecase "UC-25637\nExpose Pixel-Level Activation Logic" as UC_Q3
|
||||
usecase "UC-35956\nActivate Clinical Investigation Mode" as UC_Q4
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core <.. UC_Q2 : <<extend>>
|
||||
UC_Core <.. UC_Q3 : <<extend>>
|
||||
UC_Core <.. UC_Q4 : <<extend>>
|
||||
@enduml
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. UC-92006: Finalize & Sign Electronic Record
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Finalize & Sign Electronic Record
|
||||
* Actor: Diagnostic Radiologist (Rad), Hospital EMR System (EMR)
|
||||
* Goal: Authenticate, cryptographically seal, and sync verified diagnostic reports down to storage infrastructure.
|
||||
* Interaction: User-to-System / System-to-System
|
||||
* Stimulus: User executes the final confirmation/signature command button in the workspace utility ribbon.
|
||||
* SysResponse: Generation of a signed cryptographic log block and structured JSON transmission payload delivered to the EMR endpoint.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Finalize & Sign Electronic Record' defines a User-to-System / System-to-System interaction where the Diagnostic Radiologist (Rad) and Hospital EMR System (EMR) aim to Authenticate, cryptographically seal, and sync verified diagnostic reports down to storage infrastructure. This workflow is triggered when User executes the final confirmation/signature command button in the workspace utility ribbon, causing the system to respond by providing Generation of a signed cryptographic log block and structured JSON transmission payload delivered to the EMR endpoint."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Finalize & Sign Electronic Record
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active scan session evaluation has been resolved, and grading metrics are verified by the human specialist.
|
||||
* Local localized network channel to the hospital server framework is functional.
|
||||
* **Postconditions (Success State):**
|
||||
* Session record is transformed into a read-only state.
|
||||
* Standardized structural JSON payload data is safely stored within the Hospital EMR System sink.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **Diagnostic Radiologist** initiates the session finalization pipeline by interacting with the cryptographic signature command trigger.
|
||||
2. **System** prompts for the secure authentication credentials of the signing specialist.
|
||||
3. **System** generates a unified clinical log structure, packing structural thickness measurements (mm), final validated synovitis tier scores, and accompanying multi-agent trace logs.
|
||||
4. **System** calculates a secure cryptographic data hash, locking the session record into an immutable post-review profile.
|
||||
5. **System** delivers the structured data package across localized network pipes to the **Hospital EMR System**.
|
||||
6. **Hospital EMR System** confirms safe database commit storage updates and provides an acknowledgment packet back to the workspace.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Exception Flow A: Network Pipeline Transmission Failure**
|
||||
* At step [5], if network communications timeout or socket breaks occur, the workspace locks the finalized JSON package into a local encrypted offline buffer, changes the session status tag to "Pending Sync", and presents a clear connectivity warning alert.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-92006\nFinalize & Sign Electronic Record" as UC_Core
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core ..> EMR : Sync standardized structural JSON data
|
||||
@enduml
|
||||
```
|
||||
---
|
||||
|
||||
|
||||
# PART 2: Quadrant 1 — True Agreement Flows (AI Correct / Doctor Correct)
|
||||
|
||||
## 4. UC-25776: Generate GradCAM & CoT Explanation Panel
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Generate GradCAM & CoT Explanation Panel
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Present clear, pixel-linked visuospatial explanations and multi-modal clinical reasoning for high-trust verification.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Inclusion trigger initialized during session data intake (`Load Patient Scan Session`).
|
||||
* SysResponse: Renders heatmaps highlighting model focus zones alongside structured, clear reasoning steps.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Generate GradCAM & CoT Explanation Panel' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Present clear, pixel-linked visuospatial explanations and multi-modal clinical reasoning for high-trust verification. This workflow is triggered when Inclusion trigger initialized during session data intake (`Load Patient Scan Session`), causing the system to respond by providing Renders heatmaps highlighting model focus zones alongside structured, clear reasoning steps."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Generate GradCAM & CoT Explanation Panel
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Raw image frames and vision engine inference matrix weights have been imported via `Load Patient Scan Session`.
|
||||
* **Postconditions (Success State):**
|
||||
* Split-screen layout displays visual explanation elements without adding visual noise to the core image frame workspace canvas.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** evaluates the internal deep-learning model gradient parameters for the target ultrasound image slice.
|
||||
2. **System** generates a visual GradCAM heatmap layer mapping feature locations that dictated model classifications (e.g., hypervascularized synovial proliferation zones).
|
||||
3. **System** maps multi-modal prompt metrics through the internal LLM Explainer module to produce a concise, point-by-point clinical reasoning string.
|
||||
4. **System** populates the split-screen workspace sub-section block with this explanation data to guide human inspection efficiently.
|
||||
5. **System** includes `UC_Q1_Log` to serialize verification metadata.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-25776\nGenerate GradCAM & CoT Explanation Panel" as UC_Core
|
||||
usecase "UC-02423\nLog High-Trust Concur Block" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. UC-02423: Log High-Trust Concur Block
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Log High-Trust Concur Block
|
||||
* Actor: Hospital EMR System (EMR)
|
||||
* Goal: Secure the human-AI alignment log trace within the final diagnostic report payload.
|
||||
* Interaction: System-to-System
|
||||
* Stimulus: Explanatory panel validation completes successfully without user override actions.
|
||||
* SysResponse: Appends a tamper-evident audit trace block verifying explicit human-AI agreement into the session log cache.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Log High-Trust Concur Block' defines a System-to-System interaction where the Hospital EMR System (EMR) aims to Secure the human-AI alignment log trace within the final diagnostic report payload. This workflow is triggered when Explanatory panel validation completes successfully without user override actions, causing the system to respond by providing Appends a tamper-evident audit trace block verifying explicit human-AI agreement into the session log cache."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Log High-Trust Concur Block
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Multi-modal explanations were fully generated (`UC_Q1_Explain`) and passed without human alteration marks.
|
||||
* **Postconditions (Success State):**
|
||||
* Explicit audit string trace tracking high-trust convergence is formatted for downstream pipeline compilation.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** detects a direct consensus condition where the human expert confirms the model data without text/grading edits.
|
||||
2. **System** serializes the multi-modal text breakdown and pixel attribution coordinates into an immutable log string block.
|
||||
3. **System** assigns an explicit alignment token header flag (`HIGH_TRUST_CONCURRENCE`).
|
||||
4. **System** caches this specialized tracking trace within the localized session state data, making it ready to be appended during final data hand-off routines (`UC_Finalize`).
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-02423\nLog High-Trust Concur Block" as UC_Core
|
||||
}
|
||||
|
||||
UC_Core ..> EMR : (Prepares payload for final sync)
|
||||
@enduml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# PART 3: Quadrant 2 — Automation Override Risk Loops (AI Correct / Doctor Oversight)
|
||||
|
||||
## 6. UC-22159: Trigger Conversational Circuit Breaker
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Trigger Conversational Circuit Breaker
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Intercept premature finalization workflows if interface telemetry reveals friction, hesitation, or cognitive blind-spots.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Extended extension trace caught during review steps if user behavior markers diverge from smooth consensus paths.
|
||||
* SysResponse: Halts default workspace finalization routes and shifts the UI into a mandatory safety evaluation mode.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Trigger Conversational Circuit Breaker' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Intercept premature finalization workflows if interface telemetry reveals friction, hesitation, or cognitive blind-spots. This workflow is triggered when Extended extension trace caught during review steps if user behavior markers diverge from smooth consensus paths, causing the system to respond by providing Halts default workspace finalization routes and shifts the UI into a mandatory safety evaluation mode."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Trigger Conversational Circuit Breaker
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active session is inside the `UC_Review` workflow phase.
|
||||
* UI layer telemetry captures specific friction indicators (e.g., high-frequency cursor oscillation, repeatedly typing and deleting text, or conflicting grading inputs).
|
||||
* **Postconditions (Success State):**
|
||||
* Direct finalization path is securely locked down.
|
||||
* System-forced conversational validation interface is deployed into view.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** evaluates live workspace telemetry tracking patterns during active case validation.
|
||||
2. **System** detects user behavior triggers signaling high diagnostic friction or potential automatic oversight trends.
|
||||
3. **System** blocks the immediate execution availability of the standard finalization command sequence (`UC_Finalize`).
|
||||
4. **System** transforms workspace panel focus areas to present an interactive confirmation overlay.
|
||||
5. **System** executes `UC_Q2_Socratic` to initialize direct safety check communications.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-47988\nReview Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "UC-22159\nTrigger Conversational Circuit Breaker" as UC_Core
|
||||
usecase "UC-55146\nFacilitate Socratic Reasoning Dialogue" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Review
|
||||
UC_Review <.. UC_Core : <<extend>> (If clinician friction detected)
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 7. UC-55146: Facilitate Socratic Reasoning Dialogue
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Facilitate Socratic Reasoning Dialogue
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Engage the specialist in a targeted, conversational double-check loop regarding controversial structural markers.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Core execution request passed down by the active circuit breaker module (`UC_Q2_Intercept`).
|
||||
* SysResponse: Interactive conversational sub-panel displaying focused prompt choices that check specific diagnostic criteria.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Facilitate Socratic Reasoning Dialogue' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Engage the specialist in a targeted, conversational double-check loop regarding controversial structural markers. This workflow is triggered when Core execution request passed down by the active circuit breaker module (`UC_Q2_Intercept`), causing the system to respond by providing Interactive conversational sub-panel displaying focused prompt choices that check specific diagnostic criteria."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Facilitate Socratic Reasoning Dialogue
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Circuit breaker safety intercept sequence has completed successfully, freezing generic CRUD paths.
|
||||
* **Postconditions (Success State):**
|
||||
* User inputs conversational defense arguments or confirms specific anatomical findings.
|
||||
* Live conversation data tokens are actively streamed to automated safety monitors.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** initializes a conversational chat element right next to the ultrasound display field.
|
||||
2. **System** presents a non-confrontational, clinically grounded question regarding the identified discrepancies (e.g., *"Note the echo-free thickening layer in the suprapatellar recess; please confirm if this modification represents minor effusion or structural pannus tissue"*).
|
||||
3. **Diagnostic Radiologist** enters text responses or selects structural tag tokens to clarify their assessment.
|
||||
4. **System** includes `UC_Q2_BERT` in real time to process active conversation token patterns.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "UC-55146\nFacilitate Socratic Reasoning Dialogue" as UC_Core
|
||||
usecase "UC-74821\nMonitor Drift via BERT Sub-Layer" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core : Argue observations
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 8. UC_Q2_BERT: Monitor Drift via BERT Sub-Layer
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Monitor Drift via BERT Sub-Layer
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Continuously parse communication tokens to identify logical contradictions or semantic drift during clinical debates.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Streamed entry of communication tokens within the active dialogue loop.
|
||||
* SysResponse: Real-time semantic checking flags; extends out to the RAG referee if an impasse or severe drift is captured.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Monitor Drift via BERT Sub-Layer' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Continuously parse communication tokens to identify logical contradictions or semantic drift during clinical debates. This workflow is triggered when Streamed entry of communication tokens within the active dialogue loop, causing the system to respond by providing Real-time semantic checking flags; extends out to the RAG referee if an impasse or severe drift is captured."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Monitor Drift via BERT Sub-Layer
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active conversational dialogue module is processing user data strings (`UC_Q2_Socratic`).
|
||||
* **Postconditions (Success State):**
|
||||
* Log structures capture semantic alignment metrics.
|
||||
* System successfully catches contradictions before data parameters flow to final storage.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** continuously intercepts conversation tokens as the human expert types input strings.
|
||||
2. **System** runs token matrices through an embedded BERT checking model to calculate contextual semantic coherence scores.
|
||||
3. **System** verifies that user claims line up logically with the visual indicators under review.
|
||||
4. **System** approves the validated conversational step, allowing the specialist to complete the confirmation cycle smoothly.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Extension Flow A: Impasse or Semantic Contradiction Detected**
|
||||
* At step [3], if the specialist's input text contradicts objective structural metrics (e.g., claiming a region is "completely normal" while the visual layer registers massive synovial proliferation) or exhibits context drift, the process branches into `UC_Q2_Arbiter` to request evidence evaluation.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Monitor Drift via BERT Sub-Layer" as UC_Core
|
||||
usecase "Arbitrate Evidence via RAG-Referee" as UC_Ext
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core <.. UC_Ext : <<extend>> (If impasse or semantic drift caught)
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 9. UC_Q2_Arbiter: Arbitrate Evidence via RAG-Referee
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Arbitrate Evidence via RAG-Referee
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Query static, authoritative clinical knowledge bases to resolve human-machine disagreements with objective evidence.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Triggered when communication tracking scores cross a severe semantic mismatch or impasse threshold.
|
||||
* SysResponse: Inline injection of un-biased diagnostic text extracts and guidelines matching the active frame conditions.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Arbitrate Evidence via RAG-Referee' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Query static, authoritative clinical knowledge bases to resolve human-machine disagreements with objective evidence. This workflow is triggered when Triggered when communication tracking scores cross a severe semantic mismatch or impasse threshold, causing the system to respond by providing Inline injection of un-biased diagnostic text extracts and guidelines matching the active frame conditions."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Arbitrate Evidence via RAG-Referee
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* BERT analytics layers detect a diagnostic impasse or significant semantic drift.
|
||||
* Authoritative local clinical knowledge base index (e.g., OMERACT synovitis grading reference manuals) is online and responsive.
|
||||
* **Postconditions (Success State):**
|
||||
* Disagreement matrix is resolved via verified medical data injection.
|
||||
* Final chosen path is linked directly to a standard medical guideline anchor.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** halts active conversational dialogue inputs temporarily to execute a localized context search.
|
||||
2. **System** extracts spatial measurements and text tokens to construct a specialized RAG search string.
|
||||
3. **System** queries local, validated medical knowledge data banks to locate matching diagnostic criteria sections.
|
||||
4. **System** displays the verified guideline text extract right inside the workspace alert view block (e.g., *"OMERACT standardizes Grade 2 as hypoechoic synovial hypertrophy demonstrating fluid-filled distension up to structural boundary bounds"*).
|
||||
5. **Diagnostic Radiologist** reviews the authoritative reference framework and either adjusts their classification choice or submits a structured expert override justifying their deviation.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Arbitrate Evidence via RAG-Referee" as UC_Core
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
# PART 4: Quadrant 3 — Clinician Subservience Risk Loops (AI Hallucinates / Doctor Correct)
|
||||
|
||||
## 10. UC_Q3_Expose: Expose Pixel-Level Activation Logic
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Expose Pixel-Level Activation Logic
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Reveal fine-grained layer weights and activation responses when the human specialist challenges an automated grade prediction.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Clinician manually alters or rejects the ML-proposed classification score in the review pane.
|
||||
* SysResponse: Interactive visual mapping showing the exact high-frequency noise regions driving model prediction errors.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Expose Pixel-Level Activation Logic' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Reveal fine-grained layer weights and activation responses when the human specialist challenges an automated grade prediction. This workflow is triggered when Clinician manually alters or rejects the ML-proposed classification score in the review pane, causing the system to respond by providing Interactive visual mapping showing the exact high-frequency noise regions driving model prediction errors."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Expose Pixel-Level Activation Logic
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active session is inside the `UC_Review` interface phase.
|
||||
* Specialist chooses an option that breaks clean model agreement paths.
|
||||
* **Postconditions (Success State):**
|
||||
* Internal neural layer weight vectors are visually mapped onto the primary medical viewport.
|
||||
* Core manual artifact isolation tool sets become active on the canvas layout.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **Diagnostic Radiologist** changes the system-suggested grade classification dropdown setting.
|
||||
2. **System** captures the modification step and branches away from the standard review pathway to reveal underlying model mechanics.
|
||||
3. **System** transforms image layers to display fine-grained activation weights, revealing exactly which pixel clusters (e.g., acoustic shadowing regions or bone interfaces) skewed the model's calculation.
|
||||
4. **System** includes `UC_Q3_Isolate` to let the specialist manually clean up the noise zones.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "Expose Pixel-Level Activation Logic" as UC_Core
|
||||
usecase "Isolate Visual Noise/Artifacts" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Review
|
||||
UC_Review <.. UC_Core : <<extend>> (If clinician contests AI score)
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. UC_Q3_Isolate: Isolate Visual Noise/Artifacts
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Isolate Visual Noise/Artifacts
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Provide manual brush and selection overlays to mask out acoustic shadows, bone scattering, or artifacts causing model calculation errors.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: Human operator activates canvas cleanup tools within the exposed model layer layout.
|
||||
* SysResponse: Real-time visual updates to the pixel mask array, isolating clean anatomical structures from surrounding imaging noise.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Isolate Visual Noise/Artifacts' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Provide manual brush and selection overlays to mask out acoustic shadows, bone scattering, or artifacts causing model calculation errors. This workflow is triggered when Human operator activates canvas cleanup tools within the exposed model layer layout, causing the system to respond by providing Real-time visual updates to the pixel mask array, isolating clean anatomical structures from surrounding imaging noise."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Isolate Visual Noise/Artifacts
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* System exposure arrays are visible across the image viewport layout (`UC_Q3_Expose`).
|
||||
* **Postconditions (Success State):**
|
||||
* Corrected ground-truth frame masks are calculated and locked into memory.
|
||||
* System updates local diagnostic metrics using the isolated anatomical data.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** activates a manual canvas tool overlay, giving the user access to high-precision brush, eraser, and selection vectors.
|
||||
2. **Diagnostic Radiologist** applies brush vectors directly over areas containing acoustic artifacts or non-synovial structures that skewed the automated classification score.
|
||||
3. **System** recalculates active region dimensions in real time, excluding the masked pixels from the active grading parameters.
|
||||
4. **System** updates diagnostic panel displays to confirm the human-corrected measurements.
|
||||
5. **System** includes `UC_Q3_Commit` to lock the updated session state securely.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Isolate Visual Noise/Artifacts" as UC_Core
|
||||
usecase "Commit Validated Ground-Truth Record" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core : Tag artifacts
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 12. UC_Q3_Commit: Commit Validated Ground-Truth Record
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Commit Validated Ground-Truth Record
|
||||
* Actor: Hospital EMR System (EMR)
|
||||
* Goal: Secure the human-corrected ground-truth dataset variant while appending clean, expert-validated report payloads to the EMR.
|
||||
* Interaction: System-to-System
|
||||
* Stimulus: Completion of manual artifact masking operations and confirmation of corrected metrics.
|
||||
* SysResponse: Stores the corrected medical report in the EMR and saves the isolated image mask to an optimization cache for subsequent retraining.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Commit Validated Ground-Truth Record' defines a System-to-System interaction where the Hospital EMR System (EMR) aims to Secure the human-corrected ground-truth dataset variant while appending clean, expert-validated report payloads to the EMR. This workflow is triggered when Completion of manual artifact masking operations and confirmation of corrected metrics, causing the system to respond by providing Stores the corrected medical report in the EMR and saves the isolated image mask to an optimization cache for subsequent retraining."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Commit Validated Ground-Truth Record
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Human-directed canvas modification steps are locked in place without remaining pixel parity errors (`UC_Q3_Isolate`).
|
||||
* **Postconditions (Success State):**
|
||||
* EMR database updates receive the human expert's diagnostic findings.
|
||||
* Isolated ground-truth tensor pairs are safely cached for AI training refinement runs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** packages the human expert's corrected diagnostic data metrics into the primary transmission bundle.
|
||||
2. **System** isolates the human-brushed image mask layers alongside the initial incorrect model classification output.
|
||||
3. **System** tags the data pair as a validated retraining asset (`GROUND_TRUTH_OVERRIDE`).
|
||||
4. **System** saves the optimization asset to a secure local retraining storage folder, while preparing the primary medical report for delivery to the **Hospital EMR System**.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Commit Validated Ground-Truth Record" as UC_Core
|
||||
}
|
||||
|
||||
UC_Core ..> EMR : (Prepares clean report for EMR sync)
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# PART 5: Quadrant 4 — Double Blind Failure Loops (AI Faulty / Doctor Biased)
|
||||
|
||||
## 13. UC_Q4_Escalate: Activate Clinical Investigation Mode
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Activate Clinical Investigation Mode
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Switch the system into a strict, template-driven manual examination mode when low vision confidence values align with a lack of reference data.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: The workspace detects a critical double-blind failure criteria match during the case evaluation phase.
|
||||
* SysResponse: Disables automated diagnostic suggestions entirely and forces a standardized manual morphology review.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Activate Clinical Investigation Mode' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Switch the system into a strict, template-driven manual examination mode when low vision confidence values align with a lack of reference data. This workflow is triggered when The workspace detects a critical double-blind failure criteria match during the case evaluation phase, causing the system to respond by providing Disables automated diagnostic suggestions entirely and forces a standardized manual morphology review."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Activate Clinical Investigation Mode
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* System classification loops return low-confidence indices.
|
||||
* Authoritative RAG reference lookups return no matches, indicating an unmapped anatomical variant or a severe image anomaly.
|
||||
* **Postconditions (Success State):**
|
||||
* Automated suggestions are masked out to prevent cognitive bias.
|
||||
* Mandatory manual template verification frameworks are deployed into active workspace view.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** monitors deep-learning inference score bounds during case evaluation.
|
||||
2. **System** runs background reference data lookups and catches a dual failure state (Low confidence + Empty knowledge reference).
|
||||
3. **System** drops the standard review interface layout to prevent automated suggestion bias or human misinterpretation loops.
|
||||
4. **System** changes UI display focus markers to activate an explicit, template-driven investigation layout.
|
||||
5. **System** includes `UC_Q4_Annotate` to force manual measurement entries.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "Activate Clinical Investigation Mode" as UC_Core
|
||||
usecase "Execute Structured Morphology Annotation" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Review
|
||||
UC_Review <.. UC_Core : <<extend>> (If low confidence & empty RAG)
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 14. UC_Q4_Annotate: Execute Structured Morphology Annotation
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Execute Structured Morphology Annotation
|
||||
* Actor: Diagnostic Radiologist (Rad)
|
||||
* Goal: Force the manual plotting of anatomical coordinates and morphological anomalies using a strict, un-biased framework.
|
||||
* Interaction: User-to-System
|
||||
* Stimulus: The workspace forces a manual review layout via the active escalation workflow step.
|
||||
* SysResponse: Interactive coordinate plotting arrays and mandatory clinical documentation input boxes.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Execute Structured Morphology Annotation' defines a User-to-System interaction where the Diagnostic Radiologist (Rad) aims to Force the manual plotting of anatomical coordinates and morphological anomalies using a strict, un-biased framework. This workflow is triggered when The workspace forces a manual review layout via the active escalation workflow step, causing the system to respond by providing Interactive coordinate plotting arrays and mandatory clinical documentation input boxes."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Execute Structured Morphology Annotation
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* System UI layer has transitioned to manual investigation mode parameters (`UC_Q4_Escalate`).
|
||||
* **Postconditions (Success State):**
|
||||
* Specialist successfully plots manual structural bounds.
|
||||
* Text verification parameters capture explicit clinical observations.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** displays an empty, un-biased ultrasound canvas frame alongside a series of mandatory measurement fields.
|
||||
2. **Diagnostic Radiologist** plots coordinate points across the canvas layer to outline the boundaries of the anomalous tissue.
|
||||
3. **Diagnostic Radiologist** manually populates text fields describing structural observations (e.g., bone fragments or atypical lesion shapes).
|
||||
4. **System** compiles these manual coordinates and comments into a detailed case record.
|
||||
5. **System** includes `UC_Q4_Queue` to route the data directly to optimization pipelines.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Execute Structured Morphology Annotation" as UC_Core
|
||||
usecase "Serialize Session to Telemetry Queue" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core : Document manual findings
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 15. UC_Q4_Queue: Serialize Session to Telemetry Queue
|
||||
|
||||
### Notion Properties Input Panel
|
||||
```text
|
||||
* Name [Verb + Noun]: Serialize Session to Telemetry Queue
|
||||
* Actor: Hospital EMR System (EMR)
|
||||
* Goal: Route anomalous case data directly to engineering telemetry streams while bypassing standard hospital records to protect clinical data pipes.
|
||||
* Interaction: System-to-System
|
||||
* Stimulus: Completion of manual morphology reporting arrays within the clinical investigation interface.
|
||||
* SysResponse: Packages unencrypted image tensors, coordinate arrays, and user text blocks directly into core product telemetry queues.
|
||||
* VerboseForm (Formula Reference View): "The use case 'Serialize Session to Telemetry Queue' defines a System-to-System interaction where the Hospital EMR System (EMR) aims to Route anomalous case data directly to engineering telemetry streams while bypassing standard hospital records to protect clinical data pipes. This workflow is triggered when Completion of manual morphology reporting arrays within the clinical investigation interface, causing the system to respond by providing Packages unencrypted image tensors, coordinate arrays, and user text blocks directly into core product telemetry queues."
|
||||
|
||||
```
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Serialize Session to Telemetry Queue
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Manual morphology plotting and clinical documentation inputs are finalized (`UC_Q4_Annotate`).
|
||||
* **Postconditions (Success State):**
|
||||
* Case files containing structural anomalies bypass standard EMR storage pathways.
|
||||
* Raw image tensors are queued in engineering streams to expand future model capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** identifies the active session as an anomalous anomaly case during final compilation.
|
||||
2. **System** aggregates raw frame tensors, manual coordinate indices, and user-entered clinical commentary blocks into a secure telemetry archive package.
|
||||
3. **System** bypasses standard EMR production database pipelines to protect standard hospital operational data.
|
||||
4. **System** routes the telemetry package directly to the product engineering data pipeline for system optimization and future model training runs.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Serialize Session to Telemetry Queue" as UC_Core
|
||||
}
|
||||
|
||||
UC_Core ..> EMR : (Bypasses standard production EMR sync)
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Serialize Session to Telemetry Queue
|
||||
|
||||
Actor: Hospital EMR System (EMR)
|
||||
DateAdd: June 7, 2026 10:37 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Route anomalous case data directly to engineering telemetry streams while bypassing standard hospital records to protect clinical data pipes
|
||||
Interaction: System-to-System
|
||||
Stimulus: Completion of manual morphology reporting arrays within the clinical investigation interface
|
||||
SysResponse: Packages unencrypted image tensors, coordinate arrays, and user text blocks directly into core product telemetry queues
|
||||
Title [Verb + Noun]: Serialize Session to Telemetry Queue
|
||||
UC-ID: UC-01580
|
||||
VerboseForm: The use case 'Serialize Session to Telemetry Queue' defines a System-to-System interaction where the Hospital EMR System (EMR) aims to Route anomalous case data directly to engineering telemetry streams while bypassing standard hospital records to protect clinical data pipes. This workflow is triggered when Completion of manual morphology reporting arrays within the clinical investigation interface, causing the system to respond by providing Packages unencrypted image tensors, coordinate arrays, and user text blocks directly into core product telemetry queues.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Serialize Session to Telemetry Queue
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Manual morphology plotting and clinical documentation inputs are finalized (`UC_Q4_Annotate`).
|
||||
* **Postconditions (Success State):**
|
||||
* Case files containing structural anomalies bypass standard EMR storage pathways.
|
||||
* Raw image tensors are queued in engineering streams to expand future model capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** identifies the active session as an anomalous anomaly case during final compilation.
|
||||
2. **System** aggregates raw frame tensors, manual coordinate indices, and user-entered clinical commentary blocks into a secure telemetry archive package.
|
||||
3. **System** bypasses standard EMR production database pipelines to protect standard hospital operational data.
|
||||
4. **System** routes the telemetry package directly to the product engineering data pipeline for system optimization and future model training runs.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Serialize Session to Telemetry Queue" as UC_Core
|
||||
}
|
||||
|
||||
UC_Core ..> EMR : (Bypasses standard production EMR sync)
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,54 @@
|
||||
# Log High-Trust Concur Block
|
||||
|
||||
Actor: Hospital EMR System (EMR)
|
||||
DateAdd: June 7, 2026 10:09 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Secure the human-AI alignment log trace within the final diagnostic report payload
|
||||
Interaction: System-to-System
|
||||
Stimulus: Explanatory panel validation completes successfully without user override actions
|
||||
SysResponse: Appends a tamper-evident audit trace block verifying explicit human-AI agreement into the session log cache
|
||||
Title [Verb + Noun]: Log High-Trust Concur Block
|
||||
UC-ID: UC-02423
|
||||
VerboseForm: The use case 'Log High-Trust Concur Block' defines a System-to-System interaction where the Hospital EMR System (EMR) aims to Secure the human-AI alignment log trace within the final diagnostic report payload. This workflow is triggered when Explanatory panel validation completes successfully without user override actions, causing the system to respond by providing Appends a tamper-evident audit trace block verifying explicit human-AI agreement into the session log cache.
|
||||
|
||||

|
||||
|
||||
```markdown
|
||||
|
||||
# Use Case Deep-Dive: Log High-Trust Concur Block
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Multi-modal explanations were fully generated (`UC_Q1_Explain`) and passed without human alteration marks.
|
||||
* **Postconditions (Success State):**
|
||||
* Explicit audit string trace tracking high-trust convergence is formatted for downstream pipeline compilation.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** detects a direct consensus condition where the human expert confirms the model data without text/grading edits.
|
||||
2. **System** serializes the multi-modal text breakdown and pixel attribution coordinates into an immutable log string block.
|
||||
3. **System** assigns an explicit alignment token header flag (`HIGH_TRUST_CONCURRENCE`).
|
||||
4. **System** caches this specialized tracking trace within the localized session state data, making it ready to be appended during final data hand-off routines (`UC_Finalize`).
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Log High-Trust Concur Block" as UC_Core
|
||||
}
|
||||
|
||||
UC_Core ..> EMR : (Prepares payload for final sync)
|
||||
@enduml
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Trigger Conversational Circuit Breaker
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:11 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Intercept premature finalization workflows if interface telemetry reveals friction, hesitation, or cognitive blind-spots
|
||||
Interaction: User-to-System
|
||||
Stimulus: Extended extension trace caught during review steps if user behavior markers diverge from smooth consensus paths
|
||||
SysResponse: Halts default workspace finalization routes and shifts the UI into a mandatory safety evaluation mode
|
||||
Title [Verb + Noun]: Trigger Conversational Circuit Breaker
|
||||
UC-ID: UC-22159
|
||||
VerboseForm: The use case 'Trigger Conversational Circuit Breaker' defines a User-to-System interaction where the UP5 aims to Intercept premature finalization workflows if interface telemetry reveals friction, hesitation, or cognitive blind-spots. This workflow is triggered when Extended extension trace caught during review steps if user behavior markers diverge from smooth consensus paths, causing the system to respond by providing Halts default workspace finalization routes and shifts the UI into a mandatory safety evaluation mode.
|
||||
|
||||

|
||||
|
||||
```markdown
|
||||
|
||||
### Page Body Content (`SpecificationWithDiagram`)
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Trigger Conversational Circuit Breaker
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active session is inside the `UC_Review` workflow phase.
|
||||
* UI layer telemetry captures specific friction indicators (e.g., high-frequency cursor oscillation, repeatedly typing and deleting text, or conflicting grading inputs).
|
||||
* **Postconditions (Success State):**
|
||||
* Direct finalization path is securely locked down.
|
||||
* System-forced conversational validation interface is deployed into view.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** evaluates live workspace telemetry tracking patterns during active case validation.
|
||||
2. **System** detects user behavior triggers signaling high diagnostic friction or potential automatic oversight trends.
|
||||
3. **System** blocks the immediate execution availability of the standard finalization command sequence (`UC_Finalize`).
|
||||
4. **System** transforms workspace panel focus areas to present an interactive confirmation overlay.
|
||||
5. **System** executes `UC_Q2_Socratic` to initialize direct safety check communications.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "Trigger Conversational Circuit Breaker" as UC_Core
|
||||
usecase "Facilitate Socratic Reasoning Dialogue" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Review
|
||||
UC_Review <.. UC_Core : <<extend>> (If clinician friction detected)
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# Expose Pixel-Level Activation Logic
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:21 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Reveal fine-grained layer weights and activation responses when the human specialist challenges an automated grade prediction
|
||||
Interaction: User-to-System
|
||||
Stimulus: Clinician manually alters or rejects the ML-proposed classification score in the review pane
|
||||
SysResponse: Interactive visual mapping showing the exact high-frequency noise regions driving model prediction errors
|
||||
Title [Verb + Noun]: Expose Pixel-Level Activation Logic
|
||||
UC-ID: UC-25637
|
||||
VerboseForm: The use case ' Expose Pixel-Level Activation Logic' defines a User-to-System interaction where the UP5 aims to Reveal fine-grained layer weights and activation responses when the human specialist challenges an automated grade prediction. This workflow is triggered when Clinician manually alters or rejects the ML-proposed classification score in the review pane, causing the system to respond by providing Interactive visual mapping showing the exact high-frequency noise regions driving model prediction errors.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Expose Pixel-Level Activation Logic
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active session is inside the `UC_Review` interface phase.
|
||||
* Specialist chooses an option that breaks clean model agreement paths.
|
||||
* **Postconditions (Success State):**
|
||||
* Internal neural layer weight vectors are visually mapped onto the primary medical viewport.
|
||||
* Core manual artifact isolation tool sets become active on the canvas layout.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **Diagnostic Radiologist** changes the system-suggested grade classification dropdown setting.
|
||||
2. **System** captures the modification step and branches away from the standard review pathway to reveal underlying model mechanics.
|
||||
3. **System** transforms image layers to display fine-grained activation weights, revealing exactly which pixel clusters (e.g., acoustic shadowing regions or bone interfaces) skewed the model's calculation.
|
||||
4. **System** includes `UC_Q3_Isolate` to let the specialist manually clean up the noise zones.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "Expose Pixel-Level Activation Logic" as UC_Core
|
||||
usecase "Isolate Visual Noise/Artifacts" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Review
|
||||
UC_Review <.. UC_Core : <<extend>> (If clinician contests AI score)
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,58 @@
|
||||
# Generate GradCAM & CoT Explanation Panel
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:00 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Present clear, pixel-linked visuospatial explanations and multi-modal clinical reasoning for high-trust verification
|
||||
Interaction: User-to-System
|
||||
Stimulus: Inclusion trigger initialized during session data intake (Load Patient Scan Session)
|
||||
SysResponse: Renders heatmaps highlighting model focus zones alongside structured, clear reasoning steps
|
||||
Title [Verb + Noun]: Generate GradCAM & CoT Explanation Panel
|
||||
UC-ID: UC-25776
|
||||
VerboseForm: The use case 'Generate GradCAM & CoT Explanation Panel' defines a User-to-System interaction where the UP5 aims to Present clear, pixel-linked visuospatial explanations and multi-modal clinical reasoning for high-trust verification. This workflow is triggered when Inclusion trigger initialized during session data intake (Load Patient Scan Session), causing the system to respond by providing Renders heatmaps highlighting model focus zones alongside structured, clear reasoning steps.
|
||||
|
||||

|
||||
|
||||
```markdown
|
||||
|
||||
# Use Case Deep-Dive: Generate GradCAM & CoT Explanation Panel
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Raw image frames and vision engine inference matrix weights have been imported via `Load Patient Scan Session`.
|
||||
* **Postconditions (Success State):**
|
||||
* Split-screen layout displays visual explanation elements without adding visual noise to the core image frame workspace canvas.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** evaluates the internal deep-learning model gradient parameters for the target ultrasound image slice.
|
||||
2. **System** generates a visual GradCAM heatmap layer mapping feature locations that dictated model classifications (e.g., hypervascularized synovial proliferation zones).
|
||||
3. **System** maps multi-modal prompt metrics through the internal LLM Explainer module to produce a concise, point-by-point clinical reasoning string.
|
||||
4. **System** populates the split-screen workspace sub-section block with this explanation data to guide human inspection efficiently.
|
||||
5. **System** includes `UC_Q1_Log` to serialize verification metadata.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Generate GradCAM & CoT Explanation Panel" as UC_Core
|
||||
usecase "Log High-Trust Concur Block" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# Activate Clinical Investigation Mode
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:29 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Switch the system into a strict, template-driven manual examination mode when low vision confidence values align with a lack of reference data
|
||||
Interaction: User-to-System
|
||||
Stimulus: The workspace detects a critical double-blind failure criteria match during the case evaluation phase
|
||||
SysResponse: Disables automated diagnostic suggestions entirely and forces a standardized manual morphology review
|
||||
Title [Verb + Noun]: Activate Clinical Investigation Mode
|
||||
UC-ID: UC-35956
|
||||
VerboseForm: The use case 'Activate Clinical Investigation Mode' defines a User-to-System interaction where the UP5 aims to Switch the system into a strict, template-driven manual examination mode when low vision confidence values align with a lack of reference data. This workflow is triggered when The workspace detects a critical double-blind failure criteria match during the case evaluation phase, causing the system to respond by providing Disables automated diagnostic suggestions entirely and forces a standardized manual morphology review.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Activate Clinical Investigation Mode
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* System classification loops return low-confidence indices.
|
||||
* Authoritative RAG reference lookups return no matches, indicating an unmapped anatomical variant or a severe image anomaly.
|
||||
* **Postconditions (Success State):**
|
||||
* Automated suggestions are masked out to prevent cognitive bias.
|
||||
* Mandatory manual template verification frameworks are deployed into active workspace view.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** monitors deep-learning inference score bounds during case evaluation.
|
||||
2. **System** runs background reference data lookups and catches a dual failure state (Low confidence + Empty knowledge reference).
|
||||
3. **System** drops the standard review interface layout to prevent automated suggestion bias or human misinterpretation loops.
|
||||
4. **System** changes UI display focus markers to activate an explicit, template-driven investigation layout.
|
||||
5. **System** includes `UC_Q4_Annotate` to force manual measurement entries.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "Activate Clinical Investigation Mode" as UC_Core
|
||||
usecase "Execute Structured Morphology Annotation" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Review
|
||||
UC_Review <.. UC_Core : <<extend>> (If low confidence & empty RAG)
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,61 @@
|
||||
# Execute Structured Morphology Annotation
|
||||
|
||||
Actor: UNK
|
||||
DateAdd: June 7, 2026 10:32 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Force the manual plotting of anatomical coordinates and morphological anomalies using a strict, un-biased framework
|
||||
Interaction: User-to-System
|
||||
Stimulus: The workspace forces a manual review layout via the active escalation workflow step
|
||||
SysResponse: Interactive visual mapping showing the exact high-frequency noise regions driving model prediction errors
|
||||
Title [Verb + Noun]: Execute Structured Morphology Annotation
|
||||
UC-ID: UC-47796
|
||||
VerboseForm: The use case 'Execute Structured Morphology Annotation' defines a User-to-System interaction where the UNK aims to Force the manual plotting of anatomical coordinates and morphological anomalies using a strict, un-biased framework. This workflow is triggered when The workspace forces a manual review layout via the active escalation workflow step, causing the system to respond by providing Interactive visual mapping showing the exact high-frequency noise regions driving model prediction errors.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Execute Structured Morphology Annotation
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* System UI layer has transitioned to manual investigation mode parameters (`UC_Q4_Escalate`).
|
||||
* **Postconditions (Success State):**
|
||||
* Specialist successfully plots manual structural bounds.
|
||||
* Text verification parameters capture explicit clinical observations.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** displays an empty, un-biased ultrasound canvas frame alongside a series of mandatory measurement fields.
|
||||
2. **Diagnostic Radiologist** plots coordinate points across the canvas layer to outline the boundaries of the anomalous tissue.
|
||||
3. **Diagnostic Radiologist** manually populates text fields describing structural observations (e.g., bone fragments or atypical lesion shapes).
|
||||
4. **System** compiles these manual coordinates and comments into a detailed case record.
|
||||
5. **System** includes `UC_Q4_Queue` to route the data directly to optimization pipelines.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Execute Structured Morphology Annotation" as UC_Core
|
||||
usecase "Serialize Session to Telemetry Queue" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core : Document manual findings
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,74 @@
|
||||
# Review Suggested Synovitis Grade (0-3)
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 9:54 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Evaluate the ML engine's (VKIST-visual detector & classifier) proposed synovitis classification and structural overlays
|
||||
Interaction: User-to-System
|
||||
Stimulus: The workspace completes localized UI construction and displays the diagnostic panel
|
||||
SysResponse: Display of classification metrics (Grades 0-3), color-coded overlays, and active risk-extension hooks
|
||||
Title [Verb + Noun]: Review Suggested Synovitis Grade (0-3)
|
||||
UC-ID: UC-47988
|
||||
VerboseForm: The use case 'Review Suggested Synovitis Grade (0-3)' defines a User-to-System interaction where the UP5 aims to Evaluate the ML engine's (VKIST-visual detector & classifier) proposed synovitis classification and structural overlays. This workflow is triggered when The workspace completes localized UI construction and displays the diagnostic panel, causing the system to respond by providing Display of classification metrics (Grades 0-3), color-coded overlays, and active risk-extension hooks.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Review Suggested Synovitis Grade (0-3)
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Image frames and raw ML prediction tensors (segmentation masks, classification weights) are fully loaded in memory via `Load Patient Scan Session`.
|
||||
* **Postconditions (Success State):**
|
||||
* System records human gaze/interaction initialization flags.
|
||||
* System keeps exception-based extend vectors armed (`UC_Q2_Intercept`, `UC_Q3_Expose`, `UC_Q4_Escalate`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** presents the active ultrasound canvas with interactive, toggleable, color-coded segmentation mask overlays.
|
||||
2. **System** displays the vision engine's suggested synovitis grading estimation (Grade 0, 1, 2, or 3) alongside structural pixel-percentage distribution metrics.
|
||||
3. **Diagnostic Radiologist** inspects the spatial distribution of the synovial hypertrophy markers and reads the inline text panels.
|
||||
4. **Diagnostic Radiologist** approves the visual data metrics without requesting alterations or triggering corrective dialogue paths.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Extension Flow A: Clinician Friction / Disagreement Caught**
|
||||
* At step [3], if mouse click frequencies suggest hesitation or manual adjustments cross a conflict delta threshold, the execution path triggers `UC_Q2_Intercept` to prevent blind override errors.
|
||||
* **Extension Flow B: Expert Contests Automated Grade**
|
||||
* At step [3], if the clinician explicitly changes the classification dropdown away from the ML-proposed score, the workspace extends to `UC_Q3_Expose` to display the machine activation weights.
|
||||
* **Extension Flow C: Anomaly / Confidence Failure Detected**
|
||||
* At step [1], if the deep-learning array returned a classification confidence metric below safety bounds paired with blank knowledge base lookups, the interface branches into `UC_Q4_Escalate`.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Core
|
||||
usecase "Trigger Conversational Circuit Breaker" as UC_Q2
|
||||
usecase "Expose Pixel-Level Activation Logic" as UC_Q3
|
||||
usecase "Activate Clinical Investigation Mode" as UC_Q4
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core <.. UC_Q2 : <<extend>>
|
||||
UC_Core <.. UC_Q3 : <<extend>>
|
||||
UC_Core <.. UC_Q4 : <<extend>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
/image.png)
|
||||
@@ -0,0 +1,66 @@
|
||||
# Load Patient Scan Session
|
||||
|
||||
Actor: UP5, VKIST Vision Grader Engine (Grader)
|
||||
DateAdd: June 6, 2026 1:01 AM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Ingest raw ultrasound frame arrays and initialize the diagnostic session state
|
||||
Interaction: System-to-System, User-to-System
|
||||
Stimulus: User opens an unreviewed patient file, or the workspace catches an active DICOM stream hook
|
||||
SysResponse: Confirmation that raw frame arrays are mapped, spatial calibrations are set, and the local session state is active
|
||||
Title [Verb + Noun]: Load Patient Scan Session
|
||||
UC-ID: UC-48376
|
||||
VerboseForm: The use case 'Load Patient Scan Session' defines a User-to-System,System-to-System interaction where the UP5, VKIST Vision Grader Engine (Grader) aims to Ingest raw ultrasound frame arrays and initialize the diagnostic session state. This workflow is triggered when User opens an unreviewed patient file, or the workspace catches an active DICOM stream hook, causing the system to respond by providing Confirmation that raw frame arrays are mapped, spatial calibrations are set, and the local session state is active.
|
||||
|
||||
```markdown
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Local workspace application is authenticated and has secure socket access to the local image buffer.
|
||||
* DICOM/raw frame data payload is uncorrupted and readable.
|
||||
* **Postconditions (Success State):**
|
||||
* Core frame parameters are loaded into memory with spatial scale calibrations preserved.
|
||||
* Background parsing pipeline registers the unique session hash and prepares the context matrix for downstream agents.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **Diagnostic Radiologist** selects a patient case file from the workspace worklist interface.
|
||||
2. **VKIST Vision Grader Engine** feeds raw ultrasound image tensors, spatial calibrations, and foundational frame telemetry metadata into the workspace memory layer.
|
||||
3. **System** extracts pixel dimensions and constructs localized rendering viewports.
|
||||
4. **System** includes `UC_Q1_Explain` in the background to spin up explanation prompt matrices.
|
||||
5. **System** displays the fully loaded image frame in the workspace canvas, preparing the viewport for immediate review.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Exception Flow A: Corrupted Image Frame Payload**
|
||||
* At step [2], if the payload data fails format validation or structural check headers, the system halts execution, logs a data corruption fault code, and alerts the user with an "Unable to Parse Scan Session" dialog box.
|
||||
* **Exception Flow B: Resolution / Calibration Mismatch**
|
||||
* At step [3], if spatial aspect ratios or metadata pixel matrices lack the standardized calibration tags required by the vision engine, the workspace falls back to a safe default scale flag and displays a non-blocking diagnostic accuracy warning icon.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
actor "VKIST Vision Grader Engine" as Grader << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Load Patient Scan Session" as UC_Core
|
||||
usecase "Generate GradCAM & CoT Explanation Panel" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
Grader --> UC_Core
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,59 @@
|
||||
# Facilitate Socratic Reasoning Dialogue
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:14 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Engage the specialist in a targeted, conversational double-check loop regarding controversial structural markers.
|
||||
Interaction: User-to-System
|
||||
Stimulus: Core execution request passed down by the active circuit breaker module.
|
||||
SysResponse: Interactive conversational sub-panel displaying focused prompt choices that check specific diagnostic criteria
|
||||
Title [Verb + Noun]: Facilitate Socratic Reasoning Dialogue
|
||||
UC-ID: UC-55146
|
||||
VerboseForm: The use case 'Facilitate Socratic Reasoning Dialogue' defines a User-to-System interaction where the UP5 aims to Engage the specialist in a targeted, conversational double-check loop regarding controversial structural markers.. This workflow is triggered when Core execution request passed down by the active circuit breaker module., causing the system to respond by providing Interactive conversational sub-panel displaying focused prompt choices that check specific diagnostic criteria.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Facilitate Socratic Reasoning Dialogue
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Circuit breaker safety intercept sequence has completed successfully, freezing generic CRUD paths.
|
||||
* **Postconditions (Success State):**
|
||||
* User inputs conversational defense arguments or confirms specific anatomical findings.
|
||||
* Live conversation data tokens are actively streamed to automated safety monitors.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** initializes a conversational chat element right next to the ultrasound display field.
|
||||
2. **System** presents a non-confrontational, clinically grounded question regarding the identified discrepancies (e.g., *"Note the echo-free thickening layer in the suprapatellar recess; please confirm if this modification represents minor effusion or structural pannus tissue"*).
|
||||
3. **Diagnostic Radiologist** enters text responses or selects structural tag tokens to clarify their assessment.
|
||||
4. **System** includes `UC_Q2_BERT` in real time to process active conversation token patterns.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Facilitate Socratic Reasoning Dialogue" as UC_Core
|
||||
usecase "Monitor Drift via BERT Sub-Layer" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core : Argue observations
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,60 @@
|
||||
# Isolate Visual Noise/Artifacts
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:24 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Provide manual brush and selection overlays to mask out acoustic shadows, bone scattering, or artifacts causing model calculation errors.
|
||||
Interaction: User-to-System
|
||||
Stimulus: Human operator activates canvas cleanup tools within the exposed model layer layout
|
||||
SysResponse: Real-time visual updates to the pixel mask array, isolating clean anatomical structures from surrounding imaging noise
|
||||
Title [Verb + Noun]: Isolate Visual Noise/Artifacts
|
||||
UC-ID: UC-60739
|
||||
VerboseForm: The use case 'Isolate Visual Noise/Artifacts' defines a User-to-System interaction where the UP5 aims to Provide manual brush and selection overlays to mask out acoustic shadows, bone scattering, or artifacts causing model calculation errors.. This workflow is triggered when Human operator activates canvas cleanup tools within the exposed model layer layout, causing the system to respond by providing Real-time visual updates to the pixel mask array, isolating clean anatomical structures from surrounding imaging noise.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Isolate Visual Noise/Artifacts
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* System exposure arrays are visible across the image viewport layout (`UC_Q3_Expose`).
|
||||
* **Postconditions (Success State):**
|
||||
* Corrected ground-truth frame masks are calculated and locked into memory.
|
||||
* System updates local diagnostic metrics using the isolated anatomical data.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** activates a manual canvas tool overlay, giving the user access to high-precision brush, eraser, and selection vectors.
|
||||
2. **Diagnostic Radiologist** applies brush vectors directly over areas containing acoustic artifacts or non-synovial structures that skewed the automated classification score.
|
||||
3. **System** recalculates active region dimensions in real time, excluding the masked pixels from the active grading parameters.
|
||||
4. **System** updates diagnostic panel displays to confirm the human-corrected measurements.
|
||||
5. **System** includes `UC_Q3_Commit` to lock the updated session state securely.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Isolate Visual Noise/Artifacts" as UC_Core
|
||||
usecase "Commit Validated Ground-Truth Record" as UC_Sub
|
||||
}
|
||||
|
||||
Rad --> UC_Core : Tag artifacts
|
||||
UC_Core ..> UC_Sub : <<include>>
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,57 @@
|
||||
# Commit Validated Ground-Truth Record
|
||||
|
||||
Actor: Hospital EMR System (EMR)
|
||||
DateAdd: June 7, 2026 10:26 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Secure the human-corrected ground-truth dataset variant while appending clean, expert-validated report payloads to the EMR
|
||||
Interaction: System-to-System
|
||||
Stimulus: Completion of manual artifact masking operations and confirmation of corrected metrics
|
||||
SysResponse: Stores the corrected medical report in the EMR and saves the isolated image mask to an optimization cache for subsequent retraining
|
||||
Title [Verb + Noun]: Commit Validated Ground-Truth Record
|
||||
UC-ID: UC-62864
|
||||
VerboseForm: The use case 'Commit Validated Ground-Truth Record' defines a System-to-System interaction where the Hospital EMR System (EMR) aims to Secure the human-corrected ground-truth dataset variant while appending clean, expert-validated report payloads to the EMR. This workflow is triggered when Completion of manual artifact masking operations and confirmation of corrected metrics, causing the system to respond by providing Stores the corrected medical report in the EMR and saves the isolated image mask to an optimization cache for subsequent retraining.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Commit Validated Ground-Truth Record
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Human-directed canvas modification steps are locked in place without remaining pixel parity errors (`UC_Q3_Isolate`).
|
||||
* **Postconditions (Success State):**
|
||||
* EMR database updates receive the human expert's diagnostic findings.
|
||||
* Isolated ground-truth tensor pairs are safely cached for AI training refinement runs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** packages the human expert's corrected diagnostic data metrics into the primary transmission bundle.
|
||||
2. **System** isolates the human-brushed image mask layers alongside the initial incorrect model classification output.
|
||||
3. **System** tags the data pair as a validated retraining asset (`GROUND_TRUTH_OVERRIDE`).
|
||||
4. **System** saves the optimization asset to a secure local retraining storage folder, while preparing the primary medical report for delivery to the **Hospital EMR System**.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Commit Validated Ground-Truth Record" as UC_Core
|
||||
}
|
||||
|
||||
UC_Core ..> EMR : (Prepares clean report for EMR sync)
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,59 @@
|
||||
# Arbitrate Evidence via RAG-Referee
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:18 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Query static, authoritative clinical knowledge bases to resolve human-machine disagreements with objective evidence
|
||||
Interaction: User-to-System
|
||||
Stimulus: Triggered when communication tracking scores cross a severe semantic mismatch or impasse threshold
|
||||
SysResponse: Inline injection of un-biased diagnostic text extracts and guidelines matching the active frame conditions
|
||||
Title [Verb + Noun]: Arbitrate Evidence via RAG-Referee
|
||||
UC-ID: UC-65473
|
||||
VerboseForm: The use case 'Arbitrate Evidence via RAG-Referee' defines a User-to-System interaction where the UP5 aims to Query static, authoritative clinical knowledge bases to resolve human-machine disagreements with objective evidence. This workflow is triggered when Triggered when communication tracking scores cross a severe semantic mismatch or impasse threshold, causing the system to respond by providing Inline injection of un-biased diagnostic text extracts and guidelines matching the active frame conditions.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Arbitrate Evidence via RAG-Referee
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* BERT analytics layers detect a diagnostic impasse or significant semantic drift.
|
||||
* Authoritative local clinical knowledge base index (e.g., OMERACT synovitis grading reference manuals) is online and responsive.
|
||||
* **Postconditions (Success State):**
|
||||
* Disagreement matrix is resolved via verified medical data injection.
|
||||
* Final chosen path is linked directly to a standard medical guideline anchor.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** halts active conversational dialogue inputs temporarily to execute a localized context search.
|
||||
2. **System** extracts spatial measurements and text tokens to construct a specialized RAG search string.
|
||||
3. **System** queries local, validated medical knowledge data banks to locate matching diagnostic criteria sections.
|
||||
4. **System** displays the verified guideline text extract right inside the workspace alert view block (e.g., *"OMERACT standardizes Grade 2 as hypoechoic synovial hypertrophy demonstrating fluid-filled distension up to structural boundary bounds"*).
|
||||
5. **Diagnostic Radiologist** reviews the authoritative reference framework and either adjusts their classification choice or submits a structured expert override justifying their deviation.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Arbitrate Evidence via RAG-Referee" as UC_Core
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
@enduml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,64 @@
|
||||
# Monitor Context Drift via BERT Sub-Layer
|
||||
|
||||
Actor: UP5
|
||||
DateAdd: June 7, 2026 10:17 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Continuously parse communication tokens to identify logical contradictions or semantic drift during clinical debates
|
||||
Interaction: User-to-System
|
||||
Stimulus: Streamed entry of communication tokens within the active dialogue loop
|
||||
SysResponse: Real-time semantic checking flags; extends out to the RAG referee if an impasse or severe drift is captured
|
||||
Title [Verb + Noun]: Monitor Context Drift via BERT Sub-Layer
|
||||
UC-ID: UC-74821
|
||||
VerboseForm: The use case 'Monitor Context Drift via BERT Sub-Layer' defines a User-to-System interaction where the UP5 aims to Continuously parse communication tokens to identify logical contradictions or semantic drift during clinical debates. This workflow is triggered when Streamed entry of communication tokens within the active dialogue loop, causing the system to respond by providing Real-time semantic checking flags; extends out to the RAG referee if an impasse or severe drift is captured.
|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Monitor Drift via BERT Sub-Layer
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active conversational dialogue module is processing user data strings (`UC_Q2_Socratic`).
|
||||
* **Postconditions (Success State):**
|
||||
* Log structures capture semantic alignment metrics.
|
||||
* System successfully catches contradictions before data parameters flow to final storage.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **System** continuously intercepts conversation tokens as the human expert types input strings.
|
||||
2. **System** runs token matrices through an embedded BERT checking model to calculate contextual semantic coherence scores.
|
||||
3. **System** verifies that user claims line up logically with the visual indicators under review.
|
||||
4. **System** approves the validated conversational step, allowing the specialist to complete the confirmation cycle smoothly.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Extension Flow A: Impasse or Semantic Contradiction Detected**
|
||||
* At step [3], if the specialist's input text contradicts objective structural metrics (e.g., claiming a region is "completely normal" while the visual layer registers massive synovial proliferation) or exhibits context drift, the process branches into `UC_Q2_Arbiter` to request evidence evaluation.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Monitor Drift via BERT Sub-Layer" as UC_Core
|
||||
usecase "Arbitrate Evidence via RAG-Referee" as UC_Ext
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core <.. UC_Ext : <<extend>> (If impasse or semantic drift caught)
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||

|
||||
@@ -0,0 +1,69 @@
|
||||
# Finalize & Sign Electronic Record
|
||||
|
||||
Actor: Hospital EMR System (EMR), UP5
|
||||
DateAdd: June 7, 2026 9:54 PM
|
||||
Engineer: Đạt Trần Tiến (Daves Tran)
|
||||
Functional Requirement Engineer DB: CHUẨN ĐOÁN Phân loại Mức độ Viêm Khớp gối (https://app.notion.com/p/CHU-N-O-N-Ph-n-lo-i-M-c-Vi-m-Kh-p-g-i-375f910aea75800199d4feb8b07f9145?pvs=21)
|
||||
Goal: Authenticate, cryptographically seal, and sync verified diagnostic reports down to storage infrastructure
|
||||
Interaction: System-to-System, User-to-System
|
||||
Stimulus: User executes the final confirmation/signature command button in the workspace utility ribbon
|
||||
SysResponse: Generation of a signed cryptographic log block and structured JSON transmission payload delivered to the EMR endpoint
|
||||
Title [Verb + Noun]: Finalize & Sign Electronic Record
|
||||
UC-ID: UC-92006
|
||||
VerboseForm: The use case 'Finalize & Sign Electronic Record' defines a User-to-System,System-to-System interaction where the UP5, Hospital EMR System (EMR) aims to Authenticate, cryptographically seal, and sync verified diagnostic reports down to storage infrastructure. This workflow is triggered when User executes the final confirmation/signature command button in the workspace utility ribbon, causing the system to respond by providing Generation of a signed cryptographic log block and structured JSON transmission payload delivered to the EMR endpoint.
|
||||
|
||||

|
||||
|
||||
```markdown
|
||||
|
||||
```markdown
|
||||
# Use Case Deep-Dive: Finalize & Sign Electronic Record
|
||||
|
||||
## 1. Structural Preconditions & Postconditions
|
||||
* **Preconditions:**
|
||||
* Active scan session evaluation has been resolved, and grading metrics are verified by the human specialist.
|
||||
* Local localized network channel to the hospital server framework is functional.
|
||||
* **Postconditions (Success State):**
|
||||
* Session record is transformed into a read-only state.
|
||||
* Standardized structural JSON payload data is safely stored within the Hospital EMR System sink.
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Scenarios (Step-by-Step Flow)
|
||||
|
||||
### Main Success Scenario (Happy Path)
|
||||
1. **Diagnostic Radiologist** initiates the session finalization pipeline by interacting with the cryptographic signature command trigger.
|
||||
2. **System** prompts for the secure authentication credentials of the signing specialist.
|
||||
3. **System** generates a unified clinical log structure, packing structural thickness measurements (mm), final validated synovitis tier scores, and accompanying multi-agent trace logs.
|
||||
4. **System** calculates a secure cryptographic data hash, locking the session record into an immutable post-review profile.
|
||||
5. **System** delivers the structured data package across localized network pipes to the **Hospital EMR System**.
|
||||
6. **Hospital EMR System** confirms safe database commit storage updates and provides an acknowledgment packet back to the workspace.
|
||||
|
||||
### Alternative & Exception Flows
|
||||
* **Exception Flow A: Network Pipeline Transmission Failure**
|
||||
* At step [5], if network communications timeout or socket breaks occur, the workspace locks the finalized JSON package into a local encrypted offline buffer, changes the session status tag to "Pending Sync", and presents a clear connectivity warning alert.
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Visual Model
|
||||
```plantuml
|
||||
@startuml
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
actor "Diagnostic Radiologist" as Rad
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
|
||||
rectangle "VKIST MSK Workspace (FR-25)" {
|
||||
usecase "Finalize & Sign Electronic Record" as UC_Core
|
||||
}
|
||||
|
||||
Rad --> UC_Core
|
||||
UC_Core ..> EMR : Sync standardized structural JSON data
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -0,0 +1,282 @@
|
||||
# CONTEXT.md for the Usecase Discovery : FR-25
|
||||
|
||||
- The requirement: FR-25 == ULTS-Đánh giá và phân cấp mức độ viêm màng hoạt dịch (Synovitis Grading)
|
||||
|
||||
- Explain why the se should care on this usecase in the design process
|
||||
- **The Gap:** Lợi ích kỹ thuật & Giá trị cốt lõi của Use Case Phân cấp Viêm màng hoạt dịch đối với Kiến trúc Phần mềm (Missing Clinical rationale vs. Engineering value optimization for Synovitis Grading).
|
||||
- **The Question:** Tại sao quy trình lâm sàng chuẩn hóa này lại đảm bảo tính chính xác khi phân cấp (`Synovitis Grading`) và tại sao một Kỹ sư Phần mềm (SE) phải đặc biệt quan tâm đến Use Case này khi thiết kế sản phẩm?
|
||||
- **The Hint:** Để xây dựng một sản phẩm Y tế (Healthcare AI Product) thành công, chúng ta không được coi Use Case này chỉ là một tính năng CRUD hay hiển thị ảnh đơn thuần. Hiểu rõ bản chất toán học/vật lý của quy trình giúp SE thiết kế cơ chế lưu trữ dữ liệu (Data Schema), cấu hình Pipeline xử lý ảnh AI và tối ưu hóa trải nghiệm UIX không bị lỗi logic giải phẫu.
|
||||
- **The Recommendations:** Dưới đây là câu trả lời phân tích sâu dưới góc nhìn của một Kỹ sư Hệ thống / Lập trình viên:
|
||||
|
||||
### PHẦN 1: Tại sao quy trình này đảm bảo việc phát hiện và phân cấp chính xác? (Góc nhìn Data Pipeline & Signal Processing)
|
||||
|
||||
Nếu coi cơ thể người là một hệ thống phần cứng và máy siêu âm là một module quét dữ liệu ngoại vi (Hardware Scanner), quy trình 6 bước lâm sàng chính là các điều kiện tiền đề để **đảm bảo tính toàn vẹn của tín hiệu (Signal Integrity)** và ngăn chặn **nhiễu dữ liệu (Data Corruption)**:
|
||||
|
||||
1. **Khử nhiễu biên (Eliminating Boundary Noise - Bước 1):** việc gập đầu gối 20°–30° tương đương với việc thực hiện lệnh `Format / Standardize` bề mặt quét. Nó triệt tiêu hiện tượng *Bất đẳng hướng âm học (Acoustic Anisotropy)* – vốn là một dạng nhiễu tín hiệu vật lý khiến mô khỏe mạnh bị biến đổi thành các pixel đen giả lập tổn thương.
|
||||
2. **Cố định Hệ tọa độ (Fixing Coordinate System - Bước 2):** Đặt đầu dò dọc (`Longitudinal`) giúp mô hình AI thu được một contract dữ liệu ảnh tĩnh có cấu trúc giải phẫu phân tầng rõ ràng (`Multi-layered Frame`). Đỉnh xương bánh chè hoạt động như một điểm mốc $X, Y = (0,0)$ cố định để hệ thống chạy Edge Detection chuẩn xác.
|
||||
3. **Phân tích Đa luồng song song (Multi-threading Analytics - Bước 3 & 4):**
|
||||
- **Luồng B-Mode (Cấu trúc Hình học):** Trích xuất độ dày mô (`float thickness_mm`) $\rightarrow$ Phản ánh dung lượng thiệt hại vật lý tĩnh (Structural Damage).
|
||||
- **Luồng Power Doppler (Lưu lượng Biến động):** Trích xuất mật độ màu của dòng máu (`float vascular_percentage`) $\rightarrow$ Phản ánh lưu lượng dữ liệu thời gian thực đang chạy (Active Inflammation).
|
||||
4. **Tránh lỗi nén hệ thống (Avoiding Signal Throttling):** Việc lướt nhẹ tay đầu dò (minimal pressure) giữ cho luồng truyền dẫn tín hiệu mạch máu không bị bóp nghẹt (Throttling), tránh việc hệ thống tính toán sai lệch điểm số hoạt tử/viêm mạch dẫn đến kết quả âm tính giả.
|
||||
|
||||
### PHẦN 2: Tại sao Kỹ sư Phần mềm (SE) phải đặc biệt quan tâm đến Use Case này?
|
||||
|
||||
Từ góc nhìn sản phẩm và kiến trúc hệ thống của dự án `VKIST_ULTRASOUND`, đây không phải là một tính năng bổ sung, mà chính là **Core Core Business Logic (Lõi nghiệp vụ quyết định)** vì các lý do sau:
|
||||
|
||||
### 1. Định hình Data Model (Schema) cho toàn bộ hệ thống
|
||||
|
||||
Nếu không hiểu quy trình này, bạn sẽ thiết kế cơ sở dữ liệu bị thiếu trường dữ liệu nghiêm trọng. Điểm số `Synovitis Grade` không thể lưu dưới dạng một trường `int grade` đơn giản. Dữ liệu y khoa chuẩn hóa bắt buộc phải là một đối tượng phức hợp (Compound Object Model):
|
||||
|
||||
JSON
|
||||
|
||||
```
|
||||
{
|
||||
"patient_id": "BN-10023",
|
||||
"scan_metadata": {
|
||||
"joint": "KNEE",
|
||||
"side": "RIGHT",
|
||||
"plane": "suprapat-long",
|
||||
"patient_flexion_degree": 25
|
||||
},
|
||||
"extracted_metrics": {
|
||||
"synovial_thickness_mm": 4.2,
|
||||
"power_doppler_area_percentage": 34.5
|
||||
},
|
||||
"severity_classification": {
|
||||
"suggested_grade": 2,
|
||||
"confirmed_grade": 2,
|
||||
"is_overridden_by_doctor": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Kích hoạt State Machine & Pipeline xử lý của AI (Mô tả trong tài liệu VKIST)
|
||||
|
||||
Theo tài liệu kiến trúc của hệ thống, Use Case này là điểm kết thúc (`Final Destination`) của một Pipeline phân nhánh phức tạp. Khi ảnh DICOM/Siêu âm được đẩy lên hệ thống:
|
||||
|
||||
- **Mô hình 1 (ConvNeXt):** Kiểm tra góc chụp. Nếu và chỉ nếu kết quả trả về đúng `sup_up_long`, hệ thống mới kích hoạt State tiếp theo.
|
||||
- **Mô hình 2 (EfficientNet/MedViT):** Kiểm tra trạng thái phân loại nhị phân `Has Inflammation = True/False`.
|
||||
- **Mô hình 3 (MedSAM/UNet):** Tiến hành phân đoạn vùng ảnh (Segmentation) tạo Mask đè màu để tính toán diện tích pixel bị tổn thương, từ đó tự động map ra điểm số Phân cấp từ 0 đến 3.
|
||||
|
||||
Nếu Use Case này bị lỗi hoặc UIX thiết kế không khớp, toàn bộ Pipeline AI trị giá hàng nghìn giờ huấn luyện phía sau sẽ mất hoàn toàn giá trị sử dụng thực tế.
|
||||
|
||||
|
||||
### 3. Giải quyết bài toán Tải trọng hệ thống và Kiệt sức lâm sàng (Clinical Exhaustion)
|
||||
|
||||
Tại các bệnh viện công tuyến trung ương của Việt Nam (Bạch Mai, Bệnh viện E), một bác sĩ (UP5) phải xử lý >100 ca/ca trực.
|
||||
|
||||
- Nếu không có phần mềm: Bác sĩ phải đổi ghế, chuyển ứng dụng, gõ tay kết quả vào hệ thống EMR thủ công $\rightarrow$ Nút thắt cổ chai gây delay hệ thống (Latency).
|
||||
- Khi có Use Case này được tự động hóa trên Dashboard: AI tính toán diện tích, trả về gợi ý bậc viêm ngay trên canvas xem ảnh, bác sĩ chỉ mất đúng 1 click chuột để xác nhận và Sync tự động sang Database tổng. Bạn đang giúp bệnh viện giảm thời gian xử lý một ca từ 5 phút xuống còn 30 giây, trực tiếp tối ưu hóa thông lượng (Throughput) cho toàn bộ hạ tầng y tế.
|
||||
- **The Alignment Check:** Với góc nhìn bóc tách hệ thống và phân tích Pipeline tín hiệu/AI như trên, bạn đã thấy rõ được bài toán kiến trúc mình cần giải quyết ở Use Case này chưa, hay có điểm nào cần làm sáng tỏ thêm trước khi chúng ta vẽ sơ đồ Use Case bằng PlantUML?
|
||||
- Explain Why doctor are caring so much on this usecase
|
||||
- **The Gap:** Hiểu rõ động lực lâm sàng và giá trị chẩn đoán thực tế của Bác sĩ (Missing Clinical Drivers and Real-World Diagnostic Insights for Synovitis Grading).
|
||||
- **The Question:** Tại sao Bác sĩ Chẩn đoán hình ảnh (UP5) lại đặc biệt quan tâm đến Use Case Phân cấp Viêm màng hoạt dịch (`Synovitis Grading`), và chỉ số này tiết lộ điều gì cốt lõi trong hành trình chẩn đoán, điều trị của bệnh nhân?
|
||||
- **The Hint:** Trong y khoa, bản thân hình ảnh siêu âm xám chỉ là dữ liệu thô (raw data). Kết quả phân cấp từ Use Case này chính là **thông tin có cấu trúc (structured insights)** giúp bác sĩ trả lời câu hỏi cốt lõi của lâm sàng: Bệnh nhân này đang bị tàn phá khớp ở mức độ nào, tổn thương này là mạn tính (sẹo mô) hay cấp tính (đang bùng phát), và phác đồ điều trị bằng thuốc hoặc can thiệp ngoại khoa nào là chính xác nhất.
|
||||
- **The Recommendations:** Dưới đây là bóc tách chi tiết lý do vì sao bác sĩ cần Use Case này, được phân tích rõ ràng để một Kỹ sư Phần mềm nắm bắt trọn vẹn nghiệp vụ (Domain Knowledge):
|
||||
|
||||
### 1. Phân biệt Giữa "Tổn thương Cũ" (Mạn tính) và "Đợt Viêm Cấp" (Đang bùng phát)
|
||||
|
||||
- **Ý nghĩa lâm sàng:** Khi nhìn vào ảnh siêu âm đen trắng (B-mode), bác sĩ thấy một vùng màng hoạt dịch dày lên (ví dụ: dày 4mm). Tuy nhiên, ảnh đen trắng đơn thuần **không thể** cho biết vùng phì đại đó là vết sẹo cũ từ 3 năm trước (mô xơ đã ổn định) hay là vùng mô đang liên tục sưng tấy, ăn mòn sụn khớp.
|
||||
- **Use Case tiết lộ điều gì:** Bằng cách kết hợp luồng dữ liệu của **Power Doppler**, Use Case này bóc tách và định lượng chính xác mật độ mạch máu tăng sinh (`Hypervascularity`).
|
||||
- *Dày mô + Không có tín hiệu Doppler (Grade 1):* Tổn thương cũ, chỉ cần theo dõi hoặc vật lý trị liệu.
|
||||
- *Dày mô + Tín hiệu Doppler dày đặc (Grade 3):* Ổ viêm đang hoạt động cực kỳ dữ dội. Hệ thống miễn dịch của bệnh nhân đang tấn công nhầm vào chính các tế bào khớp gối, giải phóng hàng loạt enzyme ăn mòn sụn và xương. Bác sĩ cần phải can thiệp ngay lập tức bằng thuốc ức chế miễn dịch mạnh (như Corticoid hoặc DMARDs) để chặn đứng dòng thác phá hủy này.
|
||||
|
||||
### 2. Điểm Số Quyết Định Phác Đồ Điều Trị (Actionable Clinical Metric)
|
||||
|
||||
Điểm số Phân cấp từ 0 đến 3 không phải là một cái tag hiển thị cho đẹp, nó hoạt động giống như một **luồng điều hướng logic (Decision Tree)** quyết định trực tiếp hành động lâm sàng của bác sĩ:
|
||||
|
||||
- **Grade 0 (Bình thường):** Chuyển bệnh nhân sang chế độ phòng ngừa, xuất viện.
|
||||
- **Grade 1 (Nhẹ):** Chỉ định điều trị nội khoa bảo tồn ở mức độ thấp (Dùng thuốc kháng viêm không Steroid - NSAIDs, thay đổi lối sống, tập vật lý trị liệu với bác sĩ PT - UP6).
|
||||
- **Grade 2 (Vừa):** Cân nhắc tiêm thuốc nội khớp (tiêm Corticoid trực tiếp vào ngách khớp gối để dập dịch viêm tại chỗ) kết hợp điều trị thuốc đặc hiệu.
|
||||
- **Grade 3 (Nặng/Nghiêm trọng):** Rút dịch khớp (Arthrocentesis) để giảm áp lực, chỉ định nhập viện điều trị tích cực, hoặc chuyển tuyến ngoại khoa để thực hiện phẫu thuật nội soi cắt màng hoạt dịch (Synovectomy) nhằm cứu lấy sụn khớp trước khi gối bị biến dạng hoàn toàn không thể phục hồi.
|
||||
|
||||
### 3. Thước Đo Khách Quan Để Đánh Giá Hiệu Quả Thuốc (Treatment Monitoring Dashboard)
|
||||
|
||||
- **Nỗi đau của Bác sĩ:** Khi điều trị các bệnh tự miễn như Viêm khớp dạng thấp, bệnh nhân phải uống thuốc ròng rã nhiều tháng trời. Nếu bác sĩ chỉ hỏi bệnh nhân *"Bác có bớt đau không?"*, câu trả lời sẽ cực kỳ chủ quan và không chính xác (do ngưỡng chịu đau của mỗi người khác nhau và có hiệu ứng giả dược).
|
||||
- **Use Case tiết lộ điều gì:** Use Case này cung cấp một **Standardized Baseline (Mốc chuẩn hóa)** để so sánh liên tục qua các mốc thời gian (Time-series Analysis).
|
||||
- *Tháng 1:* Bệnh nhân đến khám $\rightarrow$ Hệ thống chấm **Grade 3** (Màng hoạt dịch dày 6mm, Doppler phủ 65% area).
|
||||
- *Tháng 3 (Sau 2 tháng uống thuốc):* Bệnh nhân tái khám $\rightarrow$ Hệ thống chấm **Grade 1** (Màng hoạt dịch giảm còn 2.5mm, Doppler chỉ còn vài chấm cô lập).
|
||||
|
||||
$\rightarrow$ Phần mềm tiết lộ cho bác sĩ một bằng chứng số liệu toán học tuyệt đối: **Phác đồ thuốc hiện tại đang hoạt động hiệu quả**, tiếp tục duy trì liều lượng. Ngược lại, nếu điểm số vẫn là Grade 3, hệ thống cảnh báo bác sĩ rằng bệnh nhân đang kháng thuốc, phải đổi sang loại thuốc sinh học đắt tiền hơn ngay lập tức.
|
||||
|
||||
|
||||
### 4. Triệt Tiêu Biến Thiên Giữa Các Bác Sĩ (Eradicating Inter-observer Variability)
|
||||
|
||||
- **Thực trạng tại bệnh viện:** Việc phân cấp siêu âm thủ công phụ thuộc rất nhiều vào mắt nhìn chủ quan và kinh nghiệm của từng người. Bác sĩ A (15 năm kinh nghiệm) nhìn ảnh đoán là Grade 2, nhưng Bác sĩ B (mới ra trường 1 năm) lại chấm là Grade 3 vì thấy túi dịch có vẻ to. Sự lệch pha này gây nguy hiểm cho tính đồng nhất của phác đồ điều trị.
|
||||
- **Use Case tiết lộ điều gì:** Khi có AI hỗ trợ trích xuất độ dày vùng ngách trên xương bánh chè (`suprapatellar recess`) và tính tỷ lệ pixel màu Doppler một cách tự động, hệ thống đóng vai trò như một **bộ kiểm tra điều kiện logic khách quan (Deterministic Gatekeeper)**. Nó đưa ra một mức điểm gợi ý dựa trên thuật toán tính toán diện tích chuẩn y khoa, giúp đồng bộ hóa chất lượng chẩn đoán tại bệnh viện. Bác sĩ dù già hay trẻ đều nhìn thấy một cơ sở số liệu minh bạch để đưa ra quyết định cuối cùng.
|
||||
- **The Alignment Check:** Qua bóc tách dưới góc nhìn lâm sàng này, bạn đã hiểu vì sao kết quả đầu ra của Use Case này (`Grade 0-3`) lại đóng vai trò là "trái tim mạch máu" cho toàn bộ Dashboard điều trị phía sau chưa, hay bạn muốn chúng ta đào sâu thêm vào điểm nào trước khi thiết kế các tương tác UIX?
|
||||
- The System suggest what currently being missed from FR and need to define in the UC:
|
||||
- **The Gap:** Ambiguous System & Actor Interaction Boundaries inside the Grading Workflow (Thiếu tường minh về ranh giới tương tác giữa Hệ thống và Bác sĩ).
|
||||
- **The Question:** How exactly should the doctor **interact** with the system's automated AI calculation when adjusting or validating the suggested severity score (e.g., from Grade 2 to Grade 3)? - The answer of this
|
||||
|
||||
The precise, clinically accurate baseline workflow executed by a Diagnostic Radiologist (UP5) consists of the following 6 sequential phases:
|
||||
|
||||
- **Step 1 (Patient Posture Standardization):** The clinician places the patient in a supine position with the target knee supported by a bolster in **20°–30° of slight flexion**. This stretches the extensor mechanism (the **Quadriceps/Patellar Tendon)** and eliminates diagnostic tracking errors caused by acoustic anisotropy.
|
||||
- Explain with simplification for Software Engineer
|
||||
- **The Clinical Action:** The patient lies flat, knee bent exactly 20°–30° over a cushion.
|
||||
- **The Software Engineer Analogy:** **Setting up consistent environment variables and running an initialization handshake.**
|
||||
- **Deep Jargon Breakdown:**
|
||||
- **Extensor Mechanism - Quadriceps/Patellar Tendon:** Think of this as a mechanical rubber band system (quadriceps muscle → tendon →kneecap → shin bone). When the leg is completely straight, this system sags and wrinkles. Bending it 20°–30° stretches it taut, creating a flat, predictable surface line.
|
||||
- **Acoustic Anisotropy (The Crucial Hardware Bug):** This is a structural physical hardware limitation of ultrasound waves. If the sound beam hits a tendon at a perfect 90° right angle, it bounces back bright white (**Echoic**). If the probe tilts even 5° offline because the tendon is curved/sagging, the wave scatters sideways, and the tissue suddenly registers on-screen as pitch black (**Hypoechoic**), mimicking a fake fluid tear or inflammatory lesion.
|
||||
- **Why this matters for your UI/Product Design:** This step is your raw input data sanity check. If the patient isn't positioned right, the ultrasound picture is filled with visual artifact bugs (garbage in, garbage out). Your system needs to know it is processing a standardized 20°–30° landscape view.
|
||||
- **Step 2 (Longitudinal Probe Alignment):** The clinician positions a high-frequency linear transducer probe over the midline of the **suprapatellar recess** (sagittal plane), aligning the distal edge over the upper pole of the patella to capture the clear multi-layered layout of the quadriceps tendon.
|
||||
- Explain with simplification for SE
|
||||
- **The Clinical Action:** The linear probe is placed lengthwise right down the middle of the upper knee, overlapping the top edge of the kneecap.
|
||||
- **The Software Engineer Analogy:** **Pointing your API client path to the exact parent database index to unpack a nested multi-layered object arrays.**
|
||||
- **Deep Jargon Breakdown:**
|
||||
- **Suprapatellar Recess:** This is the precise target memory location—a pouch-like joint cavity hiding right above the kneecap (**Supra** = above, **Patella** = kneecap) underneath the deep tissue layers.
|
||||
- **Sagittal Plane:** A vertical front-to-back cross-section cut. If your application was a 3D video game engine, this is viewing the joint asset precisely from the orthogonal **Side View Viewport**, rather than looking from the front or top down.
|
||||
- **Why this matters for your UI/Product Design:** This gives the system its coordinate system reference frame. In this view, your AI algorithm can run edge detection along standard anatomical landmarks, treating the top edge of the patella as a rock-solid structural zero-point anchor on a 2D canvas.
|
||||
- **Step 3 (B-Mode Structural Metric Capture):** Using standard Grey Scale (B-mode), the radiologist identifies the hypoechoic tissue area resting between the *prefemoral fat pad* and the *suprapatellar fat pad*. They visually calculate the maximum vertical distance of joint capsule distension/thickening using the ultrasound console's physical calipers.
|
||||
- Explain for SE
|
||||
- **The Clinical Action:** The doctor switches to a black-and-white image, identifies the space between two fat patches, and hits two points on the console to calculate the space thickness.
|
||||
- **The Software Engineer Analogy:** **Running a 2D Bounding Box Segmentation model to extract a quantitative `float` metric (distance in mm) between two fixed system nodes.**
|
||||
- **Deep Jargon Breakdown:**
|
||||
- **B-Mode (Brightness Mode):** This is the baseline structural image format. It transforms reflected sound wave amplitudes into live pixel intensity map arrays (high reflection = white pixels; zero reflection/fluid fluid pools = dark black pixels).
|
||||
- **Hypoechoic Tissue Area:** Any region that absorbs or passes sound waves easily instead of reflecting them, rendering as a dark grey or black signal pool. Inflamed synovial tissue fluid sits in this category.
|
||||
- **Prefemoral & Suprapatellar Fat Pads:** These are your permanent upper and lower hardware guardrail markers. The suprapatellar pouch sits wedged right between them like an expandable buffer queue.
|
||||
- **Why this matters for your UI/Product Design:** This is **REQ-RAD-02** in your requirement documentation. The doctor uses manual calipers to measure this space. Your product UI can introduce a digital bounding path box or automated point-to-point drawing tool overlay to automatically extract this distance variable, completely stripping away the manual console math step.
|
||||
- **Step 4 (Power Doppler Vascularity Mapping):** The clinician activates the Power Doppler mode on the console, optimizing the wall filter and PRF settings. They carefully hover the probe with **minimal contact pressure** to avoid compressing low-velocity synovial capillaries, visually counting or calculating the percentage area occupied by active blood flow signals inside the suprapatellar landscape.
|
||||
- Explain for SE
|
||||
- **The Clinical Action:** The doctor switches on the color overlay feature, tweaks the sensitivity filters, and hovers the probe extremely lightly without pressing down into the skin.
|
||||
- **The Software Engineer Analogy:** **Activating a live telemetry tracer module with a low-pass noise filter to map active server data traffic volume while avoiding an external physical choke/throttling event.**
|
||||
- **Deep Jargon Breakdown:**
|
||||
- **Power Doppler Mode:** A specialized signal tracking sub-routine. Instead of mapping structural tissue borders, it tracks shifts in frequency caused by moving targets (red blood cells). It highlights these regions with bright, glowing color maps overlaid right on top of the black-and-white structural layer.
|
||||
- **Wall Filter & PRF (Pulse Repetition Frequency):** These are variable noise gates. If configured wrong, minor hand tremors will bleed into the visual feed as massive colored pixels (**Clutter Artifact Noise**).
|
||||
- synpvia Capillary Compression Edge Case: If the doctor applies heavy hand force, they manually flatten the tiny micro-blood vessels inside the knee. This physically blocks blood flow, wiping out the signal completely on screen and returning a false negative trace.
|
||||
- **Why this matters for your UI/Product Design:** This maps directly to your **Hypervascularity parameter**. Your interface can assist by calculating the ratio of bright color pixels to the total area of the segmented pouch, converting a subjective visual guess into a precise numerical percentage readout.
|
||||
- **Step 5 (Semi-Quantitative Grade Synthesis):** The clinician combines both structural metrics mentally against standard musculoskeletal classification tiers: - THE VKIST ML-Module current stop in here
|
||||
- *Grade 0 (None):* Completely flat layers; no hypoechoic separation or vascular flow signals.
|
||||
- *Grade 1 (Mild):* Thin hypoechoic line running parallel to the femoral bone path; single or minimal isolated vascular blood flow spots.
|
||||
- *Grade 2 (Moderate):* Evident hypoechoic expansion pushing the fat pads apart, but lines remain flat; active vascular flow spots occupying less than 50% of the calculated synovial area.
|
||||
- *Grade 3 (Severe):* Clear convex or distinct bulging capsule distortion extending outward; intense confluent flow signals covering more than 50% of the calculated synovial landscape.
|
||||
- Explain for SE
|
||||
- **The Clinical Action:** The doctor looks at both parameters (the pouch thickness + the active color blood flow maps) and maps them to a standard clinical severity tier level (0 to 3).
|
||||
- **The Software Engineer Analogy:** **Evaluating raw aggregated metric values against a core business logic conditional switch block (`switch(severityGrade)`) to determine system status codes.**
|
||||
- **Deep Tiers Demystified via Code Logic:**
|
||||
- **Grade 0 (Healthy Baseline):**
|
||||
|
||||
```jsx
|
||||
if (synovialThickness === 0 && hypervascularityScore === 0) return "Grade 0: Normal Space";
|
||||
```
|
||||
|
||||
- **Grade 1 (Mild Inflammation):** Space is filled with a thin, parallel line of tissue expansion; trace color dots show up.JavaScript
|
||||
|
||||
```
|
||||
if (synovialThickness > 0 && hypervascularityScore <= 0.10) return "Grade 1: Mild Distension";
|
||||
```
|
||||
|
||||
- **Grade 2 (Moderate Inflammation):** The tissue swells enough to visibly push the flanking fat pads apart, and the active blood flow color blocks cover up to half of the pouch container zone.JavaScript
|
||||
|
||||
```
|
||||
if (synoviumDistended === true && hypervascularityScore < 0.50) return "Grade 2: Moderate Pouch Deflection";
|
||||
```
|
||||
|
||||
- **Grade 3 (Severe Inflammation):** The pouch balloons into a curved, outward bulging geometric form; intense, connected color maps take over more than half of the space landscape.JavaScript
|
||||
|
||||
```
|
||||
if (capsuleShape === 'convex_bulge' || hypervascularityScore >= 0.50) return "Grade 3: Critical Structural Flare";
|
||||
```
|
||||
|
||||
- **Step 6 (Manual Multi-Silo Transcription):** The clinician freezes the optimal reference frames on the hardware console, manually assigns a final severity index label, moves away from the ultrasound machine hardware screen to a desktop workstation PC, and types out the structural text variables into the hospital's Electronic Medical Record (EMR) text block.
|
||||
- Explain for SE
|
||||
- **The Clinical Action:** The doctor freezes the machine display screen, manually records a final tier index number, stands up, switches chairs to a secondary office computer, logs in, and re-types the exact observations by hand into a text window box.
|
||||
- **The Software Engineer Analogy:** **A total lack of system database synchronization. Hand-copying raw log data variables from a separate terminal window and typing them line-by-line into a separate decoupled microservice application.**
|
||||
- **Why this matters for your UI/Product Design:** This is the massive core workflow bottleneck. The goal of your upcoming workspace design is to build an interactive, unified web interface bridge. The AI processes the image data parameters natively, renders an automated classification tag proposal directly inside the primary viewing frame, and updates the shared patient record database with 0 manual transcript entries or physical context-switching loops.
|
||||
- Additional - from the answer in the `Question` we can model the planUML code solution
|
||||
|
||||
!image.png
|
||||
|
||||
```jsx
|
||||
@startuml
|
||||
' Settings
|
||||
left to right direction
|
||||
skin rose
|
||||
|
||||
' Actors
|
||||
actor "Diagnostic Radiologist (UP5)" as Rad
|
||||
actor "Hospital EMR System" as EMR << System >>
|
||||
actor "VKIST AI Pipeline" as AI << System >>
|
||||
|
||||
' System Boundary
|
||||
rectangle "VKIST MSK Workspace - Synovitis Grading Engine" {
|
||||
|
||||
' Core Viewing & Extraction Use Cases
|
||||
usecase "Load Patient Ultrasound Session" as UC_Load
|
||||
usecase "Extract Joint Tissue Metrics" as UC_Extract
|
||||
|
||||
' AI Suggestion Processing
|
||||
usecase "Compute Automated Severity Suggestion" as UC_AI_Compute
|
||||
usecase "Display Suggestion Tag & Canvas Overlays" as UC_Display
|
||||
|
||||
' Clinician Interaction & Decision Loop
|
||||
usecase "Review Suggested Synovitis Grade (0-3)" as UC_Review
|
||||
usecase "Manually Override Severity Grade" as UC_Override
|
||||
usecase "Sign & Finalize Diagnostic Conclusions" as UC_Finalize
|
||||
|
||||
' Data Sync Hand-off
|
||||
usecase "Synchronize Patient Record" as UC_Sync
|
||||
}
|
||||
|
||||
' Relationships & Flow Boundaries
|
||||
Rad --> UC_Load
|
||||
Rad --> UC_Review
|
||||
Rad --> UC_Finalize
|
||||
|
||||
' AI Pipeline Interactions
|
||||
UC_Load ..> UC_Extract : <<include>>
|
||||
UC_Extract --> AI : Transmit raw image streams
|
||||
AI --> UC_AI_Compute : Process thickness & Doppler maps
|
||||
UC_AI_Compute ..> UC_Display : <<include>>
|
||||
|
||||
' Review and Override Loop
|
||||
UC_Display ..> UC_Review : <<include>>
|
||||
UC_Override .up.> UC_Review : <<extend>> (If clinician disagrees with AI)
|
||||
Rad --> UC_Override
|
||||
|
||||
' Finalization and Sync Hand-offs
|
||||
UC_Finalize ..> UC_Sync : <<include>>
|
||||
UC_Sync --> EMR : Push standardized JSON structural data
|
||||
@enduml
|
||||
```
|
||||
|
||||
|
||||
→ For the Synovist Grading the interaction between the clinician & system may occur 4 potential case: —> 4 possible interactions
|
||||
|
||||
```jsx
|
||||
+-----------------------------------------------------------------------+
|
||||
| HUMAN-AI CONCURRENT STATES |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| QUADRANT 2 | QUADRANT 1 |
|
||||
| Automation Override Risk | True Agreement |
|
||||
| | |
|
||||
| AI: Grade 3 (Accurate) | AI: Grade 2 (Accurate) |
|
||||
| Human: Grade 1 (Oversight) | Human: Grade 2 (Confident) |
|
||||
| Risk: Severe Disease Missed | Risk: None (Happy Path) |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
| QUADRANT 4 | QUADRANT 3 |
|
||||
| Double-Blind Failure | Clinician Subservience Risk |
|
||||
| | |
|
||||
| AI: Grade 2 (Boundary Error) | AI: Grade 3 (Hallucinated) |
|
||||
| Human: Grade 1 (Biased Error) | Human: Grade 1 (Accurate) |
|
||||
| Risk: Cascading System Error | Risk: Over-treatment Danger |
|
||||
+-----------------------------------+-----------------------------------+
|
||||
```
|
||||
|
||||
THE ML-stack use in this scenarios:
|
||||
|
||||
- the grading ML-stack (VKIST-model) → always use (it’s the machine process on the raw-signal from device)
|
||||
- the LLM Critic & Actor for acting as explainer on the results of the grading stack (de-blackbox) + conversation with the clinics for pathologic analysis <with RAG> + critic-suggestion (this LLM shall have to loaded with SKILL / multi-agent system)
|
||||
- the LLM-RAG-Referee for prevent bias & blindness of both-side (actor & grader & clinical)
|
||||
|
||||
|
||||
| **Referee Role** | **Problem Solved** | **Mechanism** |
|
||||
| --- | --- | --- |
|
||||
| **1. Unbiased Arbiter** | **Conflict & Bias:** Prevents the LLM from hallucinating to match the clinician's incorrect bias (Confirmation Bias). | Operates as a **Session-State Arbiter**: It ignores conversation history and focuses purely on comparing the raw metrics (`GradCAM maps`, `Doppler indices`) against clinical definitions. |
|
||||
| **2. Domain Guardian** | **Knowledge Obsolescence:**Prevents the system from using outdated medical standards (e.g., guidelines from 2020 instead of 2025). | Operates as a **Knowledge-Retrieval Guardian**: It triggers when the system detects high semantic entropy, fetching the *latest* approved academic guidelines to ensure all explanations remain clinically valid. |
|
||||
|
||||
The Actor:
|
||||
|
||||
- the UP-5 user working with the hardware
|
||||
|
||||
4 scenarios can consider
|
||||
@@ -0,0 +1,173 @@
|
||||
# VKIST MSK Workspace (FR-25): Full System Use Case Specification
|
||||
|
||||
This document provides a comprehensive, unified, and standard-aligned specification of the **VKIST MSK Workspace (FR-25)** use-case model. It acts as the definitive guide to the overall system-level architecture, showing how all 15 core use cases are structurally organized, how they connect across operational pipelines, and how they implement the safety-critical human-AI decision-making frameworks.
|
||||
|
||||
The use-case design is structured around a **4-Quadrant Human-AI Interaction Framework**, designed to optimize diagnostic speed while defending against cognitive biases (such as automation bias and clinician subservience) in high-throughput clinical environments.
|
||||
|
||||
---
|
||||
|
||||
## 1. Primary System Actors
|
||||
|
||||
The platform coordinates interactions between the clinical specialist and multiple local, air-gapped system components:
|
||||
|
||||
1. **UP5 (Diagnostic Radiologist):** The primary human actor. A Vietnamese clinical specialist processing musculoskeletal (MSK) ultrasound scans, responsible for evaluating AI suggestions, annotating images, and signing final diagnostic records.
|
||||
2. **MSK-Vision-Grader System:** The edge-close & server-distribute computer vision inference runner. It ingests raw DICOM image arrays, extracts structural parameters, and generates automated synovitis grading (Grades 0–3) along with bounding boxes and Grad-CAM activation heatmaps.
|
||||
3. **LLM Explainer:** A localized language model (e.g., PhoGPT or MedGemma) that runs on bare-metal hospital GPUs to generate clinical Chain-of-Thought (CoT) reasoning paragraphs justifying model predictions.
|
||||
4. **Hospital EMR System (EMR):** The air-gapped hospital intranet electronic medical record repository, serving as the immutable sink for signed reports and ground-truth overrides.
|
||||
5. **LLM RAG-Referee-Arbitrator:** An on-premise Vector DB-backed QA model that queries static Vietnamese Ministry of Health (MOH) clinical guidelines to resolve human-AI disagreements with objective evidence.
|
||||
|
||||
---
|
||||
|
||||
## 2. Complete Traceability Matrix
|
||||
|
||||
The following table homogenizes all 15 use cases from `Sprint_1_2_UseCase_DB.csv` and details their core system goals:
|
||||
|
||||
| UC-ID | Title [Verb + Noun] | Primary Actor | System Goal | Interaction Type | Pipeline / Quadrant |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **UC-48376** | Load Patient Scan Session | UP5, MSK-Vision-Grader | Ingest raw ultrasound frame arrays and initialize session state with spatial calibrations. | System-to-System, User-to-System | Data Ingest Pipeline |
|
||||
| **UC-47988** | Review Suggested Synovitis Grade (0-3) | UP5 | Evaluate proposed synovitis classification metrics and structural visual overlays on the viewport. | User-to-System | Clinical Review Pipeline |
|
||||
| **UC-92006** | Finalize & Sign Electronic Record | UP5, Hospital EMR System | Authenticate, cryptographically seal, and sync verified diagnostic reports down to storage infrastructure. | System-to-System, User-to-System | Sync & Finalization Pipeline |
|
||||
| **UC-25776** | Generate GradCAM & CoT Explanation Panel | UP5, LLM Explainer | Present clear, pixel-linked visuospatial explanations and multi-modal clinical reasoning for high-trust verification. | User-to-System | Quadrant 1 (True Agreement) |
|
||||
| **UC-02423** | Log High-Trust Concur Block | Hospital EMR System | Secure the human-AI alignment log trace within the final diagnostic report payload. | System-to-System | Quadrant 1 (True Agreement) |
|
||||
| **UC-22159** | Trigger Conversational Circuit Breaker | UP5 | Intercept premature finalization if telemetry reveals friction, hesitation, or cognitive blind-spots. | User-to-System | Quadrant 2 (Automation Override) |
|
||||
| **UC-55146** | Facilitate Socratic Reasoning Dialogue | UP5, LLM Explainer | Engage the specialist in a targeted, conversational double-check loop regarding controversial markers. | User-to-System | Quadrant 2 (Automation Override) |
|
||||
| **UC-74821** | Monitor Context Drift via BERT Sub-Layer | UP5 | Continuously parse communication tokens to identify logical contradictions or semantic drift during clinical debates. | User-to-System | Quadrant 2 (Automation Override) |
|
||||
| **UC-65473** | Arbitrate Evidence via RAG-Referee | UP5, LLM RAG-Referee-Arbitrator | Query static, authoritative clinical knowledge bases to resolve human-machine disagreements with objective evidence. | User-to-System | Quadrant 2 (Automation Override) |
|
||||
| **UC-25637** | Expose Pixel-Level Activation Logic | UP5 | Reveal fine-grained layer weights and activation responses when the human specialist challenges an automated prediction. | User-to-System | Quadrant 3 (Clinician Subservience) |
|
||||
| **UC-60739** | Isolate Visual Noise/Artifacts | UP5 | Provide manual brush and selection overlays to mask out acoustic shadows, bone scattering, or artifacts. | User-to-System | Quadrant 3 (Clinician Subservience) |
|
||||
| **UC-62864** | Commit Validated Ground-Truth Record | Hospital EMR System | Secure the human-corrected ground-truth dataset variant while appending expert-validated reports to the EMR. | System-to-System | Quadrant 3 (Clinician Subservience) |
|
||||
| **UC-35956** | Activate Clinical Investigation Mode | UP5 | Switch the system into a strict, template-driven manual examination mode when low vision confidence aligns with missing references. | User-to-System | Quadrant 4 (Double Blind Failure) |
|
||||
| **UC-47796** | Execute Structured Morphology Annotation | UP5 | Force the manual plotting of anatomical coordinates and morphological anomalies using a strict, un-biased framework. | User-to-System | Quadrant 4 (Double Blind Failure) |
|
||||
| **UC-01580** | Serialize Session to Telemetry Queue | Hospital EMR System | Route anomalous case data directly to engineering telemetry streams while bypassing standard EMR databases. | System-to-System | Quadrant 4 (Double Blind Failure) |
|
||||
|
||||
---
|
||||
|
||||
## 3. PlantUML Architectural Model
|
||||
|
||||
The following diagram defines the physical and logical boundaries of the **VKIST MSK Workspace System (FR-25)**. It maps actor associations, internal system boundaries, and precise functional relationships (`<<include>>` and `<<extend>>` stereotypic dependencies).
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
skinparam packageStyle rectangle
|
||||
left to right direction
|
||||
|
||||
' ##########################################
|
||||
' ACTORS DEFINITION
|
||||
' ##########################################
|
||||
actor "UP5\n(Diagnostic Radiologist)" as up5
|
||||
actor :MSK-Vision-Grader\nSystem: as msk <<system>>
|
||||
actor :LLM\nExplainer: as llm_exp <<system>>
|
||||
actor :Hospital EMR System: as emr <<system>>
|
||||
actor :LLM RAG-Referee\nArbitrator: as llm_rag <<system>>
|
||||
|
||||
' ##########################################
|
||||
' VKIST MSK WORKSPACE SYSTEM BOUNDARY
|
||||
' ##########################################
|
||||
rectangle "VKIST MSK Workspace System (FR-25)" {
|
||||
|
||||
' --- Core Ingestion & Viewing Use Cases ---
|
||||
usecase "UC-48376\nLoad Patient Scan Session" as uc_48376
|
||||
usecase "UC-47988\nReview Suggested Synovitis Grade (0-3)" as uc_47988 #Khaki
|
||||
usecase "UC-92006\nFinalize & Sign Electronic Record" as uc_92006
|
||||
|
||||
' --- Quadrant 1: True Agreement ---
|
||||
usecase "UC-25776\nGenerate GradCAM & CoT Explanation Panel" as uc_25776
|
||||
usecase "UC-02423\nLog High-Trust Concur Block" as uc_02423
|
||||
|
||||
' --- Quadrant 2: Automation Override ---
|
||||
usecase "UC-22159\nTrigger Conversational Circuit Breaker" as uc_22159
|
||||
usecase "UC-55146\nFacilitate Socratic Reasoning Dialogue" as uc_55146
|
||||
usecase "UC-74821\nMonitor Context Drift via BERT Sub-Layer" as uc_74821
|
||||
usecase "UC-65473\nArbitrate Evidence via RAG-Referee" as uc_65473
|
||||
|
||||
' --- Quadrant 3: Clinician Subservience ---
|
||||
usecase "UC-25637\nExpose Pixel-Level Activation Logic" as uc_25637
|
||||
usecase "UC-60739\nIsolate Visual Noise/Artifacts" as uc_60739
|
||||
usecase "UC-62864\nCommit Validated Ground-Truth Record" as uc_62864
|
||||
|
||||
' --- Quadrant 4: Double Blind Failure ---
|
||||
usecase "UC-35956\nActivate Clinical Investigation Mode" as uc_35956
|
||||
usecase "UC-47796\nExecute Structured Morphology Annotation" as uc_47796
|
||||
usecase "UC-01580\nSerialize Session to Telemetry Queue" as uc_01580
|
||||
}
|
||||
|
||||
' ##########################################
|
||||
' RELATIONSHIPS & ASSOCIATIONS
|
||||
' ##########################################
|
||||
|
||||
' --- UP5 Actor Connections ---
|
||||
up5 --> uc_47988 : Reviews suggested grading
|
||||
up5 --> uc_48376 : Manually triggers load
|
||||
up5 --> uc_65473 : Interacts with guideline reference
|
||||
up5 --> uc_02423 : Signs with direct AI consensus
|
||||
up5 --> uc_92006 : Executes digital signature
|
||||
up5 --> uc_47796 : Documents manual finding & diagnosis
|
||||
|
||||
' --- MSK-Vision-Grader System Connections ---
|
||||
msk --> uc_47988 : Generates prediction score
|
||||
uc_48376 --> msk : Delivers raw image tensors
|
||||
msk --> uc_25776 : Generates localized Grad-CAM layers
|
||||
msk --> uc_22159 : Flags classification hesitation/uncertainty
|
||||
|
||||
' --- LLM Explainer Connections ---
|
||||
llm_exp --> uc_25776 : Generates step-by-step clinical CoT
|
||||
llm_exp --> uc_55146 : Generates Socratic discussion checks
|
||||
|
||||
' --- Hospital EMR System Connections ---
|
||||
uc_02423 --> emr : Stores sealed consensus log
|
||||
uc_92006 --> emr : Synchronizes verified medical reports
|
||||
uc_01580 --> emr : Stores isolated development packet
|
||||
|
||||
' --- LLM RAG-Referee Arbitrator Connections ---
|
||||
llm_rag --> uc_65473 : Performs retrieval on MOH guidelines
|
||||
|
||||
' --- Use Case to Use Case Interdependencies ---
|
||||
|
||||
' Extend Paths
|
||||
uc_35956 -.-> uc_47988 : <<extend>>\nTriggered by low-confidence scan\n& empty guidelines
|
||||
uc_25637 -.-> uc_47988 : <<extend>>\nTriggered when clinician\ncontests AI prediction
|
||||
|
||||
' Include Paths (Quadrant 3)
|
||||
uc_25637 -.-> uc_60739 : <<include>>
|
||||
uc_60739 -.-> uc_62864 : <<include>>
|
||||
|
||||
' Include Paths (Quadrant 2)
|
||||
uc_22159 -.-> uc_55146 : <<include>>
|
||||
uc_22159 -.-> uc_74821 : <<include>>
|
||||
|
||||
' Extend Paths (Quadrant 2 Socratic Loop)
|
||||
uc_65473 -.-> uc_74821 : <<extend>>\nTriggered if semantic drift\nor impasse is caught
|
||||
|
||||
' Include Paths (Quadrant 4)
|
||||
uc_47796 -.-> uc_01580 : <<include>>
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Architectural Analysis of Functional Relationships
|
||||
|
||||
### 4.1 Stereotyped Dependecies
|
||||
|
||||
- **`<<include>>` Dependencies:**
|
||||
- **`UC-25637` includes `UC-60739`:** When the clinical specialist contests the AI suggestion and the system exposes pixel-level activation logic, it structurally includes the manual toolset to isolate and paint over acoustic shadows and bone scattering artifacts.
|
||||
- **`UC-60739` includes `UC-62864`:** Successful isolation of physical ultrasound noise always includes committing a validated, high-value ground-truth dataset variant directly to storage for downstream model retraining.
|
||||
- **`UC-22159` includes `UC-55146` & `UC-74821`:** If the workspace's behavioral trackers detect high cognitive hesitation (intercepting normal pathways), it halts progression and launches both the Socratic conversational panel and the context-drift tracking sub-layer concurrently.
|
||||
- **`UC-47796` includes `UC-01580`:** When in clinical investigation mode, finalizing a manually plotted annotation and morphologic coordinate dataset immediately triggers telemetry serialization to route debugging data out to development queues.
|
||||
|
||||
- **`<<extend>>` Dependencies:**
|
||||
- **`UC-25637` extends `UC-47988`:** Revealing pixel-level layer weights is not a default step; it only extends the basic grading review if the doctor explicitly challenges or modifies the AI-proposed classification score.
|
||||
- **`UC-65473` extends `UC-74821`:** Querying authoritative local Vector databases and injecting MOH guidelines extends the context-drift monitoring if the BERT checker detects a logical contradiction, an impasse, or severe drift in the clinical dialogue.
|
||||
- **`UC-35956` extends `UC-47988`:** The system disables automated diagnostics and switches the entire viewport layout to clinical investigation mode (Escalation) only if model classification confidence falls below 60% and no guidelines map to the anomalies.
|
||||
|
||||
---
|
||||
|
||||
## 5. Homogenization of Business Logics & Clinical Drivers
|
||||
|
||||
By synchronizing the Use Case IDs and naming conventions across the **Use Case Database (`Sprint_1_2_UseCase_DB.csv`)**, the **System Design Specification (`SOFTWARE_SYSTEM_DESIGN_FR_25.md`)**, and this **Architectural Model**, we have solidified key business rules:
|
||||
|
||||
1. **The Role of the Diagnostic Radiologist (`UP5`):** Explicitly unified as the single human clinician in the workspace loop. The historical database typo identifying the actor of `UC-47796` as "UNK" is fully resolved under the canonical role of `UP5`.
|
||||
2. **True Socratic Agreement Pipeline (`UC-22159` -> `UC-65473`):** Resolves the risk of automation bias. The conversational circuit breaker is technically backed by a real-time BERT text classifier that checks for clinical semantic drift against the raw image and can escalate directly to local Vector RAG modules containing MOH clinical guidelines.
|
||||
3. **Continuous Retraining Pipeline (`UC-25637` -> `UC-62864`):** Turns clinical overrides into architectural assets. When `UP5` overrides the AI classification, they paint an SVG mask over the artifacts (reverberations/shadows). Saving this mask along with the ground-truth correction allows for localized, high-value model retraining on local-hospital acoustic variations.
|
||||
4. **Air-Gapped Telemetry Routing (`UC-47796` -> `UC-01580`):** Enforces Circular 46/2018/TT-BYT compliance. Highly anomalous cases or double-blind failures are stripped of standard patient EMR references and serialized directly into dedicated debugging logs on the local server, protecting primary medical data lines while providing raw materials for engineering diagnosis.
|
||||
@@ -0,0 +1,215 @@
|
||||
# User-Research Result
|
||||
|
||||
⇒ DEFINING THE PROJECT-SCOPE:
|
||||
|
||||
Develop an interactive **`musculoskeletal care and education platform`** that transforms standard DICOM X-rays into a **`collaborative workspace`**. The system **`empowers clinicians with continuous education, diagnostic support, and objective clinical checks`**, while **`actively educating patients about their locomotive health to foster a true partnership throughout their treatment journey.`**
|
||||
|
||||
**⇒ End-user I have identified in this project (The Pilot-Project) scope shall include as follow:**
|
||||
|
||||
- The User Profile & User Persona ? - Given the User-Profile as below within the domain of healthcare in general (these are matter in NFRs)
|
||||
|
||||
|
||||
### User-Profile 1: Healthcare Senior Expert
|
||||
|
||||
- **Who they are:** PhD, Specialist II, senior department heads, professors, clinical directors, and national-level experts.
|
||||
- **Age range:** about 40–60+.
|
||||
- **Domain proficiency / responsibility:** highest domain depth; they own complex diagnosis, surgery, protocol setting, teaching, research, and escalation decisions.
|
||||
- **Working environment:** central or urban referral hospitals with the highest case complexity, the heaviest consult load, and the strongest pressure to standardize quality across many juniors.
|
||||
- **What they know:** specialty medicine, clinical standards, institutional policy, and how to manage hard cases with incomplete information.
|
||||
- **What they do not know:** ML model behavior, deployment constraints, validation methodology, and how to operationalize AI into workflow without disruption.
|
||||
- **Attitude toward AI/ML:** cautious but open if it improves safety, speed, or teaching quality; skeptical of black-box outputs and unproven demos.
|
||||
- **Pain points AI should help with:** triage prioritization, guideline consistency, overload reduction, second-opinion support, and research/teaching summarization.
|
||||
- **Hindrances in solution design:** liability concerns, low tolerance for workflow friction, demand for explainability, and strong resistance to tools that look like “extra work.”
|
||||
- **Role in Dev/R&D:** decision-maker, clinical validator, and pilot sponsor; not the best first user for UX discovery, but essential for approval and governance.
|
||||
- **How to approach them:** start from patient safety, evidence, efficiency, and institutional benefit; bring concise demos, local data, and clear failure-mode handling.
|
||||
|
||||
### User-Profile 2: Professional Clinician
|
||||
|
||||
- **Who they are:** MD, Master’s, Specialist I physicians, consultants, and mid-career clinicians.
|
||||
- **Age range:** about 30–45.
|
||||
- **Domain proficiency / responsibility:** strong clinical decision-making, specialty care, supervision of routine clinicians, and participation in departmental operations.
|
||||
- **Working environment:** major cities and provincial hospitals; high volume, mixed complexity, heavy outpatient/inpatient balance, and constant multitasking.
|
||||
- **What they know:** practical evidence-based medicine, standard workflows, referrals, diagnostic pathways, and how hospital systems actually behave.
|
||||
- **What they do not know:** deep ML mechanics, model drift, data governance nuance, and how to audit AI outputs in a rigorous way.
|
||||
- **Attitude toward AI/ML:** pragmatic and curious; they will adopt if it saves time and reduces repetitive work, but they quickly lose trust if the tool is unstable or slow.
|
||||
- **Pain points AI should help with:** chart review, note drafting, imaging/radiology support, guideline lookup, coding/admin burden, and triage support.
|
||||
- **Hindrances in solution design:** fragmented systems, poor interoperability, time pressure, incomplete data, and the need to justify decisions upward.
|
||||
- **Role in Dev/R&D:** power users, domain testers, and feedback bridge between leadership and frontline users.
|
||||
- **How to approach them:** focus on time saved, accuracy, integration with current workflow, and measurable outcomes; avoid abstract “AI transformation” language.
|
||||
|
||||
### User-Profile 3: Practitioner
|
||||
|
||||
- **Who they are:** college/intermediate-degree clinicians, nurses, assistants, and routine care providers.
|
||||
- **Age range:** about 22–35.
|
||||
- **Domain proficiency / responsibility:** routine care, procedures, monitoring, handoff, and protocol execution rather than independent complex diagnosis.
|
||||
- **Working environment:** provincial, district, and some commune-level settings; constrained staffing, high patient throughput, and fewer specialist backups.
|
||||
- **What they know:** operational care, standard procedures, basic triage, record keeping, medication handling, and patient communication.
|
||||
- **What they do not know:** advanced diagnostics, edge-case management, system design, or how to judge whether AI recommendations are statistically reliable.
|
||||
- **Attitude toward AI/ML:** mixed but practical; they like tools that reduce repetitive tasks and help them work faster, but they worry about complexity and blame if something goes wrong.
|
||||
- **Pain points AI should help with:** patient queue management, reminders, checklisting, basic decision support, documentation assistance, and training support.
|
||||
- **Hindrances in solution design:** low digital confidence in some sites, limited device availability, unstable connectivity, and insufficient time for training.
|
||||
- **Role in Dev/R&D:** frontline reality check; they expose whether the tool actually works in crowded, low-resource settings.
|
||||
- **How to approach them:** keep interfaces simple, mobile-friendly, low-click, and aligned with routine tasks; demonstrate value in under one minute.
|
||||
|
||||
#### User-Profile 4: Support Staff & Patients
|
||||
|
||||
- **Who they are:** vocational/basic-level workers, assistants, clerks, pharmacy support, and commune/district support staff.
|
||||
- **Age range:** about 18–30+ depending on role.
|
||||
- **Domain proficiency / responsibility:** foundational care support, dispensing, scheduling, registration, logistics, and basic patient flow handling.
|
||||
- **Working environment:** district/commune level, often with staff shortages, limited equipment, and very high dependency on manual coordination.
|
||||
- **What they know:** local workflow, patient flow, administrative routines, inventory, and practical daily operations.
|
||||
- **What they do not know:** clinical reasoning, advanced data interpretation, or how AI should be validated in medicine.
|
||||
- **Attitude toward AI/ML:** mostly utilitarian; they like anything that reduces repetitive admin work, but they fear systems that increase confusion or slow service.
|
||||
- **Pain points AI should help with:** registration, queue routing, reminder systems, inventory support, duplicate entry reduction, and scheduling.
|
||||
- **Hindrances in solution design:** low training time, little tolerance for complex setup, language/interface issues, and dependence on supervisor approval.
|
||||
- **Role in Dev/R&D:** workflow witnesses; they reveal hidden process bottlenecks and administrative failure points.
|
||||
- **How to approach them:** use concrete use cases, local language, visual flows, and very short onboarding; do not assume technical familiarity.
|
||||
- The User Profile (key-attribute) for these end-user in this product context includes (more focus on FRs)
|
||||
- [**→ The results was developed based on Gemini-DeepResearch given the methodology of Online Ethnography as follow:**](https://gemini.google.com/share/a4e65f2f47c8)
|
||||
|
||||
|
||||
| Platform Type | Selected Target Groups/Channels | Primary User Base Identified | Ethnographic Value & Observed Behaviors |
|
||||
| --- | --- | --- | --- |
|
||||
| **Professional Forums** | ykhoa.net, Vietnam Orthopaedic Association (VOA) portal | Radiologists, Surgeons, Medical Students, Rural Physicians | Discussions on uncompensated workloads, mHealth resistance in rural areas, formal professional networking, CME tracking. |
|
||||
| **Patient Forums** | Webtretho | Mothers, Adult Family Caregivers, Female Patients | Highlighting the primacy of family-centric healthcare decision-making, peer-to-peer sharing of alternative/folk remedies, high anxiety expression. |
|
||||
| **Facebook Groups** | Chẩn Đoán Hình Ảnh Online, Trung tâm Chấn thương chỉnh hình Tâm Anh, Thoát vị đĩa đệm | Radiologists, Orthopedic Surgeons, Patients | Clinicians sharing edge-case DICOMs for peer review. Patients posting raw X-rays seeking asynchronous online consultations. |
|
||||
| **YouTube Channels** | cdhaonline, USAC, ACC Chiropractic, Bác sĩ Trần Văn Phúc Official | Sonographers, Patients seeking conservative treatments | Visual tutorials for MSK ultrasound, patient consumption of non-pharmacological treatment videos (sciatica, knee pain). |
|
||||
|
||||
### **User Profile 5: The Diagnostic Radiologist (Bác sĩ Chẩn đoán hình ảnh)**
|
||||
|
||||
- **Who they are** within the MSK workflow is foundational; they are the primary data generators and the initial analytical layer. Diagnostic Radiologists in the Vietnamese public sector are highly specialized medical doctors tasked with interpreting complex, multi-modal imaging (X-ray, CT, MRI) and producing the definitive reports that guide all subsequent orthopedic or rheumatological interventions. They operate exactly at the critical intersection of deep human anatomy, pathological manifestation, and advanced digital imaging technology.
|
||||
- **Age range** for this demographic typically spans from 28 to 55 years old. This broad range encompasses highly tech-fluent, digitally native junior attendings who have trained entirely on digital screens, up to senior department heads who have had to actively transition their cognitive models from legacy analog film-based interpretation to modern digital PACS environments over the last critical decade.
|
||||
- **Domain proficiency / responsibility** for these users lies in rapid, highly accurate pattern recognition across various tissue densities and skeletal structures. They are explicitly responsible for detecting micro-fractures, subtle degenerative joint changes, early-stage bone tumors, and complex structural anomalies within the MSK system. Furthermore, in modernized environments like Bach Mai or Viet Duc, they are increasingly burdened with the responsibility of monitoring patient radiation dose exposure, a complex safety metric now deeply integrated into modern PACS software implementations.
|
||||
- **Working environment** `constraints are severe.` They predominantly work in high-stress, low-ambient-light reading rooms situated within severely overloaded central and provincial hospitals. Their work is highly asynchronous and entirely detached from direct, physical patient interaction. They face immense cognitive load due to the sheer, overwhelming volume of daily scans generated by a public healthcare system where patients often bypass primary care facilities to visit central hospitals directly, leading to massive backlogs.
|
||||
- **What they know** is exhaustive and deeply technical. They possess a profound understanding of skeletal anatomy, radiological physics, and pathological manifestations on high-resolution digital monitors. They know the intimate intricacies of complex DICOM viewers, including windowing, leveling, multi-planar reconstruction (MPR), and maximum intensity projections (MIP). They are intimately familiar with the limitations of specific imaging modalities and the technical artifacts that can obscure or mimic diagnoses.
|
||||
- **What they do not know** represents a critical clinical gap. They typically do not know the patient's full longitudinal clinical history or the nuanced, subjective experiences of the patient's localized pain, primarily because the clinical order notes provided by referring public sector physicians are often exceedingly brief due to extreme hospital overload. Furthermore, they are often unaware of the specific downstream surgical or conservative treatment decisions made by the orthopedic surgeons unless a formal multi-disciplinary case review is specifically initiated.
|
||||
- **Attitude toward AI/ML** among Vietnamese radiologists is a complex, evolving mixture of cautious optimism and defensive professional skepticism. The highly publicized and successful deployment of AI applications like VinBrain's DrAid for liver cancer screening and COVID-19 triage—which have won prestigious international awards and proven highly effective in real-world triage—has established a strong, tangible baseline of trust in algorithmic capabilities. They view AI highly favorably when it functions as an automated, invisible "second reader" that accurately highlights anomalies, quantifies tedious measurements, or triages obvious normal versus critical abnormal scans to help manage their overwhelming workload. However, profound resistance manifests if the AI is perceived as an opaque "black box" that disrupts established mental models without providing transparent, explainable reasoning.
|
||||
- **Pain points AI should help with** center entirely on cognitive fatigue and volume management. The primary pain point is the severe visual fatigue that leads to the risk of missing secondary, subtle findings (incidentalomas) due to the necessity of rapid patient throughput. AI must step in to automate highly tedious, repetitive measurements—such as joint space narrowing quantification in osteoarthritis or complex Cobb angles for scoliosis—and provide immediate professional objective checks. Furthermore, AI should actively assist in standardizing reports to reduce the heavy cognitive burden of continuous dictation and manual typing.
|
||||
- **Hindrances in solution design** are heavily rooted in behavioral economics and legacy system performance. A critical ethnographic hindrance observed in forums like ykhoa.net is the profound fear of uncompensated workload. If the proposed collaborative platform introduces features that require the radiologist to answer direct, text-based queries from patients or spend excessive time manually validating AI outputs, the system will face universal rejection in the public sector. Additionally, interface latency is a severe technical hindrance; any collaborative tool that slows down the rendering of massive, gigabyte-sized DICOM files will immediately disrupt their highly optimized, fast-paced diagnostic workflow.
|
||||
- **Role in Dev/R&D** processes is paramount; they serve as the ultimate ground-truth validators. In the R&D phase, their expertise is strictly required for accurately annotating MSK datasets, defining the specific clinical relevance of algorithmic edge cases, and rigorously evaluating the ergonomic efficiency of the DICOM viewing workspace. Their continuous feedback is absolutely paramount in finely tuning the AI's sensitivity-to-specificity ratio to prevent alert fatigue.
|
||||
- **How to approach & percieve them (as Product-Dev Team)** requires a strategy of deep professional respect and workflow preservation. The product development team must approach radiologists as overwhelmed, elite experts whose primary and most scarce currency is time. They must never be perceived as luddites or obstacles to digital innovation, but rather as the vital gatekeepers of clinical safety. Engagement and UX research should be focused entirely on invisible workflow optimization, strongly emphasizing how the platform will actively reduce their cognitive load and protect them from malpractice liabilities via AI-backed objective checks, rather than framing the platform as a disruptive tool meant to alter their fundamental diagnostic philosophy.
|
||||
|
||||
### **User Profile 7: The Rheumatologist & Orthopedic Surgeon (Bác sĩ Cơ xương khớp / Chấn thương chỉnh hình)**
|
||||
|
||||
- **Who they are** dictates the final clinical outcome. `These clinical specialists are the ultimate consumers of the diagnostic imaging data and the primary architects of the patient's comprehensive treatment plan.` Orthopedic surgeons focus heavily on mechanical and surgical interventions for acute trauma, severe degenerative diseases, and complex congenital anomalies, while rheumatologists manage chronic, systemic autoimmune conditions affecting the MSK system. They are the definitive authoritative figures in the patient's healthcare journey.
|
||||
- **Age range** is typically clustered between `30 and 60 years old.` Senior surgeons and veteran department heads hold immense, systemic influence over departmental procurement decisions and the formal adoption of new clinical pathways, while younger, highly digital attendings are significantly more likely to engage directly with novel digital patient consultation platforms and mobile health applications.
|
||||
- **Domain proficiency / responsibility** is vast and encompassing. They are highly proficient in complex clinical diagnosis, advanced surgical biomechanics, systemic pharmacology, and long-term, multi-variable disease management. Their massive responsibility encompasses `synthesizing fragmented patient histories`, `physical examination findings,` and varied, highly technical radiological data to `formulate a definitive, actionable treatment strategy.` Ultimately, they bear the `complete clinical, ethical, and legal responsibility for the patient's surgical or therapeutic outcomes.`
|
||||
- **Working environment** is notoriously highly fragmented and intensely pressured. Their daily environment is violently split between the sterile concentration of the operating theater, bustling and chaotic outpatient clinics, and crowded inpatient wards. In major public hospitals like Bach Mai, outpatient clinics are characterized by severe, systemic overcrowding, where a single senior doctor may be forced to see well over a hundred individual patients in a single grueling shift, necessitating incredibly rapid, highly decisive consultations that leave almost no time for extended dialogue.
|
||||
- **What they know** is highly pragmatic. They intimately `know the practical, actionable clinical implications` of specific radiological findings. They know precisely which structural abnormalities require immediate, aggressive surgical intervention, which can be managed conservatively with physical therapy, and the specific surgical approaches required. They possess a deep, practical understanding of patient psychology, recognizing that their patients are often highly anxious, vulnerable, `and highly susceptible to community misinformation.`
|
||||
- **What they do not know** highlights the necessity of the collaborative platform. They often lack the deep, pixel-level technical expertise of the dedicated radiologist regarding complex, multi-layered imaging artifacts or subtle MRI physics. `More significantly, and dangerously, they frequently do not know what the patient is actively doing outside the hospital walls—specifically, if the desperate patient is pursuing highly dangerous folk remedies, unregulated herbal treatments, or inappropriate, aggressive physical manipulations that could severely exacerbate their delicate condition.`
|
||||
- **Attitude toward AI/ML** is strictly pragmatic and highly outcome-oriented`. They are notably less interested in basic AI that merely identifies an obvious femur fracture (which they can easily see from across the room)` and are vastly more interested in complex AI that provides `deep predictive analytics.` For example, they **desire algorithms forecasting the exact rate of cartilage degeneration in knee osteoarthritis or** predicting the **statistical likelihood of hardware failure in spinal fusions based on localized bone density metrics**. Crucially, they view `AI as a massive potential force multiplier for patient education, provided it visually translates complex DICOM data into beautifully simple formats the patient can instantly understand, thereby saving precious consultation minutes.`
|
||||
- **Pain points AI should help with** are heavily centered around time deficits. The most critical pain point is the absolute `lack of time for comprehensive, empathetic patient education during severely overloaded clinical shifts`. AI `must explicitly help by automatically generating visually intuitive, patient-friendly, 3D summaries of the MSK issues directly from the complex DICOM workspace.` Furthermore, they desperately `need the platform to automatically aggregate highly fragmented patient data (historical X-rays, recent MRIs, scattered lab results) into a single, unified, rapid-consumption dashboard to exponentially expedite their clinical decision-making process.`
|
||||
- **Hindrances in solution design** revolve around communication boundaries. A major, system-killing hindrance is the risk of the platform inadvertently encouraging patients to continuously message the busy doctor for minor, non-clinical queries. The ethnographic data shows the fear of an uncompensated, unmanageable volume of digital communication is profound among Vietnamese clinicians. If the proposed collaborative workspace functions too much like a standard, open-ended messaging app (like Zalo or Messenger), it will be aggressively abandoned. The communication flow must be heavily structured and tightly gated.
|
||||
- **Role in Dev/R&D** requires strategic clinical oversight. They are absolutely crucial in defining the core clinical logic and the hierarchical, prioritized display of information within the platform's UI. They must strictly dictate what specific information is clinically relevant enough to be prominently displayed on the primary dashboard and what secondary data should be relegated to background menus. In R&D, they define the acceptable clinical thresholds for the AI's predictive models and dictate the required authoritative tone of the patient-facing educational modules.
|
||||
- **How to approach & percieve them (as Product-Dev Team)** requires positioning the product as a shield. The product development team must perceive these senior doctors as the entire system's core orchestrators who are operating under extreme, punishing time deficits. The UX approach should emphatically emphasize the platform's ability to act as a "clinical force multiplier." Design discussions should entirely center on how the interactive workspace can automate tedious patient education, definitively dispel dangerous, culturally prevalent folk medicine myths , and heavily streamline multi-disciplinary case reviews without adding a single minute or a single extra click to their currently exhausted workflow.
|
||||
|
||||
****
|
||||
|
||||
### **~~User Profile 6: The MSK Sonographer (Bác sĩ Siêu âm Cơ xương khớp)~~**
|
||||
|
||||
- **Who they are** within the clinical ecosystem represents a highly specialized, dynamic subset of diagnostics. MSK Sonographers in Vietnam are often distinct from general dark-room radiologists; they frequently operate as specialized clinicians or general practitioners who have undergone extensive, highly specific supplementary training to master the dynamic, real-time imaging of the musculoskeletal system. They physically operate the high-frequency ultrasound probes to intimately evaluate soft tissues, tendons, ligaments, and superficial bone structures in real-time motion.
|
||||
- **Age range** for this highly active profile is typically `25 to 50` years old. This group includes a significantly large cohort of younger, ambitious medical professionals actively seeking continuing medical education (CME) to differentiate their clinical skill sets, a trend heavily evidenced by the high demand and `rapid enrollment in structured online` and offline training courses offered by institutions like Pham Ngoc Thach University of Medicine and Medic Medical Center.
|
||||
- **Domain proficiency / responsibility** is uniquely tactile, highly kinetic, and heavily operator-dependent. Their primary proficiency lies in real-time functional assessments, such as directly observing tendon gliding or impingement during active patient movement. `They must possess a profound, innate spatial understanding to instantly translate two-dimensional, grainy ultrasound slices into comprehensive three-dimensional anatomical comprehension.` They are also frequently responsible for `precision-guiding interventional procedures, such as targeted joint injections or aspirations.`
|
||||
- **Working environment** constraints are physical and chaotic. Unlike radiologists isolated in quiet, dark reading rooms, sonographers work directly at the patient's bedside, in bustling emergency departments, or in highly trafficked dedicated ultrasound clinics. Their environment is inherently fast-paced, highly `interactive`, and requires continuous, complex physical maneuvering of both the bulky ultrasound equipment and the patient's body. They operate in a unique space where immediate, `continuous verbal communication with the patient regarding pain localization is standard practice`.
|
||||
- **What they know** is rooted in soft-tissue dynamics. They possess deep, specialized knowledge of soft-tissue pathologies, inflammatory markers visible via `Power Doppler,` and the dynamic, mechanical relationships between muscles, tendons, and joints. They explicitly know how to manually manipulate acoustic windows with the probe to actively bypass bone and gas artifacts. They intimately understand the immediate physical limitations and specific pain thresholds of the patient currently lying on their examination table.
|
||||
- **What they do not know** is the deeper structural context. They inherently lack the comprehensive, multi-system, deep-tissue overview provided by an MRI or CT scan. They cannot visualize deep bone marrow pathologies or assess the complete structural integrity of deep, complex joints (like the central hip) that are inaccessible to high-frequency ultrasound waves. Crucially, because ultrasound is so subjective, they are highly dependent on their own mechanical skill, meaning they often do not know how their real-time findings perfectly correlate with universally standardized baselines without subsequent external validation.
|
||||
- **Attitude toward AI/ML** is largely nascent but highly receptive to real-time, assistive technologies. Because MSK ultrasound is so heavily operator-dependent and challenging to master, there is a strong, articulated desire within the community for AI tools that can provide on-the-fly, augmented-reality style anatomical labeling, automate the tedious measurement of tendon thickness during dynamic movement, or standardize the grading of complex inflammatory signals (e.g., synovial hypertrophy in rheumatoid arthritis).
|
||||
- **Pain points AI should help with** revolve around standardization and documentation. The primary pain point is the extreme inter-operator variability inherent in ultrasound diagnostics. `AI must help by enforcing standardized image capture protocols`, providing real-time quality assurance of the acquired `images before the patient leaves the table, and offering automated comparative analysis against the patient's previous scans to track disease progression objectively.` Additionally, AI must desperately assist in generating automated, highly structured preliminary reports based directly on the captured images to `drastically reduce post-examination administrative typing time.`
|
||||
- **Hindrances in solution design** are heavily dominated by the physical constraints of the ultrasound examination itself. A sonographer's hands are perpetually occupied (one firmly manipulating the probe, the other adjusting the machine console), and their eyes are strictly locked on the ultrasound monitor. Therefore, any collaborative platform that requires extensive keyboard input, complex mouse navigation away from the primary viewing area, or disruptive screen toggling during the live examination will be physically unviable and immediately rejected in a clinical setting.
|
||||
- **Role in Dev/R&D** requires physical simulation. In the R&D process, MSK sonographers are absolutely critical for the live usability testing of the platform's interface within a physical, dynamic, bedside environment. They are essential for defining the strict parameters of real-time AI visual overlay features, rigorously ensuring that the added visual interface does not accidentally obscure critical, subtle diagnostic data on the monitor.
|
||||
- **How to approach & percieve them (as Product-Dev Team)** demands a focus on ergonomics. The product team must explicitly perceive sonographers as highly kinetic, physically engaged users. The UX approach must aggressively prioritize hands-free (e.g., `voice-activated or pedal-driven`) or highly streamlined, `single-tap interactions.` The team should focus collaborative discussions on how the new platform can act as an invisible, silent assistant that automatically captures, precisely labels, and seamlessly integrates their isolated ultrasound findings into the broader MSK collaborative workspace alongside the static X-rays and MRIs, effectively bridging the data gap between dynamic and static imaging modalities.
|
||||
|
||||
### [**User Profile 6: The Physical Therapist / PhysioTherapy (Chuyên Viên Vật Lý Trị Liệu)**](https://gemini.google.com/share/4399a5d9c85d)
|
||||
|
||||
- Who They Are & Age Range
|
||||
- **The Downstream Executor:** They are allied health professionals who operate strictly under physician-dictated prescriptions, meaning our system's Role-Based Access Control (RBAC) must legally prevent them from modifying primary medical diagnoses while giving them full autonomy over physical therapy application workflows.
|
||||
- **Highly Stratified Educational Tiers:** Approximately 53% to 54.9% of the user base holds entry-level vocational certificates or 3-year diplomas (Tier 1) with low clinical autonomy, while the remaining cohort consists of 4-year [B.Sc](http://b.sc/). or rare postgraduate practitioners (Tier 2) who handle advanced clinical reasoning. Less than 1% of the entire active field holds a Master's degree or higher.
|
||||
- **The Youth Avalanche Demographic:** This workforce is exceptionally young, with 36% to 40% falling into the 20 to 29 age bracket, and 41.7% between 30 and 39. Less than 10% to 14% of active practitioners are over the age of 40.
|
||||
- **Gender-Variable Physical Mechanics:** Females comprise 60% to 62% of the workforce in general urban hospital units, but the user base shifts to 78.9% male in high-intensity environments like military field hospitals. Your frontend design must feature scalable touch targets and layout adaptability to support varying hand sizes and physical environments.
|
||||
- **The Digital Native Vulnerability Paradox:** While their youth makes them highly tech-literate and fast adopters of cloud-based collaborative platforms, their lack of long-term clinical experience makes them highly prone to early-career physical burnout.
|
||||
- Domain Proficiency / Responsibility
|
||||
- **Hardware and Modality Calibration:** They are highly proficient in calibrating and operating physical treatment agents including TENS, NMES, Shortwave Diathermy, and therapeutic ultrasound machines.
|
||||
- **High-Exertion Manual Interventions:** They routinely execute physically taxing techniques such as joint mobilizations, deep tissue manipulation, trigger point releases, and intensive stretching protocols.
|
||||
- **Kinetic Patient Handling:** They are responsible for the heavy physical labor of lifting, transferring, and repositioning immobile, post-surgical, or neurological patients.
|
||||
- **Chaotic Context Switching:** Their workflow requires them to switch instantly between intense cognitive patient assessments, fine-tuning hardware configuration parameters, and performing exhausting physical manual therapy.
|
||||
- Working Environment
|
||||
- **The Resource-Constrained Public Sector:** The majority of your users operate in general hospitals characterized by extreme patient volumes, high ambient noise, and cramped treatment rooms often smaller than 20 square meters. Stationary desktop workstations are scarce, shared by dozens of staff members, and physically isolated in department corners.
|
||||
- **The State-of-the-Art Private Sector:** A smaller subset works in international or specialized sports clinics featuring advanced, combined therapy units, spacious layouts, and lower patient-to-therapist ratios that afford the time for deep data interaction.
|
||||
- **Austere and Remote Settings:** Some practitioners operate in military field installations or rural clinics with severe spatial constraints and highly volatile, low-bandwidth network infrastructure.
|
||||
- What They Know
|
||||
- **Granular Musculoskeletal Biomechanics:** They possess an expert, native understanding of human skeletal anatomy, muscle origins, insertions, innervations, and compound kinetic chains.
|
||||
- **Physiological Tissue Dynamics:** They understand exactly how different sound wave frequencies, heat agents, and electrical currents interact with and alter biological tissue elasticity.
|
||||
- **Acute Tactile Literacy:** They have highly conditioned palpatory skills, meaning they can instantly detect underlying structural changes, muscle guarding, and tissue density abnormalities purely through manual physical touch.
|
||||
- What They Do Not Know
|
||||
- **Raw Radiological Imaging Literacy:** Unless they hold specialized postgraduate certifications, reading raw, grayscale imaging data, recognizing subtle bone pathomechanics on an X-ray, or interpreting artifact anomalies on a scan is entirely outside their foundational education.
|
||||
- **Formal Evidence-Based Practice (EBP) Architecture:** The vast majority have received zero academic training in research methodologies. They do not know how to construct standardized PICO search strings or navigate academic index databases like Medline.
|
||||
- **AI Mathematical Frameworks:** Computational models, computer vision neural networks, and algorithmic prediction logic are completely opaque to them, meaning they perceive any unexplained AI output as an untrustworthy "black box".
|
||||
- **Integrated Longitudinal Records:** Due to systemic hospital data silos, they operate without any automated visibility into a patient's cross-departmental records, lab results, or concurrent medical prescriptions.
|
||||
- Attitude Toward AI/ML
|
||||
- **Defensiveness Against Clinical Devaluation:** Due to local market saturation and low entry-level compensation, younger therapists harbor economic anxiety about being commoditized. If our AI delivers authoritarian, rigid treatment mandates that bypass their personal clinical judgment, they will view the app as a threat and hostilely reject it.
|
||||
- **Appetite for Objective Validation Tools:** Conversely, they are highly receptive to features that provide empirical, quantifiable feedback (such as automatically calculating structural changes over a 6-week protocol) because it gives them undeniable data to validate their clinical expertise to skeptical doctors and patients.
|
||||
- **Acceptance Contingent on Transparancy:** They will only trust machine learning suggestions if the interface utilizes Explainable AI (XAI) principles, explicitly displaying the underlying clinical metrics behind a recommendation.
|
||||
- Pain Points AI Should Help With
|
||||
- **The Information Bottleneck:** Vietnamese Physiotherapists struggle to accurately target internal tissue pathologies during therapy because doctors rarely share full digital DICOM imaging data down the chain, providing only brief text prescription sheets, leading to semi-blind therapeutic execution via estimated surface anatomy and reduced treatment efficacy.
|
||||
- **The WMSD Crisis:** Vietnamese Physiotherapists struggle to maintain their own physical health and career longevity because constant manual strain and repetitive high-intensity clinical interventions overload their bodies, leading to an alarming 76.4% prevalence rate of painful, debilitating Work-Related Musculoskeletal Disorders.
|
||||
- **Crippling Time Poverty:** Vietnamese Physiotherapists struggle to implement evidence-based clinical practices and complete manual charting because extreme caseload pressures force them to treat 11 to 20+ patients per single daily shift, leading to severe operational time constraints, administrative exhaustion, and zero available time for typing medical records.
|
||||
- **`The Cross-Domain Literacy Gap`:** Vietnamese Physiotherapists struggle to accurately interpret diagnostic findings and seamlessly communicate with prescribing physicians because their training in kinetic biomechanics completely mismatches the physician’s world of raw radiological data, a barrier compounded by a massive 84% foreign language deficit and zero formal training in clinical statistics, leading to severe inter-departmental communication barriers, a reliance on subjective guesswork, and highly ineffective or contraindicated treatment suggestions.
|
||||
- Hindrances in Solution Design
|
||||
- **The Severe Foreign Language Barrier:** A massive 84% of Vietnamese PTs cite reading professional literature in foreign languages as their primary professional barrier, with only 25% reporting adequate English reading skills. Any unlocalized English UI elements or untranslated clinical documentation will result in total system failure.
|
||||
- **The "Informal Peer" Workflow Loop:** When facing a complex clinical decision, 96% rely on personal experience, 93% rely on informal chats with nearby peers, and 86% rely on old textbooks. They almost never utilize formal digital research dockets.
|
||||
- **Physical UI Contamination:** Because their hands are continuously covered in conductive ultrasound transmission gels, massage oils, or sweat, precise multi-touch menus and complex text inputs are completely unusable during patient sessions.
|
||||
- Role in Dev/R&D
|
||||
- **Physical Usability Grounding:** They must be utilized in ethnographic shadowing and beta testing to ensure our interface supports knuckle taps, gestures, or stylus interactions that function seamlessly when hands are covered in gel.
|
||||
- **FRONTEND CLINICAL VALIDATION:** They act as our real-time feedback loop, manually flagging when automated AI protocol recommendations conflict with a patient's physical pain tolerance or structural presentation.
|
||||
- How to Approach & Perceive Them (as Product-Dev Team)
|
||||
- **The "Clinical Athlete" Engineering Model:** Never design this software as if it is for a stationary desktop user. You must treat the PT as a highly kinetic, physically exhausted movement athlete who requires an interface optimized for extreme speed, low cognitive friction, and rapid physical access.
|
||||
- **The Ergonomic Value Proposition:** Frame the entire platform to the client not as a data tracking utility, but as an **intelligent digital exoskeleton** engineered specifically to absorb their administrative workload, protect their bodies from chronic injury, and visually elevate their clinical standing.
|
||||
|
||||
---
|
||||
|
||||
### Out-of-Domain Glossary (Physiotherapy & MSK Jargon)
|
||||
|
||||
### 1. Core Clinical Infrastructure & Data Types
|
||||
|
||||
- **DICOM (Digital Imaging and Communications in Medicine):** The international file format standard for medical imaging. A single DICOM file contains uncompressed high-fidelity image data bundled with heavy, embedded metadata tables detailing patient demographics, scanning coordinates, and precise machine calibration arrays.
|
||||
- **MSK (Musculoskeletal):** The complex anatomical framework comprising muscles, bones, cartilage, joints, ligaments, tendons, and bursa that support structure and enable physical locomotion.
|
||||
- **EBP (Evidence-Based Practice):** A medical decision-making framework requiring clinicians to systematically integrate the highest quality current published scientific research with their personal clinical expertise and the unique preferences of the patient.
|
||||
- **PICO Formulation:** A standardized syntax used to convert complex, vague clinical problems into clean, structured queries. It isolates variables into: **P**atient/Problem, **I**ntervention, **C**omparison, and **O**utcome.
|
||||
|
||||
### 2. Physical Therapy Modalities & Hardware Targets
|
||||
|
||||
- **Therapeutic Ultrasound:** A physical therapy device that emits high-frequency sound waves (0.8 to 3 MHz) directly into tissue via a handheld transducer to generate deep localized heat, dilate blood vessels, and increase collagen elasticity. Note: Unlike diagnostic ultrasound, it cannot generate an image on a screen.
|
||||
- **Diagnostic Ultrasound (Sonography):** An imaging modality used by doctors to capture and display real-time, dynamic grayscale representations of soft tissue boundaries, structural inflammation, and fluid accumulation.
|
||||
- **TENS (Transcutaneous Electrical Nerve Stimulation):** A modality that delivers low-voltage electrical currents via skin-surface electrodes to stimulate sensory nerves, effectively jamming the neural pathways that transmit pain signals to the brain.
|
||||
- **NMES (Neuromuscular Electrical Stimulation):** A device that passes electrical currents directly into a muscle belly to force involuntary muscle contractions, commonly utilized to reverse muscle atrophy or re-train pathways after neurological trauma.
|
||||
- **Shortwave Diathermy (SWD):** A high-frequency electromagnetic modality that uses deep wave currents to apply uniform therapeutic heat to extensive, deep-seated muscle masses and joint structures.
|
||||
- **Extracorporeal Shockwave Therapy:** A machine that emits high-energy acoustic shock waves into chronic, calcified soft tissues to intentionally cause localized micro-trauma, forcing the body to restart its natural healing and inflammatory cascades.
|
||||
|
||||
### 3. Manual Techniques & Structural Frameworks
|
||||
|
||||
- **Joint Mobilization:** A skilled, passive manual therapy technique where a PT applies targeted, graded physical forces at specific angles to glide, slide, or distract a patient's joint structures to restore normal structural motion.
|
||||
- **PNF (Proprioceptive Neuromuscular Facilitation):** An advanced therapeutic stretching framework that alternatingly triggers muscle contraction and passive relaxation against manual resistance to override standard neurological muscle-tightness reflexes.
|
||||
- **Palpatory Literacy:** The highly trained ability of a healthcare professional to identify underlying structural pathologies, knots, density variances, and fluid effusions solely via physical fingertip touch and manual assessment.
|
||||
- **RUSI (Rehabilitative Ultrasound Imaging):** The practice of using real-time ultrasound imaging during a physical therapy session as a visual biofeedback tool so both the therapist and the patient can immediately see if deep postural stabilization muscles are firing correctly.
|
||||
- **Anisotropic Artifact:** A physics-based visual error on an ultrasound scan where a completely healthy, uniform tendon appears falsely dark, hypoechoic, or "torn" simply because the acoustic beam did not strike the tissue fibers at a perfect 90-degree perpendicular angle.
|
||||
- **WMSDs (Work-Related Musculoskeletal Disorders):** Inflammation or structural damage to muscles, nerves, tendons, or ligaments that is directly caused, accelerated, or aggravated by repetitive work tasks, sustained poor ergonomics, or heavy physical lifting.
|
||||
|
||||
---
|
||||
|
||||
### **User Profile 8: The MSK Patient & Family Caregiver (Bệnh nhân Cơ xương khớp & Người nhà)**
|
||||
|
||||
- **Who they are** represents the most vulnerable and most populous user segment. This profile comprehensively encompasses individuals suffering from chronic or acute locomotive disorders, ranging from severe, mobility-limiting osteoarthritis to debilitating, acute herniated discs, alongside their immediate, highly involved family members. In the specific Vietnamese socio-cultural context, serious medical decisions are practically never made in isolation by the individual; adult children frequently and aggressively navigate the complex healthcare system on behalf of their elderly parents, while parents aggressively seek the absolute best care for their children, driven by deep, unwavering cultural imperatives.
|
||||
- **Age range** is bifurcated. The primary patients themselves are predominantly middle-aged to elderly (45 to 80+ years old), firmly representing the demographic most biologically afflicted by degenerative MSK diseases. However, the primary active users of the digital platform are very often their younger, more agile family caregivers (20 to 45 years old) who actually possess the required digital literacy to effectively navigate complex hospital apps, successfully book "green lane" digital appointments, and actively participate in digital social media health forums to crowdsource opinions.
|
||||
- **Domain proficiency / responsibility** regarding formal medical science is generally very low. However, they often possess high, albeit heavily skewed, experiential knowledge based on vast amounts of anecdotal evidence frantically gathered from community networks, massive online forums like Webtretho, or specialized, highly active Facebook groups. Their primary, exhausting responsibility is managing daily, chronic pain, attempting to adhere to complex treatment protocols (often poorly due to misunderstanding), and physically navigating the logistical nightmares of public hospital attendance.
|
||||
- **Working environment** is the home, the pharmacy, and the crowded hospital waiting room. Their environment sharply contrasts with the highly structured clinical setting. Their primary interaction with the healthcare system is deeply characterized by exceptionally long wait times, profound anxiety, and a pervasive feeling of disenfranchisement within the massive, impersonal machinery of central hospitals. They exist in a daily information ecosystem completely saturated with highly conflicting information regarding the efficacy of modern surgical medicine versus traditional, highly accessible remedies.
|
||||
- **What they know** is highly localized and experiential. They intimately know the exact nature of their own pain, their specific mobility limitations, and the severe financial strain their chronic condition places on the entire family unit. They explicitly know how to aggressively seek out alternative solutions when left frustrated by the extreme brevity of public hospital consultations, often turning in desperation to slick chiropractic videos on YouTube or utilizing unregulated folk healers operating within their local communities.
|
||||
- **What they do not know** poses a severe health risk. They fundamentally do not understand the underlying biomechanical realities of their specific conditions. They absolutely do not know how to read a standard X-ray or MRI, seeing only confusing, intimidating shades of gray. Crucially, they often do not understand the severe, irreversible risks associated with unscientific treatments, such as the distinct potential for permanent paralysis from incorrect, aggressive acupressure or massive spinal infection from applying raw leaves directly to spinal regions. They also deeply struggle to comprehend the realistic limitations of surgical interventions, often erroneously expecting immediate, permanent, pain-free cures.
|
||||
- **Attitude toward AI/ML** is largely unformed, highly malleable, but exceptionally susceptible to the precise UI framing of the technology. If the AI is expertly presented as a high-tech, highly objective authority that visually validates their human doctor's hurried diagnosis, it can significantly and immediately increase institutional trust. Because ethnographic data proves many patients already actively seek second opinions on Facebook groups by desperately posting their raw scans , an AI tool integrated into a patient portal that provides an immediate, highly understandable, beautifully rendered visual analysis of their DICOM images would be viewed as highly empowering, deeply reassuring, and vastly superior to social media crowdsourcing.
|
||||
- **Pain points AI should help with** are driven by fear of the unknown. The overwhelming pain point is profound confusion and anxiety stemming directly from a lack of comprehensible, visually accessible information. AI must explicitly help by transforming terrifying, complex DICOM scans into clear, color-coded, interactive 3D anatomical models that explicitly illustrate their specific injury or degeneration in laymen's terms. The platform must actively provide localized, deeply culturally contextualized educational content that patiently explains *why* specific medical treatments are necessary and explicitly *why* specific, popular folk remedies like leaf-wrapping are actively dangerous.
|
||||
- **Hindrances in solution design** are heavily rooted in digital inequity. The digital divide is the most profound, systemic hindrance. Elderly patients exhibit significant, stubborn resistance and a literal inability to adopt new mobile technologies, often actively preferring traditional, highly inefficient "arrive early and wait" methods over interacting with modern digital scheduling apps, directly undermining efficiency efforts like the "luồng xanh". Furthermore, if the platform's educational material is heavily text-based or overly academic in tone, it will be immediately ignored in favor of highly engaging, visually stimulating, but medically inaccurate, social media video content. The necessary reliance on a proxy user (the younger caregiver) vastly complicates the technical design of direct-to-patient privacy, data security, and consent architectures.
|
||||
- **Role in Dev/R&D** demands emotional intelligence from the researchers. Patients and their caregivers must be central to the rigorous evaluation of the platform's accessibility, emotional resonance, and comprehensibility. Their required role in R&D involves deep usability testing of the patient portal, specifically ensuring that the AI-translated educational visuals are actually lowering clinical anxiety rather than inadvertently increasing it by displaying terrifyingly realistic pathology. They provide critical, irreplaceable feedback on the emotional tone of the entire user interface.
|
||||
- **How to approach & percieve them (as Product-Dev Team)** requires profound empathy and accessible design principles. The product development team must perceive this vast profile with deep empathy, explicitly recognizing them as highly vulnerable users frantically navigating a complex, high-anxiety ecosystem. The UX approach must aggressively prioritize extreme, uncompromising simplicity, absolute visual clarity, and highly robust accessibility features (e.g., massive font scaling, high-contrast modes, voice-over capabilities). The team must strategically view the younger caregiver as the primary digital conduit to the elderly patient, thereby requiring the design of sophisticated dual-access interface architectures that allow entire families to collaboratively view, discuss, and understand the educational outputs securely generated from the clinical workspace.
|
||||
- The Multi-hat Scenarios (where 1 doctors shall involve multiple-hats of professional is usual in Vietnam)
|
||||
|
||||
---
|
||||
|
||||
Submodule workspace/sprint_1_2/Design_Material/LEGACY/VKIST_ML/codebase-vkist-ultrasound-legacy added at 8b2758928a
@@ -0,0 +1,578 @@
|
||||
# KNEE ULTRASOUND ANALYSIS API: TÀI LIỆU KỸ THUẬT & HƯỚNG DẪN SỬ DỤNG
|
||||
|
||||
**Phiên bản:** 1.0 | **Ngày:** 03/2026
|
||||
**Framework:** FastAPI + PyTorch | **Python:** 3.10+
|
||||
|
||||
---
|
||||
|
||||
## 1. Tổng quan hệ thống
|
||||
|
||||
Knee Ultrasound Analysis API được xây dựng bằng FastAPI, chuyên phục vụ tác vụ phân tích ảnh siêu âm đầu gối. Hệ thống tích hợp nhiều mô hình Học máy sâu (Deep Learning) nhằm tự động hóa quy trình phân loại mặt cắt, phát hiện dấu hiệu viêm, phân đoạn cấu trúc giải phẫu và đo lường kích thước tổn thương định lượng.
|
||||
|
||||
### 1.1 Các chức năng chính
|
||||
|
||||
* **Phân loại góc chụp siêu âm (Angle Classification):** Tự động nhận dạng mặt cắt/góc chụp từ ảnh đầu vào bao gồm các nhãn: `med-lat`, `post-trans`, `sup-trans-flex`, và `sup-up-long`.
|
||||
|
||||
* **Phát hiện viêm (Inflammation Detection):** Xác định sự hiện diện của tình trạng viêm khớp gối qua hai góc chụp chính là `sup-up-long` và `post-trans`.
|
||||
|
||||
* **Phân đoạn ảnh ngữ nghĩa (Segmentation):** Tách biệt các cấu trúc giải phẫu đích (dịch khớp, gân, xương, màng hoạt dịch...) thành các phân vùng mặt nạ màu riêng biệt.
|
||||
|
||||
* **Đo độ dày tự động (Thickness Measurement):** Tự động tính toán khoảng cách hình học theo đơn vị milimét ($mm$) giữa các phân vùng mô mềm đã được phân đoạn (chỉ áp dụng đối với mặt cắt góc `sup-up-long`).
|
||||
|
||||
* **Đánh giá mức độ viêm (Severity Analysis):** Xếp hạng thang điểm mức độ nghiêm trọng của viêm từ cấp độ 0 (Rất nhẹ) đến cấp độ 3 (Nặng) dựa trên tỷ lệ diện tích dịch khớp và sự tăng sinh màng hoạt dịch.
|
||||
|
||||
|
||||
|
||||
### 1.2 Luồng xử lý dữ liệu tổng thể (Pipeline Processing)
|
||||
|
||||
Khi nhận tập tin ảnh từ máy trạm (Client), hệ thống sẽ thực hiện phân nhánh xử lý logic động dựa trên kết quả của khối phân loại góc chụp:
|
||||
|
||||
| Góc chụp phát hiện | Quy trình xử lý chi tiết trong Backend pipeline |
|
||||
| --- | --- |
|
||||
| **`post-trans`** | Phân loại góc $\rightarrow$ Phát hiện viêm $\rightarrow$ Phân đoạn ảnh POST $\rightarrow$ Trả kết quả JSON & Mask.|
|
||||
| **`sup-up-long`** | Phân loại góc $\rightarrow$ Phát hiện viêm $\rightarrow$ Phân đoạn ảnh SUP $\rightarrow$ Đo độ dày mô $\rightarrow$ Đánh giá mức độ nặng $\rightarrow$ Trả kết quả.|
|
||||
| **`med-lat`** or **`sup-trans-flex`** | Chỉ thực hiện phân loại góc $\rightarrow$ Trả kết quả trực tiếp (Bỏ qua nhánh phân đoạn & đo lường).|
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
skinparam PackageStyle rect
|
||||
skinparam BackgroundColor #FFFFFF
|
||||
skinparam ArrowColor #2C3E50
|
||||
|
||||
title KIẾN TRÚC ĐIỀU HƯỚNG PIPELINE BACKEND (FASTAPI)
|
||||
|
||||
start
|
||||
|
||||
:Nhận ảnh siêu âm thô (Multipart HTTP Post);
|
||||
:Chạy mô hình Phân loại Góc chụp (Angle Classification);
|
||||
|
||||
if (Góc chụp nhận diện được là gì?) then (post-trans)
|
||||
:Chạy mô hình Phát hiện Viêm;
|
||||
:Chạy mô hình Phân đoạn góc POST (Deeplabv3 ResNet101);
|
||||
:Kết xuất Ảnh Overlay Mask & Metadata;
|
||||
elseif (sup-up-long) then (sup-up-long)
|
||||
:Chạy mô hình Phát hiện Viêm;
|
||||
:Chạy mô hình Phân đoạn góc SUP (Tùy chọn: Deeplabv3 / UNet3+ / vv.);
|
||||
:Tính toán độ dày tự động (Thickness Measurement);
|
||||
:Tính toán điểm Severity (Severity Score Analysis);
|
||||
:Kết xuất Ảnh Overlay Mask & Thống kê định lượng;
|
||||
else (med-lat / sup-trans-flex)
|
||||
:Ghi nhận nhãn góc chụp;
|
||||
note right: Không cấu hình phân đoạn\ncho các mặt cắt này
|
||||
endif
|
||||
|
||||
:Đóng gói Response JSON (Success=True);
|
||||
:Trả kết quả về cho Client Application;
|
||||
stop
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Hướng dẫn cài đặt & Triển khai môi trường
|
||||
|
||||
### 2.1 Yêu cầu hệ thống tối thiểu
|
||||
|
||||
| Thành phần cấu phần | Thông số kỹ thuật yêu cầu tối thiểu |
|
||||
| --- | --- |
|
||||
| **Hệ điều hành** | Ubuntu 20.04+ / Windows 10+ / macOS 12+ |
|
||||
| **Môi trường Python** | Phiên bản 3.10 cố định |
|
||||
| **Bộ nhớ RAM** | 16 GB trở lên |
|
||||
| **Bộ xử lý đồ họa (GPU)** | NVIDIA GPU hỗ trợ nền tảng CUDA 12.4 (Khuyến nghị để tối ưu tốc độ) |
|
||||
| **Dung lượng VRAM** | Tối thiểu 8 GB (Khuyến nghị 16 GB nếu chạy song song đồng thời nhiều mô hình)|
|
||||
| **Ổ cứng lưu trữ** | Tối thiểu 15 GB dung lượng trống (Dành cho bộ cài đặt và file weights `.pth`) |
|
||||
| **Bộ công cụ bổ trợ** | CUDA Toolkit 12.4 & cuDNN 9.x tương thích tương ứng|
|
||||
|
||||
### 2.2 Khởi tạo môi trường ảo
|
||||
|
||||
Tải mã nguồn dự án từ kho lưu trữ Git:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/itvkist/vkist-ultrasound.git
|
||||
cd vkist-ultrasound
|
||||
|
||||
```
|
||||
|
||||
Khuyến nghị thiết lập môi trường bằng **Conda** nhằm quản lý và cô lập các gói phụ thuộc:
|
||||
|
||||
```bash
|
||||
conda create -n vkist-ultrasound python=3.10 -y
|
||||
conda activate vkist-ultrasound
|
||||
|
||||
```
|
||||
|
||||
*Hoặc khởi tạo nhanh bằng mô-đun thư viện chuẩn `venv` nếu hệ thống chưa cài đặt Anaconda*:
|
||||
|
||||
```bash
|
||||
# Trên nền tảng hệ điều hành Linux / macOS
|
||||
python3.10 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Trên nền tảng hệ điều hành Windows
|
||||
venv\Scripts\activate
|
||||
|
||||
```
|
||||
|
||||
### 2.3 Cài đặt các gói thư viện phụ thuộc (Dependencies)
|
||||
|
||||
Thực hiện cài đặt các thư viện lõi quy định trong tệp cấu hình:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
|
||||
```
|
||||
|
||||
Trong trường hợp nhân của khung phần mềm PyTorch không nhận diện được phần cứng CUDA, tiến hành ghi đè cài đặt thủ công phiên bản biên dịch GPU:
|
||||
|
||||
```bash
|
||||
pip install torch==2.5.0+cu124 torchvision==0.20.0+cu124 --index-url https://download.pytorch.org/whl/cu124
|
||||
|
||||
```
|
||||
|
||||
> ⚠️ **LƯU Ý QUAN TRỌNG VỀ PACKAGE NATTEN:**
|
||||
> Dòng cấu hình cài đặt gói `natten==0.17.3+torch250cu124` mặc định đã bị gắn chú thích (`#` comment out) trong tệp `requirements.txt`. Nếu bạn sử dụng các kiến trúc mạng Transformer nâng cao yêu cầu gói này, bắt buộc cài đặt thủ công qua liên kết phân phối bánh xe (wheels) chính thức:
|
||||
> `pip install natten==0.17.3+torch250cu124 -f https://shi-labs.com/natten/wheels/`
|
||||
>
|
||||
>
|
||||
|
||||
### 2.4 Cấu trúc cây thư mục dự án chuẩn
|
||||
|
||||
Để đảm bảo hệ thống FastAPI khởi chạy chính xác và tự động nạp các tệp trọng số mô hình, cấu trúc cây thư mục dự án cần được sắp xếp như sau:
|
||||
|
||||
```text
|
||||
project/
|
||||
├── app.py # Trình khởi chạy máy chủ chính - FastAPI Server
|
||||
├── requirements.txt # Danh sách các gói thư viện phụ thuộc hệ thống
|
||||
├── arch/ # Thư mục mã nguồn chứa định nghĩa kiến trúc mạng Custom
|
||||
│ ├── efficientfeedback.py # Định nghĩa mạng EfficientFeedbackNetwork
|
||||
│ └── unet3plus_att.py # Định nghĩa mạng phân đoạn UNet3Plus_Attention
|
||||
├── models/ # Thư mục lưu trữ tệp trọng số nhị phân (.pth)
|
||||
│ ├── best_convnext_tiny.pth
|
||||
│ ├── best_densenet.pth
|
||||
│ ├── best_resnet50.pth
|
||||
│ ├── best_efficientnet_b2.pth
|
||||
│ ├── best_swin_v2_s.pth
|
||||
│ ├── efficientnet_b0_ultrasound_2_class.pth
|
||||
│ ├── best_model_Deeplav3.pth
|
||||
│ ├── unet_resnet101.pth
|
||||
│ ├── efficientfeedback.pth
|
||||
│ ├── unet3plus_att.pth
|
||||
│ └── best_model_deeplabv3_resnet101_seed_16.pth
|
||||
├── templates/ # Các tài nguyên phục vụ giao diện Web tích hợp
|
||||
│ ├── index.html
|
||||
│ ├── css/
|
||||
│ └── js/
|
||||
├── uploads/ # Thư mục lưu trữ tạm ảnh thô đầu vào (Tự động khởi tạo)
|
||||
└── results/ # Thư mục lưu trữ ảnh kết quả phân đoạn (Tự động khởi tạo)
|
||||
|
||||
```
|
||||
|
||||
### 2.5 Khởi động máy chủ dịch vụ
|
||||
|
||||
Thực thi lệnh chạy máy chủ tại thư mục gốc:
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
|
||||
```
|
||||
|
||||
**Mô phỏng nhật ký màn hình console khi máy chủ khởi chạy thành công:**
|
||||
|
||||
```độc_thoại
|
||||
[INFO] Loading deep learning weights safely to memory...
|
||||
[INFO] Device successfully mapped: cuda (NVIDIA GeForce RTX 4090)
|
||||
[INFO] FastAPI Application core initialized.
|
||||
[INFO] Uvicorn server running on http://localhost:8000 (Press CTRL+C to quit)
|
||||
|
||||
```
|
||||
|
||||
* **Giao diện Web UI kiểm thử trực quan:** `http://localhost:8000`
|
||||
|
||||
|
||||
* **Tài liệu API tương tác tự động (Swagger UI):** `http://localhost:8000/docs`
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. Tài liệu đặc tả API (API Reference)
|
||||
|
||||
### 3.1 Trạng thái hoạt động (Health Check)
|
||||
|
||||
* **Endpoint:** `GET /api/health`
|
||||
|
||||
|
||||
* **Chức năng:** Kiểm tra tính sẵn sàng phục vụ của cụm dịch vụ API Backend.
|
||||
|
||||
|
||||
* **Định dạng dữ liệu phản hồi (Response JSON):**
|
||||
```json
|
||||
{
|
||||
"status": "healthy"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3.2 Phân tích ảnh siêu âm chính - `POST /api/analyze`
|
||||
|
||||
Mã hóa dữ liệu đầu vào dưới dạng tệp tin `multipart/form-data` để gửi tới mạng mạng nơ-ron xử lý.
|
||||
|
||||
#### Các tham số yêu cầu (Request Parameters)
|
||||
|
||||
| Tham số cấu hình | Phương thức truyền | Kiểu dữ liệu | Giá trị mặc định | Định nghĩa chức năng chi tiết |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **`image`** | Multipart Form | Binary File | *Bắt buộc* | Tệp tin ảnh siêu âm đầu gối cần xử lý (Hỗ trợ mở rộng định dạng: `.jpg`, `.png`, `.bmp`).|
|
||||
| **`angle_model`** | Query String | String | `convnext` | Tên định danh mô hình đảm nhận tác vụ phân loại góc chụp.|
|
||||
| **`inflammation_model`** | Query String | String | `efficientnet_b0` | Mô hình phát hiện tình trạng viêm (Hiện tại cố định cấu hình mạng).|
|
||||
| **`segment_model_sup`** | Query String | String | `deeplabv3` | Mô hình phân đoạn cấu trúc giải phẫu dành cho mặt cắt góc `sup-up-long`.|
|
||||
| **`segment_model_post`** | Query String | String | `deeplabv3_resnet101` | Mô hình phân đoạn cấu trúc giải phẫu dành cho mặt cắt góc `post-trans`.|
|
||||
|
||||
#### Danh sách định danh mô hình khả dụng trong hệ thống
|
||||
|
||||
| Phân nhóm Task | Tên tham số truyền vào | Kiến trúc mạng nơ-ron gốc | Mô tả đặc tính đầu ra |
|
||||
| --- | --- | --- | --- |
|
||||
| **Phân loại Góc chụp** | `convnext` | ConvNeXt Tiny | Phân cấp phân loại ra 4 lớp nhãn đầu ra.|
|
||||
| | `densenet` | DenseNet-121 | Mạng kết nối dày đặc.|
|
||||
| | `resnet50` | ResNet-50 | Kiến trúc mạng dư thừa tiêu chuẩn.|
|
||||
| | `efficientnet_b2` | EfficientNet-B2 | Tối ưu hóa đa quy mô tài nguyên mạng.|
|
||||
| | `swin` | Swin Transformer V2-S | Kiến trúc Attention cửa sổ dịch chuyển.|
|
||||
| **Phân đoạn góc SUP** | `deeplabv3` | DeepLabV3 ResNet-50 | Trích xuất đặc trưng đa tỷ lệ với 7 lớp đầu ra.|
|
||||
| | `unet_resnet101` | UNet + ResNet-101 | Kiến trúc Encoder-Decoder kết hợp ResNet.|
|
||||
| | `efficientfeedback` | EfficientFeedbackNetwork | Thiết kế tùy biến riêng có liên kết phản hồi dữ liệu.|
|
||||
| | `unet3plus` | UNet3+ with Attention | Cơ chế Attention kết hợp kết nối toàn diện Full-scale.|
|
||||
| **Phân đoạn góc POST** | `deeplabv3_resnet101` | DeepLabV3 ResNet-101 | Cấu trúc chuyên sâu phân đoạn góc nhìn mặt sau.|
|
||||
|
||||
#### Cấu trúc dữ liệu JSON phản hồi (Response Body Schema)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"filename": "d3b07384-d113-4ec8-a141-8664b07fb8d3.jpg",
|
||||
"images": {
|
||||
"original": "/uploads/d3b07384-d113-4ec8-a141-8664b07fb8d3.jpg",
|
||||
"segmented": "/results/seg_d3b07384-d113-4ec8-a141-8664b07fb8d3.jpg"
|
||||
},
|
||||
"models_used": {
|
||||
"angle_model": "convnext",
|
||||
"inflammation_model": "efficientnet_b0",
|
||||
"segmentation_model": "deeplabv3"
|
||||
},
|
||||
"angle": {
|
||||
"class": "sup-up-long",
|
||||
"confidence": 98.45
|
||||
},
|
||||
"inflammation": {
|
||||
"detected": true,
|
||||
"confidence": 94.20
|
||||
},
|
||||
"segmentation": {
|
||||
"angle_type": "sup",
|
||||
"classes_detected": ["background", "effusion", "fat", "femur", "synovium", "tendon"],
|
||||
"color_legend": {
|
||||
"background": [0, 0, 0],
|
||||
"effusion": [255, 0, 0],
|
||||
"synovium": [255, 0, 255]
|
||||
}
|
||||
},
|
||||
"measurement": {
|
||||
"thickness_mm": 6.87,
|
||||
"thickness_px": 100,
|
||||
"location_x": 256
|
||||
},
|
||||
"severity": {
|
||||
"level": 3,
|
||||
"severity": "Nặng",
|
||||
"combined_score": 18.4,
|
||||
"effusion": {
|
||||
"pixels": 25400,
|
||||
"ratio": 0.096,
|
||||
"thickness": 6.87
|
||||
},
|
||||
"synovium": {
|
||||
"pixels": 12500,
|
||||
"ratio": 0.047
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Ví dụ mã triển khai gọi dịch vụ (Client Invocations)
|
||||
|
||||
* **Sử dụng lệnh Client cURL CLI:**
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/analyze?angle_model=convnext&segment_model_sup=deeplabv3" \
|
||||
-F "image=@/data/medical/knee_sample.jpg"
|
||||
|
||||
```
|
||||
|
||||
* **Triển khai ứng dụng gọi qua script Python (Requests):**
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "http://localhost:8000/api/analyze"
|
||||
query_parameters = {
|
||||
"angle_model": "swin",
|
||||
"segment_model_sup": "unet3plus"
|
||||
}
|
||||
|
||||
target_image_path = "path/to/clinical_knee.jpg"
|
||||
with open(target_image_path, "rb") as image_file:
|
||||
payload = {"image": image_file}
|
||||
api_response = requests.post(url, params=query_parameters, files=payload)
|
||||
|
||||
parsed_result = api_response.json()
|
||||
print("Phân loại góc:", parsed_result["angle"]["class"])
|
||||
print("Số liệu đo lường hình học:", parsed_result.get("measurement"))
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Các thông số cấu hình lõi hệ thống
|
||||
|
||||
### 4.1 Hằng số hệ thống trong `app.py`
|
||||
|
||||
| Tên định danh hằng số | Giá trị mặc định | Diễn giải chức năng kỹ thuật |
|
||||
| --- | --- | --- |
|
||||
| `UPLOAD_FOLDER` | `'uploads'` | Đường dẫn cục bộ lưu trữ file ảnh thô nhận từ máy trạm.|
|
||||
| `RESULTS_FOLDER` | `'results'` | Đường dẫn lưu ảnh màu sau phân đoạn (Color Mask Overlayed).|
|
||||
| `TEMPLATES_FOLDER` | `'templates'` | Thư mục chứa mã nguồn giao diện phân tích Web UI.|
|
||||
| `PIXEL_TO_MM` | $\frac{45.0}{655.0} \approx 0.0687$ | Hệ số chuyển đổi từ độ phân giải pixel sang kích thước thực tế ($mm$). Phụ thuộc cố định vào cấu hình đầu ra của phần cứng máy quét siêu âm.|
|
||||
| `DEFAULT_MEASURE_IDS` | `[1, 5]` | Danh sách mảng chứa ID nhãn lớp cấu trúc giải phẫu kích hoạt thuật toán đo độ dày: `1 = effusion` (Dịch khớp), `5 = synovium` (Màng hoạt dịch).|
|
||||
| `device` | `cuda` hoặc `cpu` | Khối phần cứng thực thi tính toán đồ họa (Tự động thiết lập dựa trên tính khả dụng của driver NVIDIA).|
|
||||
|
||||
### 4.2 Cấu hình Pipeline tiền xử lý và biến đổi ma trận ảnh (Transforms)
|
||||
|
||||
Hệ thống phân tách ảnh đầu vào thành các luồng biến đổi riêng biệt trước khi nạp vào tensor mô hình tùy thuộc vào mục tiêu xử lý chuyên biệt:
|
||||
|
||||
| Luồng xử lý Pipeline ảnh | Kích thước chuyển đổi (Resize) | Quy định chuẩn hóa phân phối ma trận (Normalization) |
|
||||
| --- | --- | --- |
|
||||
| **Phân loại góc & Phát hiện viêm** | <br>$224 \times 224$ pixel | Áp dụng phân phối phân cấp:<br> $\text{mean} = [0.485, 0.456, 0.406]$, <br> $\text{std} = [0.229, 0.224, 0.225]$ |
|
||||
| **Phân đoạn cấu trúc (Segmentation)** | <br>$512 \times 512$ pixel | Không áp dụng chuẩn hóa phân phối (Chỉ thực thi hàm chuyển đổi tensor `ToTensor()`) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Ràng buộc kỹ thuật & Quy tắc thiết kế hệ thống
|
||||
|
||||
### 5.1 Quản lý và giải phóng tài nguyên bộ nhớ GPU (VRAM Leak Warning)
|
||||
|
||||
Trong phiên bản hiện tại, logic xử lý nội tại của API kích hoạt các hàm `load_angle_model()`, `load_inflammation_model()`, và `load_segmentation_model_*()` trực tiếp bên trong vòng đời của mỗi phiên request nhận về. Hành vi này ép buộc GPU liên tục nạp lại dữ liệu tệp `.pth` vào VRAM cho mỗi giao dịch HTTP, sinh ra độ trễ (Overhead) I/O lớn và tiềm ẩn nguy cơ tràn bộ nhớ hệ thống. Khi triển khai môi trường Production, bắt buộc phải tái cấu trúc chuyển các hàm này thành Singleton dịch vụ (Tải một lần duy nhất lúc khởi động tiến trình Web Server).
|
||||
|
||||
### 5.2 Ràng buộc phi tuyến tính của tham số vật lý `PIXEL_TO_MM`
|
||||
|
||||
Hằng số quy đổi $\text{PIXEL\_TO\_MM} = \frac{45.0}{655.0}$ là một giá trị được cấu hình cứng (Hardcoded) trong mã nguồn, đặc trưng duy nhất cho một dòng máy siêu âm lâm sàng có tỷ lệ hiển thị $45mm$ tương đương với độ phân giải vùng quét $655\text{ px}$. Khi hệ thống thu thập ảnh siêu âm từ các thiết bị chuẩn đoán hình ảnh khác, hoặc thay đổi độ phân giải ảnh xuất ra, số liệu đo khoảng cách tổn thương sẽ sai lệch nghiêm trọng nếu hằng số này không được hiệu chuẩn lại thông qua ma trận nội quan của máy quét mới.
|
||||
|
||||
### 5.3 Quy tắc ánh xạ phân lớp (Class Remapping Matrix) đối với mô hình Custom
|
||||
|
||||
Hai mô hình tùy biến sâu phục vụ mặt cắt góc nhìn phía trên bánh chè (`UNet3+` và `EfficientFeedback Network`) được huấn luyện trên tập dữ liệu đặc thù sở hữu thứ tự cấu trúc mảng nhãn đầu ra lệch pha hoàn toàn so với kiến trúc phân cấp chuẩn của hệ thống. Để thống nhất dữ liệu trả về cho Client, khối Backend API thực hiện cơ chế tự động chuyển đổi chỉ mục mảng (Index Remapping) theo bảng đặc tả logic dưới đây:
|
||||
|
||||
| Chỉ mục Mô hình gốc (Output Model Index) | Chỉ mục chuẩn hóa hệ thống (Standard System Index) | Tên nhãn lớp giải phẫu tương ứng (Anatomical Label Class) |
|
||||
| --- | --- | --- |
|
||||
| `0` | `0` | **`background`** (Nền ảnh không chứa cấu trúc) |
|
||||
| `1` | `2` | **`fat`** (Lớp mô mỡ dưới da) |
|
||||
| `2` | `6` | **`tendon`** (Cấu trúc gân cơ) |
|
||||
| `3` | `1` | **`effusion`** (Vùng tụ dịch khớp gối ổ viêm) |
|
||||
| `4` | `4` | **`femur`** (Ranh giới cấu trúc xương đùi) |
|
||||
| `5` | `5` | **`synovium`** (Màng hoạt dịch bao quanh khớp) |
|
||||
| `6` | `3` | **`fat-pat`** (Tổ chức mỡ Hoffa) |
|
||||
|
||||
### 5.4 Cơ chế tự động dọn dẹp tập tin tồn đọng (Garbage Collection Task)
|
||||
|
||||
Các tập tin ảnh thô tải lên thư mục `uploads/` và ảnh xử lý nhị phân kết xuất lưu trong `results/` được ghi dưới dạng định danh chuỗi không trùng lặp UUID và lưu trữ vô thời hạn trên đĩa cứng hệ thống. Hệ thống lõi của ứng dụng không tích hợp cơ chế tự động giải phóng (Auto-deletion) các tệp cũ. Khi chạy vận hành dài hạn trong hệ thống y tế thực tế, bắt buộc phải cấu hình thêm Background Task (sử dụng thư viện Asyncio) hoặc thiết lập dịch vụ Cronjob của hệ điều hành để dọn dẹp định kỳ tránh cạn kiệt dung lượng ổ đĩa lưu trữ.
|
||||
|
||||
---
|
||||
|
||||
## 6. Giải pháp mở rộng tính năng mã nguồn (Backend Optimization Guide)
|
||||
|
||||
### 6.1 Tăng tốc độ phản hồi bằng Cơ chế Caching Mô hình Toàn cục
|
||||
|
||||
Thay thế kiến trúc nạp tải mô hình cũ bằng một kho lưu trữ Cache tĩnh trong bộ nhớ RAM, tối ưu hóa thời gian xử lý request từ mức giây xuống mức mili-giây:
|
||||
|
||||
```python
|
||||
# Cấu hình biến lưu trữ toàn cục ở đầu tệp app.py
|
||||
global_model_cache = {}
|
||||
|
||||
def get_cached_angle_model(selected_model_name: str):
|
||||
cache_lookup_key = f"angle_classification_{selected_model_name}"
|
||||
if cache_lookup_key not in global_model_cache:
|
||||
# Thực hiện nạp trọng số mô hình từ đĩa cứng lần đầu tiên
|
||||
global_model_cache[cache_lookup_key] = load_angle_model(selected_model_name)
|
||||
return global_model_cache[cache_lookup_key]
|
||||
|
||||
# Áp dụng logic tương tự cho get_inflammation_model(), get_seg_model_sup(), get_seg_model_post()
|
||||
|
||||
```
|
||||
|
||||
### 6.2 Thêm mới một kiến trúc phân loại góc chụp (Ví dụ: Vision Transformer - ViT)
|
||||
|
||||
Để tích hợp một mạng nơ-ron mới vào hệ thống xử lý, tuân thủ nghiêm ngặt quy trình 3 bước sau:
|
||||
|
||||
* **Bước 1:** Bổ sung khối xử lý điều kiện rẽ nhánh logic vào hàm khởi tạo mô hình `load_angle_model()`:
|
||||
|
||||
```python
|
||||
elif model_name == 'vit':
|
||||
from torchvision.models import vit_b_16
|
||||
# Khởi tạo mạng mạng ViT không nạp trọng số mặc định ImageNet
|
||||
model = vit_b_16(weights=None)
|
||||
# Tái cấu trúc tầng phân loại tuyến tính cuối cùng tương thích với 4 lớp nhãn đầu gối
|
||||
model.heads[0] = nn.Linear(model.heads[0].in_features, 4)
|
||||
# Tải tệp trọng số huấn luyện cục bộ từ thư mục mô hình
|
||||
checkpoint_tensor = torch.load('models/best_vit_b16.pth', map_location=device, weights_only=False)
|
||||
model.load_state_dict(checkpoint_tensor)
|
||||
|
||||
```
|
||||
|
||||
* **Bước 2:** Di chuyển tệp trọng số huấn luyện nhị phân của mạng (`best_vit_b16.pth`) vào chính xác không gian lưu trữ của thư mục `/models/`.
|
||||
|
||||
* **Bước 3:** Ứng dụng phía Client có thể kích hoạt mạng mới bằng cách truyền giá trị định danh qua tham số URL: `/api/analyze?angle_model=vit`.
|
||||
|
||||
|
||||
|
||||
### 6.3 Xử lý luồng dữ liệu phân đoạn song song đồng thời (Batch Processing API)
|
||||
|
||||
Tối ưu hóa năng lực phục vụ của Server đối với bài toán nhận diện hàng loạt ảnh cùng lúc từ phòng khám bằng endpoint xử lý không đồng bộ:
|
||||
|
||||
```python
|
||||
from fastapi import errors, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List
|
||||
|
||||
@app.post('/api/analyze_batch', status_code=status.HTTP_200_OK)
|
||||
async def analyze_batch_images(images: List[UploadFile] = File(...)):
|
||||
import asyncio
|
||||
|
||||
# Đóng gói các tác vụ phân tích ảnh đơn lẻ vào một danh sách hàng đợi task không đồng bộ
|
||||
async_tasks_queue = [analyze_single_image_pipeline(img) for img in images]
|
||||
|
||||
# Kích hoạt thực thi đồng thời trên luồng phần cứng thông qua gather cơ chế
|
||||
compiled_batch_results = await asyncio.gather(*async_tasks_queue)
|
||||
|
||||
return JSONResponse({
|
||||
"results": compiled_batch_results,
|
||||
"processed_count": len(compiled_batch_results)
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### 6.4 Bản đóng gói container hóa ứng dụng (Production Dockerfile)
|
||||
|
||||
Đóng gói toàn bộ ML Stack bao gồm trình điều khiển GPU NVIDIA CUDA để triển khai đồng bộ trên các hạ tầng Cloud hoặc máy chủ On-Premise của bệnh viện:
|
||||
|
||||
```dockerfile
|
||||
# Sử dụng Base Image chứa sẵn môi trường CUDA 12.4 và cuDNN 9 của NVIDIA
|
||||
FROM nvidia/cuda:12.4.0-cudnn9-runtime-ubuntu22.04
|
||||
|
||||
# Cài đặt môi trường Python 3.10 và các gói hệ thống cốt lõi
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3.10 \
|
||||
python3-pip \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Thiết lập không gian làm việc nội bộ bên trong container
|
||||
WORKDIR /app
|
||||
|
||||
# Sao chép và cài đặt danh sách các thư viện Python
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Sao chép toàn bộ mã nguồn ứng dụng vào Container
|
||||
COPY . .
|
||||
|
||||
# Lắng nghe và kích hoạt ứng dụng Web Server FastAPI
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
```
|
||||
|
||||
* **Lệnh khởi dựng Image hệ thống:** `docker build -t medical-api-service .`
|
||||
|
||||
* **Lệnh kích hoạt Container chia sẻ tài nguyên phần cứng GPU vật lý:**
|
||||
|
||||
```bash
|
||||
docker run --gpus all -p 8000:8000 -v $(pwd)/models:/app/models medical-api-service
|
||||
|
||||
```
|
||||
|
||||
### 6.5 Bộ chuyển đổi tiếp nhận trực tiếp luồng dữ liệu ảnh y tế chuẩn DICOM
|
||||
|
||||
Mở rộng chức năng cho phép hệ thống API đọc trực tiếp tệp tin ảnh gốc dạng `.dcm` trích xuất trực tiếp từ các thiết bị siêu âm chuẩn lâm sàng trong bệnh viện mà không cần qua bước chuyển đổi định dạng thủ công:
|
||||
|
||||
```python
|
||||
import pydicom
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
@app.post('/api/analyze_dicom')
|
||||
async def analyze_dicom_file(file: UploadFile = File(...)):
|
||||
# Đọc luồng byte nhị phân trực tiếp từ tệp DICOM tải lên
|
||||
dicom_dataset = pydicom.dcmread(file.file)
|
||||
|
||||
# Trích xuất ma trận pixel thô từ thẻ DICOM Pixel Data
|
||||
raw_pixel_array = dicom_dataset.pixel_array
|
||||
|
||||
# Chuyển đổi ma trận mảng Numpy sang định dạng ảnh PIL tương thích với Pipeline biến đổi
|
||||
converted_image_pil = Image.fromarray(raw_pixel_array).convert('RGB')
|
||||
|
||||
# Chuyển tiếp ảnh đã đổi định dạng vào luồng xử lý tự động nội bộ của API
|
||||
analysis_output_json = await execute_core_analysis_pipeline(converted_image_pil)
|
||||
return analysis_output_json
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phụ lục: Đặc tả Dữ liệu định lượng lâm sàng
|
||||
|
||||
### Phụ lục A: Bảng phân định mã màu mặt nạ phân đoạn ngữ nghĩa (Color Map Legend)
|
||||
|
||||
1. Cấu trúc Mặt cắt mặt trên bánh chè - Góc SUP (`sup-up-long`)
|
||||
|
||||
Góc SUP tập trung khoanh vùng các lớp mô mềm phía trước đầu gối phục vụ thuật toán tính toán độ dày dịch tụ.
|
||||
|
||||
| Từ khóa nhãn (Key) | Cấu trúc giải phẫu đích | Mã màu hiển thị (RGB) | Trực quan hóa màu sắc |
|
||||
| --- | --- | --- | --- |
|
||||
| `background` | Nền ảnh siêu âm | `[0, 0, 0]` | ⬛ Đen (Không chứa dữ liệu) |
|
||||
| `effusion` | Vùng dịch khớp tụ ổ viêm | `[255, 0, 0]` | 🟥 Đỏ |
|
||||
| `fat` | Tổ chức mô mỡ dưới da | `[255, 255, 0]` | 🟨 Vàng |
|
||||
| `fat-pat` | Khối mỡ Hoffa | `[0, 255, 255]` | 🟦 Lam sáng |
|
||||
| `femur` | Cấu trúc bề mặt xương đùi | `[0, 255, 0]` | 🟩 Xanh lá |
|
||||
| `synovium` | Lớp màng hoạt dịch tăng sinh | `[255, 0, 255]` | 🟪 Tím |
|
||||
| `tendon` | Vùng bó gân cơ | `[0, 0, 255]` | 🟦 Xanh dương |
|
||||
|
||||
> 🔄 **QUY TẮC CHUYỂN ĐỔI CHUYỂN GÓC (SUP $\rightarrow$ POST):**
|
||||
> Khi hệ thống chuyển đổi trạng thái phân tích sang mặt cắt phía sau khớp gối (Góc `POST`), ma trận thuật toán phân đoạn sẽ tự động tái cấu trúc màu sắc ngữ nghĩa: Vùng tổn thương chứa **`effusion`** (màu đỏ) sẽ chuyển trạng thái biểu diễn thành **`baker's cyst`** (Kén Baker), và tổ chức cấu trúc vùng **`fat-pat`** (màu lam sáng) sẽ hoán đổi ý nghĩa thành vùng **`muscle`** (Cơ bắp vùng khoeo).
|
||||
>
|
||||
>
|
||||
|
||||
2. Cấu trúc Mặt cắt mặt sau vùng khoeo chân - Góc POST (`post-trans`)
|
||||
|
||||
| Từ khóa nhãn (Key) | Cấu trúc giải phẫu đích | Mã màu hiển thị (RGB) | Trực quan hóa màu sắc |
|
||||
| --- | --- | --- | --- |
|
||||
| `background` | Nền ảnh siêu âm | `[0, 0, 0]` | ⬛ Đen |
|
||||
| `baker's cyst` | Tổ chức kén hoạt dịch vùng khoeo (Baker) | `[255, 0, 0]` | 🟥 Đỏ |
|
||||
| `fat` | Lớp mô mỡ | `[255, 255, 0]` | 🟨 Vàng |
|
||||
| `muscle` | Các nhóm cơ bắp vùng sau gối | `[0, 255, 255]` | 🟦 Lam sáng |
|
||||
| `femur` | Cấu trúc xương đùi sau | `[0, 255, 0]` | 🟩 Xanh lá |
|
||||
| `synovium` | Màng hoạt dịch mặt sau | `[255, 0, 255]` | 🟪 Tím |
|
||||
| `tendon` | Hệ thống gân cơ mặt sau | `[0, 0, 255]` | 🟦 Xanh dương |
|
||||
|
||||
---
|
||||
|
||||
### Phụ lục B: Thang điểm đánh giá mức độ nghiêm trọng của ổ viêm (Clinical Severity Score)
|
||||
|
||||
Hệ thống chấm điểm toán học tự động căn cứ trên trọng số diện tích và độ dày phân tách để đưa ra kết luận mức độ bệnh lý lâm sàng thông qua phương trình tuyến tính tổng hợp:
|
||||
|
||||
$$\text{combined\_score} = \text{effusion\_score} \times 0.6 + \text{synovium\_score} \times 0.4$$
|
||||
|
||||
Dựa trên kết quả giá trị của biến số $\text{combined\_score}$, hệ thống tự động phân cấp thành 4 ngưỡng trạng thái lâm sàng tương ứng:
|
||||
|
||||
* **Mức 0 - Rất nhẹ ($\text{score} < 3$):** Trạng thái ổ dịch khớp và cấu trúc màng hoạt dịch nằm hoàn toàn trong giới hạn sinh lý bình thường của cơ thể.
|
||||
* **Mức 1 - Nhẹ ($\text{score}$ từ $3$ đến $7.9$):** Xuất hiện hiện tượng tụ dịch khớp lớp mỏng, màng hoạt dịch có dấu hiệu tăng sinh nhẹ cấu trúc màng.
|
||||
* **Mức 2 - Trung bình ($\text{score}$ từ $8$ đến $15$):** Lượng dịch tụ khớp gối ở mức độ vừa phải, màng hoạt dịch bắt đầu phì đại và tăng sinh rõ nét.
|
||||
* **Mức 3 - Nặng ($\text{score} > 15$):** Lớp tụ dịch khớp gối dày kích thước lớn, màng hoạt dịch tăng sinh phì đại mạnh, lan rộng diện tích cấu trúc giải phẫu xung quanh.
|
||||
@@ -0,0 +1,267 @@
|
||||
Dưới đây là toàn bộ nội dung tài liệu về **Công nghệ siêu âm khớp gối (PILOT)** đã được làm sạch, đồng bộ hóa cấu trúc và làm giàu thông tin (enrichment). Các hình ảnh mất mát và sơ đồ quy trình đã được mã hóa chi tiết bằng ngôn ngữ **PlantUML** cùng với các mô tả dữ liệu cấu trúc trực quan nhằm phục vụ tối ưu cho việc huấn luyện, tích hợp hoặc phát triển hệ thống backend của kỹ sư AI/ML stack.
|
||||
|
||||
---
|
||||
|
||||
# CÔNG NGHỆ SIÊU ÂM KHỚP GỐI (PILOT)
|
||||
|
||||
**Tác giả:** Nguyễn Đăng Hà
|
||||
**Đơn vị phát triển:** VKIST (Viện Khoa học và Công nghệ Việt Nam - Hàn Quốc)
|
||||
|
||||
---
|
||||
|
||||
## 1. Giới thiệu chung & Mục tiêu nghiên cứu
|
||||
|
||||
### Giới thiệu chung
|
||||
|
||||
* Tràn dịch khớp gối là một trong những biểu hiện lâm sàng xuất hiện thường gặp của các bệnh lý liên quan đến cơ - xương - khớp.
|
||||
|
||||
|
||||
* Việc nhận diện chính xác và đánh giá chi tiết mức độ viêm khớp gối có ý nghĩa vô cùng quan trọng đối với quá trình điều trị cũng như phục hồi chức năng của bệnh nhân.
|
||||
|
||||
|
||||
* Hiện nay, phương pháp siêu âm được xem là giải pháp hiệu quả hàng đầu để đánh giá tình trạng này nhờ vào các đặc tính: an toàn, hoàn toàn không xâm lấn và tiết kiệm tối đa chi phí cho người bệnh.
|
||||
|
||||
|
||||
|
||||
### Mục tiêu nghiên cứu của dự án
|
||||
|
||||
1. Xây dựng một quy trình chuẩn hóa trong siêu âm chẩn đoán và thiết lập một cơ sở dữ liệu hình ảnh lớn (Large Dataset) chuyên biệt về tràn dịch khớp gối.
|
||||
|
||||
|
||||
2. Nghiên cứu và phát triển phần mềm ứng dụng Trí tuệ nhân tạo (AI) giúp hỗ trợ các bác sĩ lâm sàng chẩn đoán nhanh chóng, hiệu quả tình trạng tràn dịch khớp gối.
|
||||
|
||||
|
||||
3. Tiến hành thử nghiệm lâm sàng, kiểm thử và đánh giá độ chính xác của thuật toán AI trên các tập dữ liệu thực tế thu thập tại Bệnh viện E.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. Quy trình xử lý dữ liệu và Kiến trúc hệ thống (Pipeline AI)
|
||||
|
||||
Hệ thống xử lý ảnh siêu âm được thiết kế theo một chuỗi pipeline tuần tự từ ảnh thô (Raw Image) đầu vào cho đến khi kết xuất báo cáo định lượng. Dưới đây là sơ đồ kiến trúc luồng dữ liệu của hệ thống:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
skinparam handwritten false
|
||||
skinparam monochrome false
|
||||
skinparam packageStyle rect
|
||||
skinparam shadowing true
|
||||
|
||||
title SƠ ĐỒ PIPELINE XỬ LÝ ẢNH SIÊU ÂM KHỚP GỐI (AI BACKEND)
|
||||
|
||||
start
|
||||
|
||||
:Ảnh siêu âm đầu gối thô (Raw Image Input);
|
||||
note right: Tải lên từ client thông qua Web UI
|
||||
|
||||
partition "Khối Tiền Xử Lý (Preprocessing Block)" {
|
||||
:Tiền xử lý dữ liệu ảnh siêu âm;
|
||||
note right: Chuẩn hóa kích thước, lọc nhiễu, cân bằng độ tương phản
|
||||
}
|
||||
|
||||
partition "Khối Phân Loại (Classification Block)" {
|
||||
:Phân loại góc chụp (Scan View Classification);
|
||||
note right: Nhận diện mặt cắt: L SUPRAPAT LONG, L POST TRANS, vv.
|
||||
}
|
||||
|
||||
partition "Khối Nhận Diện & Phân Đoạn (Detection & Segmentation Block)" {
|
||||
:Nhận biết tình trạng viêm;
|
||||
if (Phát hiện thấy có viêm?) then (Có viêm)
|
||||
:Khoanh vùng viêm (Lesion Segmentation);
|
||||
note right: Sử dụng các mô hình: DeepLabV3, MedSAM, UltraSAM
|
||||
else (Không viêm)
|
||||
:Kết luận: Bình thường;
|
||||
detach
|
||||
endif
|
||||
}
|
||||
|
||||
partition "Khối Định Lượng & Phân Tích (Quantitative Analytics Block)" {
|
||||
:Phân tích mức độ viêm;
|
||||
note right: Tính toán diện tích vùng viêm,\ntỷ lệ phần trăm pixel viêm, quan hệ không gian với mô gân
|
||||
}
|
||||
|
||||
:Kết xuất kết quả (Output Analytics & Report);
|
||||
note right: Trả về file JSON kết quả, ảnh phân đoạn \nvà lưu trữ vào cơ sở dữ liệu hệ thống
|
||||
|
||||
stop
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Các Mô hình Deep Learning áp dụng trong hệ thống
|
||||
|
||||
Mô hình AI được chia tách thành ba nhiệm vụ chính: Phân loại mặt cắt ảnh siêu âm, Phát hiện viêm (biến cố cố định) và Phân đoạn ngữ nghĩa (Semantic Segmentation) các vùng giải phẫu tổn thương.
|
||||
|
||||
### 3.1. Mô hình Phân loại Góc chụp (Scan View Classification - `angle_model`)
|
||||
|
||||
* **Nhiệm vụ:** Tự động nhận diện và phân loại cấu trúc tư thế đặt đầu dò siêu âm.
|
||||
* **Mô hình mặc định:** `convnext`
|
||||
* **Các kiến trúc hỗ trợ:**
|
||||
* **ConvNeXt Tiny:** Cấu hình gồm 4 lớp đầu ra (Mặc định).
|
||||
* **DenseNet-121:** Trích xuất đặc trưng sâu nhờ cơ chế kết nối dày đặc.
|
||||
* **ResNet-50:** Kiến trúc mạng phần dư chuẩn hóa giúp tối ưu hóa quá trình hội tụ.
|
||||
* **EfficientNet-B2:** Cân bằng tối ưu giữa hiệu năng và tài nguyên tính toán.
|
||||
* **Swin Transformer V2-S:** Mô hình dựa trên cơ chế Attention dịch chuyển cửa sổ, nâng cao khả năng học đặc trưng toàn cục.
|
||||
|
||||
* **Các mặt cắt mục tiêu chính:**
|
||||
* `L SUPRAPAT LONG` (Mặt cắt dọc trên xương bánh chè khớp gối trái).
|
||||
* `L POST TRANS` (Mặt cắt ngang phía sau khớp gối trái).
|
||||
|
||||
### 3.2. Mô hình Phát hiện Viêm (Inflammation Detection - `inflammation_model`)
|
||||
|
||||
* **Nhiệm vụ:** Tự động phát hiện và đánh giá tình trạng viêm (hiện cố định trong hệ thống).
|
||||
* **Mô hình mặc định:** `efficientnet_b0`
|
||||
|
||||
### 3.3. Mô hình Khoanh vùng & Phân đoạn tổn thương (Segmentation Models)
|
||||
|
||||
Hệ thống cho phép tùy chọn linh hoạt các kiến trúc mạng tiên tiến nhằm khoanh vùng chính xác các cấu trúc giải phẫu và ổ viêm theo từng nhóm góc chụp:
|
||||
|
||||
#### 3.3.1. Nhóm Phân đoạn SUP (Mặt cắt dọc trên xương bánh chè - `segment_model_sup`)
|
||||
|
||||
| Tên Mô Hình (Giá trị truyền vào) | Kiến trúc kĩ thuật & Vai trò trong hệ thống |
|
||||
| --- | --- |
|
||||
| **deeplabv3** | Kiến trúc DeepLabV3 ResNet-50 — 7 lớp (Mặc định). Phân đoạn phân cấp chuẩn giúp nhận diện ranh giới vùng mô mềm phức tạp. |
|
||||
| **unet_resnet101** | Kiến trúc UNet kết hợp với encoder ResNet-101 mạnh mẽ, tối ưu việc khôi phục chi tiết không gian ảnh. |
|
||||
| **efficientfeedback** | Kiến trúc EfficientFeedbackNetwork (tùy chỉnh), tăng cường cơ chế phản hồi đặc trưng để làm mịn biên tổn thương. |
|
||||
| **unet3plus** | Kiến trúc UNet3+ kết hợp cơ chế Attention (tùy chỉnh), tối ưu khả năng kết nối bỏ qua full-scale cho ảnh siêu âm. |
|
||||
|
||||
#### 3.3.2. Nhóm Phân đoạn POST (Mặt cắt ngang phía sau - `segment_model_post`)
|
||||
|
||||
| Tên Mô Hình (Giá trị truyền vào) | Kiến trúc kĩ thuật & Vai trò trong hệ thống |
|
||||
| --- | --- |
|
||||
| **deeplabv3_resnet101** | Kiến trúc DeepLabV3 ResNet-101 — 7 lớp (Mặc định). Chuyên biệt cho việc phân đoạn góc post-trans, tối ưu hóa độ chính xác biên tổn thương vùng khoeo sau gối. |
|
||||
|
||||
*(Lưu ý: Các mô hình MedSAM và UltraSAM không xuất hiện trong danh mục cấu hình kỹ thuật của phiên bản Pilot 2025 này, nên đã được thay thế bằng các mô hình thực tế hệ thống đang hỗ trợ bao gồm UNet và EfficientFeedback).*
|
||||
|
||||
### 3.3. Cơ chế phân tích định lượng mức độ viêm (Severity Analytics)
|
||||
|
||||
Sau khi mô hình phân đoạn (ví dụ: DeepLabV3) đưa ra mặt nạ phân đoạn (Segmentation Mask) , hệ thống thực hiện thuật toán đếm pixel để đưa ra báo cáo tự động:
|
||||
|
||||
* **Phân cấp độ viêm:** Hệ thống tự động phân loại thành 3 mức độ từ nhẹ đến nặng bao gồm: **Viêm mức 1**, **Viêm mức 2**, và **Viêm mức 3** (Nặng).
|
||||
|
||||
|
||||
* **Các chỉ số định lượng đầu ra (Output Metrics):**
|
||||
* **Tỷ lệ vùng viêm:** $\text{Tỷ lệ \%} = \frac{\text{Tổng số pixel vùng viêm}}{\text{Tổng số pixel của toàn bộ ảnh}} \times 100\%$.
|
||||
|
||||
|
||||
* **Phân tích quan hệ không gian (Spatial/Boundary Analysis):** Xác định vị trí tương quan của ổ viêm với các cấu trúc giải phẫu lân cận như: Vùng mô mỡ (`Fat`), Vùng gân (`Tendon`), Xương đùi (`Femur`).
|
||||
|
||||
|
||||
* *Ví dụ phân tích hệ thống:* "Vùng viêm nằm giữa vùng gân chiếm tỷ lệ $14.4\%$ vùng xung quanh".
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. Đặc tả chức năng của Phần mềm Web (Web UI/UX Specification)
|
||||
|
||||
Phần mềm được xây dựng dưới dạng ứng dụng Web tích hợp (Integrated Ultrasound Analytics Platform) sở hữu các chức năng cụ thể sau:
|
||||
|
||||
* **Tải lên ảnh đầu vào:** Giao diện hỗ trợ Kéo & thả ảnh siêu âm trực tiếp hoặc nút chọn file ảnh từ máy tính.
|
||||
|
||||
|
||||
* **Tiền xử lý tự động:** Tự động chuẩn hóa ảnh, khử nhiễu đầu vào.
|
||||
|
||||
|
||||
* **Nhận diện tự động:** Xác định góc chụp/mặt cắt ảnh tự động thông qua khối phân loại.
|
||||
|
||||
|
||||
* **Phân đoạn thông minh:** Tự động định vị, khoanh vùng tổn thương viêm bằng màu trực quan đè lên ảnh gốc (Overlay Mask).
|
||||
|
||||
|
||||
* **Định lượng đa chỉ số:** Trả về độ tin cậy của mô hình ($\%$ Confidence), phân loại mức độ nặng, tính toán diện tích pixel tổn thương.
|
||||
|
||||
|
||||
* **Tương tác lâm sàng:** Cho phép bác sĩ nhập ghi chú, nhận xét và các khuyến nghị điều trị trực tiếp trên giao diện.
|
||||
|
||||
|
||||
* **Lưu trữ & Kết xuất:** Lưu trữ kết quả phân tích vào DB và cho phép xuất báo cáo, tải ảnh phân đoạn về máy.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. Kết quả thử nghiệm dữ liệu thực tế tại Bệnh viện E
|
||||
|
||||
Hệ thống đã được đánh giá chéo giữa kết luận tự động của mô hình AI và chẩn đoán lâm sàng thực tế của các bác sĩ chuyên khoa tại Bệnh viện E mang lại kết quả có độ tương quan rất cao:
|
||||
|
||||
Bảng so sánh dữ liệu thử nghiệm thực tế 1
|
||||
|
||||
### Bảng 1: So sánh dữ liệu thử nghiệm thực tế 1
|
||||
|
||||
| Tiêu chí | Kết luận tự động từ AI | Kết luận lâm sàng của Bác sĩ |
|
||||
| :--- | :--- | :--- |
|
||||
| **Ca bệnh 1** | - Phân loại: **Có viêm**<br>- Độ tin cậy: **94.5%**<br>- Mức độ: **Nặng**<br>- Tỷ lệ diện tích viêm: **6.95%**<br>- Mô tả không gian: Vùng viêm lan rộng, nằm giữa vùng gân (chiếm 37.2% vùng xung quanh). | **Khớp gối phải:** Màng hoạt dịch dày, có dịch kích thước ~6.8mm, xuất hiện gai xương nhỏ khe đùi chày trong ngoài khớp.<br><br>**Khớp gối trái:** Màng hoạt dịch dày, có dịch ~11mm (dịch đồng nhất).<br><br>**KẾT LUẬN:** Viêm màng hoạt dịch, tràn dịch khớp gối hai bên kèm thoái hóa khớp gối hai bên. |
|
||||
| **Ca bệnh 2** | - Phân loại: **Có viêm**<br>- Độ tin cậy: **95.1%**<br>- Mức độ: **Nặng**<br>- Tỷ lệ diện tích viêm: **7.54%**<br>- Mô tả không gian: Vùng viêm lan rộng, nằm giữa vùng gân (chiếm 12.4% vùng xung quanh). | (Đồng nhất dữ liệu chẩn đoán lâm sàng tương tự ca bệnh 1 cho thấy độ tương quan cao về mặt định lượng của mô hình đối với các ca tràn dịch mức độ nặng). |
|
||||
|
||||
---
|
||||
|
||||
### Bảng 2: So sánh dữ liệu thử nghiệm thực tế 2
|
||||
|
||||
| Tiêu chí | Kết luận tự động từ AI | Kết luận lâm sàng của Bác sĩ |
|
||||
| :--- | :--- | :--- |
|
||||
| **Đánh giá Khớp Gối Phải** | - Trạng thái: **Có viêm**<br>- Độ tin cậy: **86.5%**<br>- Mức độ: **Nặng**<br>- Tỷ lệ vùng viêm: **6.77%**<br>- Vùng viêm nằm giữa vùng gân (24.6% vùng bao quanh). | - Màng hoạt dịch dày, có dịch kích thước **10mm** (tính chất dịch đồng nhất).<br>- Xuất hiện gai xương tại vị trí khe đùi chày ngoài khớp.<br>- Bề mặt sụn khớp đều, tuy nhiên cấu trúc âm không đồng nhất.<br>- Không có hình ảnh kén khoeo. |
|
||||
| **Đánh giá Khớp Gối Trái** | - Trạng thái: **Có viêm**<br>- Độ tin cậy: **91.7%**<br>- Mức độ: **Nặng**<br>- Tỷ lệ vùng viêm: **15.96%**<br>- Vùng viêm nằm giữa vùng gân (30.5% vùng bao quanh). | - Màng hoạt dịch dày, ghi nhận có ít dịch.<br>- Xuất hiện gai xương tại khe đùi chày trong ngoài khớp.<br>- Bề mặt sụn khớp đều, cấu trúc âm không đồng nhất.<br>- Không có hình ảnh kén khoeo. |
|
||||
| **KẾT LUẬN CHUNG** | *(AI hỗ trợ định lượng diện tích tổn thương chuyên sâu)* | **Hình ảnh:** Viêm màng hoạt dịch khớp gối hai bên, Tràn dịch khớp gối phải kèm theo Thoái hóa khớp gối hai bên. |
|
||||
|
||||
## 6. Hướng dẫn cài đặt và Triển khai mã nguồn Backend (ML Stack Deployment)
|
||||
|
||||
Dành cho coding agent vận hành hoặc deploy môi trường ứng dụng:
|
||||
|
||||
### 6.1. Yêu cầu hệ thống (Prerequisites)
|
||||
|
||||
* **Hệ điều hành hỗ trợ:** Ubuntu / Windows.
|
||||
|
||||
* **Trình quản lý gói:** Anaconda / Miniconda.
|
||||
|
||||
* **Hỗ trợ phần cứng:** CUDA Toolkit được cài đặt sẵn (Bắt buộc nếu cấu hình sử dụng GPU tăng tốc suy luận).
|
||||
|
||||
|
||||
|
||||
### 6.2. Các bước triển khai chi tiết
|
||||
|
||||
**Bước 1: Tải mã nguồn từ kho lưu trữ (Git Clone)**
|
||||
|
||||
```bash
|
||||
# Thực hiện clone thư mục chứa dự án từ Git Server hệ thống
|
||||
git clone https://vkist-hub.com/itvkist/vkist-ultrasound.git
|
||||
|
||||
# Di chuyển vào thư mục và cập nhật nếu đã tồn tại phiên bản trước đó
|
||||
cd vkist-ultrasound
|
||||
git pull
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Bước 2: Khởi tạo môi trường ảo và Cài đặt các thư viện phụ thuộc**
|
||||
|
||||
```bash
|
||||
# Tạo môi trường ảo conda mới chạy Python phiên bản ổn định 3.10
|
||||
conda create -n vkist-ultrasound python=3.10 -y
|
||||
|
||||
# Kích hoạt môi trường ảo vừa tạo
|
||||
conda activate vkist-ultrasound
|
||||
|
||||
# Tiến hành cài đặt tất cả các dependencies phụ thuộc nằm trong file requirements
|
||||
pip install -r requirements.txt
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Bước 3: Khởi chạy ứng dụng Web Server Backend**
|
||||
|
||||
```bash
|
||||
# Chạy file khởi tạo ứng dụng chính
|
||||
python app.py
|
||||
|
||||
```
|
||||
|
||||
|
||||
>
|
||||
> **Thông tin triển khai thành công:** Sau khi dòng lệnh thực thi hoàn tất, phần mềm cục bộ sẽ khởi chạy và lắng nghe kết nối tại địa chỉ URL mặc định: `http://localhost:8000`.
|
||||
>
|
||||
>
|
||||
@@ -0,0 +1,256 @@
|
||||
# VKIST ULTRASOUND - TÀI LIỆU HƯỚNG DẪN & GIỚI THIỆU DỰ ÁN
|
||||
|
||||
---
|
||||
|
||||
## 1. Giới thiệu chung
|
||||
|
||||
VKIST Ultrasound là hệ thống hỗ trợ chẩn đoán viêm khớp gối tiên tiến ứng dụng Trí tuệ nhân tạo (AI). Hệ thống giúp bác sĩ phân tích hình ảnh siêu âm một cách tự động, từ việc nhận diện chính xác góc chụp đến việc đo đạc các chỉ số bệnh lý phức tạp. Qua đó, giải pháp này giúp tối ưu hóa quy trình khám chữa bệnh, giảm thiểu sai sót chủ quan và nâng cao hiệu suất làm việc tại các cơ sở y tế.
|
||||
|
||||
---
|
||||
|
||||
## 2. Quy trình xử lý toàn diện (AI Pipeline Workflow)
|
||||
|
||||
Hệ thống vận hành theo một quy trình khép kín, tự động phân nhánh logic dựa trên tính chất hình ảnh đầu vào:
|
||||
|
||||
1. **Tiếp nhận & Tiền xử lý ảnh:** Tải ảnh siêu âm thô lên hệ thống. AI tự động áp dụng thuật toán tăng cường tương phản CLAHE để làm rõ nét các cấu trúc giải phẫu bị mờ hoặc nhiễu.
|
||||
|
||||
|
||||
2. **Phân loại góc chụp (Angle Classification):** Tự động xác định tư thế chụp/mặt cắt nhằm kích hoạt nhánh pipeline phân tích chuyên biệt.
|
||||
|
||||
|
||||
3. **Phát hiện Viêm (Inflammation Detection):** Đánh giá sơ bộ sự hiện diện của dịch khớp hoặc tình trạng tăng sinh màng hoạt dịch.
|
||||
|
||||
|
||||
4. **Phân vùng & Đo đạc (Segmentation & Measurement):** Tách biệt các lớp mô giải phẫu (xương, dịch, màng, gân...) và tự động xác định độ dày tổn thương tại các vùng trọng yếu.
|
||||
|
||||
|
||||
5. **Đánh giá mức độ nặng (Severity Scoring):** Tính toán chỉ số tổng hợp để phân cấp mức độ viêm khớp.
|
||||
|
||||
|
||||
6. **Quản lý hồ sơ & Báo cáo:** Lưu trữ dữ liệu chuẩn hóa vào hệ thống nội bộ và kết xuất phiếu kết quả khám bệnh dạng PDF.
|
||||
|
||||
|
||||
|
||||
Dưới đây là sơ đồ luồng logic của hệ thống được thiết kế bằng **PlantUML** giúp lập trình viên backend dễ dàng triển khai:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
skinparam handwritten false
|
||||
skinparam monochrome false
|
||||
skinparam packageStyle rect
|
||||
skinparam shadowing true
|
||||
|
||||
title SƠ ĐỒ ĐIỀU HƯỚNG LOGIC PIPELINE - VKIST ULTRASOUND AI
|
||||
|
||||
start
|
||||
|
||||
:Tiếp nhận ảnh siêu âm từ Web UI;
|
||||
:Áp dụng thuật toán tiền xử lý CLAHE (Tăng cường độ nét);
|
||||
|
||||
:Mô hình Phân loại góc chụp (Angle Model);
|
||||
note right: Tích hợp ConvNeXt / Swin / DenseNet
|
||||
|
||||
if (Góc chụp hợp lệ?) then (Không thuộc góc Sup_up_long / Post_trans)
|
||||
:Hiển thị nhãn góc chụp (ví dụ: Med-Lat Long);
|
||||
:Thông báo lỗi: Góc chụp đầu vào không hỗ trợ xử lý tiếp;
|
||||
stop
|
||||
else (Hợp lệ)
|
||||
:Mô hình Phát hiện viêm (EfficientNet-B0);
|
||||
|
||||
if (Trạng thái ổ khớp?) then (Không viêm)
|
||||
:Hiển thị trạng thái KHÔNG VIÊM trên giao diện;
|
||||
:Cho phép nhập thông tin và lưu trữ cơ bản;
|
||||
else (Có viêm)
|
||||
:Hiển thị trạng thái CÓ VIÊM;
|
||||
|
||||
if (Góc chụp phát hiện?) then (Post_trans)
|
||||
:Chạy phân đoạn vùng tổn thương (DeepLabV3-ResNet101);
|
||||
:Khoanh vùng, gắn nhãn màu Nang Baker (Baker's Cyst);
|
||||
else (Sup_up_long / Suprapat)
|
||||
:Chạy phân đoạn vùng tổn thương (DeepLabV3-ResNet50);
|
||||
partition "Thuật toán Đo đạc thông minh" {
|
||||
:Smart ROI (Tập trung 1/3 khu vực giữa);
|
||||
:Continuous Segment Search (Tìm đoạn dịch liên tục dài nhất);
|
||||
:Xác định độ dày ổ dịch/màng hoạt dịch (mm);
|
||||
}
|
||||
:Tính điểm toán học phân cấp Mức độ bệnh (Cấp 0 -> 3);
|
||||
endif
|
||||
fi
|
||||
endif
|
||||
|
||||
split
|
||||
:Nhập thông tin bệnh nhân & Chẩn đoán lâm sàng;
|
||||
:Lưu trữ cấu trúc thư mục nội bộ (patients/);
|
||||
split again
|
||||
:Xuất phiếu kết quả khám bệnh dạng PDF (report.pdf);
|
||||
end split
|
||||
|
||||
stop
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Các Tính năng Chi tiết & Mô hình học máy
|
||||
|
||||
### 3.1. Hệ thống Mô hình AI Đa dạng (Multi-Model Architecture)
|
||||
|
||||
* **Khối Phân loại Góc chụp:** Tích hợp các kiến trúc mạng mạng nơ-ron tiên tiến (SOTA) bao gồm ConvNeXt-Tiny (đạt độ chính xác tuyệt đối 100% trên tập kiểm thử) , Swin Transformer, và DenseNet. Hệ thống mặc định sử dụng ConvNeXt làm lõi phân loại chính đầu tiên cho mọi ảnh.
|
||||
|
||||
|
||||
* **Khối Phân vùng (Semantic Segmentation):** Hỗ trợ linh hoạt các mô hình DeepLabV3+ (hoặc DeepLabV3-ResNet50 với độ chính xác 91.67%), UNet3+ Attention, và EfficientFeedback. Các cấu trúc này được tối ưu hóa sâu nhằm nhận diện chính xác đường biên ranh giới màng hoạt dịch phức tạp.
|
||||
|
||||
|
||||
|
||||
### 3.2. Tính năng Đo đạc Thông minh (Smart Measurement)
|
||||
|
||||
Nhánh xử lý góc `Sup_up_long` tích hợp hai thuật toán hình học độc quyền nhằm loại bỏ sai số đo đạc:
|
||||
|
||||
* **Smart ROI:** Hệ thống tự động khoanh vùng và tập trung phân tích vào khu vực 1/3 chính giữa của vùng nghi ngờ, nơi có giá trị và chỉ số chẩn đoán lâm sàng cao nhất.
|
||||
|
||||
|
||||
* **Continuous Segment Search:** Thuật toán tự động tìm kiếm đoạn mặt nạ (mask) liên tục dài nhất. Cơ chế này đảm bảo kết quả tính toán độ dày của màng và dịch khớp phản ánh đúng thực tế, khách quan và không bị ảnh hưởng bởi nhiễu ảnh cục bộ.
|
||||
|
||||
|
||||
|
||||
### 3.3. Phân cấp Mức độ Bệnh (Severity Classification)
|
||||
|
||||
Hệ thống tính toán chỉ số kết hợp giữa diện tích dịch khớp tụ và mức độ tăng sinh màng hoạt dịch để phân định thành 4 cấp độ bệnh lý rõ ràng:
|
||||
|
||||
| Cấp độ viêm | Phân loại lâm sàng | Đặc điểm cấu trúc hình ảnh siêu âm |
|
||||
| --- | --- | --- |
|
||||
| **Cấp 0** | Rất nhẹ | Lượng dịch khớp nằm trong giới hạn sinh lý bình thường.|
|
||||
| **Cấp 1** | Nhẹ | Lớp dịch khớp mỏng, xuất hiện tăng sinh màng hoạt dịch mức độ nhẹ.|
|
||||
| **Cấp 2** | Trung bình | Lượng dịch khớp và màng hoạt dịch tăng sinh phì đại rõ rệt.|
|
||||
| **Cấp 3** | Nặng | Ổ dịch khớp dày, màng hoạt dịch phát triển mạnh và xâm lấn sâu.|
|
||||
|
||||
---
|
||||
|
||||
## 4. Đặc tả Giao diện & Hướng dẫn Vận hành Hệ thống
|
||||
|
||||
Giao diện Web UI của ứng dụng được thiết kế phân cấp tối giản theo bố cục 3 luồng dọc: **Bảng trái (Cấu hình Mô hình)** $\rightarrow$ **Bảng giữa (Tải ảnh & Hiển thị)** $\rightarrow$ **Bảng phải (Nhập dữ liệu & Xem kết quả phân tích)**.
|
||||
|
||||
### Quy trình vận hành 4 bước dành cho Bác sĩ:
|
||||
|
||||
#### Bước 1: Cấu hình Mô hình AI (Left Panel)
|
||||
|
||||
* Trước khi tiến hành tải ảnh siêu âm, bác sĩ có thể linh hoạt tùy chọn thuật toán AI mong muốn cho từng block tác vụ tại bảng điều khiển bên trái. Hệ thống đã được cấu hình mặc định sẵn các mô hình tối ưu nhất.
|
||||
|
||||
|
||||
|
||||
#### Bước 2: Tải ảnh siêu âm lên hệ thống (Middle Panel)
|
||||
|
||||
* Bác sĩ thực hiện kéo thả tập tin ảnh trực tiếp vào vùng nhận diện hoặc nhấn nút `"Chọn ảnh"`.
|
||||
|
||||
|
||||
* Ngay sau khi tệp tin được nạp, hệ thống sẽ tự động kích hoạt toàn bộ Pipeline phân tích ngầm theo luồng xử lý tự động.
|
||||
|
||||
|
||||
|
||||
#### Bước 3: Đọc kết quả phân tích tự động từ AI (Right Panel & Modal Popup)
|
||||
|
||||
Hệ thống tự động hiển thị kết quả trực quan dựa trên nhãn mặt cắt nhận diện được:
|
||||
|
||||
*
|
||||
**Trường hợp góc chụp không hỗ trợ (ví dụ: `Med-Lat Long`):** Hệ thống lập tức xuất nhãn cảnh báo màu tím `"GÓC CHỤP: Med-Lat Long"` và dừng tiến trình xử lý tiếp theo.
|
||||
|
||||
|
||||
*
|
||||
**Trường hợp góc mặt sau `Post_trans`:** Hệ thống kiểm tra tình trạng viêm. Nếu có viêm, ảnh phân đoạn sẽ hiển thị màu overlay trực quan khoanh vùng chính xác ổ tổn thương **Nang Baker** (Baker's cyst) kèm bảng chú thích màu sắc mô giải phẫu tương ứng.
|
||||
|
||||
|
||||
*
|
||||
**Trường hợp góc mặt trước `Sup_up_long`:** Hệ thống tiến hành phân đoạn, hiển thị đường lưới đo đạc thông minh. Đồng thời tính toán chi tiết độ dày ổ dịch bằng đơn vị milimét ($mm$) và đưa ra chẩn đoán mức độ viêm chính xác.
|
||||
|
||||
|
||||
|
||||
#### Bước 4: Lưu trữ hồ sơ dữ liệu & Xuất bản phiếu khám lâm sàng
|
||||
|
||||
* Bác sĩ thực hiện điền đầy đủ thông tin vào form biểu mẫu bệnh nhân ở bảng bên phải (Trong đó hai trường dữ liệu **Mã bệnh nhân** và **Họ và tên** là bắt buộc). Ghi nhận thêm nhận xét lâm sàng cá nhân vào ô "Chẩn đoán của bác sĩ".
|
||||
|
||||
|
||||
* Nhấn nút `"Lưu dữ liệu"` để hệ thống đóng gói và lưu trữ nội bộ vào máy chủ.
|
||||
|
||||
|
||||
* Nhấn nút `"Xuất phiếu khám (PDF)"` để tải về máy mẫu phiếu in kết quả y khoa chính thức trả cho bệnh nhân.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. Đặc tả Kiến trúc Lưu trữ Dữ liệu Đầu ra
|
||||
|
||||
### 5.1. Cấu trúc cây thư mục lưu trữ hồ sơ bệnh nhân (Storage Structure)
|
||||
|
||||
Khi bác sĩ nhấn chọn chức năng lưu trữ dữ liệu , hệ thống tự động khởi tạo cấu trúc thư mục phân cấp động theo mốc thời gian thực tại phân vùng `patients/` nhằm tránh trùng lặp thông tin:
|
||||
|
||||
```text
|
||||
patients/
|
||||
└── <Mã_Bệnh_Nhân>_<Họ_Và_Tên_Không_Dấu>/
|
||||
└── <NamThangNgay>_<GioPhutGiay>/
|
||||
├── info.txt # Tệp tin cấu trúc lưu trữ siêu dữ liệu định lượng của ca bệnh
|
||||
├── original.png # File hình ảnh siêu âm gốc (hoặc ảnh đã tăng cường CLAHE)
|
||||
├── segmented.png # File ảnh overlay mặt nạ phân đoạn màu từ AI mô hình
|
||||
└── report.pdf # File phiếu kết quả chẩn đoán y khoa chính thức xuất cho bệnh nhân
|
||||
|
||||
```
|
||||
|
||||
*Ví dụ thực tế:* `patients\BN0001_Nguyen_Van_A\20260505_170011\`
|
||||
|
||||
### 5.2. Định dạng cấu trúc tệp dữ liệu `info.txt`
|
||||
|
||||
Tệp tin `info.txt` đóng vai trò lưu trữ toàn bộ thông tin tối giản, giúp hệ thống hoặc các agent xử lý số liệu có thể dễ dàng phân tích (parse) dữ liệu mà không cần đọc file PDF:
|
||||
|
||||
```text
|
||||
--- THÔNG TIN BỆNH NHÂN ---
|
||||
Mã bệnh nhân: BN0001
|
||||
Họ tên: Nguyễn Văn A
|
||||
Giới tính: Nam
|
||||
Tuổi: 88
|
||||
Ghi chú lâm sàng: Tràn dịch và có thể viêm
|
||||
|
||||
--- KẾT QUẢ PHÂN TÍCH AI ---
|
||||
Góc chụp: sup-up-long (99.93%)
|
||||
Viêm nhiễm: Có (94.44%)
|
||||
Độ dày màng: 6.53 mm (95 px)
|
||||
Vị trí x: 420
|
||||
Mức độ: Trung bình
|
||||
Mô tả: Dịch khớp trung bình (51px), màng hoạt dịch tăng sinh vừa
|
||||
|
||||
```
|
||||
|
||||
(Ghi chú: Nội dung trên được ánh xạ chính xác từ các chỉ số định lượng thu được qua mô hình phân đoạn hình học của hệ thống ).
|
||||
|
||||
### 5.3. Quy chuẩn nội dung phiếu kết quả y khoa `report.pdf`
|
||||
|
||||
Phiếu kết quả chẩn đoán hình ảnh dạng PDF được tạo tự động với cấu trúc bố cục chuẩn hóa y tế gồm 4 phần chính:
|
||||
|
||||
1.
|
||||
**Thông tin Cơ sở & Tiêu đề:** Biểu trưng nhận diện VKIST, tên "TRUNG TÂM CHẨN ĐOÁN HÌNH ẢNH VKIST", địa chỉ "Khu Công nghệ cao Hòa Lạc, Thạch Thất, Hà Nội" kèm tiêu đề lớn **"PHIẾU KẾT QUẢ SIÊU ÂM KHỚP GỐI"**.
|
||||
|
||||
|
||||
2. I. Thông tin bệnh nhân: Hiển thị chi tiết Họ tên, Giới tính, Mã BN, Tuổi của người bệnh.
|
||||
|
||||
|
||||
3. II. Hình ảnh siêu âm: Chèn song song hai khung hình trực quan bao gồm `Hình 1: Ảnh gốc / Tăng cường` và `Hình 2: Ảnh phân đoạn AI` (có kèm sơ đồ lưới đo đạc và chú thích màu).
|
||||
|
||||
|
||||
4. **III. Kết quả phân tích tự động (AI Metric):**
|
||||
* Góc chụp dự đoán đạt tỷ lệ tương ứng (Ví dụ: `sup-up-long` - Độ tin cậy: `99.93%`).
|
||||
|
||||
|
||||
* Tình trạng viêm ổ khớp (Ví dụ: `Có khả năng viêm` - Độ tin cậy: `94.44%`).
|
||||
|
||||
|
||||
* Chỉ số đo đạc vật lý: Độ dày dịch & màng hoạt dịch tính toán được đạt mức `6.53 mm`.
|
||||
|
||||
|
||||
* Đánh giá mức độ viêm tổng hợp: `Trung bình`.
|
||||
|
||||
|
||||
* Chi tiết mô tả định lượng: Dịch khớp trung bình ($51\text{ px}$), màng hoạt dịch tăng sinh vừa ($95\text{ px}$).
|
||||
|
||||
|
||||
|
||||
5. IV. Chẩn đoán và kết luận của Bác sĩ: Trích xuất nguyên vẹn nội dung ghi chú lâm sàng do bác sĩ trực tiếp nhập vào hệ thống (Ví dụ: `"Tràn dịch và có thể viêm"`).
|
||||
@@ -0,0 +1,11 @@
|
||||
# ML Model Architecture Report
|
||||
|
||||
| File | Architectures (code ranges) |
|
||||
|------|-----------------------------|
|
||||
| `ML/arch/unet3plus_att.py` | - **UNet3Plus_Attention** (87‑268)<br> - SelfAttention (7‑25)<br> - AttentionGate (28‑66)<br> - conv_block (69‑83) |
|
||||
| `ML/arch/efficientfeedback.py` | - **EfficientFeedbackNetwork** (171‑239)<br> - convblock (9‑14)<br> - DecoderBlock (17‑35)<br> - ASPP_module (37‑66)<br> - CAM_Module (68‑86)<br> - S_Module (93‑131)<br> - FeedbackSpatialAttention (134‑151)<br> - StageAttentionwCAM (153‑168) |
|
||||
| `ML/segment_anything/modeling/image_encoder.py` | - **ImageEncoderViT** (18‑119)<br> - PatchEmbed (389‑420)<br> - Block (122‑187)<br> - Attention (190‑254) |
|
||||
| `ML/segment_anything/modeling/prompt_encoder.py` | - **PromptEncoder** (17‑181)<br> - PositionEmbeddingRandom (183‑226) |
|
||||
| `ML/segment_anything/modeling/mask_decoder.py` | - **MaskDecoder** (17‑190)<br> - MLP (168‑190) |
|
||||
| `ML/segment_anything/modeling/transformer.py` | - **TwoWayTransformer** (17‑108)<br> - TwoWayAttentionBlock (110‑183)<br> - Attention (186‑243) |
|
||||
| `ML/segment_anything/modeling/sam.py` | - **Sam** (19‑181) *(no internal sub‑architectures; uses imported modules)* |
|
||||
@@ -0,0 +1,46 @@
|
||||
# Backend Specification
|
||||
|
||||
## Purpose
|
||||
Orchestrates API routers, role checks, Socratic circuit-breaker state evaluations, and coordinates ML inference, telemetry collection, and data persistence.
|
||||
|
||||
## Owner
|
||||
Core Backend Team
|
||||
|
||||
## Boundary
|
||||
FastAPI server, API routers, authentication middleware, circuit breaker engine, report generator, RAG coordinator, ledger logger, and connections to Postgres, S3, Redis, Triton, Qdrant, ladybugDB.
|
||||
|
||||
## Internal Design
|
||||
- Built with FastAPI (Python) and Uvicorn for async HTTP server.
|
||||
- Authentication middleware validates JWT tokens and enforces RBAC (roles: RO_RADIOLOGIST, RO_THERAPIST).
|
||||
- Socratic circuit-breaker engine monitors interaction telemetry (hover duration, decision time, override magnitude) and triggers safety dialogs.
|
||||
- Clinical Report Engine uses ReportLab to generate bilingual PDF reports per Circular 46/2018/TT-BYT.
|
||||
- RAG Coordinator orchestrates Retrieval-Augmented Generation: dense vector lookup in Qdrant, graph traversal in ladybugDB, prompt enrichment, LLM generation on Triton (PhoGPT/MedGemma), and hallucination guarding.
|
||||
- Ledger Logger appends immutable, cryptographically chained audit logs to Postgres via triggers preventing UPDATE/DELETE.
|
||||
- Connections: Postgres (via SQLAlchemy), S3 (via boto3), Redis (via redis-py), Triton (via gRPC), Qdrant (via gRPC/HTTP), ladybugDB (via in-process C++ bindings).
|
||||
- Model weights loaded at startup from internal registry; cached in memory.
|
||||
- API endpoints layered: public clinical (sessions, analysis, reports, feedback) and internal/local safety (explanations, safety, drift, RAG, activations, annotations, ground-truth, escalation, morphology, telemetry).
|
||||
|
||||
## Interface Contract
|
||||
See `bento/backend/spec/interface-contract.md`.
|
||||
|
||||
## Consumers
|
||||
- frontend
|
||||
|
||||
## Breaking-change Policy
|
||||
See `bento/backend/spec/interface-contract.md`.
|
||||
|
||||
## References
|
||||
- NFR-7 (Real-Time UI Screen Refresh ≤200ms)
|
||||
- NFR-10 (Generative Safety Guardrails)
|
||||
- NFR-11 (Frontline Usability & Training)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-47988 (Review Suggested Synovitis Grade)
|
||||
- UC-25776 (Generate GradCAM & CoT Explanation Panel)
|
||||
- UC-02423 (Log High-Trust Concur Block)
|
||||
- UC_Q2_* (All Quadrant 2 safety workflows)
|
||||
- UC_Q3_* (All Quadrant 3 subservience workflows)
|
||||
- UC_Q4_* (All Quadrant 4 double-blind workflows)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Sections 1.2, 2.1-2.6)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Sections 2.1-2.6)
|
||||
- DATA_ENGINEERING_SPEC.md (Sections 4-12 for domain objects)
|
||||
- CI_CD_DEPLOYMENT_PIPELINE.md (Section 9.2 for docker-compose)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Backend Interface Contract
|
||||
|
||||
## Purpose
|
||||
Orchestrates API routers, role checks, Socratic circuit-breaker state evaluations, and coordinates ML inference, telemetry collection, and data persistence.
|
||||
|
||||
## Owner
|
||||
Core Backend Team
|
||||
|
||||
## Provides
|
||||
- api endpoints (session management, frame upload, analysis jobs, reporting, feedback, safety endpoints)
|
||||
- model inference orchestration (dispatches to Triton, aggregates results)
|
||||
- telemetry collection (edge-based behavioral summaries, audit logs)
|
||||
- data persistence coordination (writes to Postgres, S3, Redis)
|
||||
|
||||
## Consumes
|
||||
- data:storage-spec (Postgres DB, S3 object store, Redis cache)
|
||||
- ml:inference-spec (Triton server for angle, inflammation, segmentation, severity)
|
||||
- knowledge:guideline-spec (Qdrant vector DB, ladybugDB graph DB for grounded explanations)
|
||||
|
||||
## Consumers
|
||||
- frontend
|
||||
|
||||
## Not Directly Consumable
|
||||
- data internals (Postgres tables, S3 object layout, Redis keys)
|
||||
- ml internals (Triton model details, GPU kernels)
|
||||
- knowledge internals (Qdrant vectors, ladybugDB graph)
|
||||
|
||||
## Breaking-change Policy
|
||||
- API versioning via path (e.g., /api/v1/).
|
||||
- Backward compatibility maintained for one minor version.
|
||||
- Deprecation notices issued in release notes.
|
||||
- Model interface changes (input/output tensors) require version bump.
|
||||
|
||||
## References
|
||||
- NFR-7 (Real-Time UI Screen Refresh ≤200ms)
|
||||
- NFR-10 (Generative Safety Guardrails)
|
||||
- NFR-11 (Frontline Usability & Training)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-47988 (Review Suggested Synovitis Grade)
|
||||
- UC-25776 (Generate GradCAM & CoT Explanation Panel)
|
||||
- UC-02423 (Log High-Trust Concur Block)
|
||||
- UC_Q2_* (All Quadrant 2 safety workflows)
|
||||
- UC_Q3_* (All Quadrant 3 subservience workflows)
|
||||
- UC_Q4_* (All Quadrant 4 double-blind workflows)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Sections 1.2, 2.1-2.6)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Sections 2.1-2.6)
|
||||
@@ -0,0 +1,52 @@
|
||||
# Data Model Specification
|
||||
|
||||
## Purpose
|
||||
Manages persistent storage, caching, and object storage for clinical data including patient records, imaging studies, analysis results, and audit trails using PostgreSQL, Redis, and S3 with calibration services.
|
||||
|
||||
## Owner
|
||||
Data / Domain Team
|
||||
|
||||
## Boundary
|
||||
PostgreSQL database clusters, Redis cache instances, S3 buckets/object storage, database connection pools, cache invalidation strategies, and storage lifecycle management policies.
|
||||
|
||||
## Internal Design
|
||||
- PostgreSQL 15 with TimescaleDB extension for temporal data
|
||||
- Schema organization: patient, study, session, analysis, audit, calibration namespaces
|
||||
- Connection pooling via PgBouncer for efficient database access
|
||||
- Redis 7 for session caching, rate limiting, and temporary computation results
|
||||
- S3 bucket structure: raw-imagery, processed-results, exports, backups with lifecycle policies
|
||||
- Encryption-at-rest for sensitive data (PHI) using AES-256-GCM
|
||||
- Automated backups with point-in-time recovery (PITR) capabilities
|
||||
- Read replicas for query distribution and reporting workloads
|
||||
- Calibration service: pixel-to-mm conversion factors stored per device/protocol
|
||||
- Data retention policies: active data (2 years), archived data (7 years), purged data (>7 years)
|
||||
- Migration system using Flyway for schema versioning
|
||||
- Monitoring: query performance, connection pool utilization, replication lag
|
||||
|
||||
## Interface Contract
|
||||
See `bento/data/spec/interface-contract.md`.
|
||||
|
||||
## Consumers
|
||||
- backend:api-spec (for CRUD operations on patient/study/session data)
|
||||
- backend:ledger-spec (for audit trail storage)
|
||||
- ml:engine-spec (for model weights and training data)
|
||||
- knowledge:spec (for exporting vectorizable content)
|
||||
|
||||
## Breaking-change Policy
|
||||
- Database schema versioning via semantic versioning aligned with API versions
|
||||
- Table/column additions: backward compatible (MINOR version)
|
||||
- Table/column removals or type changes: require MAJOR version with migration path
|
||||
- API contract changes follow backend interface contract policies
|
||||
- Data migration scripts provided for breaking changes
|
||||
- Deprecation notices for schema changes 60 days in advance
|
||||
|
||||
## References
|
||||
- NFR-7 (Data Durability: 99.999999999% annual)
|
||||
- NFR-8 (Recovery Time Objective ≤4 hours)
|
||||
- NFR-9 (Storage Cost Efficiency)
|
||||
- NFR-13 (Audit Trail Immutability)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-92006 (Save Analysis Results)
|
||||
- UC-01580 (Export Study Package)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Section 2.3)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Section 5.2)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Data Model Interface Contract
|
||||
|
||||
## Purpose
|
||||
Manages persistent storage, caching, and object storage for clinical data including patient records, imaging studies, analysis results, and audit trails using PostgreSQL, Redis, and S3 with calibration services.
|
||||
|
||||
## Owner
|
||||
Data / Domain Team
|
||||
|
||||
## Provides
|
||||
- persistent-storage (ACID-compliant patient/study/session records)
|
||||
- object-storage (binary imagery, masks, overlays, exported reports)
|
||||
- caching-layer (session state, rate limiting counters, temp computation results)
|
||||
- calibration-service (pixel-to-mm conversion factors per device/protocol)
|
||||
- backup-and-recovery (point-in-time recovery, cross-region replication)
|
||||
- data-export-functionality (DICOM, PDF, CSV formats)
|
||||
|
||||
## Consumes
|
||||
- (None - foundational storage layer)
|
||||
|
||||
## Consumers
|
||||
- backend:api-spec (patient/study/session CRUD operations, search)
|
||||
- backend:ledger-spec (audit trail append-only storage)
|
||||
- ml:engine-spec (model artifacts storage/retrieval, training datasets)
|
||||
- knowledge:spec (guideline documents, vectorization source material)
|
||||
- infra:spec (shared PostgreSQL/Redis instances for platform services)
|
||||
|
||||
## Not Directly Consumable
|
||||
- internal table structures beyond published schemas
|
||||
- connection pool tuning parameters
|
||||
- Redis key naming conventions for internal caching
|
||||
- S3 bucket policies beyond published access patterns
|
||||
- backup encryption key management
|
||||
- vacuum/analyze maintenance schedules
|
||||
|
||||
## Breaking-change Policy
|
||||
- Database schema versioning via semantic versioning (MAJOR.MINOR.PATCH aligned with API)
|
||||
- Additive changes (new tables/columns): backward compatible (MINOR version)
|
||||
- Breaking changes (removed columns, type alterations): require MAJOR version
|
||||
- Migration scripts provided for all breaking changes with rollback procedures
|
||||
- Deprecation notices for schema changes issued 90 days in advance
|
||||
- Storage interface changes (S3 prefixes, Redis keys) follow same versioning
|
||||
- Consumers must opt-in to breaking changes via feature flags
|
||||
|
||||
## References
|
||||
- NFR-7 (Data Durability: 99.999999999% annual)
|
||||
- NFR-8 (Recovery Time Objective ≤4 hours)
|
||||
- NFR-9 (Storage Cost Efficiency)
|
||||
- NFR-13 (Audit Trail Immutability)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-92006 (Save Analysis Results)
|
||||
- UC-01580 (Export Study Package)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Frontend Specification
|
||||
|
||||
## Purpose
|
||||
Provides interactive clinical workspace, runs edge models (LiteRT, MediaPipe), handles client-side encryption (WebCrypto), offline sync via IndexedDB/Service Worker, and renders UI viewport with graphics adapter fallback.
|
||||
|
||||
## Owner
|
||||
Web Experience Team
|
||||
|
||||
## Boundary
|
||||
PWA service worker, graphics adapter layer (IGraphicsViewport), local browser storage (IndexedDB), and UI components (React, Zustand).
|
||||
|
||||
## Internal Design
|
||||
- Built as a Single Page Application (SPA) using React with TypeScript.
|
||||
- State managed via Zustand store.
|
||||
- Client-side encryption via WebCrypto API (AES-256-GCM) before local storage.
|
||||
- Offline synchronization via Dexie.js (IndexedDB) and Service Worker that queues actions and retries on reconnection.
|
||||
- Graphics rendering abstracted via IGraphicsViewport interface with WebGLThreeAdapter (Three.js) and CPUSpriteAdapter fallback.
|
||||
- Edge ML executed in Web Workers: DICOM parser (cornerstone-core), LiteRT angle classifier (MobileNetV4), MediaPipe ROI pre-cropper.
|
||||
- UI components render multi-layered canvas, workspace controls, diagnostic ribbons, and explanation panels.
|
||||
- Communication with backend via HTTPS to NGINX gateway, JWT-based authentication, role-based access control (RBAC).
|
||||
|
||||
## Interface Contract
|
||||
See `bento/frontend/spec/interface-contract.md`.
|
||||
|
||||
## Consumers
|
||||
(None)
|
||||
|
||||
## Breaking-change Policy
|
||||
See `bento/frontend/spec/interface-contract.md`.
|
||||
|
||||
## References
|
||||
- NFR-1 (Collaborative Rendering Speed ≤3s)
|
||||
- NFR-4 (Client Memory Footprint ≤150MB)
|
||||
- NFR-14 (Legacy Hardware Compatibility)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-47988 (Review Suggested Synovitis Grade)
|
||||
- UC-25637 (Expose Pixel-Level Activation Logic)
|
||||
- UC-60739 (Isolate Visual Noise/Artifacts)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Sections 2.4, 3.1, 3.2, 3.3)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Section 2.4)
|
||||
- PROJECT_VIS.md (Section 3.1, 3.2)
|
||||
@@ -0,0 +1,39 @@
|
||||
# Frontend Interface Contract
|
||||
|
||||
## Purpose
|
||||
Provides interactive clinical workspace, runs edge models (LiteRT, MediaPipe), handles client-side encryption (WebCrypto), offline sync via IndexedDB/Service Worker, and renders UI viewport with graphics adapter fallback.
|
||||
|
||||
## Owner
|
||||
Web Experience Team
|
||||
|
||||
## Provides
|
||||
- ui viewport (interactive canvas, overlays, controls)
|
||||
- edge ml (angle classification, inflammation detection, ROI pre-cropping)
|
||||
- offline sync (local cache, sync queue, background synchronization)
|
||||
|
||||
## Consumes
|
||||
- backend:api-spec (REST API endpoints for session, analysis, reporting, feedback)
|
||||
- knowledge:guideline-spec (GraphRAG pipeline for grounded explanations, evidence arbitration)
|
||||
|
||||
## Consumers
|
||||
(None)
|
||||
|
||||
## Not Directly Consumable
|
||||
- backend internals (e.g., FastAPI route implementations, Triton model details)
|
||||
- knowledge internals (Qdrant vectors, ladybugDB graph structure)
|
||||
- data internals (Postgres schema, S3 object layout)
|
||||
|
||||
## Breaking-change Policy
|
||||
- API versioning via path (e.g., /api/v1/).
|
||||
- Backward compatibility maintained for one minor version.
|
||||
- Deprecation notices issued in release notes.
|
||||
- Breaking changes require major version bump.
|
||||
|
||||
## References
|
||||
- NFR-1 (Collaborative Rendering Speed ≤3s)
|
||||
- NFR-4 (Client Memory Footprint ≤150MB)
|
||||
- NFR-14 (Legacy Hardware Compatibility)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-47988 (Review Suggested Synovitis Grade)
|
||||
- UC-25637 (Expose Pixel-Level Activation Logic)
|
||||
- UC-60739 (Isolate Visual Noise/Artifacts)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Offline Interface Contract
|
||||
|
||||
## Purpose
|
||||
Provides offline caching and synchronization for the PWA frontend, enabling continued operation during network interruptions and seamless sync upon reconnection.
|
||||
|
||||
## Owner
|
||||
Web Experience Team (same as frontend)
|
||||
|
||||
## Parent
|
||||
frontend
|
||||
|
||||
Boundary
|
||||
IndexedDB database via Dexie.js, Service Worker for background sync, and local queue for offline actions.
|
||||
|
||||
## Provides
|
||||
- offline cache (IndexedDB storage of encrypted patient sessions, DICOM frames, annotation vectors)
|
||||
- sync queue (Service Worker-intercepted pending actions)
|
||||
- local persistence (survives browser reloads, network drops)
|
||||
|
||||
## Consumes
|
||||
(None)
|
||||
|
||||
## Consumers
|
||||
- frontend
|
||||
|
||||
## Not Directly Consumable
|
||||
- frontend internals (e.g., React components, Zustand store)
|
||||
- backend internals
|
||||
|
||||
## Breaking-change Policy
|
||||
- Changes to IndexedDB schema require version migration scripts.
|
||||
- Sync protocol changes are backward-compatible; old clients can still sync via tombstone markers.
|
||||
|
||||
## References
|
||||
- NFR-8 (Local Network Fault Tolerance)
|
||||
- NFR-4 (Client Memory Footprint)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-47988 (Review Suggested Synovitis Grade)
|
||||
- UC-25637 (Expose Pixel-Level Activation Logic)
|
||||
- UC-60739 (Isolate Visual Noise/Artifacts)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Section 3.2)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Section 3.2)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Offline Specification
|
||||
|
||||
## Purpose
|
||||
Provides offline caching and synchronization for the PWA frontend, enabling continued operation during network interruptions and seamless sync upon reconnection.
|
||||
|
||||
## Owner
|
||||
Web Experience Team (same as frontend)
|
||||
|
||||
## Parent
|
||||
frontend
|
||||
|
||||
Boundary
|
||||
IndexedDB database via Dexie.js, Service Worker for background sync, and local queue for offline actions.
|
||||
|
||||
## Internal Design
|
||||
- Dexie.js wrapper around IndexedDB with encrypted stores for patient sessions, frames, and annotation layers.
|
||||
- Service Worker intercepts network requests (fetch, XMLHttpRequest) and caches responses; queues POST/PUT/PATCH actions when offline.
|
||||
- On reconnection, Service Worker processes queued actions in order, with idempotent retries.
|
||||
- Data stored in IndexedDB is encrypted via WebCrypto before writing; decrypted on read.
|
||||
- Schema includes tables: sessions, frames, annotations, audit logs, calibration data.
|
||||
- Versioning handled via Dexie.js version upgrades with migration scripts.
|
||||
|
||||
## Interface Contract
|
||||
See `bento/frontend/subprojects/offline/spec/interface-contract.md`.
|
||||
|
||||
## Consumers
|
||||
- frontend
|
||||
|
||||
## Breaking-change Policy
|
||||
See `bento/frontend/subprojects/offline/spec/interface-contract.md`.
|
||||
|
||||
## References
|
||||
- NFR-8 (Local Network Fault Tolerance)
|
||||
- NFR-4 (Client Memory Footprint)
|
||||
- UC-48376 (Load Patient Scan Session)
|
||||
- UC-47988 (Review Suggested Synovitis Grade)
|
||||
- UC-25637 (Expose Pixel-Level Activation Logic)
|
||||
- UC-60739 (Isolate Visual Noise/Artifacts)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Section 3.2)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Section 3.2)
|
||||
@@ -0,0 +1,31 @@
|
||||
# Install Docker image
|
||||
docker network create jenkins
|
||||
|
||||
# install docker-integratable image
|
||||
docker run --name jenkins-docker --rm --detach \
|
||||
-p 8080:8080 -p 50000:50000 \
|
||||
--restart=on-failure \
|
||||
--privileged --network jenkins --network-alias docker \
|
||||
--env DOCKER_TLS_CERTDIR=/certs \
|
||||
--volume jenkins-docker-certs:/certs/client \
|
||||
--volume jenkins-data:/var/jenkins_home \
|
||||
--publish 2376:2376 \
|
||||
docker:dind --storage-driver overlay2 \
|
||||
-v jenkins_home:/var/jenkins_home jenkins/jenkins:lts
|
||||
|
||||
# install the docker images
|
||||
docker run \
|
||||
--name jenkins-blueocean \
|
||||
--restart=on-failure \
|
||||
--detach \
|
||||
--network jenkins \
|
||||
--env DOCKER_HOST=tcp://docker:2376 \
|
||||
--env DOCKER_CERT_PATH=/certs/client \
|
||||
--env DOCKER_TLS_VERIFY=1 \
|
||||
--publish 8080:8080 \
|
||||
--publish 50000:50000 \
|
||||
--volume jenkins-data:/var/jenkins_home \
|
||||
--volume jenkins-docker-certs:/certs/client:ro \
|
||||
jenkins/jenkins:lts
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
@@ -0,0 +1,12 @@
|
||||
global:
|
||||
scrape_interval: 30s # Poll every 30 seconds instead of hammering it every 5s
|
||||
scrape_timeout: 25s # Give it 25 full seconds to respond before timing out
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'triton'
|
||||
metrics_path: '/metrics'
|
||||
scheme: 'https'
|
||||
tls_config:
|
||||
insecure_skip_verify: true
|
||||
static_configs:
|
||||
- targets: ['dtj-tran--triton-s3-service-unified-triton-server.modal.run']
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "angle_classify_convnext_tiny"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "angle_classify_densenet"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "angle_classify_efficientnet"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "angle_classify_resnet50"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "angle_classify_swin_v2_s"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,412 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a Triton ensemble model repository for the VKIST vision pipeline.
|
||||
|
||||
The VKIST design docs define the logical vision flow as:
|
||||
|
||||
1. Angle classification selects the scan view.
|
||||
2. Inflammation detection checks whether synovitis/effusion is present.
|
||||
3. Segmentation models produce anatomical masks for supported view branches.
|
||||
|
||||
The 11 resident Triton models in this repository all consume the same raw image
|
||||
tensor (`input_image`) and return `logits`. Triton ensemble scheduling moves
|
||||
those tensors through the ensemble graph internally, so this generator maps the
|
||||
external client input once and exposes every component model's logits as a
|
||||
terminal ensemble output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
ENSEMBLE_NAME = "my_vision_pipeline_ensemble"
|
||||
MODEL_ROOT_DEFAULT = Path(__file__).resolve().parent
|
||||
OUTPUT_DIR_DEFAULT = Path(__file__).resolve().parent / ENSEMBLE_NAME
|
||||
VERSION_DIR = "1"
|
||||
|
||||
# Ordered by the architecture documents: angle classification -> inflammation
|
||||
# detection -> segmentation. These are the 11 component models already resident
|
||||
# under s3://vkist-ml-model/.
|
||||
ANGLE_CLASSIFICATION_MODELS = [
|
||||
"angle_classify_convnext_tiny",
|
||||
"angle_classify_resnet50",
|
||||
"angle_classify_swin_v2_s",
|
||||
"angle_classify_densenet",
|
||||
"angle_classify_efficientnet",
|
||||
]
|
||||
|
||||
INFLAMMATION_MODELS = [
|
||||
"inflammation_model_efficientnet_b0_ultrasound_2_cls",
|
||||
]
|
||||
|
||||
SEGMENTATION_MODELS = [
|
||||
"segmentation_model_unet_resnet101",
|
||||
"segmentation_model_unet3plus_att",
|
||||
"segmentation_model_post_deeplabv3_resnet101",
|
||||
"segmentation_model_post_deeplabv3",
|
||||
"segmentation_model_post_efficientfeedback",
|
||||
]
|
||||
|
||||
ALL_MODEL_NAMES = [
|
||||
*ANGLE_CLASSIFICATION_MODELS,
|
||||
*INFLAMMATION_MODELS,
|
||||
*SEGMENTATION_MODELS,
|
||||
]
|
||||
|
||||
DEFAULT_INPUT_NAME = "input"
|
||||
DEFAULT_INPUT_DATA_TYPE = "TYPE_UINT8"
|
||||
DEFAULT_INPUT_DIMS = [-1, -1, -1, 3]
|
||||
DEFAULT_MODEL_INPUT_NAME = "input_image"
|
||||
DEFAULT_MODEL_OUTPUT_NAME = "logits"
|
||||
DEFAULT_MODEL_OUTPUT_DATA_TYPE = "TYPE_FP32"
|
||||
DEFAULT_MAX_BATCH_SIZE = 8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModelConfig:
|
||||
"""Parsed Triton config for one resident component model."""
|
||||
|
||||
name: str
|
||||
platform: str
|
||||
max_batch_size: int
|
||||
input_name: str
|
||||
input_data_type: str
|
||||
input_dims: list[int]
|
||||
output_name: str
|
||||
output_data_type: str
|
||||
output_dims: list[int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnsembleTensor:
|
||||
"""One ensemble output and its matching internal model-output tensor."""
|
||||
|
||||
model_name: str
|
||||
internal_name: str
|
||||
output_name: str
|
||||
data_type: str
|
||||
dims: list[int]
|
||||
|
||||
|
||||
def parse_int_list(text: str) -> list[int]:
|
||||
"""Parse a Triton dims list such as '[ -1, 7, 512, 512 ]'."""
|
||||
|
||||
return [int(item) for item in re.findall(r"-?\d+", text)]
|
||||
|
||||
|
||||
def parse_scalar(text: str, key: str, default: str | None = None) -> str | None:
|
||||
"""Parse a quoted scalar from pbtxt text."""
|
||||
|
||||
match = re.search(rf"^\s*{re.escape(key)}:\s*\"([^\"]+)\"", text, flags=re.MULTILINE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return default
|
||||
|
||||
|
||||
def parse_block_fields(block_text: str) -> dict[str, str]:
|
||||
"""Parse the simple scalar fields used by the existing model configs."""
|
||||
|
||||
fields: dict[str, str] = {}
|
||||
for key in ("name", "platform", "data_type"):
|
||||
value = parse_scalar(block_text, key)
|
||||
if value is not None:
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
|
||||
def parse_first_model_config(config_path: Path, fallback_name: str) -> ModelConfig:
|
||||
"""Parse a component model config.pbtxt and fall back to known defaults."""
|
||||
|
||||
text = config_path.read_text(encoding="utf-8")
|
||||
name = parse_scalar(text, "name", fallback_name) or fallback_name
|
||||
platform = parse_scalar(text, "platform", "unknown") or "unknown"
|
||||
max_batch_match = re.search(r"^\s*max_batch_size:\s*(-?\d+)", text, flags=re.MULTILINE)
|
||||
max_batch_size = int(max_batch_match.group(1)) if max_batch_match else DEFAULT_MAX_BATCH_SIZE
|
||||
|
||||
input_match = re.search(r"input\s*\[(.*?)\]\s*output", text, flags=re.DOTALL)
|
||||
output_match = re.search(r"output\s*\[(.*?)\]\s*$", text, flags=re.DOTALL)
|
||||
|
||||
input_fields = parse_block_fields(input_match.group(1)) if input_match else {}
|
||||
output_fields = parse_block_fields(output_match.group(1)) if output_match else {}
|
||||
|
||||
input_dims_match = re.search(r"dims:\s*\[(.*?)\]", input_match.group(1), flags=re.DOTALL) if input_match else None
|
||||
output_dims_match = re.search(r"dims:\s*\[(.*?)\]", output_match.group(1), flags=re.DOTALL) if output_match else None
|
||||
|
||||
input_dims = parse_int_list(input_dims_match.group(1)) if input_dims_match else DEFAULT_INPUT_DIMS
|
||||
output_dims = parse_int_list(output_dims_match.group(1)) if output_dims_match else [4]
|
||||
|
||||
return ModelConfig(
|
||||
name=name,
|
||||
platform=platform,
|
||||
max_batch_size=max_batch_size,
|
||||
input_name=input_fields.get("name", DEFAULT_MODEL_INPUT_NAME),
|
||||
input_data_type=input_fields.get("data_type", DEFAULT_INPUT_DATA_TYPE),
|
||||
input_dims=input_dims,
|
||||
output_name=output_fields.get("name", DEFAULT_MODEL_OUTPUT_NAME),
|
||||
output_data_type=output_fields.get("data_type", DEFAULT_MODEL_OUTPUT_DATA_TYPE),
|
||||
output_dims=output_dims,
|
||||
)
|
||||
|
||||
|
||||
def load_model_configs(model_root: Path, model_names: Iterable[str]) -> dict[str, ModelConfig]:
|
||||
"""Load component configs from the local S3 mirror used by Triton."""
|
||||
|
||||
configs: dict[str, ModelConfig] = {}
|
||||
for model_name in model_names:
|
||||
config_path = model_root / model_name / "config.pbtxt"
|
||||
configs[model_name] = parse_first_model_config(config_path, model_name)
|
||||
return configs
|
||||
|
||||
|
||||
def tensor_name_for_model(model_name: str) -> str:
|
||||
"""Create a stable internal ensemble tensor name for one model."""
|
||||
|
||||
return f"{model_name}_logits"
|
||||
|
||||
|
||||
def output_name_for_model(model_name: str) -> str:
|
||||
"""Create the public ensemble output name for one component model."""
|
||||
|
||||
return model_name.upper()
|
||||
|
||||
|
||||
def ensemble_output_dims(model_config: ModelConfig, max_batch_size: int) -> list[int]:
|
||||
"""Return non-batch output dims for an ensemble output block.
|
||||
|
||||
Existing component configs include a leading `-1` output dim for dynamic
|
||||
batching. Triton ensemble output blocks should declare only non-batch dims
|
||||
when `max_batch_size` is positive.
|
||||
"""
|
||||
|
||||
dims = list(model_config.output_dims)
|
||||
if max_batch_size > 0 and dims and dims[0] == -1:
|
||||
return dims[1:]
|
||||
return dims
|
||||
|
||||
|
||||
def format_dims(dims: Iterable[int]) -> str:
|
||||
"""Format Triton dims as `[ 7, 512, 512 ]`."""
|
||||
|
||||
return "[ " + ", ".join(str(dim) for dim in dims) + " ]"
|
||||
|
||||
|
||||
def build_ensemble_tensors(
|
||||
model_configs: dict[str, ModelConfig],
|
||||
max_batch_size: int,
|
||||
) -> list[EnsembleTensor]:
|
||||
"""Build ordered public outputs for the ensemble config."""
|
||||
|
||||
tensors: list[EnsembleTensor] = []
|
||||
for model_name in ALL_MODEL_NAMES:
|
||||
model_config = model_configs[model_name]
|
||||
tensors.append(
|
||||
EnsembleTensor(
|
||||
model_name=model_name,
|
||||
internal_name=tensor_name_for_model(model_name),
|
||||
output_name=output_name_for_model(model_name),
|
||||
data_type=model_config.output_data_type,
|
||||
dims=ensemble_output_dims(model_config, max_batch_size),
|
||||
)
|
||||
)
|
||||
return tensors
|
||||
|
||||
|
||||
def format_input_block(input_name: str, input_data_type: str, input_dims: list[int]) -> str:
|
||||
"""Render the ensemble input block."""
|
||||
|
||||
return f""" {{
|
||||
name: "{input_name}"
|
||||
data_type: {input_data_type}
|
||||
dims: {format_dims(input_dims)}
|
||||
}}"""
|
||||
|
||||
|
||||
def format_output_block(tensor: EnsembleTensor) -> str:
|
||||
"""Render one ensemble output block."""
|
||||
|
||||
return f""" {{
|
||||
name: "{tensor.output_name}"
|
||||
data_type: {tensor.data_type}
|
||||
dims: {format_dims(tensor.dims)}
|
||||
}}"""
|
||||
|
||||
|
||||
def format_step_block(model_name: str, model_input_name: str, input_value: str, model_output_name: str, output_value: str) -> str:
|
||||
"""Render one Triton ensemble_scheduling step."""
|
||||
|
||||
return f""" {{
|
||||
model_name: "{model_name}"
|
||||
model_version: -1
|
||||
input_map {{
|
||||
key: "{model_input_name}"
|
||||
value: "{input_value}"
|
||||
}}
|
||||
output_map {{
|
||||
key: "{model_output_name}"
|
||||
value: "{output_value}"
|
||||
}}
|
||||
}}"""
|
||||
|
||||
|
||||
def build_config_pbtxt(
|
||||
ensemble_name: str,
|
||||
max_batch_size: int,
|
||||
input_name: str,
|
||||
input_data_type: str,
|
||||
input_dims: list[int],
|
||||
tensors: list[EnsembleTensor],
|
||||
model_configs: dict[str, ModelConfig],
|
||||
) -> str:
|
||||
"""Build the complete Triton ensemble config.pbtxt string."""
|
||||
|
||||
input_blocks = [format_input_block(input_name, input_data_type, input_dims)]
|
||||
output_blocks = [format_output_block(tensor) for tensor in tensors]
|
||||
|
||||
steps: list[str] = []
|
||||
for model_name in ALL_MODEL_NAMES:
|
||||
model_config = model_configs[model_name]
|
||||
tensor = next(item for item in tensors if item.model_name == model_name)
|
||||
steps.append(
|
||||
format_step_block(
|
||||
model_name=model_name,
|
||||
model_input_name=model_config.input_name,
|
||||
input_value=input_name,
|
||||
model_output_name=model_config.output_name,
|
||||
output_value=tensor.internal_name,
|
||||
)
|
||||
)
|
||||
|
||||
sections = [
|
||||
f"name: \"{ensemble_name}\"",
|
||||
"platform: \"ensemble\"",
|
||||
f"max_batch_size: {max_batch_size}",
|
||||
"input [\n" + ",\n".join(input_blocks) + "\n]",
|
||||
"output [\n" + ",\n".join(output_blocks) + "\n]",
|
||||
"ensemble_scheduling {\n step [\n" + ",\n".join(steps) + "\n ]\n}",
|
||||
"",
|
||||
]
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
def parse_dims_arg(value: str) -> list[int]:
|
||||
"""Parse a comma-separated dims CLI value."""
|
||||
|
||||
return [int(item.strip()) for item in value.split(",") if item.strip()]
|
||||
|
||||
|
||||
def prepare_output_dir(output_dir: Path, clean: bool) -> None:
|
||||
"""Create or refresh the local Triton model repository directory."""
|
||||
|
||||
if clean and output_dir.exists():
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(output_dir)
|
||||
version_dir = output_dir / VERSION_DIR
|
||||
version_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def generate_ensemble(
|
||||
model_root: Path,
|
||||
output_dir: Path,
|
||||
ensemble_name: str,
|
||||
max_batch_size: int,
|
||||
input_name: str,
|
||||
input_data_type: str,
|
||||
input_dims: list[int],
|
||||
clean: bool,
|
||||
) -> Path:
|
||||
"""Generate the ensemble repository and return the written config path."""
|
||||
|
||||
model_configs = load_model_configs(model_root, ALL_MODEL_NAMES)
|
||||
tensors = build_ensemble_tensors(model_configs, max_batch_size)
|
||||
config_text = build_config_pbtxt(
|
||||
ensemble_name=ensemble_name,
|
||||
max_batch_size=max_batch_size,
|
||||
input_name=input_name,
|
||||
input_data_type=input_data_type,
|
||||
input_dims=input_dims,
|
||||
tensors=tensors,
|
||||
model_configs=model_configs,
|
||||
)
|
||||
|
||||
prepare_output_dir(output_dir, clean=clean)
|
||||
config_path = output_dir / VERSION_DIR / "config.pbtxt"
|
||||
config_path.write_text(config_text, encoding="utf-8")
|
||||
return config_path
|
||||
|
||||
|
||||
def build_arg_parser() -> argparse.ArgumentParser:
|
||||
"""Build CLI arguments for local generation."""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Triton ensemble config for the VKIST 11-model vision pipeline."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-root",
|
||||
type=Path,
|
||||
default=MODEL_ROOT_DEFAULT,
|
||||
help="Local directory containing the 11 resident model config.pbtxt files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=OUTPUT_DIR_DEFAULT,
|
||||
help="Local Triton model repository directory to create.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ensemble-name",
|
||||
default=ENSEMBLE_NAME,
|
||||
help="Triton ensemble model name and S3 top-level folder name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-batch-size",
|
||||
type=int,
|
||||
default=DEFAULT_MAX_BATCH_SIZE,
|
||||
help="Triton max_batch_size for the ensemble.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-name",
|
||||
default=DEFAULT_INPUT_NAME,
|
||||
help="External client input tensor name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-data-type",
|
||||
default=DEFAULT_INPUT_DATA_TYPE,
|
||||
help="External client input tensor data type.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-dims",
|
||||
default=",".join(str(item) for item in DEFAULT_INPUT_DIMS),
|
||||
help="Comma-separated external input dims, excluding batch dimension.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-clean",
|
||||
action="store_true",
|
||||
help="Do not remove an existing output directory before generation.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
|
||||
args = build_arg_parser().parse_args()
|
||||
config_path = generate_ensemble(
|
||||
model_root=args.model_root,
|
||||
output_dir=args.output_dir,
|
||||
ensemble_name=args.ensemble_name,
|
||||
max_batch_size=args.max_batch_size,
|
||||
input_name=args.input_name,
|
||||
input_data_type=args.input_data_type,
|
||||
input_dims=parse_dims_arg(args.input_dims),
|
||||
clean=not args.no_clean,
|
||||
)
|
||||
print(f"Wrote Triton ensemble config: {config_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "inflammation_model_efficientnet_b0_ultrasound_2_cls"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 2 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,215 @@
|
||||
name: "msk_vision_pipeline_ensemble"
|
||||
platform: "ensemble"
|
||||
backend: "ensemble"
|
||||
max_batch_size: 8
|
||||
|
||||
# MODIFY HERE: Declare 2 separate input ports with fixed dimensions; no longer using the flexible -1 dimension
|
||||
input [
|
||||
{
|
||||
name: "input_224"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 224, 224 ]
|
||||
},
|
||||
{
|
||||
name: "input_512"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 512, 512 ]
|
||||
}
|
||||
]
|
||||
|
||||
output [
|
||||
{
|
||||
name: "angle_classify_convnext_tiny_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
},
|
||||
{
|
||||
name: "angle_classify_resnet50_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
},
|
||||
{
|
||||
name: "angle_classify_swin_v2_s_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
},
|
||||
{
|
||||
name: "angle_classify_densenet_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
},
|
||||
{
|
||||
name: "angle_classify_efficientnet_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 4 ]
|
||||
},
|
||||
{
|
||||
name: "inflammation_model_efficientnet_b0_ultrasound_2_cls_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 2 ]
|
||||
},
|
||||
{
|
||||
name: "segmentation_model_unet_resnet101_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
},
|
||||
{
|
||||
name: "segmentation_model_unet3plus_att_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
},
|
||||
{
|
||||
name: "segmentation_model_post_deeplabv3_resnet101_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
},
|
||||
{
|
||||
name: "segmentation_model_post_deeplabv3_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
},
|
||||
{
|
||||
name: "segmentation_model_post_efficientfeedback_logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
}
|
||||
]
|
||||
|
||||
ensemble_scheduling {
|
||||
step [
|
||||
# ---- 224x224 MODEL GROUP (Processes input data from the input_224 variable) ----
|
||||
{
|
||||
model_name: "angle_classify_convnext_tiny"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_224"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "angle_classify_convnext_tiny_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "angle_classify_resnet50"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_224"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "angle_classify_resnet50_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "angle_classify_swin_v2_s"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_224"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "angle_classify_swin_v2_s_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "angle_classify_densenet"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_224"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "angle_classify_densenet_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "angle_classify_efficientnet"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_224"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "angle_classify_efficientnet_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "inflammation_model_efficientnet_b0_ultrasound_2_cls"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_224"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "inflammation_model_efficientnet_b0_ultrasound_2_cls_logits"
|
||||
}
|
||||
},
|
||||
# ---- 512x512 MODEL GROUP (Processes input data from the input_512 variable) ----
|
||||
{
|
||||
model_name: "segmentation_model_unet_resnet101"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_512"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "segmentation_model_unet_resnet101_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "segmentation_model_unet3plus_att"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_512"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "segmentation_model_unet3plus_att_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "segmentation_model_post_deeplabv3_resnet101"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_512"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "segmentation_model_post_deeplabv3_resnet101_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "segmentation_model_post_deeplabv3"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_512"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "segmentation_model_post_deeplabv3_logits"
|
||||
}
|
||||
},
|
||||
{
|
||||
model_name: "segmentation_model_post_efficientfeedback"
|
||||
model_version: -1
|
||||
input_map {
|
||||
key: "input_image"
|
||||
value: "input_512"
|
||||
}
|
||||
output_map {
|
||||
key: "logits"
|
||||
value: "segmentation_model_post_efficientfeedback_logits"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "segmentation_model_post_deeplabv3"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 512, 512 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "segmentation_model_post_deeplabv3_resnet101"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 512, 512 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "segmentation_model_post_efficientfeedback"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 512, 512 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "segmentation_model_unet3plus_att"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 512, 512 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
name: "segmentation_model_unet_resnet101"
|
||||
platform: "pytorch_libtorch"
|
||||
max_batch_size: 8
|
||||
input [
|
||||
{
|
||||
name: "input_image"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 3, 512, 512 ]
|
||||
}
|
||||
]
|
||||
output [
|
||||
{
|
||||
name: "logits"
|
||||
data_type: TYPE_FP32
|
||||
dims: [ 7, 512, 512 ]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Upload the generated Triton ensemble repository to AWS S3.
|
||||
|
||||
This script mirrors the local Triton model repository folder into the active
|
||||
VKIST model bucket root:
|
||||
|
||||
s3://vkist-ml-model/my_vision_pipeline_ensemble/
|
||||
|
||||
It uploads every file and also creates zero-byte directory marker objects so the
|
||||
S3 prefix reflects the same nested structure Triton expects in a model
|
||||
repository.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from boto3.s3.transfer import TransferConfig
|
||||
except ImportError: # pragma: no cover - exercised only when boto3 is absent.
|
||||
boto3 = None
|
||||
TransferConfig = None
|
||||
|
||||
|
||||
ENSEMBLE_NAME = "my_vision_pipeline_ensemble"
|
||||
DEFAULT_SOURCE_DIR = Path(__file__).resolve().parent / ENSEMBLE_NAME
|
||||
DEFAULT_BUCKET_URI = "s3://vkist-ml-model/"
|
||||
DEFAULT_TRANSFER_CONFIG = None
|
||||
|
||||
|
||||
def require_env(name: str) -> str:
|
||||
"""Read a required AWS credential/environment value."""
|
||||
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
raise RuntimeError(f"Missing required environment variable: {name}")
|
||||
return value
|
||||
|
||||
|
||||
def parse_s3_uri(uri: str) -> tuple[str, str]:
|
||||
"""Parse an S3 URI into bucket and prefix."""
|
||||
|
||||
if not uri.startswith("s3://"):
|
||||
raise ValueError(f"S3 URI must start with 's3://': {uri}")
|
||||
|
||||
body = uri.removeprefix("s3://").strip()
|
||||
if not body:
|
||||
raise ValueError("S3 URI must include a bucket name.")
|
||||
|
||||
parts = body.split("/", 1)
|
||||
bucket = parts[0]
|
||||
prefix = parts[1] if len(parts) > 1 else ""
|
||||
return bucket, normalize_prefix(prefix)
|
||||
|
||||
|
||||
def normalize_prefix(prefix: str) -> str:
|
||||
"""Ensure an S3 prefix ends with '/' when non-empty."""
|
||||
|
||||
prefix = prefix.strip().replace("\\", "/")
|
||||
if prefix and not prefix.endswith("/"):
|
||||
return prefix + "/"
|
||||
return prefix
|
||||
|
||||
|
||||
def relpath_for(path: Path, root: Path) -> Path:
|
||||
"""Return a POSIX-style relative path under a root directory."""
|
||||
|
||||
return path.relative_to(root).as_posix()
|
||||
|
||||
|
||||
def collect_local_tree(source_dir: Path) -> tuple[list[Path], list[Path]]:
|
||||
"""Collect local files and directories to mirror into S3."""
|
||||
|
||||
if not source_dir.exists():
|
||||
raise FileNotFoundError(f"Source directory does not exist: {source_dir}")
|
||||
if not source_dir.is_dir():
|
||||
raise NotADirectoryError(f"Source path is not a directory: {source_dir}")
|
||||
|
||||
files = [path for path in source_dir.rglob("*") if path.is_file()]
|
||||
directories = [path for path in source_dir.rglob("*") if path.is_dir()]
|
||||
directories.append(source_dir)
|
||||
return sorted(files), sorted(directories, reverse=True)
|
||||
|
||||
|
||||
def file_key_for(source_dir: Path, file_path: Path, prefix: str) -> str:
|
||||
"""Build the S3 object key for a local file."""
|
||||
|
||||
return prefix + relpath_for(file_path, source_dir).replace("\\", "/")
|
||||
|
||||
|
||||
def directory_marker_key_for(source_dir: Path, directory_path: Path, prefix: str) -> str:
|
||||
"""Build the S3 directory marker key for a local directory."""
|
||||
|
||||
if directory_path == source_dir:
|
||||
return prefix
|
||||
return prefix + relpath_for(directory_path, source_dir).replace("\\", "/") + "/"
|
||||
|
||||
|
||||
def content_type_for(path: Path) -> str:
|
||||
"""Guess a safe MIME type for an S3 object."""
|
||||
|
||||
guessed_type, _ = mimetypes.guess_type(str(path))
|
||||
return guessed_type or "application/octet-stream"
|
||||
|
||||
|
||||
def create_s3_client() -> object:
|
||||
"""Create a Boto3 S3 client from local AWS environment variables."""
|
||||
|
||||
if boto3 is None:
|
||||
raise RuntimeError("boto3 is required. Install it with: pip install boto3")
|
||||
|
||||
access_key = require_env("AWS_ACCESS_KEY_ID")
|
||||
secret_key = require_env("AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
client_kwargs = {
|
||||
"aws_access_key_id": access_key,
|
||||
"aws_secret_access_key": secret_key,
|
||||
}
|
||||
|
||||
session_token = os.environ.get("AWS_SESSION_TOKEN")
|
||||
if session_token:
|
||||
client_kwargs["aws_session_token"] = session_token
|
||||
|
||||
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION")
|
||||
if region:
|
||||
client_kwargs["region_name"] = region
|
||||
|
||||
endpoint_url = os.environ.get("AWS_ENDPOINT_URL")
|
||||
if endpoint_url:
|
||||
client_kwargs["endpoint_url"] = endpoint_url
|
||||
|
||||
return boto3.client("s3", **client_kwargs)
|
||||
|
||||
|
||||
def put_directory_marker(
|
||||
s3_client: object,
|
||||
bucket: str,
|
||||
key: str,
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""Create a zero-byte S3 marker object for one directory prefix."""
|
||||
|
||||
if dry_run:
|
||||
print(f"[dry-run] marker s3://{bucket}/{key}")
|
||||
return
|
||||
|
||||
s3_client.put_object(
|
||||
Bucket=bucket,
|
||||
Key=key,
|
||||
Body=b"",
|
||||
ContentType="application/x-directory",
|
||||
)
|
||||
|
||||
|
||||
def upload_file(
|
||||
s3_client: object,
|
||||
bucket: str,
|
||||
local_path: Path,
|
||||
key: str,
|
||||
transfer_config: TransferConfig | None,
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""Upload one local file to S3."""
|
||||
|
||||
if dry_run:
|
||||
print(f"[dry-run] upload {local_path} -> s3://{bucket}/{key}")
|
||||
return
|
||||
|
||||
if transfer_config is None:
|
||||
if TransferConfig is None:
|
||||
raise RuntimeError("boto3 is required. Install it with: pip install boto3")
|
||||
transfer_config = TransferConfig(
|
||||
multipart_threshold=64 * 1024 * 1024,
|
||||
multipart_chunksize=16 * 1024 * 1024,
|
||||
max_concurrency=8,
|
||||
use_threads=True,
|
||||
)
|
||||
|
||||
s3_client.upload_file(
|
||||
Filename=str(local_path),
|
||||
Bucket=bucket,
|
||||
Key=key,
|
||||
ExtraArgs={
|
||||
"ContentType": content_type_for(local_path),
|
||||
"CacheControl": "no-store",
|
||||
},
|
||||
Config=transfer_config,
|
||||
)
|
||||
|
||||
|
||||
def list_existing_keys(s3_client: object, bucket: str, prefix: str) -> set[str]:
|
||||
"""List all existing object keys below an S3 prefix."""
|
||||
|
||||
paginator = s3_client.get_paginator("list_objects_v2")
|
||||
keys: set[str] = set()
|
||||
|
||||
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
||||
for item in page.get("Contents", []):
|
||||
keys.add(item["Key"])
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def delete_stale_keys(
|
||||
s3_client: object,
|
||||
bucket: str,
|
||||
prefix: str,
|
||||
desired_keys: Iterable[str],
|
||||
dry_run: bool = False,
|
||||
) -> int:
|
||||
"""Delete objects under the prefix that are not present locally."""
|
||||
|
||||
desired = set(desired_keys)
|
||||
existing = list_existing_keys(s3_client, bucket, prefix)
|
||||
stale = sorted(existing - desired)
|
||||
|
||||
if not stale:
|
||||
return 0
|
||||
|
||||
if dry_run:
|
||||
for key in stale:
|
||||
print(f"[dry-run] delete s3://{bucket}/{key}")
|
||||
return len(stale)
|
||||
|
||||
for key in stale:
|
||||
s3_client.delete_object(Bucket=bucket, Key=key)
|
||||
|
||||
return len(stale)
|
||||
|
||||
|
||||
def mirror_to_s3(
|
||||
source_dir: Path,
|
||||
bucket_uri: str,
|
||||
prefix: str | None = None,
|
||||
delete: bool = False,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, int]:
|
||||
"""Mirror a local Triton model repository directory into S3."""
|
||||
|
||||
bucket, bucket_prefix = parse_s3_uri(bucket_uri)
|
||||
effective_prefix = normalize_prefix(prefix if prefix is not None else source_dir.name + "/")
|
||||
s3_prefix = bucket_prefix + effective_prefix
|
||||
|
||||
files, directories = collect_local_tree(source_dir)
|
||||
s3_client = create_s3_client() if (not dry_run) or delete else None
|
||||
|
||||
desired_keys: set[str] = set()
|
||||
|
||||
for directory_path in directories:
|
||||
key = directory_marker_key_for(source_dir, directory_path, s3_prefix)
|
||||
desired_keys.add(key)
|
||||
put_directory_marker(s3_client, bucket, key, dry_run=dry_run)
|
||||
|
||||
for file_path in files:
|
||||
key = file_key_for(source_dir, file_path, s3_prefix)
|
||||
desired_keys.add(key)
|
||||
upload_file(
|
||||
s3_client=s3_client,
|
||||
bucket=bucket,
|
||||
local_path=file_path,
|
||||
key=key,
|
||||
transfer_config=DEFAULT_TRANSFER_CONFIG,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
deleted = 0
|
||||
if delete:
|
||||
deleted = delete_stale_keys(
|
||||
s3_client=s3_client,
|
||||
bucket=bucket,
|
||||
prefix=s3_prefix,
|
||||
desired_keys=desired_keys,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
return {
|
||||
"files_uploaded": len(files),
|
||||
"directories_marked": len(directories),
|
||||
"keys_desired": len(desired_keys),
|
||||
"keys_deleted": deleted,
|
||||
}
|
||||
|
||||
|
||||
def build_arg_parser() -> argparse.ArgumentParser:
|
||||
"""Build CLI arguments for S3 upload."""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Mirror a generated Triton ensemble repository to AWS S3."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_SOURCE_DIR,
|
||||
help="Local generated Triton model repository directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bucket-uri",
|
||||
default=DEFAULT_BUCKET_URI,
|
||||
help="S3 model bucket root URI, for example s3://vkist-ml-model/.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefix",
|
||||
default=None,
|
||||
help="Optional S3 prefix under the bucket. Defaults to the source folder name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delete",
|
||||
action="store_true",
|
||||
help="Delete stale objects under the target prefix that are missing locally.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print S3 actions without writing or deleting objects.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
|
||||
args = build_arg_parser().parse_args()
|
||||
summary = mirror_to_s3(
|
||||
source_dir=args.source_dir,
|
||||
bucket_uri=args.bucket_uri,
|
||||
prefix=args.prefix,
|
||||
delete=args.delete,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
||||
print(
|
||||
"Uploaded ensemble mirror: "
|
||||
f"files={summary['files_uploaded']}, "
|
||||
f"directories={summary['directories_marked']}, "
|
||||
f"desired_keys={summary['keys_desired']}, "
|
||||
f"deleted={summary['keys_deleted']}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
# Create folder
|
||||
|
||||
# aws s3api put-object --bucket vkist-ml-model --key angle_classify_densenet/
|
||||
aws s3api put-object --bucket vkist-ml-model --key angle_classify_efficientnet/ # best_efficientnet_b2.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key angle_classify_resnet50/ # best_resnet50.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key angle_classify_swin_v2_s/ # best_swin_v2_s.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key segmentation_model_post_deeplabv3_resnet101/ # best_model_deeplabv3_resnet101_seed_16.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key segmentation_model_post_deeplabv3/ # best_model_Deeplav3.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key segmentation_model_post_efficientfeedback/ # efficientfeedback.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key segmentation_model_unet_resnet101/ # unet_resnet101.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key segmentation_model_unet3plus_att/ # unet3plus_att.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key inflammation_model_efficientnet_b0_ultrasound_2_cls/ # efficientnet_b0_ultrasound_2_class.pth
|
||||
aws s3api put-object --bucket vkist-ml-model --key msk_vision_pipeline_ensemble
|
||||
|
||||
# upload model
|
||||
aws s3 mv s3://vkist-ml-model/best_densenet.pth s3://vkist-ml-model/angle_classify_densenet/best_densenet.pth
|
||||
aws s3 mv s3://vkist-ml-model/best_efficientnet_b2.pth s3://vkist-ml-model/angle_classify_efficientnet/best_efficientnet_b2.pth
|
||||
aws s3 mv s3://vkist-ml-model/best_model_deeplabv3_resnet101_seed_16.pth s3://vkist-ml-model/segmentation_model_post_deeplabv3_resnet101/best_model_deeplabv3_resnet101_seed_16.pth
|
||||
aws s3 mv s3://vkist-ml-model/best_model_Deeplav3.pth s3://vkist-ml-model/segmentation_model_post_deeplabv3/best_model_Deeplav3.pth
|
||||
aws s3 mv s3://vkist-ml-model/best_resnet50.pth s3://vkist-ml-model/angle_classify_resnet50/best_resnet50.pth
|
||||
aws s3 mv s3://vkist-ml-model/best_swin_v2_s.pth s3://vkist-ml-model/angle_classify_swin_v2_s/best_swin_v2_s.pth
|
||||
aws s3 mv s3://vkist-ml-model/efficientfeedback.pth s3://vkist-ml-model/segmentation_model_post_efficientfeedback/efficientfeedback.pth
|
||||
aws s3 mv s3://vkist-ml-model/efficientnet_b0_ultrasound_2_class.pth s3://vkist-ml-model/inflammation_model_efficientnet_b0_ultrasound_2_cls/efficientnet_b0_ultrasound_2_class.pth
|
||||
aws s3 mv s3://vkist-ml-model/unet_resnet101.pth s3://vkist-ml-model/segmentation_model_unet_resnet101/unet_resnet101.pth
|
||||
aws s3 mv s3://vkist-ml-model/unet3plus_att.pth s3://vkist-ml-model/segmentation_model_unet3plus_att/unet3plus_att.pth
|
||||
aws s3 mv s3://vkist-ml-model/best_convnext_tiny.pth s3://vkist-ml-model/angle_classify_convnext_tiny/best_convnext_tiny.pth
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Classification Models
|
||||
aws s3 mv s3://vkist-ml-model/angle_classify_densenet/best_densenet.pth s3://vkist-ml-model/angle_classify_densenet/1/best_densenet.pth
|
||||
aws s3 mv s3://vkist-ml-model/angle_classify_efficientnet/best_efficientnet_b2.pth s3://vkist-ml-model/angle_classify_efficientnet/1/best_efficientnet_b2.pth
|
||||
aws s3 mv s3://vkist-ml-model/angle_classify_resnet50/best_resnet50.pth s3://vkist-ml-model/angle_classify_resnet50/1/best_resnet50.pth
|
||||
aws s3 mv s3://vkist-ml-model/angle_classify_swin_v2_s/best_swin_v2_s.pth s3://vkist-ml-model/angle_classify_swin_v2_s/1/best_swin_v2_s.pth
|
||||
aws s3 mv s3://vkist-ml-model/angle_classify_convnext_tiny/best_convnext_tiny.pth s3://vkist-ml-model/angle_classify_convnext_tiny/1/best_convnext_tiny.pth
|
||||
aws s3 mv s3://vkist-ml-model/inflammation_model_efficientnet_b0_ultrasound_2_cls/efficientnet_b0_ultrasound_2_class.pth s3://vkist-ml-model/inflammation_model_efficientnet_b0_ultrasound_2_cls/1/efficientnet_b0_ultrasound_2_class.pth
|
||||
|
||||
# Segmentation Models
|
||||
aws s3 mv s3://vkist-ml-model/segmentation_model_post_deeplabv3_resnet101/best_model_deeplabv3_resnet101_seed_16.pth s3://vkist-ml-model/segmentation_model_post_deeplabv3_resnet101/1/best_model_deeplabv3_resnet101_seed_16.pth
|
||||
aws s3 mv s3://vkist-ml-model/segmentation_model_post_deeplabv3/best_model_Deeplav3.pth s3://vkist-ml-model/segmentation_model_post_deeplabv3/1/best_model_Deeplav3.pth
|
||||
aws s3 mv s3://vkist-ml-model/segmentation_model_post_efficientfeedback/efficientfeedback.pth s3://vkist-ml-model/segmentation_model_post_efficientfeedback/1/efficientfeedback.pth
|
||||
aws s3 mv s3://vkist-ml-model/segmentation_model_unet_resnet101/unet_resnet101.pth s3://vkist-ml-model/segmentation_model_unet_resnet101/1/unet_resnet101.pth
|
||||
aws s3 mv s3://vkist-ml-model/segmentation_model_unet3plus_att/unet3plus_att.pth s3://vkist-ml-model/segmentation_model_unet3plus_att/1/unet3plus_att.pth
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
S3_BUCKET="s3://vkist-ml-model"
|
||||
|
||||
# Set the exact local baseline path where your updated config.pbtxt models reside
|
||||
LOCAL_CONFIG_DIR="/Users/davestran/Downloads/vkist_internship/PILOT_PROJECT/workspace/sprint_1_2/codebase/infra/implementation/s3"
|
||||
|
||||
echo "📤 Syncing local config.pbtxt modifications up to S3 bucket repository..."
|
||||
|
||||
# Classification Configs
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/angle_classify_convnext_tiny/config.pbtxt" "$S3_BUCKET/angle_classify_convnext_tiny/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/angle_classify_densenet/config.pbtxt" "$S3_BUCKET/angle_classify_densenet/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/angle_classify_efficientnet/config.pbtxt" "$S3_BUCKET/angle_classify_efficientnet/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/angle_classify_resnet50/config.pbtxt" "$S3_BUCKET/angle_classify_resnet50/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/angle_classify_swin_v2_s/config.pbtxt" "$S3_BUCKET/angle_classify_swin_v2_s/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/inflammation_model_efficientnet_b0_ultrasound_2_cls/config.pbtxt" "$S3_BUCKET/inflammation_model_efficientnet_b0_ultrasound_2_cls/config.pbtxt"
|
||||
|
||||
# Segmentation Configs
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/segmentation_model_post_deeplabv3/config.pbtxt" "$S3_BUCKET/segmentation_model_post_deeplabv3/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/segmentation_model_post_deeplabv3_resnet101/config.pbtxt" "$S3_BUCKET/segmentation_model_post_deeplabv3_resnet101/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/segmentation_model_post_efficientfeedback/config.pbtxt" "$S3_BUCKET/segmentation_model_post_efficientfeedback/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/segmentation_model_unet3plus_att/config.pbtxt" "$S3_BUCKET/segmentation_model_unet3plus_att/config.pbtxt"
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/segmentation_model_unet_resnet101/config.pbtxt" "$S3_BUCKET/segmentation_model_unet_resnet101/config.pbtxt"
|
||||
|
||||
|
||||
# Ensemble Config
|
||||
aws s3 cp "$LOCAL_CONFIG_DIR/msk_vision_pipeline_ensemble/config.pbtxt" "$S3_BUCKET/msk_vision_pipeline_ensemble/config.pbtxt"
|
||||
|
||||
echo "✅ Configuration files successfully synced to S3 backend!"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define the absolute local path to your compiled TorchScript folder
|
||||
LOCAL_DIR="PILOT_PROJECT/workspace/sprint_1_2/codebase/infra/implementation/MODEL_ZIP_PILOT_LT"
|
||||
S3_BUCKET="s3://vkist-ml-model"
|
||||
|
||||
echo "📤 Starting upload workflow of LibTorch binaries to S3 bucket layout..."
|
||||
|
||||
# ==========================================
|
||||
# 1. Classification Models
|
||||
# ==========================================
|
||||
|
||||
echo "🔄 Uploading: Classification models..."
|
||||
aws s3 cp "$LOCAL_DIR/best_densenet.pth" "$S3_BUCKET/angle_classify_densenet/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/best_efficientnet_b2.pth" "$S3_BUCKET/angle_classify_efficientnet/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/best_resnet50.pth" "$S3_BUCKET/angle_classify_resnet50/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/best_swin_v2_s.pth" "$S3_BUCKET/angle_classify_swin_v2_s/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/best_convnext_tiny.pth" "$S3_BUCKET/angle_classify_convnext_tiny/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/efficientnet_b0_ultrasound_2_class.pth" "$S3_BUCKET/inflammation_model_efficientnet_b0_ultrasound_2_cls/1/model.pt"
|
||||
|
||||
# ==========================================
|
||||
# 2. Segmentation Models
|
||||
# ==========================================
|
||||
|
||||
echo "🔄 Uploading: Segmentation models..."
|
||||
aws s3 cp "$LOCAL_DIR/best_model_deeplabv3_resnet101_seed_16.pth" "$S3_BUCKET/segmentation_model_post_deeplabv3_resnet101/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/best_model_Deeplav3.pth" "$S3_BUCKET/segmentation_model_post_deeplabv3/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/efficientfeedback.pth" "$S3_BUCKET/segmentation_model_post_efficientfeedback/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/unet_resnet101.pth" "$S3_BUCKET/segmentation_model_unet_resnet101/1/model.pt"
|
||||
aws s3 cp "$LOCAL_DIR/unet3plus_att.pth" "$S3_BUCKET/segmentation_model_unet3plus_att/1/model.pt"
|
||||
|
||||
echo "🎉 All local LibTorch models compiled down and synchronized with S3 Triton targets successfully!"
|
||||
@@ -0,0 +1,216 @@
|
||||
import subprocess
|
||||
import modal
|
||||
import time
|
||||
|
||||
# run from root: vkist_internship (will manaage later)
|
||||
|
||||
triton_image = (
|
||||
modal.Image.from_registry(
|
||||
tag="nvcr.io/nvidia/tritonserver:24.02-py3",
|
||||
add_python="3.12"
|
||||
)
|
||||
# Step B: Install minimal system dependencies (replacing your apt-get RUN command)
|
||||
.apt_install(
|
||||
"libgl1",
|
||||
"libglib2.0-0" # Crucial runtime hook for OpenCV / Ultralytics
|
||||
)
|
||||
# Step C: Install PyTorch pinned strictly to CUDA 12.1 wheel indices
|
||||
.run_commands(
|
||||
"python3 -m pip install --upgrade pip setuptools",
|
||||
"python3 -m pip install torch==2.5.0 torchaudio==2.5.0 torchvision==0.20.0 --index-url https://download.pytorch.org/whl/cu121",
|
||||
"python3 -m pip install transformers==4.57.3 timm==1.0.22 ultralytics==8.3.0 opencv-python grpcio protobuf",
|
||||
"python3 -m pip install fastapi[standard]",
|
||||
"python3 -m pip install tritonclient[http,cuda]"
|
||||
)
|
||||
)
|
||||
|
||||
app = modal.App("triton-s3-service", image=triton_image)
|
||||
from fastapi import FastAPI, Response, Request,HTTPException
|
||||
from fastapi.responses import StreamingResponse # 👈 ADD THIS IMPORT
|
||||
import httpx
|
||||
web_app = FastAPI()
|
||||
# -------------------------------------------------------------
|
||||
# FASTAPI PROXY ROUTING (Living inside the container)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
@web_app.get("/v2/health/ready")
|
||||
async def forward_health():
|
||||
"""Proxies external HTTP REST calls straight to Triton's internal inference engine"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get("http://127.0.0.1:8000/v2/health/ready")
|
||||
return Response(content=response.content, status_code=response.status_code, media_type=response.headers.get("content-type"))
|
||||
except Exception as e:
|
||||
return Response(content=f"Triton booting models from S3... Error: {str(e)}", status_code=503)
|
||||
|
||||
@web_app.get("/metrics")
|
||||
@web_app.get("/")
|
||||
async def forward_metrics():
|
||||
"""Proxies external metric calls straight to Triton's internal metrics engine"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get("http://127.0.0.1:8002/metrics")
|
||||
return Response(content=response.content, status_code=response.status_code, media_type=response.headers.get("content-type"))
|
||||
except Exception as e:
|
||||
return Response(content=f"Waiting for metrics channel... Error: {str(e)}", status_code=503)
|
||||
|
||||
# 👇 ADD THIS CATCH-ALL ROUTE HERE 👇
|
||||
@web_app.api_route("/v2/{path:path}", methods=["GET", "POST"])
|
||||
async def proxy_all_triton_request(path: str, request: Request):
|
||||
import tritonclient.grpc.aio as grpcclient
|
||||
from tritonclient.grpc import service_pb2, service_pb2_grpc
|
||||
from tritonclient.grpc import _utils as grpc_utils #InferenceServerClient
|
||||
import grpc
|
||||
import numpy as np
|
||||
# 1. Keep HTTP proxy ONLY for metadata/health checks
|
||||
if "infer" not in path:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
url = f"http://127.0.0.1:8000/v2/{path}"
|
||||
headers = dict(request.headers)
|
||||
headers.pop("host", None)
|
||||
triton_response = await client.request(
|
||||
method=request.method, url=url, headers=headers, content=await request.body()
|
||||
)
|
||||
return Response(
|
||||
content=triton_response.content,
|
||||
status_code=triton_response.status_code,
|
||||
headers=dict(triton_response.headers)
|
||||
)
|
||||
|
||||
# 2. 🚀 FOR INFERENCE: Convert the incoming HTTP raw body into a gRPC call
|
||||
if "infer" in path:
|
||||
try:
|
||||
# Extract model name from the route path (e.g., v2/models/MODEL_NAME/infer)
|
||||
parts = path.split("/")
|
||||
model_name = parts[parts.index("models") + 1]
|
||||
|
||||
# Read incoming raw binary HTTP payload
|
||||
raw_http_body = await request.body()
|
||||
|
||||
header_length_str = request.headers.get("Inference-Header-Content-Length")
|
||||
if not header_length_str:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Missing 'Inference-Header-Content-Length' header required for binary Triton transcoding."
|
||||
)
|
||||
|
||||
header_length = int(header_length_str)
|
||||
|
||||
# --- 💥 KSERVE V2 MULTI-PART BODY PARSING ---
|
||||
# Extract the front JSON metadata and the trailing raw binary tensors
|
||||
import json
|
||||
json_bytes = raw_http_body[:header_length]
|
||||
binary_data = raw_http_body[header_length:]
|
||||
request_metadata = json.loads(json_bytes.decode('utf-8'))
|
||||
|
||||
# Setup async gRPC connection
|
||||
triton_url = "127.0.0.1:8001"
|
||||
|
||||
# Configure channels to accept large payload returns (100MB limit override)
|
||||
max_msg_length = 100 * 1024 * 1024
|
||||
channel_options = [
|
||||
('grpc.max_receive_message_length', max_msg_length),
|
||||
('grpc.max_send_message_length', max_msg_length),
|
||||
]
|
||||
async with grpc.aio.insecure_channel(triton_url, options=channel_options) as channel:
|
||||
stub = service_pb2_grpc.GRPCInferenceServiceStub(channel=channel)
|
||||
|
||||
# Construct the native ModelInferRequest protobuf
|
||||
grpc_request = service_pb2.ModelInferRequest()
|
||||
grpc_request.model_name = model_name
|
||||
grpc_request.model_version = ""
|
||||
|
||||
# Populate inputs dynamically from incoming KServe metadata
|
||||
binary_offset = 0
|
||||
for input_tensor in request_metadata.get("inputs", []):
|
||||
# Correct Protobuf repeated field instantiation via .add()
|
||||
infer_input = grpc_request.inputs.add()
|
||||
infer_input.name = input_tensor["name"]
|
||||
infer_input.datatype = input_tensor["datatype"]
|
||||
infer_input.shape.extend(input_tensor["shape"]) # Explicit clean integers!
|
||||
|
||||
# Extract the binary slice matching this tensor out of the raw payload block
|
||||
if "parameters" in input_tensor and "binary_data_size" in input_tensor["parameters"]:
|
||||
data_size = input_tensor["parameters"]["binary_data_size"]
|
||||
grpc_request.raw_input_contents.append(
|
||||
binary_data[binary_offset : binary_offset + data_size]
|
||||
)
|
||||
binary_offset += data_size
|
||||
|
||||
# Request output tensor mappings dynamically based on what the client requested
|
||||
for output_tensor in request_metadata.get("outputs", []):
|
||||
infer_output = grpc_request.outputs.add()
|
||||
infer_output.name = output_tensor["name"]
|
||||
# Signal Triton to return output via raw binary buffers
|
||||
infer_output.parameters["binary_data"].bool_param = True
|
||||
|
||||
# ✅ Send the transcoding payload straight into Triton over internal gRPC loop
|
||||
grpc_response = await stub.ModelInfer(request=grpc_request, timeout=None)
|
||||
|
||||
# --- 💥 TRANSCODE gRPC RESPONSE BACK TO MULTI-PART KSERVE HTTP ---
|
||||
response_metadata = {
|
||||
"model_name": grpc_response.model_name,
|
||||
"model_version": grpc_response.model_version,
|
||||
"outputs": []
|
||||
}
|
||||
|
||||
response_binary_body = b""
|
||||
for i, output in enumerate(grpc_response.outputs):
|
||||
out_desc = {
|
||||
"name": output.name,
|
||||
"datatype": output.datatype,
|
||||
"shape": list(output.shape),
|
||||
"parameters": {
|
||||
"binary_data_size": len(grpc_response.raw_output_contents[i])
|
||||
}
|
||||
}
|
||||
response_metadata["outputs"].append(out_desc)
|
||||
response_binary_body += grpc_response.raw_output_contents[i]
|
||||
|
||||
# Re-bundle into [JSON metadata] + [Raw binary output chunks]
|
||||
response_json_bytes = json.dumps(response_metadata).encode('utf-8')
|
||||
output_http_body = response_json_bytes + response_binary_body
|
||||
|
||||
return Response(
|
||||
content=output_http_body,
|
||||
status_code=200,
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Inference-Header-Content-Length": str(len(response_json_bytes))
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"CRITICAL TRANSLATION EXCEPTION: {traceback.format_exc()}")
|
||||
return Response(
|
||||
content=f"Internal gRPC Pipeline Multiplex Error: {str(e)}",
|
||||
status_code=502
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# THE UNIFIED SERVICE FUNCTION (1 Container, 1 GPU, 1 Triton Process)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
@app.function(
|
||||
gpu="T4", # for the expense
|
||||
timeout=3600,
|
||||
max_containers=3, # Strict production capping
|
||||
min_containers=1, # for keeping warm and prevention,
|
||||
buffer_containers=2, # Number of additional idle containers to maintain under active load.
|
||||
scaledown_window=30, # Max time (in seconds) a container can remain idle while scaling down.
|
||||
secrets=[modal.Secret.from_name("aws-secrets")]
|
||||
)
|
||||
@modal.asgi_app()
|
||||
def unified_triton_server():
|
||||
print("🚀 Booting ONE Triton Instance inside ONE A100 Container...")
|
||||
|
||||
# Spawns Triton in the background. It will automatically read
|
||||
# your "aws-secrets" environment keys to mount s3://vkist-ml-model/
|
||||
cmd = ["tritonserver", "--model-repository=s3://vkist-ml-model/"]
|
||||
subprocess.Popen(cmd)
|
||||
|
||||
print("📋 Triton background process delegated. Handing routing control over to FastAPI.")
|
||||
|
||||
# Returns immediately! FastAPI now takes over the container lifecycle
|
||||
return web_app
|
||||
@@ -0,0 +1 @@
|
||||
modal deploy PILOT_PROJECT/workspace/sprint_1_2/codebase/infra/implementation/triton_run/modal_triton.py
|
||||
@@ -0,0 +1,41 @@
|
||||
# Infrastructure Specification
|
||||
|
||||
## Purpose
|
||||
Provides platform-level infrastructure services including network routing, high availability, reverse proxy, and foundational resource management to ensure secure, reliable, and observable operation of the VKIST MSK system.
|
||||
|
||||
## Owner
|
||||
Platform Engineering Team
|
||||
|
||||
## Boundary
|
||||
- NGINX reverse proxy (TLS termination, request routing, rate limiting)
|
||||
- Keepalived for VRRP-based high availability and failover
|
||||
- Shared PostgreSQL and Redis instances (coordinated with Data room for provisioning)
|
||||
- System-level monitoring, logging, and alerting foundations
|
||||
- Network segmentation, firewall rules, and VPN/gateway configuration
|
||||
- Infrastructure-as-code (Terraform) for provisioning and lifecycle management
|
||||
|
||||
## Internal Design
|
||||
- NGINX configured as ingress controller with SSL/TLS termination, path-based routing to backend services, and WebSocket support for real-time features.
|
||||
- Keepalived deployed in active-passive mode across cluster nodes, assigning a virtual IP (VIP) for seamless failover.
|
||||
- PostgreSQL and Redis instances are provisioned and managed via Terraform; connection details are exposed as environment variables to consuming rooms.
|
||||
- Foundational logging: structured JSON logs shipped to centralized observability stack (outside scope of this spec).
|
||||
- Security: network policies restrict inter-room communication to declared interfaces; NGINX enforces authentication headers and rate limits.
|
||||
- Observability: exposes Prometheus metrics endpoints; health checks for liveness and readiness.
|
||||
|
||||
## Interface Contract
|
||||
See `infra/spec/interface-contract.md`.
|
||||
|
||||
## Consumers
|
||||
- All rooms (frontend, backend, ml, data, knowledge) consume networking and availability guarantees.
|
||||
- Backend and ML rooms consume reverse proxy for external API exposure.
|
||||
- Data room consumes shared storage provisioning (Postgres, Redis) for stateful services.
|
||||
|
||||
## Breaking-change Policy
|
||||
See `infra/spec/interface-contract.md`.
|
||||
|
||||
## References
|
||||
- NFR-2 (System Availability ≥99.9% Monthly)
|
||||
- NFR-8 (Network Latency ≤50ms Inter-Region)
|
||||
- NFR-12 (Infrastructure as Code & Immutable Deployments)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Sections 3.1-3.4)
|
||||
- DATA_ENGINEERING_SPEC.md (Section 2 for storage provisioning)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Infrastructure Interface Contract
|
||||
|
||||
## Purpose
|
||||
Provides platform-level infrastructure services including network routing, high availability, reverse proxy, and foundational resource management to ensure secure, reliable, and observable operation of the VKIST MSK system.
|
||||
|
||||
## Owner
|
||||
Platform Engineering Team
|
||||
|
||||
## Provides
|
||||
- network routing and security (NGINX reverse proxy with TLS termination, request routing, rate limiting, WAF)
|
||||
- high availability and failover (Keepalived VRRP, virtual IP, health checks)
|
||||
- foundational resource provisioning (PostgreSQL and Redis connection details via environment variables)
|
||||
- infrastructure observability (Prometheus metrics endpoints, health check endpoints)
|
||||
- foundational logging and monitoring (structured logs, alerting foundations)
|
||||
|
||||
## Consumes
|
||||
- (none) – Infra room provides foundational services; it does not consume other rooms' interfaces for its core purpose.
|
||||
Note: Infra relies on underlying cloud/provider services (VMs, networking, storage) which are outside the scope of this interface contract.
|
||||
|
||||
## Consumers
|
||||
- frontend: consumes network routing and availability for accessing the application.
|
||||
- backend: consumes reverse proxy for external API exposure, HA for service continuity.
|
||||
- ml: consumes reverse proxy for model serving endpoints (Triton), HA for inference reliability.
|
||||
- data: consumes foundational resource provisioning (Postgres, Redis) for stateful services; consumes HA for storage durability.
|
||||
- knowledge: consumes reverse proxy for external access to knowledge services (if exposed), HA for service durability.
|
||||
|
||||
## Not Directly Consumable
|
||||
- internal NGINX configuration details (upstreams, SSL certificates)
|
||||
- Keepalived VRRP configuration and scripts
|
||||
- Terraform state and provider specifics
|
||||
- underlying VM/hardware details
|
||||
|
||||
## Breaking-change Policy
|
||||
- Changes to provided network endpoints (e.g., port, path prefixes) require version bump and backward compatibility period of one release.
|
||||
- Deprecation of any provided interface will be communicated with at least one release notice.
|
||||
- Resource provisioning interface (environment variable names) is considered stable; changes will be backward compatible where possible.
|
||||
|
||||
## References
|
||||
- NFR-2 (System Availability ≥99.9% Monthly)
|
||||
- NFR-8 (Network Latency ≤50ms Inter-Region)
|
||||
- NFR-12 (Infrastructure as Code & Immutable Deployments)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Sections 3.1-3.4)
|
||||
- DATA_ENGINEERING_SPEC.md (Section 2 for storage provisioning)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Knowledge Stack Interface Contract
|
||||
|
||||
## Purpose
|
||||
Provides semantic and graph-based retrieval augmented generation (RAG) for clinical guideline explanations, evidence arbitration, and diagnostic reasoning using vector embeddings (Qdrant) and ontology relationships (ladybugDB) with LLM grounding.
|
||||
|
||||
## Owner
|
||||
Knowledge Engineering Team
|
||||
|
||||
## Provides
|
||||
- guideline embeddings and vector similarity search (Qdrant)
|
||||
- ontology relationships and graph traversal (ladybugDB)
|
||||
- grounded explanation generation (retrieval + LLM + grounding)
|
||||
- evidence arbitration and belief propagation for conflicting evidence
|
||||
- knowledge versioning and temporal validity tracking
|
||||
- hallucination detection and policy enforcement
|
||||
|
||||
## Consumes
|
||||
- (none) – Knowledge stack provides foundational AI services; it does not consume other rooms' interfaces for its core purpose.
|
||||
Note: Knowledge consumes internal storage (Qdrant, ladybugDB) and embedding/LLM services which are part of its boundary.
|
||||
|
||||
## Consumers
|
||||
- frontend: consumes grounded explanations for UI display via `frontend:guideline-spec`.
|
||||
- backend: consumes explanation generation for analysis reporting via `backend:api-spec`.
|
||||
- ml: consumes model activation explanations via `ml:engine-spec`.
|
||||
|
||||
## Not Directly Consumable
|
||||
- internal Qdrant collection names, vector dimensions, and indexing parameters
|
||||
- ladybugDB schema details (predicate names, ontology version)
|
||||
- embedding model specifics (model ID, tensor shapes)
|
||||
- LLM prompt templates and decoding parameters
|
||||
- knowledge curation pipeline details (source ingestion, validation)
|
||||
|
||||
## Breaking-change Policy
|
||||
- Knowledge schema versioning via semantic versioning (MAJOR.MINOR.PATCH)
|
||||
- Embedding model changes: require MAJOR version if dimension or architecture changes
|
||||
- Ontology updates: backward compatible additions (MINOR), breaking changes require MAJOR
|
||||
- LLM interface changes: versioned endpoints with deprecation windows
|
||||
- Knowledge consumers must validate compatibility with new versions
|
||||
- Deprecation notices for breaking changes 60 days in advance
|
||||
- Automated migration tools for knowledge base version upgrades
|
||||
|
||||
## References
|
||||
- NFR-3 (Explanation Latency ≤2s @ 95th percentile)
|
||||
- NFR-6 (Guideline Coverage ≥95% of common synovitis queries)
|
||||
- NFR-10 (Explanation Factuality Score ≥0.9)
|
||||
- UC-25776 (Generate Grounded Explanation for Analysis)
|
||||
- UC-65473 (Resolve Conflicting Evidence)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Section 3.3)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Section 4.3)
|
||||
- GUIDELINE_SOURCES.md (Appendix B)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Knowledge Stack Specification
|
||||
|
||||
## Purpose
|
||||
Provides semantic and graph-based retrieval augmented generation (RAG) for clinical guideline explanations, evidence arbitration, and diagnostic reasoning using vector embeddings (Qdrant) and ontology relationships (ladybugDB) with LLM grounding.
|
||||
|
||||
## Owner
|
||||
Knowledge Engineering Team
|
||||
|
||||
## Boundary
|
||||
Qdrant vector database instances, ladybugDB graph database instances, embedding model servers, LLM inference endpoints, knowledge curation pipelines, and validation/verification workflows.
|
||||
|
||||
## Internal Design
|
||||
- Hybrid knowledge architecture: vector similarity search + graph traversal
|
||||
- Qdrant: stores guideline section embeddings (BioClinicalBERT, PubMedBERT) with payload metadata
|
||||
- ladybugDB: stores ontology concepts (SNOMED-CT, LOINC, RadLex) and relational axioms
|
||||
- EmbeddingGemma: generates 768-dimension vectors for text chunks
|
||||
- PhoGPT/MedGPT: LLM for answer generation with constrained decoding
|
||||
- Retrieval pipeline: hybrid search (vector + BM25) → graph expansion → reranking
|
||||
- Grounding module: verifies LLM outputs against source guidelines with citation extraction
|
||||
- Arbitration engine: resolves conflicting evidence using belief propagation
|
||||
- Continuous integration: automated guideline ingestion from trusted sources (NIH, CDC, radiology societies)
|
||||
- Versioned knowledge bases with temporal validity tracking
|
||||
- Monitoring: retrieval relevance, grounding accuracy, latency SLOs
|
||||
|
||||
## Interface Contract
|
||||
See `bento/knowledge/spec/interface-contract.md`.
|
||||
|
||||
## Consumers
|
||||
- frontend:guideline-spec (for displaying grounded explanations in UI)
|
||||
- backend:api-spec (for analysis explanation generation)
|
||||
- ml:engine-spec (for generating model activation explanations)
|
||||
|
||||
## Breaking-change Policy
|
||||
- Knowledge schema versioning via semantic versioning
|
||||
- Embedding model changes: require MAJOR version if dimension or architecture changes
|
||||
- Ontology updates: backward compatible additions (MINOR), breaking changes require MAJOR
|
||||
- LLM interface changes: versioned endpoints with deprecation windows
|
||||
- Knowledge consumers must validate compatibility with new versions
|
||||
- Deprecation notices for breaking changes 60 days in advance
|
||||
- Automated migration tools for knowledge base version upgrades
|
||||
|
||||
## References
|
||||
- NFR-3 (Explanation Latency ≤2s @ 95th percentile)
|
||||
- NFR-6 (Guideline Coverage ≥95% of common synovitis queries)
|
||||
- NFR-10 (Explanation Factuality Score ≥0.9)
|
||||
- UC-25776 (Generate Grounded Explanation for Analysis)
|
||||
- UC-65473 (Resolve Conflicting Evidence)
|
||||
- SOLUTION_ARCHITECTURE_SPEC.md (Section 3.3)
|
||||
- SOFTWARE_SYSTEM_DESIGN_FR_25.md (Section 4.3)
|
||||
- GUIDELINE_SOURCES.md (Appendix B)
|
||||
@@ -0,0 +1,232 @@
|
||||
import torch.nn as nn
|
||||
import torch
|
||||
import timm
|
||||
import torch.nn.functional as F
|
||||
from torchinfo import summary
|
||||
from torch.nn import Softmax, Parameter
|
||||
|
||||
|
||||
def convblock(in_channels,out_channels,kernel_size=3,stride=1,dilation=1,padding=1):
|
||||
return nn.Sequential(
|
||||
nn.Conv2d(in_channels=in_channels,out_channels=out_channels,kernel_size=kernel_size,stride=stride,dilation=dilation,padding=padding),
|
||||
nn.BatchNorm2d(out_channels),
|
||||
nn.ReLU()
|
||||
)
|
||||
|
||||
|
||||
class DecoderBlock(nn.Module):
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
skip_channels,
|
||||
out_channels,):
|
||||
super().__init__()
|
||||
self.conv1 = nn.Sequential(
|
||||
convblock(in_channels=in_channels + skip_channels,out_channels=out_channels,kernel_size=1,padding=0),
|
||||
convblock(in_channels=out_channels,out_channels=out_channels,kernel_size=3,padding=1)
|
||||
)
|
||||
self.conv2 = convblock(in_channels=out_channels,out_channels=out_channels,kernel_size=3,padding=1)
|
||||
|
||||
def forward(self,x,skip=None):
|
||||
x = F.interpolate(x, scale_factor=2, mode="bilinear")
|
||||
if skip is not None:
|
||||
x = torch.cat([x, skip], dim=1)
|
||||
x = self.conv1(x)
|
||||
x = self.conv2(x)
|
||||
return x
|
||||
|
||||
class ASPP_module(nn.Module):
|
||||
def __init__(self, inplanes, planes, rate):
|
||||
super(ASPP_module, self).__init__()
|
||||
if rate == 1:
|
||||
kernel_size = 1
|
||||
padding = 0
|
||||
else:
|
||||
kernel_size = 3
|
||||
padding = rate
|
||||
self.atrous_convolution = nn.Conv2d(inplanes, planes, kernel_size=kernel_size,
|
||||
stride=1, padding=padding, dilation=rate, bias=False)
|
||||
self.bn = nn.BatchNorm2d(planes)
|
||||
self.relu = nn.ReLU()
|
||||
|
||||
self._init_weight()
|
||||
|
||||
def forward(self, x):
|
||||
x = self.atrous_convolution(x)
|
||||
x = self.bn(x)
|
||||
|
||||
return self.relu(x)
|
||||
|
||||
def _init_weight(self):
|
||||
for m in self.modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
torch.nn.init.kaiming_normal_(m.weight)
|
||||
elif isinstance(m, nn.BatchNorm2d):
|
||||
m.weight.data.fill_(1)
|
||||
m.bias.data.zero_()
|
||||
|
||||
class CAM_Module(nn.Module):
|
||||
def __init__(self):
|
||||
super(CAM_Module, self).__init__()
|
||||
self.gamma = Parameter(torch.zeros(1))
|
||||
self.softmax = Softmax(dim=-1)
|
||||
|
||||
def forward(self, x, fb):
|
||||
batch_size, chnnels, width, height = x.shape
|
||||
proj_query = fb.view(batch_size, chnnels, -1)
|
||||
proj_key = fb.view(batch_size, chnnels, -1).permute(0, 2, 1)
|
||||
energy = torch.bmm(proj_query, proj_key)
|
||||
energy_new = torch.max(energy, -1, keepdim=True)[0].expand_as(energy) - energy
|
||||
attention = self.softmax(energy_new)
|
||||
proj_value = fb.view(batch_size, chnnels, -1)
|
||||
|
||||
out = torch.bmm(attention, proj_value)
|
||||
out = out.view(batch_size, chnnels, height, width)
|
||||
|
||||
return x + self.gamma * out
|
||||
|
||||
def INF(B, H, W):
|
||||
return -torch.diag(torch.tensor(float("inf")).to('cuda').repeat(H), 0).unsqueeze(0).repeat(B * W, 1, 1)
|
||||
|
||||
class S_Module(nn.Module):
|
||||
def __init__(self, in_dim):
|
||||
super(S_Module, self).__init__()
|
||||
self.query_conv = nn.Conv2d(in_channels=in_dim, out_channels=in_dim // 8, kernel_size=1)
|
||||
self.key_conv = nn.Conv2d(in_channels=in_dim, out_channels=in_dim // 8, kernel_size=1)
|
||||
self.value_conv = nn.Conv2d(in_channels=in_dim, out_channels=in_dim, kernel_size=1)
|
||||
self.softmax = Softmax(dim=3)
|
||||
self.INF = INF
|
||||
self.gamma = nn.Parameter(torch.zeros(1))
|
||||
|
||||
def forward(self, x):
|
||||
m_batchsize, _, height, width = x.size()
|
||||
proj_query = self.query_conv(x)
|
||||
proj_query_H = proj_query.permute(0, 3, 1, 2).contiguous().view(m_batchsize * width, -1, height).permute(0, 2,
|
||||
1)
|
||||
proj_query_W = proj_query.permute(0, 2, 1, 3).contiguous().view(m_batchsize * height, -1, width).permute(0, 2,
|
||||
1)
|
||||
proj_key = self.key_conv(x)
|
||||
proj_key_H = proj_key.permute(0, 3, 1, 2).contiguous().view(m_batchsize * width, -1, height)
|
||||
proj_key_W = proj_key.permute(0, 2, 1, 3).contiguous().view(m_batchsize * height, -1, width)
|
||||
proj_value = self.value_conv(x) #D
|
||||
proj_value_H = proj_value.permute(0, 3, 1, 2).contiguous().view(m_batchsize * width, -1, height)
|
||||
proj_value_W = proj_value.permute(0, 2, 1, 3).contiguous().view(m_batchsize * height, -1, width)
|
||||
energy_H = (torch.bmm(proj_query_H, proj_key_H) + self.INF(m_batchsize, height, width)).view(m_batchsize, width,
|
||||
height,
|
||||
height).permute(0,
|
||||
2,
|
||||
1,
|
||||
3)
|
||||
energy_W = torch.bmm(proj_query_W, proj_key_W).view(m_batchsize, height, width, width)
|
||||
concate = self.softmax(torch.cat([energy_H, energy_W], 3))
|
||||
|
||||
self.pau_attention = concate
|
||||
att_H = concate[:, :, :, 0:height].permute(0, 2, 1, 3).contiguous().view(m_batchsize * width, height, height)
|
||||
att_W = concate[:, :, :, height:height + width].contiguous().view(m_batchsize * height, width, width)
|
||||
out_H = torch.bmm(proj_value_H, att_H.permute(0, 2, 1)).view(m_batchsize, width, -1, height).permute(0, 2, 3, 1)
|
||||
out_W = torch.bmm(proj_value_W, att_W.permute(0, 2, 1)).view(m_batchsize, height, -1, width).permute(0, 2, 1, 3)
|
||||
|
||||
return self.gamma * (out_H + out_W) + x
|
||||
|
||||
class FeedbackSpatialAttention(nn.Module):
|
||||
def __init__(self, in_channel,feedback=False) :
|
||||
super().__init__()
|
||||
self.x_att = S_Module(in_dim=in_channel)
|
||||
self.fb_att = S_Module(in_dim=in_channel)
|
||||
self.feedback = feedback
|
||||
self.gamma = nn.Parameter(torch.zeros(1))
|
||||
self.gamma2 = nn.Parameter(torch.zeros(1))
|
||||
|
||||
def forward(self,x,fb=None):
|
||||
x_att = self.gamma*self.x_att(x)
|
||||
if fb!=None:
|
||||
fb_att = self.fb_att(fb)
|
||||
x_att = x_att + self.gamma2*fb_att
|
||||
|
||||
output = x + x_att
|
||||
return output
|
||||
|
||||
class StageAttentionwCAM(nn.Module):
|
||||
def __init__(self,in_channel,out_channel,cbam=False):
|
||||
super().__init__()
|
||||
self.down = nn.MaxPool2d(kernel_size=2,stride=2)
|
||||
self.oneconv = convblock(in_channels=in_channel,out_channels=out_channel,kernel_size=1,padding=0)
|
||||
self.pam = FeedbackSpatialAttention(in_channel=out_channel)
|
||||
self.cam = CAM_Module()
|
||||
|
||||
def forward(self,x,fb=None):
|
||||
if fb!=None:
|
||||
fb = self.down(fb)
|
||||
fb = self.oneconv(fb)
|
||||
out = self.pam(x,fb)
|
||||
out2 = self.cam(x,fb)
|
||||
return out+out2
|
||||
|
||||
class EfficientFeedbackNetwork(nn.Module):
|
||||
def __init__(self, in_channels=3,num_class=3,feedback=False):
|
||||
super().__init__()
|
||||
self.encoder = timm.create_model(model_name='efficientnet_b0',pretrained=True,features_only=True)
|
||||
channel_size = [24,40,112,320]
|
||||
skip_channel = [16,24,40,112]
|
||||
out_channel = [16,24,40,112]
|
||||
|
||||
sa_input = [num_class,16,24,40,112]
|
||||
sa_out = [16,24,40,112,320]
|
||||
|
||||
blocks = [
|
||||
DecoderBlock(in_ch, skip_ch, out_ch)
|
||||
for in_ch, skip_ch, out_ch in zip(channel_size, skip_channel, out_channel)
|
||||
]
|
||||
self.decoder = nn.ModuleList(blocks)
|
||||
attblocks = [
|
||||
StageAttentionwCAM(in_channel=in_c,out_channel=out_c) for in_c,out_c in zip(sa_input,sa_out)
|
||||
]
|
||||
self.attblocks = nn.ModuleList(attblocks)
|
||||
|
||||
rates = [2, 4, 6, 8]
|
||||
self.aspp1 = ASPP_module(320, 100, rate=rates[0])
|
||||
self.aspp2 = ASPP_module(320, 100, rate=rates[1])
|
||||
self.aspp3 = ASPP_module(320, 100, rate=rates[2])
|
||||
self.aspp4 = ASPP_module(320, 100, rate=rates[3])
|
||||
self.global_avg_pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)),
|
||||
nn.Conv2d(320, 100, 1, stride=1, bias=False),
|
||||
nn.BatchNorm2d(100),
|
||||
nn.ReLU(),
|
||||
)
|
||||
self.ref_aspp = nn.Conv2d(500, 320, 1, bias=False)
|
||||
|
||||
self.head = nn.Sequential(
|
||||
nn.Upsample(scale_factor=2,mode='bilinear',align_corners=True),
|
||||
nn.Conv2d(in_channels=16,out_channels=num_class,kernel_size=1)
|
||||
)
|
||||
self.feedback = feedback
|
||||
if feedback:
|
||||
strides = [2,4,8,16]
|
||||
|
||||
def forward(self,x,fb=None):
|
||||
encoder = self.encoder(x)
|
||||
if fb!=None:
|
||||
for i in range(len(encoder)-1):
|
||||
if i==0:
|
||||
encoder[i] = self.attblocks[i](encoder[i],fb)
|
||||
else:
|
||||
encoder[i] = self.attblocks[i](encoder[i],encoder[i-1])
|
||||
aspp1 = self.aspp1(encoder[-1])
|
||||
aspp2 = self.aspp2(encoder[-1])
|
||||
aspp3 = self.aspp3(encoder[-1])
|
||||
aspp4 = self.aspp4(encoder[-1])
|
||||
aspp5 = self.global_avg_pool(encoder[-1])
|
||||
aspp5 = F.interpolate(aspp5, size=aspp4.size()[2:], mode='bilinear', align_corners=True)
|
||||
aspp_all = torch.cat([aspp1,aspp2,aspp3,aspp4,aspp5],dim=1)
|
||||
|
||||
dec0 = self.ref_aspp(aspp_all)
|
||||
if fb!=None:
|
||||
dec0 = self.attblocks[-1](dec0,encoder[-2])
|
||||
for i in range(len(encoder)-2,-1,-1):
|
||||
dec0 = self.decoder[i](dec0,encoder[i])
|
||||
head = self.head(dec0)
|
||||
return head
|
||||
|
||||
|
||||
# if __name__=='__main__':
|
||||
# model = EfficientFeedbackNetwork(num_class=2)
|
||||
# summary(model,((1,3,512,512),(1,2,512,512)))
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from .build_sam import (
|
||||
build_sam,
|
||||
build_sam_vit_h,
|
||||
build_sam_vit_l,
|
||||
build_sam_vit_b,
|
||||
sam_model_registry,
|
||||
)
|
||||
from .predictor import SamPredictor
|
||||
from .automatic_mask_generator import SamAutomaticMaskGenerator
|
||||
@@ -0,0 +1,383 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torchvision.ops.boxes import batched_nms, box_area # type: ignore
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from .modeling import Sam
|
||||
from .predictor import SamPredictor
|
||||
from .utils.amg import (
|
||||
MaskData,
|
||||
area_from_rle,
|
||||
batch_iterator,
|
||||
batched_mask_to_box,
|
||||
box_xyxy_to_xywh,
|
||||
build_all_layer_point_grids,
|
||||
calculate_stability_score,
|
||||
coco_encode_rle,
|
||||
generate_crop_boxes,
|
||||
is_box_near_crop_edge,
|
||||
mask_to_rle_pytorch,
|
||||
remove_small_regions,
|
||||
rle_to_mask,
|
||||
uncrop_boxes_xyxy,
|
||||
uncrop_masks,
|
||||
uncrop_points,
|
||||
)
|
||||
|
||||
|
||||
class SamAutomaticMaskGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
model: Sam,
|
||||
points_per_side: Optional[int] = 32,
|
||||
points_per_batch: int = 64,
|
||||
pred_iou_thresh: float = 0.88,
|
||||
stability_score_thresh: float = 0.95,
|
||||
stability_score_offset: float = 1.0,
|
||||
box_nms_thresh: float = 0.7,
|
||||
crop_n_layers: int = 0,
|
||||
crop_nms_thresh: float = 0.7,
|
||||
crop_overlap_ratio: float = 512 / 1500,
|
||||
crop_n_points_downscale_factor: int = 1,
|
||||
point_grids: Optional[List[np.ndarray]] = None,
|
||||
min_mask_region_area: int = 0,
|
||||
output_mode: str = "binary_mask",
|
||||
) -> None:
|
||||
"""
|
||||
Using a SAM model, generates masks for the entire image.
|
||||
Generates a grid of point prompts over the image, then filters
|
||||
low quality and duplicate masks. The default settings are chosen
|
||||
for SAM with a ViT-H backbone.
|
||||
|
||||
Arguments:
|
||||
model (Sam): The SAM model to use for mask prediction.
|
||||
points_per_side (int or None): The number of points to be sampled
|
||||
along one side of the image. The total number of points is
|
||||
points_per_side**2. If None, 'point_grids' must provide explicit
|
||||
point sampling.
|
||||
points_per_batch (int): Sets the number of points run simultaneously
|
||||
by the model. Higher numbers may be faster but use more GPU memory.
|
||||
pred_iou_thresh (float): A filtering threshold in [0,1], using the
|
||||
model's predicted mask quality.
|
||||
stability_score_thresh (float): A filtering threshold in [0,1], using
|
||||
the stability of the mask under changes to the cutoff used to binarize
|
||||
the model's mask predictions.
|
||||
stability_score_offset (float): The amount to shift the cutoff when
|
||||
calculated the stability score.
|
||||
box_nms_thresh (float): The box IoU cutoff used by non-maximal
|
||||
suppression to filter duplicate masks.
|
||||
crop_n_layers (int): If >0, mask prediction will be run again on
|
||||
crops of the image. Sets the number of layers to run, where each
|
||||
layer has 2**i_layer number of image crops.
|
||||
crop_nms_thresh (float): The box IoU cutoff used by non-maximal
|
||||
suppression to filter duplicate masks between different crops.
|
||||
crop_overlap_ratio (float): Sets the degree to which crops overlap.
|
||||
In the first crop layer, crops will overlap by this fraction of
|
||||
the image length. Later layers with more crops scale down this overlap.
|
||||
crop_n_points_downscale_factor (int): The number of points-per-side
|
||||
sampled in layer n is scaled down by crop_n_points_downscale_factor**n.
|
||||
point_grids (list(np.ndarray) or None): A list over explicit grids
|
||||
of points used for sampling, normalized to [0,1]. The nth grid in the
|
||||
list is used in the nth crop layer. Exclusive with points_per_side.
|
||||
min_mask_region_area (int): If >0, postprocessing will be applied
|
||||
to remove disconnected regions and holes in masks with area smaller
|
||||
than min_mask_region_area. Requires opencv.
|
||||
output_mode (str): The form masks are returned in. Can be 'binary_mask',
|
||||
'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools.
|
||||
For large resolutions, 'binary_mask' may consume large amounts of
|
||||
memory.
|
||||
"""
|
||||
|
||||
assert (points_per_side is None) != (
|
||||
point_grids is None
|
||||
), "Exactly one of points_per_side or point_grid must be provided."
|
||||
if points_per_side is not None:
|
||||
self.point_grids = build_all_layer_point_grids(
|
||||
points_per_side,
|
||||
crop_n_layers,
|
||||
crop_n_points_downscale_factor,
|
||||
)
|
||||
elif point_grids is not None:
|
||||
self.point_grids = point_grids
|
||||
else:
|
||||
raise ValueError("Can't have both points_per_side and point_grid be None.")
|
||||
|
||||
assert output_mode in [
|
||||
"binary_mask",
|
||||
"uncompressed_rle",
|
||||
"coco_rle",
|
||||
], f"Unknown output_mode {output_mode}."
|
||||
if output_mode == "coco_rle":
|
||||
from pycocotools import mask as mask_utils # type: ignore # noqa: F401
|
||||
|
||||
if min_mask_region_area > 0:
|
||||
import cv2 # type: ignore # noqa: F401
|
||||
|
||||
self.predictor = SamPredictor(model)
|
||||
self.points_per_batch = points_per_batch
|
||||
self.pred_iou_thresh = pred_iou_thresh
|
||||
self.stability_score_thresh = stability_score_thresh
|
||||
self.stability_score_offset = stability_score_offset
|
||||
self.box_nms_thresh = box_nms_thresh
|
||||
self.crop_n_layers = crop_n_layers
|
||||
self.crop_nms_thresh = crop_nms_thresh
|
||||
self.crop_overlap_ratio = crop_overlap_ratio
|
||||
self.crop_n_points_downscale_factor = crop_n_points_downscale_factor
|
||||
self.min_mask_region_area = min_mask_region_area
|
||||
self.output_mode = output_mode
|
||||
|
||||
@torch.no_grad()
|
||||
def generate(self, image: np.ndarray) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generates masks for the given image.
|
||||
|
||||
Arguments:
|
||||
image (np.ndarray): The image to generate masks for, in HWC uint8 format.
|
||||
|
||||
Returns:
|
||||
list(dict(str, any)): A list over records for masks. Each record is
|
||||
a dict containing the following keys:
|
||||
segmentation (dict(str, any) or np.ndarray): The mask. If
|
||||
output_mode='binary_mask', is an array of shape HW. Otherwise,
|
||||
is a dictionary containing the RLE.
|
||||
bbox (list(float)): The box around the mask, in XYWH format.
|
||||
area (int): The area in pixels of the mask.
|
||||
predicted_iou (float): The model's own prediction of the mask's
|
||||
quality. This is filtered by the pred_iou_thresh parameter.
|
||||
point_coords (list(list(float))): The point coordinates input
|
||||
to the model to generate this mask.
|
||||
stability_score (float): A measure of the mask's quality. This
|
||||
is filtered on using the stability_score_thresh parameter.
|
||||
crop_box (list(float)): The crop of the image used to generate
|
||||
the mask, given in XYWH format.
|
||||
"""
|
||||
|
||||
# Generate masks
|
||||
mask_data = self._generate_masks(image)
|
||||
|
||||
# Filter small disconnected regions and holes in masks
|
||||
if self.min_mask_region_area > 0:
|
||||
mask_data = self.postprocess_small_regions(
|
||||
mask_data,
|
||||
self.min_mask_region_area,
|
||||
max(self.box_nms_thresh, self.crop_nms_thresh),
|
||||
)
|
||||
|
||||
# Encode masks
|
||||
if self.output_mode == "coco_rle":
|
||||
mask_data["segmentations"] = [
|
||||
coco_encode_rle(rle) for rle in mask_data["rles"]
|
||||
]
|
||||
elif self.output_mode == "binary_mask":
|
||||
mask_data["segmentations"] = [rle_to_mask(rle) for rle in mask_data["rles"]]
|
||||
else:
|
||||
mask_data["segmentations"] = mask_data["rles"]
|
||||
|
||||
# Write mask records
|
||||
curr_anns = []
|
||||
for idx in range(len(mask_data["segmentations"])):
|
||||
ann = {
|
||||
"segmentation": mask_data["segmentations"][idx],
|
||||
"area": area_from_rle(mask_data["rles"][idx]),
|
||||
"bbox": box_xyxy_to_xywh(mask_data["boxes"][idx]).tolist(),
|
||||
"predicted_iou": mask_data["iou_preds"][idx].item(),
|
||||
"point_coords": [mask_data["points"][idx].tolist()],
|
||||
"stability_score": mask_data["stability_score"][idx].item(),
|
||||
"crop_box": box_xyxy_to_xywh(mask_data["crop_boxes"][idx]).tolist(),
|
||||
}
|
||||
curr_anns.append(ann)
|
||||
|
||||
return curr_anns
|
||||
|
||||
def _generate_masks(self, image: np.ndarray) -> MaskData:
|
||||
orig_size = image.shape[:2]
|
||||
crop_boxes, layer_idxs = generate_crop_boxes(
|
||||
orig_size, self.crop_n_layers, self.crop_overlap_ratio
|
||||
)
|
||||
|
||||
# Iterate over image crops
|
||||
data = MaskData()
|
||||
for crop_box, layer_idx in zip(crop_boxes, layer_idxs):
|
||||
crop_data = self._process_crop(image, crop_box, layer_idx, orig_size)
|
||||
data.cat(crop_data)
|
||||
|
||||
# Remove duplicate masks between crops
|
||||
if len(crop_boxes) > 1:
|
||||
# Prefer masks from smaller crops
|
||||
scores = 1 / box_area(data["crop_boxes"])
|
||||
scores = scores.to(data["boxes"].device)
|
||||
keep_by_nms = batched_nms(
|
||||
data["boxes"].float(),
|
||||
scores,
|
||||
torch.zeros_like(data["boxes"][:, 0]), # categories
|
||||
iou_threshold=self.crop_nms_thresh,
|
||||
)
|
||||
data.filter(keep_by_nms)
|
||||
|
||||
data.to_numpy()
|
||||
return data
|
||||
|
||||
def _process_crop(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
crop_box: List[int],
|
||||
crop_layer_idx: int,
|
||||
orig_size: Tuple[int, ...],
|
||||
) -> MaskData:
|
||||
# Crop the image and calculate embeddings
|
||||
x0, y0, x1, y1 = crop_box
|
||||
cropped_im = image[y0:y1, x0:x1, :]
|
||||
cropped_im_size = cropped_im.shape[:2]
|
||||
self.predictor.set_image(cropped_im)
|
||||
|
||||
# Get points for this crop
|
||||
points_scale = np.array(cropped_im_size)[None, ::-1]
|
||||
points_for_image = self.point_grids[crop_layer_idx] * points_scale
|
||||
|
||||
# Generate masks for this crop in batches
|
||||
data = MaskData()
|
||||
for (points,) in batch_iterator(self.points_per_batch, points_for_image):
|
||||
batch_data = self._process_batch(
|
||||
points, cropped_im_size, crop_box, orig_size
|
||||
)
|
||||
data.cat(batch_data)
|
||||
del batch_data
|
||||
self.predictor.reset_image()
|
||||
|
||||
# Remove duplicates within this crop.
|
||||
keep_by_nms = batched_nms(
|
||||
data["boxes"].float(),
|
||||
data["iou_preds"],
|
||||
torch.zeros_like(data["boxes"][:, 0]), # categories
|
||||
iou_threshold=self.box_nms_thresh,
|
||||
)
|
||||
data.filter(keep_by_nms)
|
||||
|
||||
# Return to the original image frame
|
||||
data["boxes"] = uncrop_boxes_xyxy(data["boxes"], crop_box)
|
||||
data["points"] = uncrop_points(data["points"], crop_box)
|
||||
data["crop_boxes"] = torch.tensor([crop_box for _ in range(len(data["rles"]))])
|
||||
|
||||
return data
|
||||
|
||||
def _process_batch(
|
||||
self,
|
||||
points: np.ndarray,
|
||||
im_size: Tuple[int, ...],
|
||||
crop_box: List[int],
|
||||
orig_size: Tuple[int, ...],
|
||||
) -> MaskData:
|
||||
orig_h, orig_w = orig_size
|
||||
|
||||
# Run model on this batch
|
||||
transformed_points = self.predictor.transform.apply_coords(points, im_size)
|
||||
in_points = torch.as_tensor(transformed_points, device=self.predictor.device)
|
||||
in_labels = torch.ones(
|
||||
in_points.shape[0], dtype=torch.int, device=in_points.device
|
||||
)
|
||||
masks, iou_preds, _ = self.predictor.predict_torch(
|
||||
in_points[:, None, :],
|
||||
in_labels[:, None],
|
||||
multimask_output=True,
|
||||
return_logits=True,
|
||||
)
|
||||
|
||||
# Serialize predictions and store in MaskData
|
||||
data = MaskData(
|
||||
masks=masks.flatten(0, 1),
|
||||
iou_preds=iou_preds.flatten(0, 1),
|
||||
points=torch.as_tensor(points.repeat(masks.shape[1], axis=0)),
|
||||
)
|
||||
del masks
|
||||
|
||||
# Filter by predicted IoU
|
||||
if self.pred_iou_thresh > 0.0:
|
||||
keep_mask = data["iou_preds"] > self.pred_iou_thresh
|
||||
data.filter(keep_mask)
|
||||
|
||||
# Calculate stability score
|
||||
data["stability_score"] = calculate_stability_score(
|
||||
data["masks"],
|
||||
self.predictor.model.mask_threshold,
|
||||
self.stability_score_offset,
|
||||
)
|
||||
if self.stability_score_thresh > 0.0:
|
||||
keep_mask = data["stability_score"] >= self.stability_score_thresh
|
||||
data.filter(keep_mask)
|
||||
|
||||
# Threshold masks and calculate boxes
|
||||
data["masks"] = data["masks"] > self.predictor.model.mask_threshold
|
||||
data["boxes"] = batched_mask_to_box(data["masks"])
|
||||
|
||||
# Filter boxes that touch crop boundaries
|
||||
keep_mask = ~is_box_near_crop_edge(
|
||||
data["boxes"], crop_box, [0, 0, orig_w, orig_h]
|
||||
)
|
||||
if not torch.all(keep_mask):
|
||||
data.filter(keep_mask)
|
||||
|
||||
# Compress to RLE
|
||||
data["masks"] = uncrop_masks(data["masks"], crop_box, orig_h, orig_w)
|
||||
data["rles"] = mask_to_rle_pytorch(data["masks"])
|
||||
del data["masks"]
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def postprocess_small_regions(
|
||||
mask_data: MaskData, min_area: int, nms_thresh: float
|
||||
) -> MaskData:
|
||||
"""
|
||||
Removes small disconnected regions and holes in masks, then reruns
|
||||
box NMS to remove any new duplicates.
|
||||
|
||||
Edits mask_data in place.
|
||||
|
||||
Requires open-cv as a dependency.
|
||||
"""
|
||||
if len(mask_data["rles"]) == 0:
|
||||
return mask_data
|
||||
|
||||
# Filter small disconnected regions and holes
|
||||
new_masks = []
|
||||
scores = []
|
||||
for rle in mask_data["rles"]:
|
||||
mask = rle_to_mask(rle)
|
||||
|
||||
mask, changed = remove_small_regions(mask, min_area, mode="holes")
|
||||
unchanged = not changed
|
||||
mask, changed = remove_small_regions(mask, min_area, mode="islands")
|
||||
unchanged = unchanged and not changed
|
||||
|
||||
new_masks.append(torch.as_tensor(mask).unsqueeze(0))
|
||||
# Give score=0 to changed masks and score=1 to unchanged masks
|
||||
# so NMS will prefer ones that didn't need postprocessing
|
||||
scores.append(float(unchanged))
|
||||
|
||||
# Recalculate boxes and remove any new duplicates
|
||||
masks = torch.cat(new_masks, dim=0)
|
||||
boxes = batched_mask_to_box(masks)
|
||||
keep_by_nms = batched_nms(
|
||||
boxes.float(),
|
||||
torch.as_tensor(scores),
|
||||
torch.zeros_like(boxes[:, 0]), # categories
|
||||
iou_threshold=nms_thresh,
|
||||
)
|
||||
|
||||
# Only recalculate RLEs for masks that have changed
|
||||
for i_mask in keep_by_nms:
|
||||
if scores[i_mask] == 0.0:
|
||||
mask_torch = masks[i_mask].unsqueeze(0)
|
||||
mask_data["rles"][i_mask] = mask_to_rle_pytorch(mask_torch)[0]
|
||||
mask_data["boxes"][i_mask] = boxes[i_mask] # update res directly
|
||||
mask_data.filter(keep_by_nms)
|
||||
|
||||
return mask_data
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
import torch
|
||||
|
||||
from .modeling import (
|
||||
ImageEncoderViT,
|
||||
MaskDecoder,
|
||||
PromptEncoder,
|
||||
Sam,
|
||||
TwoWayTransformer,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_h(checkpoint=None):
|
||||
return _build_sam(
|
||||
encoder_embed_dim=1280,
|
||||
encoder_depth=32,
|
||||
encoder_num_heads=16,
|
||||
encoder_global_attn_indexes=[7, 15, 23, 31],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
build_sam = build_sam_vit_h
|
||||
|
||||
|
||||
def build_sam_vit_l(checkpoint=None):
|
||||
return _build_sam(
|
||||
encoder_embed_dim=1024,
|
||||
encoder_depth=24,
|
||||
encoder_num_heads=16,
|
||||
encoder_global_attn_indexes=[5, 11, 17, 23],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_b(checkpoint=None):
|
||||
return _build_sam(
|
||||
encoder_embed_dim=768,
|
||||
encoder_depth=12,
|
||||
encoder_num_heads=12,
|
||||
encoder_global_attn_indexes=[2, 5, 8, 11],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
sam_model_registry = {
|
||||
"default": build_sam_vit_h,
|
||||
"vit_h": build_sam_vit_h,
|
||||
"vit_l": build_sam_vit_l,
|
||||
"vit_b": build_sam_vit_b,
|
||||
}
|
||||
|
||||
|
||||
def _build_sam(
|
||||
encoder_embed_dim,
|
||||
encoder_depth,
|
||||
encoder_num_heads,
|
||||
encoder_global_attn_indexes,
|
||||
checkpoint=None,
|
||||
):
|
||||
prompt_embed_dim = 256
|
||||
image_size = 1024
|
||||
vit_patch_size = 16
|
||||
image_embedding_size = image_size // vit_patch_size
|
||||
sam = Sam(
|
||||
image_encoder=ImageEncoderViT(
|
||||
depth=encoder_depth,
|
||||
embed_dim=encoder_embed_dim,
|
||||
img_size=image_size,
|
||||
mlp_ratio=4,
|
||||
norm_layer=partial(torch.nn.LayerNorm, eps=1e-6),
|
||||
num_heads=encoder_num_heads,
|
||||
patch_size=vit_patch_size,
|
||||
qkv_bias=True,
|
||||
use_rel_pos=True,
|
||||
global_attn_indexes=encoder_global_attn_indexes,
|
||||
window_size=14,
|
||||
out_chans=prompt_embed_dim,
|
||||
),
|
||||
prompt_encoder=PromptEncoder(
|
||||
embed_dim=prompt_embed_dim,
|
||||
image_embedding_size=(image_embedding_size, image_embedding_size),
|
||||
input_image_size=(image_size, image_size),
|
||||
mask_in_chans=16,
|
||||
),
|
||||
mask_decoder=MaskDecoder(
|
||||
num_multimask_outputs=3,
|
||||
transformer=TwoWayTransformer(
|
||||
depth=2,
|
||||
embedding_dim=prompt_embed_dim,
|
||||
mlp_dim=2048,
|
||||
num_heads=8,
|
||||
),
|
||||
transformer_dim=prompt_embed_dim,
|
||||
iou_head_depth=3,
|
||||
iou_head_hidden_dim=256,
|
||||
),
|
||||
pixel_mean=[123.675, 116.28, 103.53],
|
||||
pixel_std=[58.395, 57.12, 57.375],
|
||||
)
|
||||
sam.eval()
|
||||
checkpoint = Path(checkpoint)
|
||||
if checkpoint.name == "sam_vit_b_01ec64.pth" and not checkpoint.exists():
|
||||
cmd = input("Download sam_vit_b_01ec64.pth from facebook AI? [y]/n: ")
|
||||
if len(cmd) == 0 or cmd.lower() == "y":
|
||||
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
||||
print("Downloading SAM ViT-B checkpoint...")
|
||||
urllib.request.urlretrieve(
|
||||
"https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
|
||||
checkpoint,
|
||||
)
|
||||
print(checkpoint.name, " is downloaded!")
|
||||
elif checkpoint.name == "sam_vit_h_4b8939.pth" and not checkpoint.exists():
|
||||
cmd = input("Download sam_vit_h_4b8939.pth from facebook AI? [y]/n: ")
|
||||
if len(cmd) == 0 or cmd.lower() == "y":
|
||||
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
||||
print("Downloading SAM ViT-H checkpoint...")
|
||||
urllib.request.urlretrieve(
|
||||
"https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
|
||||
checkpoint,
|
||||
)
|
||||
print(checkpoint.name, " is downloaded!")
|
||||
elif checkpoint.name == "sam_vit_l_0b3195.pth" and not checkpoint.exists():
|
||||
cmd = input("Download sam_vit_l_0b3195.pth from facebook AI? [y]/n: ")
|
||||
if len(cmd) == 0 or cmd.lower() == "y":
|
||||
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
||||
print("Downloading SAM ViT-L checkpoint...")
|
||||
urllib.request.urlretrieve(
|
||||
"https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
|
||||
checkpoint,
|
||||
)
|
||||
print(checkpoint.name, " is downloaded!")
|
||||
|
||||
if checkpoint is not None:
|
||||
with open(checkpoint, "rb") as f:
|
||||
# state_dict = torch.load(f["model"], map_location=torch.device('cpu'))
|
||||
checkpoint = torch.load(f, map_location=torch.device('cpu'))
|
||||
state_dict = checkpoint["model"]
|
||||
|
||||
sam.load_state_dict(state_dict)
|
||||
return sam
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from .sam import Sam
|
||||
from .image_encoder import ImageEncoderViT
|
||||
from .mask_decoder import MaskDecoder
|
||||
from .prompt_encoder import PromptEncoder
|
||||
from .transformer import TwoWayTransformer
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from typing import Type
|
||||
|
||||
|
||||
class MLPBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
mlp_dim: int,
|
||||
act: Type[nn.Module] = nn.GELU,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.lin1 = nn.Linear(embedding_dim, mlp_dim)
|
||||
self.lin2 = nn.Linear(mlp_dim, embedding_dim)
|
||||
self.act = act()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.lin2(self.act(self.lin1(x)))
|
||||
|
||||
|
||||
# From https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/batch_norm.py # noqa
|
||||
# Itself from https://github.com/facebookresearch/ConvNeXt/blob/d1fa8f6fef0a165b27399986cc2bdacc92777e40/models/convnext.py#L119 # noqa
|
||||
class LayerNorm2d(nn.Module):
|
||||
def __init__(self, num_channels: int, eps: float = 1e-6) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(num_channels))
|
||||
self.bias = nn.Parameter(torch.zeros(num_channels))
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
u = x.mean(1, keepdim=True)
|
||||
s = (x - u).pow(2).mean(1, keepdim=True)
|
||||
x = (x - u) / torch.sqrt(s + self.eps)
|
||||
x = self.weight[:, None, None] * x + self.bias[:, None, None]
|
||||
return x
|
||||
@@ -0,0 +1,420 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from typing import Optional, Tuple, Type
|
||||
|
||||
from .common import LayerNorm2d, MLPBlock
|
||||
|
||||
|
||||
# This class and its supporting functions below lightly adapted from the ViTDet backbone available at: https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/vit.py # noqa
|
||||
class ImageEncoderViT(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
img_size: int = 1024,
|
||||
patch_size: int = 16,
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
depth: int = 12,
|
||||
num_heads: int = 12,
|
||||
mlp_ratio: float = 4.0,
|
||||
out_chans: int = 256,
|
||||
qkv_bias: bool = True,
|
||||
norm_layer: Type[nn.Module] = nn.LayerNorm,
|
||||
act_layer: Type[nn.Module] = nn.GELU,
|
||||
use_abs_pos: bool = True,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 0,
|
||||
global_attn_indexes: Tuple[int, ...] = (),
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
img_size (int): Input image size.
|
||||
patch_size (int): Patch size.
|
||||
in_chans (int): Number of input image channels.
|
||||
embed_dim (int): Patch embedding dimension.
|
||||
depth (int): Depth of ViT.
|
||||
num_heads (int): Number of attention heads in each ViT block.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
norm_layer (nn.Module): Normalization layer.
|
||||
act_layer (nn.Module): Activation layer.
|
||||
use_abs_pos (bool): If True, use absolute positional embeddings.
|
||||
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
window_size (int): Window size for window attention blocks.
|
||||
global_attn_indexes (list): Indexes for blocks using global attention.
|
||||
"""
|
||||
super().__init__()
|
||||
self.img_size = img_size
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
kernel_size=(patch_size, patch_size),
|
||||
stride=(patch_size, patch_size),
|
||||
in_chans=in_chans,
|
||||
embed_dim=embed_dim,
|
||||
)
|
||||
|
||||
self.pos_embed: Optional[nn.Parameter] = None
|
||||
if use_abs_pos:
|
||||
# Initialize absolute positional embedding with pretrain image size.
|
||||
self.pos_embed = nn.Parameter(
|
||||
torch.zeros(
|
||||
1, img_size // patch_size, img_size // patch_size, embed_dim
|
||||
)
|
||||
)
|
||||
|
||||
self.blocks = nn.ModuleList()
|
||||
for i in range(depth):
|
||||
block = Block(
|
||||
dim=embed_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_ratio=mlp_ratio,
|
||||
qkv_bias=qkv_bias,
|
||||
norm_layer=norm_layer,
|
||||
act_layer=act_layer,
|
||||
use_rel_pos=use_rel_pos,
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
window_size=window_size if i not in global_attn_indexes else 0,
|
||||
input_size=(img_size // patch_size, img_size // patch_size),
|
||||
)
|
||||
self.blocks.append(block)
|
||||
|
||||
self.neck = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
embed_dim,
|
||||
out_chans,
|
||||
kernel_size=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(out_chans),
|
||||
nn.Conv2d(
|
||||
out_chans,
|
||||
out_chans,
|
||||
kernel_size=3,
|
||||
padding=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(out_chans),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = self.patch_embed(x)
|
||||
if self.pos_embed is not None:
|
||||
x = x + self.pos_embed
|
||||
|
||||
for blk in self.blocks:
|
||||
x = blk(x)
|
||||
|
||||
x = self.neck(x.permute(0, 3, 1, 2))
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class Block(nn.Module):
|
||||
"""Transformer blocks with support of window attention and residual propagation blocks"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
qkv_bias: bool = True,
|
||||
norm_layer: Type[nn.Module] = nn.LayerNorm,
|
||||
act_layer: Type[nn.Module] = nn.GELU,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 0,
|
||||
input_size: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
num_heads (int): Number of attention heads in each ViT block.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
norm_layer (nn.Module): Normalization layer.
|
||||
act_layer (nn.Module): Activation layer.
|
||||
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
window_size (int): Window size for window attention blocks. If it equals 0, then
|
||||
use global attention.
|
||||
input_size (tuple(int, int) or None): Input resolution for calculating the relative
|
||||
positional parameter size.
|
||||
"""
|
||||
super().__init__()
|
||||
self.norm1 = norm_layer(dim)
|
||||
self.attn = Attention(
|
||||
dim,
|
||||
num_heads=num_heads,
|
||||
qkv_bias=qkv_bias,
|
||||
use_rel_pos=use_rel_pos,
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
input_size=input_size if window_size == 0 else (window_size, window_size),
|
||||
)
|
||||
|
||||
self.norm2 = norm_layer(dim)
|
||||
self.mlp = MLPBlock(
|
||||
embedding_dim=dim, mlp_dim=int(dim * mlp_ratio), act=act_layer
|
||||
)
|
||||
|
||||
self.window_size = window_size
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
shortcut = x
|
||||
x = self.norm1(x)
|
||||
# Window partition
|
||||
if self.window_size > 0:
|
||||
H, W = x.shape[1], x.shape[2]
|
||||
x, pad_hw = window_partition(x, self.window_size)
|
||||
|
||||
x = self.attn(x)
|
||||
# Reverse window partition
|
||||
if self.window_size > 0:
|
||||
x = window_unpartition(x, self.window_size, pad_hw, (H, W))
|
||||
|
||||
x = shortcut + x
|
||||
x = x + self.mlp(self.norm2(x))
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
"""Multi-head Attention block with relative position embeddings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int = 8,
|
||||
qkv_bias: bool = True,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
input_size: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
num_heads (int): Number of attention heads.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
input_size (tuple(int, int) or None): Input resolution for calculating the relative
|
||||
positional parameter size.
|
||||
"""
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
head_dim = dim // num_heads
|
||||
self.scale = head_dim**-0.5
|
||||
|
||||
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
||||
self.proj = nn.Linear(dim, dim)
|
||||
|
||||
self.use_rel_pos = use_rel_pos
|
||||
if self.use_rel_pos:
|
||||
assert (
|
||||
input_size is not None
|
||||
), "Input size must be provided if using relative positional encoding."
|
||||
# initialize relative positional embeddings
|
||||
self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, head_dim))
|
||||
self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, head_dim))
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
B, H, W, _ = x.shape
|
||||
# qkv with shape (3, B, nHead, H * W, C)
|
||||
qkv = (
|
||||
self.qkv(x).reshape(B, H * W, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4)
|
||||
)
|
||||
# q, k, v with shape (B * nHead, H * W, C)
|
||||
q, k, v = qkv.reshape(3, B * self.num_heads, H * W, -1).unbind(0)
|
||||
|
||||
attn = (q * self.scale) @ k.transpose(-2, -1)
|
||||
|
||||
if self.use_rel_pos:
|
||||
attn = add_decomposed_rel_pos(
|
||||
attn, q, self.rel_pos_h, self.rel_pos_w, (H, W), (H, W)
|
||||
)
|
||||
|
||||
attn = attn.softmax(dim=-1)
|
||||
x = (
|
||||
(attn @ v)
|
||||
.view(B, self.num_heads, H, W, -1)
|
||||
.permute(0, 2, 3, 1, 4)
|
||||
.reshape(B, H, W, -1)
|
||||
)
|
||||
x = self.proj(x)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def window_partition(
|
||||
x: torch.Tensor, window_size: int
|
||||
) -> Tuple[torch.Tensor, Tuple[int, int]]:
|
||||
"""
|
||||
Partition into non-overlapping windows with padding if needed.
|
||||
Args:
|
||||
x (tensor): input tokens with [B, H, W, C].
|
||||
window_size (int): window size.
|
||||
|
||||
Returns:
|
||||
windows: windows after partition with [B * num_windows, window_size, window_size, C].
|
||||
(Hp, Wp): padded height and width before partition
|
||||
"""
|
||||
B, H, W, C = x.shape
|
||||
|
||||
pad_h = (window_size - H % window_size) % window_size
|
||||
pad_w = (window_size - W % window_size) % window_size
|
||||
if pad_h > 0 or pad_w > 0:
|
||||
x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h))
|
||||
Hp, Wp = H + pad_h, W + pad_w
|
||||
|
||||
x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C)
|
||||
windows = (
|
||||
x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
|
||||
)
|
||||
return windows, (Hp, Wp)
|
||||
|
||||
|
||||
def window_unpartition(
|
||||
windows: torch.Tensor,
|
||||
window_size: int,
|
||||
pad_hw: Tuple[int, int],
|
||||
hw: Tuple[int, int],
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Window unpartition into original sequences and removing padding.
|
||||
Args:
|
||||
windows (tensor): input tokens with [B * num_windows, window_size, window_size, C].
|
||||
window_size (int): window size.
|
||||
pad_hw (Tuple): padded height and width (Hp, Wp).
|
||||
hw (Tuple): original height and width (H, W) before padding.
|
||||
|
||||
Returns:
|
||||
x: unpartitioned sequences with [B, H, W, C].
|
||||
"""
|
||||
Hp, Wp = pad_hw
|
||||
H, W = hw
|
||||
B = windows.shape[0] // (Hp * Wp // window_size // window_size)
|
||||
x = windows.view(
|
||||
B, Hp // window_size, Wp // window_size, window_size, window_size, -1
|
||||
)
|
||||
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1)
|
||||
|
||||
if Hp > H or Wp > W:
|
||||
x = x[:, :H, :W, :].contiguous()
|
||||
return x
|
||||
|
||||
|
||||
def get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Get relative positional embeddings according to the relative positions of
|
||||
query and key sizes.
|
||||
Args:
|
||||
q_size (int): size of query q.
|
||||
k_size (int): size of key k.
|
||||
rel_pos (Tensor): relative position embeddings (L, C).
|
||||
|
||||
Returns:
|
||||
Extracted positional embeddings according to relative positions.
|
||||
"""
|
||||
max_rel_dist = int(2 * max(q_size, k_size) - 1)
|
||||
# Interpolate rel pos if needed.
|
||||
if rel_pos.shape[0] != max_rel_dist:
|
||||
# Interpolate rel pos.
|
||||
rel_pos_resized = F.interpolate(
|
||||
rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1),
|
||||
size=max_rel_dist,
|
||||
mode="linear",
|
||||
)
|
||||
rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0)
|
||||
else:
|
||||
rel_pos_resized = rel_pos
|
||||
|
||||
# Scale the coords with short length if shapes for q and k are different.
|
||||
q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0)
|
||||
k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0)
|
||||
relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0)
|
||||
|
||||
return rel_pos_resized[relative_coords.long()]
|
||||
|
||||
|
||||
def add_decomposed_rel_pos(
|
||||
attn: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
rel_pos_h: torch.Tensor,
|
||||
rel_pos_w: torch.Tensor,
|
||||
q_size: Tuple[int, int],
|
||||
k_size: Tuple[int, int],
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Calculate decomposed Relative Positional Embeddings from :paper:`mvitv2`.
|
||||
https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py # noqa B950
|
||||
Args:
|
||||
attn (Tensor): attention map.
|
||||
q (Tensor): query q in the attention layer with shape (B, q_h * q_w, C).
|
||||
rel_pos_h (Tensor): relative position embeddings (Lh, C) for height axis.
|
||||
rel_pos_w (Tensor): relative position embeddings (Lw, C) for width axis.
|
||||
q_size (Tuple): spatial sequence size of query q with (q_h, q_w).
|
||||
k_size (Tuple): spatial sequence size of key k with (k_h, k_w).
|
||||
|
||||
Returns:
|
||||
attn (Tensor): attention map with added relative positional embeddings.
|
||||
"""
|
||||
q_h, q_w = q_size
|
||||
k_h, k_w = k_size
|
||||
Rh = get_rel_pos(q_h, k_h, rel_pos_h)
|
||||
Rw = get_rel_pos(q_w, k_w, rel_pos_w)
|
||||
|
||||
B, _, dim = q.shape
|
||||
r_q = q.reshape(B, q_h, q_w, dim)
|
||||
rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh)
|
||||
rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw)
|
||||
|
||||
attn = (
|
||||
attn.view(B, q_h, q_w, k_h, k_w)
|
||||
+ rel_h[:, :, :, :, None]
|
||||
+ rel_w[:, :, :, None, :]
|
||||
).view(B, q_h * q_w, k_h * k_w)
|
||||
|
||||
return attn
|
||||
|
||||
|
||||
class PatchEmbed(nn.Module):
|
||||
"""
|
||||
Image to Patch Embedding.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
kernel_size: Tuple[int, int] = (16, 16),
|
||||
stride: Tuple[int, int] = (16, 16),
|
||||
padding: Tuple[int, int] = (0, 0),
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
kernel_size (Tuple): kernel size of the projection layer.
|
||||
stride (Tuple): stride of the projection layer.
|
||||
padding (Tuple): padding size of the projection layer.
|
||||
in_chans (int): Number of input image channels.
|
||||
embed_dim (int): Patch embedding dimension.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.proj = nn.Conv2d(
|
||||
in_chans, embed_dim, kernel_size=kernel_size, stride=stride, padding=padding
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = self.proj(x)
|
||||
# B C H W -> B H W C
|
||||
x = x.permute(0, 2, 3, 1)
|
||||
return x
|
||||
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from typing import List, Tuple, Type
|
||||
|
||||
from .common import LayerNorm2d
|
||||
|
||||
|
||||
class MaskDecoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
transformer_dim: int,
|
||||
transformer: nn.Module,
|
||||
num_multimask_outputs: int = 3,
|
||||
activation: Type[nn.Module] = nn.GELU,
|
||||
iou_head_depth: int = 3,
|
||||
iou_head_hidden_dim: int = 256,
|
||||
) -> None:
|
||||
"""
|
||||
Predicts masks given an image and prompt embeddings, using a
|
||||
transformer architecture.
|
||||
|
||||
Arguments:
|
||||
transformer_dim (int): the channel dimension of the transformer
|
||||
transformer (nn.Module): the transformer used to predict masks
|
||||
num_multimask_outputs (int): the number of masks to predict
|
||||
when disambiguating masks
|
||||
activation (nn.Module): the type of activation to use when
|
||||
upscaling masks
|
||||
iou_head_depth (int): the depth of the MLP used to predict
|
||||
mask quality
|
||||
iou_head_hidden_dim (int): the hidden dimension of the MLP
|
||||
used to predict mask quality
|
||||
"""
|
||||
super().__init__()
|
||||
self.transformer_dim = transformer_dim
|
||||
self.transformer = transformer
|
||||
|
||||
self.num_multimask_outputs = num_multimask_outputs
|
||||
|
||||
self.iou_token = nn.Embedding(1, transformer_dim)
|
||||
self.num_mask_tokens = num_multimask_outputs + 1
|
||||
self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim)
|
||||
|
||||
self.output_upscaling = nn.Sequential(
|
||||
nn.ConvTranspose2d(
|
||||
transformer_dim, transformer_dim // 4, kernel_size=2, stride=2
|
||||
),
|
||||
LayerNorm2d(transformer_dim // 4),
|
||||
activation(),
|
||||
nn.ConvTranspose2d(
|
||||
transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2
|
||||
),
|
||||
activation(),
|
||||
)
|
||||
self.output_hypernetworks_mlps = nn.ModuleList(
|
||||
[
|
||||
MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3)
|
||||
for i in range(self.num_mask_tokens)
|
||||
]
|
||||
)
|
||||
|
||||
self.iou_prediction_head = MLP(
|
||||
transformer_dim, iou_head_hidden_dim, self.num_mask_tokens, iou_head_depth
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
multimask_output: bool,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Predict masks given image and prompt embeddings.
|
||||
|
||||
Arguments:
|
||||
image_embeddings (torch.Tensor): the embeddings from the image encoder
|
||||
image_pe (torch.Tensor): positional encoding with the shape of image_embeddings
|
||||
sparse_prompt_embeddings (torch.Tensor): the embeddings of the points and boxes
|
||||
dense_prompt_embeddings (torch.Tensor): the embeddings of the mask inputs
|
||||
multimask_output (bool): Whether to return multiple masks or a single
|
||||
mask.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: batched predicted masks
|
||||
torch.Tensor: batched predictions of mask quality
|
||||
"""
|
||||
masks, iou_pred = self.predict_masks(
|
||||
image_embeddings=image_embeddings,
|
||||
image_pe=image_pe,
|
||||
sparse_prompt_embeddings=sparse_prompt_embeddings,
|
||||
dense_prompt_embeddings=dense_prompt_embeddings,
|
||||
)
|
||||
|
||||
# Select the correct mask or masks for output
|
||||
if multimask_output:
|
||||
mask_slice = slice(1, None)
|
||||
else:
|
||||
mask_slice = slice(0, 1)
|
||||
masks = masks[:, mask_slice, :, :]
|
||||
iou_pred = iou_pred[:, mask_slice]
|
||||
|
||||
# Prepare output
|
||||
return masks, iou_pred
|
||||
|
||||
def predict_masks(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Predicts masks. See 'forward' for more details."""
|
||||
# Concatenate output tokens
|
||||
output_tokens = torch.cat(
|
||||
[self.iou_token.weight, self.mask_tokens.weight], dim=0
|
||||
)
|
||||
output_tokens = output_tokens.unsqueeze(0).expand(
|
||||
sparse_prompt_embeddings.size(0), -1, -1
|
||||
)
|
||||
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)
|
||||
|
||||
# Expand per-image data in batch direction to be per-mask
|
||||
if image_embeddings.shape[0] != tokens.shape[0]:
|
||||
src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
|
||||
else:
|
||||
src = image_embeddings
|
||||
src = src + dense_prompt_embeddings
|
||||
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)
|
||||
b, c, h, w = src.shape
|
||||
|
||||
# Run the transformer
|
||||
hs, src = self.transformer(src, pos_src, tokens)
|
||||
iou_token_out = hs[:, 0, :]
|
||||
mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :]
|
||||
|
||||
# Upscale mask embeddings and predict masks using the mask tokens
|
||||
src = src.transpose(1, 2).view(b, c, h, w)
|
||||
upscaled_embedding = self.output_upscaling(src)
|
||||
hyper_in_list: List[torch.Tensor] = []
|
||||
for i in range(self.num_mask_tokens):
|
||||
hyper_in_list.append(
|
||||
self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :])
|
||||
)
|
||||
hyper_in = torch.stack(hyper_in_list, dim=1)
|
||||
b, c, h, w = upscaled_embedding.shape
|
||||
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
|
||||
|
||||
# Generate mask quality predictions
|
||||
iou_pred = self.iou_prediction_head(iou_token_out)
|
||||
|
||||
return masks, iou_pred
|
||||
|
||||
|
||||
# Lightly adapted from
|
||||
# https://github.com/facebookresearch/MaskFormer/blob/main/mask_former/modeling/transformer/transformer_predictor.py # noqa
|
||||
class MLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int,
|
||||
hidden_dim: int,
|
||||
output_dim: int,
|
||||
num_layers: int,
|
||||
sigmoid_output: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.num_layers = num_layers
|
||||
h = [hidden_dim] * (num_layers - 1)
|
||||
self.layers = nn.ModuleList(
|
||||
nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim])
|
||||
)
|
||||
self.sigmoid_output = sigmoid_output
|
||||
|
||||
def forward(self, x):
|
||||
for i, layer in enumerate(self.layers):
|
||||
x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x)
|
||||
if self.sigmoid_output:
|
||||
x = F.sigmoid(x)
|
||||
return x
|
||||
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from typing import Any, Optional, Tuple, Type
|
||||
|
||||
from .common import LayerNorm2d
|
||||
|
||||
|
||||
class PromptEncoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim: int,
|
||||
image_embedding_size: Tuple[int, int],
|
||||
input_image_size: Tuple[int, int],
|
||||
mask_in_chans: int,
|
||||
activation: Type[nn.Module] = nn.GELU,
|
||||
) -> None:
|
||||
"""
|
||||
Encodes prompts for input to SAM's mask decoder.
|
||||
|
||||
Arguments:
|
||||
embed_dim (int): The prompts' embedding dimension
|
||||
image_embedding_size (tuple(int, int)): The spatial size of the
|
||||
image embedding, as (H, W).
|
||||
input_image_size (int): The padded size of the image as input
|
||||
to the image encoder, as (H, W).
|
||||
mask_in_chans (int): The number of hidden channels used for
|
||||
encoding input masks.
|
||||
activation (nn.Module): The activation to use when encoding
|
||||
input masks.
|
||||
"""
|
||||
super().__init__()
|
||||
self.embed_dim = embed_dim
|
||||
self.input_image_size = input_image_size
|
||||
self.image_embedding_size = image_embedding_size
|
||||
self.pe_layer = PositionEmbeddingRandom(embed_dim // 2)
|
||||
|
||||
self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners
|
||||
point_embeddings = [
|
||||
nn.Embedding(1, embed_dim) for i in range(self.num_point_embeddings)
|
||||
]
|
||||
self.point_embeddings = nn.ModuleList(point_embeddings)
|
||||
self.not_a_point_embed = nn.Embedding(1, embed_dim)
|
||||
|
||||
self.mask_input_size = (
|
||||
4 * image_embedding_size[0],
|
||||
4 * image_embedding_size[1],
|
||||
)
|
||||
self.mask_downscaling = nn.Sequential(
|
||||
nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2),
|
||||
LayerNorm2d(mask_in_chans // 4),
|
||||
activation(),
|
||||
nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2),
|
||||
LayerNorm2d(mask_in_chans),
|
||||
activation(),
|
||||
nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1),
|
||||
)
|
||||
self.no_mask_embed = nn.Embedding(1, embed_dim)
|
||||
|
||||
def get_dense_pe(self) -> torch.Tensor:
|
||||
"""
|
||||
Returns the positional encoding used to encode point prompts,
|
||||
applied to a dense set of points the shape of the image encoding.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Positional encoding with shape
|
||||
1x(embed_dim)x(embedding_h)x(embedding_w)
|
||||
"""
|
||||
return self.pe_layer(self.image_embedding_size).unsqueeze(0)
|
||||
|
||||
def _embed_points(
|
||||
self,
|
||||
points: torch.Tensor,
|
||||
labels: torch.Tensor,
|
||||
pad: bool,
|
||||
) -> torch.Tensor:
|
||||
"""Embeds point prompts."""
|
||||
points = points + 0.5 # Shift to center of pixel
|
||||
if pad:
|
||||
padding_point = torch.zeros((points.shape[0], 1, 2), device=points.device)
|
||||
padding_label = -torch.ones((labels.shape[0], 1), device=labels.device)
|
||||
points = torch.cat([points, padding_point], dim=1)
|
||||
labels = torch.cat([labels, padding_label], dim=1)
|
||||
point_embedding = self.pe_layer.forward_with_coords(
|
||||
points, self.input_image_size
|
||||
)
|
||||
point_embedding[labels == -1] = 0.0
|
||||
point_embedding[labels == -1] += self.not_a_point_embed.weight
|
||||
point_embedding[labels == 0] += self.point_embeddings[0].weight
|
||||
point_embedding[labels == 1] += self.point_embeddings[1].weight
|
||||
return point_embedding
|
||||
|
||||
def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor:
|
||||
"""Embeds box prompts."""
|
||||
boxes = boxes + 0.5 # Shift to center of pixel
|
||||
coords = boxes.reshape(-1, 2, 2)
|
||||
corner_embedding = self.pe_layer.forward_with_coords(
|
||||
coords, self.input_image_size
|
||||
)
|
||||
corner_embedding[:, 0, :] += self.point_embeddings[2].weight
|
||||
corner_embedding[:, 1, :] += self.point_embeddings[3].weight
|
||||
return corner_embedding
|
||||
|
||||
def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor:
|
||||
"""Embeds mask inputs."""
|
||||
mask_embedding = self.mask_downscaling(masks)
|
||||
return mask_embedding
|
||||
|
||||
def _get_batch_size(
|
||||
self,
|
||||
points: Optional[Tuple[torch.Tensor, torch.Tensor]],
|
||||
boxes: Optional[torch.Tensor],
|
||||
masks: Optional[torch.Tensor],
|
||||
) -> int:
|
||||
"""
|
||||
Gets the batch size of the output given the batch size of the input prompts.
|
||||
"""
|
||||
if points is not None:
|
||||
return points[0].shape[0]
|
||||
elif boxes is not None:
|
||||
return boxes.shape[0]
|
||||
elif masks is not None:
|
||||
return masks.shape[0]
|
||||
else:
|
||||
return 1
|
||||
|
||||
def _get_device(self) -> torch.device:
|
||||
return self.point_embeddings[0].weight.device
|
||||
|
||||
def forward(
|
||||
self,
|
||||
points: Optional[Tuple[torch.Tensor, torch.Tensor]],
|
||||
boxes: Optional[torch.Tensor],
|
||||
masks: Optional[torch.Tensor],
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Embeds different types of prompts, returning both sparse and dense
|
||||
embeddings.
|
||||
|
||||
Arguments:
|
||||
points (tuple(torch.Tensor, torch.Tensor) or none): point coordinates
|
||||
and labels to embed.
|
||||
boxes (torch.Tensor or none): boxes to embed
|
||||
masks (torch.Tensor or none): masks to embed
|
||||
|
||||
Returns:
|
||||
torch.Tensor: sparse embeddings for the points and boxes, with shape
|
||||
BxNx(embed_dim), where N is determined by the number of input points
|
||||
and boxes.
|
||||
torch.Tensor: dense embeddings for the masks, in the shape
|
||||
Bx(embed_dim)x(embed_H)x(embed_W)
|
||||
"""
|
||||
bs = self._get_batch_size(points, boxes, masks)
|
||||
sparse_embeddings = torch.empty(
|
||||
(bs, 0, self.embed_dim), device=self._get_device()
|
||||
)
|
||||
if points is not None:
|
||||
coords, labels = points
|
||||
point_embeddings = self._embed_points(coords, labels, pad=(boxes is None))
|
||||
sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1)
|
||||
if boxes is not None:
|
||||
box_embeddings = self._embed_boxes(boxes)
|
||||
sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1)
|
||||
|
||||
if masks is not None:
|
||||
dense_embeddings = self._embed_masks(masks)
|
||||
else:
|
||||
dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand(
|
||||
bs, -1, self.image_embedding_size[0], self.image_embedding_size[1]
|
||||
)
|
||||
|
||||
return sparse_embeddings, dense_embeddings
|
||||
|
||||
|
||||
class PositionEmbeddingRandom(nn.Module):
|
||||
"""
|
||||
Positional encoding using random spatial frequencies.
|
||||
"""
|
||||
|
||||
def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None:
|
||||
super().__init__()
|
||||
if scale is None or scale <= 0.0:
|
||||
scale = 1.0
|
||||
self.register_buffer(
|
||||
"positional_encoding_gaussian_matrix",
|
||||
scale * torch.randn((2, num_pos_feats)),
|
||||
)
|
||||
|
||||
def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor:
|
||||
"""Positionally encode points that are normalized to [0,1]."""
|
||||
# assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape
|
||||
coords = 2 * coords - 1
|
||||
coords = coords @ self.positional_encoding_gaussian_matrix
|
||||
coords = 2 * np.pi * coords
|
||||
# outputs d_1 x ... x d_n x C shape
|
||||
return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1)
|
||||
|
||||
def forward(self, size: Tuple[int, int]) -> torch.Tensor:
|
||||
"""Generate positional encoding for a grid of the specified size."""
|
||||
h, w = size
|
||||
device: Any = self.positional_encoding_gaussian_matrix.device
|
||||
grid = torch.ones((h, w), device=device, dtype=torch.float32)
|
||||
y_embed = grid.cumsum(dim=0) - 0.5
|
||||
x_embed = grid.cumsum(dim=1) - 0.5
|
||||
y_embed = y_embed / h
|
||||
x_embed = x_embed / w
|
||||
|
||||
pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1))
|
||||
return pe.permute(2, 0, 1) # C x H x W
|
||||
|
||||
def forward_with_coords(
|
||||
self, coords_input: torch.Tensor, image_size: Tuple[int, int]
|
||||
) -> torch.Tensor:
|
||||
"""Positionally encode points that are not normalized to [0,1]."""
|
||||
coords = coords_input.clone()
|
||||
coords[:, :, 0] = coords[:, :, 0] / image_size[1]
|
||||
coords[:, :, 1] = coords[:, :, 1] / image_size[0]
|
||||
return self._pe_encoding(coords.to(torch.float)) # B x N x C
|
||||
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from .image_encoder import ImageEncoderViT
|
||||
from .mask_decoder import MaskDecoder
|
||||
from .prompt_encoder import PromptEncoder
|
||||
|
||||
|
||||
class Sam(nn.Module):
|
||||
mask_threshold: float = 0.0
|
||||
image_format: str = "RGB"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image_encoder: ImageEncoderViT,
|
||||
prompt_encoder: PromptEncoder,
|
||||
mask_decoder: MaskDecoder,
|
||||
pixel_mean: List[float] = [123.675, 116.28, 103.53],
|
||||
pixel_std: List[float] = [58.395, 57.12, 57.375],
|
||||
) -> None:
|
||||
"""
|
||||
SAM predicts object masks from an image and input prompts.
|
||||
|
||||
Arguments:
|
||||
image_encoder (ImageEncoderViT): The backbone used to encode the
|
||||
image into image embeddings that allow for efficient mask prediction.
|
||||
prompt_encoder (PromptEncoder): Encodes various types of input prompts.
|
||||
mask_decoder (MaskDecoder): Predicts masks from the image embeddings
|
||||
and encoded prompts.
|
||||
pixel_mean (list(float)): Mean values for normalizing pixels in the input image.
|
||||
pixel_std (list(float)): Std values for normalizing pixels in the input image.
|
||||
"""
|
||||
super().__init__()
|
||||
self.image_encoder = image_encoder
|
||||
self.prompt_encoder = prompt_encoder
|
||||
self.mask_decoder = mask_decoder
|
||||
self.register_buffer(
|
||||
"pixel_mean", torch.Tensor(pixel_mean).view(-1, 1, 1), False
|
||||
)
|
||||
self.register_buffer("pixel_std", torch.Tensor(pixel_std).view(-1, 1, 1), False)
|
||||
|
||||
@property
|
||||
def device(self) -> Any:
|
||||
return self.pixel_mean.device
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
batched_input: List[Dict[str, Any]],
|
||||
multimask_output: bool,
|
||||
) -> List[Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Predicts masks end-to-end from provided images and prompts.
|
||||
If prompts are not known in advance, using SamPredictor is
|
||||
recommended over calling the model directly.
|
||||
|
||||
Arguments:
|
||||
batched_input (list(dict)): A list over input images, each a
|
||||
dictionary with the following keys. A prompt key can be
|
||||
excluded if it is not present.
|
||||
'image': The image as a torch tensor in 3xHxW format,
|
||||
already transformed for input to the model.
|
||||
'original_size': (tuple(int, int)) The original size of
|
||||
the image before transformation, as (H, W).
|
||||
'point_coords': (torch.Tensor) Batched point prompts for
|
||||
this image, with shape BxNx2. Already transformed to the
|
||||
input frame of the model.
|
||||
'point_labels': (torch.Tensor) Batched labels for point prompts,
|
||||
with shape BxN.
|
||||
'boxes': (torch.Tensor) Batched box inputs, with shape Bx4.
|
||||
Already transformed to the input frame of the model.
|
||||
'mask_inputs': (torch.Tensor) Batched mask inputs to the model,
|
||||
in the form Bx1xHxW.
|
||||
multimask_output (bool): Whether the model should predict multiple
|
||||
disambiguating masks, or return a single mask.
|
||||
|
||||
Returns:
|
||||
(list(dict)): A list over input images, where each element is
|
||||
as dictionary with the following keys.
|
||||
'masks': (torch.Tensor) Batched binary mask predictions,
|
||||
with shape BxCxHxW, where B is the number of input prompts,
|
||||
C is determined by multimask_output, and (H, W) is the
|
||||
original size of the image.
|
||||
'iou_predictions': (torch.Tensor) The model's predictions
|
||||
of mask quality, in shape BxC.
|
||||
'low_res_logits': (torch.Tensor) Low resolution logits with
|
||||
shape BxCxHxW, where H=W=256. Can be passed as mask input
|
||||
to subsequent iterations of prediction.
|
||||
"""
|
||||
input_images = torch.stack(
|
||||
[self.preprocess(x["image"]) for x in batched_input], dim=0
|
||||
)
|
||||
image_embeddings = self.image_encoder(input_images)
|
||||
|
||||
outputs = []
|
||||
for image_record, curr_embedding in zip(batched_input, image_embeddings):
|
||||
if "point_coords" in image_record:
|
||||
points = (image_record["point_coords"], image_record["point_labels"])
|
||||
else:
|
||||
points = None
|
||||
sparse_embeddings, dense_embeddings = self.prompt_encoder(
|
||||
points=points,
|
||||
boxes=image_record.get("boxes", None),
|
||||
masks=image_record.get("mask_inputs", None),
|
||||
)
|
||||
low_res_masks, iou_predictions = self.mask_decoder(
|
||||
image_embeddings=curr_embedding.unsqueeze(0),
|
||||
image_pe=self.prompt_encoder.get_dense_pe(),
|
||||
sparse_prompt_embeddings=sparse_embeddings,
|
||||
dense_prompt_embeddings=dense_embeddings,
|
||||
multimask_output=multimask_output,
|
||||
)
|
||||
masks = self.postprocess_masks(
|
||||
low_res_masks,
|
||||
input_size=image_record["image"].shape[-2:],
|
||||
original_size=image_record["original_size"],
|
||||
)
|
||||
masks = masks > self.mask_threshold
|
||||
outputs.append(
|
||||
{
|
||||
"masks": masks,
|
||||
"iou_predictions": iou_predictions,
|
||||
"low_res_logits": low_res_masks,
|
||||
}
|
||||
)
|
||||
return outputs
|
||||
|
||||
def postprocess_masks(
|
||||
self,
|
||||
masks: torch.Tensor,
|
||||
input_size: Tuple[int, ...],
|
||||
original_size: Tuple[int, ...],
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Remove padding and upscale masks to the original image size.
|
||||
|
||||
Arguments:
|
||||
masks (torch.Tensor): Batched masks from the mask_decoder,
|
||||
in BxCxHxW format.
|
||||
input_size (tuple(int, int)): The size of the image input to the
|
||||
model, in (H, W) format. Used to remove padding.
|
||||
original_size (tuple(int, int)): The original size of the image
|
||||
before resizing for input to the model, in (H, W) format.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Batched masks in BxCxHxW format, where (H, W)
|
||||
is given by original_size.
|
||||
"""
|
||||
masks = F.interpolate(
|
||||
masks,
|
||||
(self.image_encoder.img_size, self.image_encoder.img_size),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
masks = masks[..., : input_size[0], : input_size[1]]
|
||||
masks = F.interpolate(
|
||||
masks, original_size, mode="bilinear", align_corners=False
|
||||
)
|
||||
return masks
|
||||
|
||||
def preprocess(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Normalize pixel values and pad to a square input."""
|
||||
# Normalize colors
|
||||
x = (x - self.pixel_mean) / self.pixel_std
|
||||
|
||||
# Pad
|
||||
h, w = x.shape[-2:]
|
||||
padh = self.image_encoder.img_size - h
|
||||
padw = self.image_encoder.img_size - w
|
||||
x = F.pad(x, (0, padw, 0, padh))
|
||||
return x
|
||||
@@ -0,0 +1,243 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
|
||||
import math
|
||||
from typing import Tuple, Type
|
||||
|
||||
from .common import MLPBlock
|
||||
|
||||
|
||||
class TwoWayTransformer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
depth: int,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
mlp_dim: int,
|
||||
activation: Type[nn.Module] = nn.ReLU,
|
||||
attention_downsample_rate: int = 2,
|
||||
) -> None:
|
||||
"""
|
||||
A transformer decoder that attends to an input image using
|
||||
queries whose positional embedding is supplied.
|
||||
|
||||
Args:
|
||||
depth (int): number of layers in the transformer
|
||||
embedding_dim (int): the channel dimension for the input embeddings
|
||||
num_heads (int): the number of heads for multihead attention. Must
|
||||
divide embedding_dim
|
||||
mlp_dim (int): the channel dimension internal to the MLP block
|
||||
activation (nn.Module): the activation to use in the MLP block
|
||||
"""
|
||||
super().__init__()
|
||||
self.depth = depth
|
||||
self.embedding_dim = embedding_dim
|
||||
self.num_heads = num_heads
|
||||
self.mlp_dim = mlp_dim
|
||||
self.layers = nn.ModuleList()
|
||||
|
||||
for i in range(depth):
|
||||
self.layers.append(
|
||||
TwoWayAttentionBlock(
|
||||
embedding_dim=embedding_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_dim=mlp_dim,
|
||||
activation=activation,
|
||||
attention_downsample_rate=attention_downsample_rate,
|
||||
skip_first_layer_pe=(i == 0),
|
||||
)
|
||||
)
|
||||
|
||||
self.final_attn_token_to_image = Attention(
|
||||
embedding_dim, num_heads, downsample_rate=attention_downsample_rate
|
||||
)
|
||||
self.norm_final_attn = nn.LayerNorm(embedding_dim)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embedding: Tensor,
|
||||
image_pe: Tensor,
|
||||
point_embedding: Tensor,
|
||||
) -> Tuple[Tensor, Tensor]:
|
||||
"""
|
||||
Args:
|
||||
image_embedding (torch.Tensor): image to attend to. Should be shape
|
||||
B x embedding_dim x h x w for any h and w.
|
||||
image_pe (torch.Tensor): the positional encoding to add to the image. Must
|
||||
have the same shape as image_embedding.
|
||||
point_embedding (torch.Tensor): the embedding to add to the query points.
|
||||
Must have shape B x N_points x embedding_dim for any N_points.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: the processed point_embedding
|
||||
torch.Tensor: the processed image_embedding
|
||||
"""
|
||||
# BxCxHxW -> BxHWxC == B x N_image_tokens x C
|
||||
bs, c, h, w = image_embedding.shape
|
||||
image_embedding = image_embedding.flatten(2).permute(0, 2, 1)
|
||||
image_pe = image_pe.flatten(2).permute(0, 2, 1)
|
||||
|
||||
# Prepare queries
|
||||
queries = point_embedding
|
||||
keys = image_embedding
|
||||
|
||||
# Apply transformer blocks and final layernorm
|
||||
for layer in self.layers:
|
||||
queries, keys = layer(
|
||||
queries=queries,
|
||||
keys=keys,
|
||||
query_pe=point_embedding,
|
||||
key_pe=image_pe,
|
||||
)
|
||||
|
||||
# Apply the final attention layer from the points to the image
|
||||
q = queries + point_embedding
|
||||
k = keys + image_pe
|
||||
attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm_final_attn(queries)
|
||||
|
||||
return queries, keys
|
||||
|
||||
|
||||
class TwoWayAttentionBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
mlp_dim: int = 2048,
|
||||
activation: Type[nn.Module] = nn.ReLU,
|
||||
attention_downsample_rate: int = 2,
|
||||
skip_first_layer_pe: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
A transformer block with four layers: (1) self-attention of sparse
|
||||
inputs, (2) cross attention of sparse inputs to dense inputs, (3) mlp
|
||||
block on sparse inputs, and (4) cross attention of dense inputs to sparse
|
||||
inputs.
|
||||
|
||||
Arguments:
|
||||
embedding_dim (int): the channel dimension of the embeddings
|
||||
num_heads (int): the number of heads in the attention layers
|
||||
mlp_dim (int): the hidden dimension of the mlp block
|
||||
activation (nn.Module): the activation of the mlp block
|
||||
skip_first_layer_pe (bool): skip the PE on the first layer
|
||||
"""
|
||||
super().__init__()
|
||||
self.self_attn = Attention(embedding_dim, num_heads)
|
||||
self.norm1 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.cross_attn_token_to_image = Attention(
|
||||
embedding_dim, num_heads, downsample_rate=attention_downsample_rate
|
||||
)
|
||||
self.norm2 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.mlp = MLPBlock(embedding_dim, mlp_dim, activation)
|
||||
self.norm3 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.norm4 = nn.LayerNorm(embedding_dim)
|
||||
self.cross_attn_image_to_token = Attention(
|
||||
embedding_dim, num_heads, downsample_rate=attention_downsample_rate
|
||||
)
|
||||
|
||||
self.skip_first_layer_pe = skip_first_layer_pe
|
||||
|
||||
def forward(
|
||||
self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor
|
||||
) -> Tuple[Tensor, Tensor]:
|
||||
# Self attention block
|
||||
if self.skip_first_layer_pe:
|
||||
queries = self.self_attn(q=queries, k=queries, v=queries)
|
||||
else:
|
||||
q = queries + query_pe
|
||||
attn_out = self.self_attn(q=q, k=q, v=queries)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm1(queries)
|
||||
|
||||
# Cross attention block, tokens attending to image embedding
|
||||
q = queries + query_pe
|
||||
k = keys + key_pe
|
||||
attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm2(queries)
|
||||
|
||||
# MLP block
|
||||
mlp_out = self.mlp(queries)
|
||||
queries = queries + mlp_out
|
||||
queries = self.norm3(queries)
|
||||
|
||||
# Cross attention block, image embedding attending to tokens
|
||||
q = queries + query_pe
|
||||
k = keys + key_pe
|
||||
attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries)
|
||||
keys = keys + attn_out
|
||||
keys = self.norm4(keys)
|
||||
|
||||
return queries, keys
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
"""
|
||||
An attention layer that allows for downscaling the size of the embedding
|
||||
after projection to queries, keys, and values.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
downsample_rate: int = 1,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.embedding_dim = embedding_dim
|
||||
self.internal_dim = embedding_dim // downsample_rate
|
||||
self.num_heads = num_heads
|
||||
assert (
|
||||
self.internal_dim % num_heads == 0
|
||||
), "num_heads must divide embedding_dim."
|
||||
|
||||
self.q_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.k_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.v_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.out_proj = nn.Linear(self.internal_dim, embedding_dim)
|
||||
|
||||
def _separate_heads(self, x: Tensor, num_heads: int) -> Tensor:
|
||||
b, n, c = x.shape
|
||||
x = x.reshape(b, n, num_heads, c // num_heads)
|
||||
return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head
|
||||
|
||||
def _recombine_heads(self, x: Tensor) -> Tensor:
|
||||
b, n_heads, n_tokens, c_per_head = x.shape
|
||||
x = x.transpose(1, 2)
|
||||
return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C
|
||||
|
||||
def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor:
|
||||
# Input projections
|
||||
q = self.q_proj(q)
|
||||
k = self.k_proj(k)
|
||||
v = self.v_proj(v)
|
||||
|
||||
# Separate into heads
|
||||
q = self._separate_heads(q, self.num_heads)
|
||||
k = self._separate_heads(k, self.num_heads)
|
||||
v = self._separate_heads(v, self.num_heads)
|
||||
|
||||
# Attention
|
||||
_, _, _, c_per_head = q.shape
|
||||
attn = q @ k.permute(0, 1, 3, 2) # B x N_heads x N_tokens x N_tokens
|
||||
attn = attn / math.sqrt(c_per_head)
|
||||
attn = torch.softmax(attn, dim=-1)
|
||||
|
||||
# Get output
|
||||
out = attn @ v
|
||||
out = self._recombine_heads(out)
|
||||
out = self.out_proj(out)
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,286 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from PILOT_PROJECT.workspace.sprint_1_2.Design_Material.codebase.ml.implementation.cv.arch.segment_anything.modeling import Sam
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from .utils.transforms import ResizeLongestSide
|
||||
|
||||
|
||||
class SamPredictor:
|
||||
def __init__(
|
||||
self,
|
||||
sam_model: Sam,
|
||||
) -> None:
|
||||
"""
|
||||
Uses SAM to calculate the image embedding for an image, and then
|
||||
allow repeated, efficient mask prediction given prompts.
|
||||
|
||||
Arguments:
|
||||
sam_model (Sam): The model to use for mask prediction.
|
||||
"""
|
||||
super().__init__()
|
||||
self.model = sam_model
|
||||
self.transform = ResizeLongestSide(sam_model.image_encoder.img_size)
|
||||
self.reset_image()
|
||||
|
||||
def set_image(
|
||||
self,
|
||||
image: np.ndarray,
|
||||
image_format: str = "RGB",
|
||||
) -> None:
|
||||
"""
|
||||
Calculates the image embeddings for the provided image, allowing
|
||||
masks to be predicted with the 'predict' method.
|
||||
|
||||
Arguments:
|
||||
image (np.ndarray): The image for calculating masks. Expects an
|
||||
image in HWC uint8 format, with pixel values in [0, 255].
|
||||
image_format (str): The color format of the image, in ['RGB', 'BGR'].
|
||||
"""
|
||||
assert image_format in [
|
||||
"RGB",
|
||||
"BGR",
|
||||
], f"image_format must be in ['RGB', 'BGR'], is {image_format}."
|
||||
if image_format != self.model.image_format:
|
||||
image = image[..., ::-1]
|
||||
|
||||
# Transform the image to the form expected by the model
|
||||
input_image = self.transform.apply_image(image)
|
||||
input_image_torch = torch.as_tensor(input_image, device=self.device)
|
||||
input_image_torch = input_image_torch.permute(2, 0, 1).contiguous()[
|
||||
None, :, :, :
|
||||
]
|
||||
|
||||
self.set_torch_image(input_image_torch, image.shape[:2])
|
||||
|
||||
@torch.no_grad()
|
||||
def set_torch_image(
|
||||
self,
|
||||
transformed_image: torch.Tensor,
|
||||
original_image_size: Tuple[int, ...],
|
||||
) -> None:
|
||||
"""
|
||||
Calculates the image embeddings for the provided image, allowing
|
||||
masks to be predicted with the 'predict' method. Expects the input
|
||||
image to be already transformed to the format expected by the model.
|
||||
|
||||
Arguments:
|
||||
transformed_image (torch.Tensor): The input image, with shape
|
||||
1x3xHxW, which has been transformed with ResizeLongestSide.
|
||||
original_image_size (tuple(int, int)): The size of the image
|
||||
before transformation, in (H, W) format.
|
||||
"""
|
||||
assert (
|
||||
len(transformed_image.shape) == 4
|
||||
and transformed_image.shape[1] == 3
|
||||
and max(*transformed_image.shape[2:]) == self.model.image_encoder.img_size
|
||||
), f"set_torch_image input must be BCHW with long side {self.model.image_encoder.img_size}."
|
||||
self.reset_image()
|
||||
|
||||
self.original_size = original_image_size
|
||||
self.input_size = tuple(transformed_image.shape[-2:])
|
||||
input_image = self.model.preprocess(transformed_image)
|
||||
self.features = self.model.image_encoder(input_image)
|
||||
self.is_image_set = True
|
||||
|
||||
def predict(
|
||||
self,
|
||||
point_coords: Optional[np.ndarray] = None,
|
||||
point_labels: Optional[np.ndarray] = None,
|
||||
box: Optional[np.ndarray] = None,
|
||||
mask_input: Optional[np.ndarray] = None,
|
||||
multimask_output: bool = True,
|
||||
return_logits: bool = False,
|
||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Predict masks for the given input prompts, using the currently set image.
|
||||
|
||||
Arguments:
|
||||
point_coords (np.ndarray or None): A Nx2 array of point prompts to the
|
||||
model. Each point is in (X,Y) in pixels.
|
||||
point_labels (np.ndarray or None): A length N array of labels for the
|
||||
point prompts. 1 indicates a foreground point and 0 indicates a
|
||||
background point.
|
||||
box (np.ndarray or None): A length 4 array given a box prompt to the
|
||||
model, in XYXY format.
|
||||
mask_input (np.ndarray): A low resolution mask input to the model, typically
|
||||
coming from a previous prediction iteration. Has form 1xHxW, where
|
||||
for SAM, H=W=256.
|
||||
multimask_output (bool): If true, the model will return three masks.
|
||||
For ambiguous input prompts (such as a single click), this will often
|
||||
produce better masks than a single prediction. If only a single
|
||||
mask is needed, the model's predicted quality score can be used
|
||||
to select the best mask. For non-ambiguous prompts, such as multiple
|
||||
input prompts, multimask_output=False can give better results.
|
||||
return_logits (bool): If true, returns un-thresholded masks logits
|
||||
instead of a binary mask.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): The output masks in CxHxW format, where C is the
|
||||
number of masks, and (H, W) is the original image size.
|
||||
(np.ndarray): An array of length C containing the model's
|
||||
predictions for the quality of each mask.
|
||||
(np.ndarray): An array of shape CxHxW, where C is the number
|
||||
of masks and H=W=256. These low resolution logits can be passed to
|
||||
a subsequent iteration as mask input.
|
||||
"""
|
||||
if not self.is_image_set:
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) before mask prediction."
|
||||
)
|
||||
|
||||
# Transform input prompts
|
||||
coords_torch, labels_torch, box_torch, mask_input_torch = None, None, None, None
|
||||
if point_coords is not None:
|
||||
assert (
|
||||
point_labels is not None
|
||||
), "point_labels must be supplied if point_coords is supplied."
|
||||
point_coords = self.transform.apply_coords(point_coords, self.original_size)
|
||||
coords_torch = torch.as_tensor(
|
||||
point_coords, dtype=torch.float, device=self.device
|
||||
)
|
||||
labels_torch = torch.as_tensor(
|
||||
point_labels, dtype=torch.int, device=self.device
|
||||
)
|
||||
coords_torch, labels_torch = coords_torch[None, :, :], labels_torch[None, :]
|
||||
if box is not None:
|
||||
box = self.transform.apply_boxes(box, self.original_size)
|
||||
box_torch = torch.as_tensor(box, dtype=torch.float, device=self.device)
|
||||
box_torch = box_torch[None, :]
|
||||
if mask_input is not None:
|
||||
mask_input_torch = torch.as_tensor(
|
||||
mask_input, dtype=torch.float, device=self.device
|
||||
)
|
||||
mask_input_torch = mask_input_torch[None, :, :, :]
|
||||
|
||||
masks, iou_predictions, low_res_masks = self.predict_torch(
|
||||
coords_torch,
|
||||
labels_torch,
|
||||
box_torch,
|
||||
mask_input_torch,
|
||||
multimask_output,
|
||||
return_logits=return_logits,
|
||||
)
|
||||
|
||||
masks_np = masks[0].detach().cpu().numpy()
|
||||
iou_predictions_np = iou_predictions[0].detach().cpu().numpy()
|
||||
low_res_masks_np = low_res_masks[0].detach().cpu().numpy()
|
||||
return masks_np, iou_predictions_np, low_res_masks_np
|
||||
|
||||
@torch.no_grad()
|
||||
def predict_torch(
|
||||
self,
|
||||
point_coords: Optional[torch.Tensor],
|
||||
point_labels: Optional[torch.Tensor],
|
||||
boxes: Optional[torch.Tensor] = None,
|
||||
mask_input: Optional[torch.Tensor] = None,
|
||||
multimask_output: bool = True,
|
||||
return_logits: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Predict masks for the given input prompts, using the currently set image.
|
||||
Input prompts are batched torch tensors and are expected to already be
|
||||
transformed to the input frame using ResizeLongestSide.
|
||||
|
||||
Arguments:
|
||||
point_coords (torch.Tensor or None): A BxNx2 array of point prompts to the
|
||||
model. Each point is in (X,Y) in pixels.
|
||||
point_labels (torch.Tensor or None): A BxN array of labels for the
|
||||
point prompts. 1 indicates a foreground point and 0 indicates a
|
||||
background point.
|
||||
boxes (np.ndarray or None): A Bx4 array given a box prompt to the
|
||||
model, in XYXY format.
|
||||
mask_input (np.ndarray): A low resolution mask input to the model, typically
|
||||
coming from a previous prediction iteration. Has form Bx1xHxW, where
|
||||
for SAM, H=W=256. Masks returned by a previous iteration of the
|
||||
predict method do not need further transformation.
|
||||
multimask_output (bool): If true, the model will return three masks.
|
||||
For ambiguous input prompts (such as a single click), this will often
|
||||
produce better masks than a single prediction. If only a single
|
||||
mask is needed, the model's predicted quality score can be used
|
||||
to select the best mask. For non-ambiguous prompts, such as multiple
|
||||
input prompts, multimask_output=False can give better results.
|
||||
return_logits (bool): If true, returns un-thresholded masks logits
|
||||
instead of a binary mask.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): The output masks in BxCxHxW format, where C is the
|
||||
number of masks, and (H, W) is the original image size.
|
||||
(torch.Tensor): An array of shape BxC containing the model's
|
||||
predictions for the quality of each mask.
|
||||
(torch.Tensor): An array of shape BxCxHxW, where C is the number
|
||||
of masks and H=W=256. These low res logits can be passed to
|
||||
a subsequent iteration as mask input.
|
||||
"""
|
||||
if not self.is_image_set:
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) before mask prediction."
|
||||
)
|
||||
|
||||
if point_coords is not None:
|
||||
points = (point_coords, point_labels)
|
||||
else:
|
||||
points = None
|
||||
|
||||
# Embed prompts
|
||||
sparse_embeddings, dense_embeddings = self.model.prompt_encoder(
|
||||
points=points,
|
||||
boxes=boxes,
|
||||
masks=mask_input,
|
||||
)
|
||||
|
||||
# Predict masks
|
||||
low_res_masks, iou_predictions = self.model.mask_decoder(
|
||||
image_embeddings=self.features,
|
||||
image_pe=self.model.prompt_encoder.get_dense_pe(),
|
||||
sparse_prompt_embeddings=sparse_embeddings,
|
||||
dense_prompt_embeddings=dense_embeddings,
|
||||
multimask_output=multimask_output,
|
||||
)
|
||||
|
||||
# Upscale the masks to the original image resolution
|
||||
masks = self.model.postprocess_masks(
|
||||
low_res_masks, self.input_size, self.original_size
|
||||
)
|
||||
|
||||
if not return_logits:
|
||||
masks = masks > self.model.mask_threshold
|
||||
|
||||
return masks, iou_predictions, low_res_masks
|
||||
|
||||
def get_image_embedding(self) -> torch.Tensor:
|
||||
"""
|
||||
Returns the image embeddings for the currently set image, with
|
||||
shape 1xCxHxW, where C is the embedding dimension and (H,W) are
|
||||
the embedding spatial dimension of SAM (typically C=256, H=W=64).
|
||||
"""
|
||||
if not self.is_image_set:
|
||||
raise RuntimeError(
|
||||
"An image must be set with .set_image(...) to generate an embedding."
|
||||
)
|
||||
assert (
|
||||
self.features is not None
|
||||
), "Features must exist if an image has been set."
|
||||
return self.features
|
||||
|
||||
@property
|
||||
def device(self) -> torch.device:
|
||||
return self.model.device
|
||||
|
||||
def reset_image(self) -> None:
|
||||
"""Resets the currently set image."""
|
||||
self.is_image_set = False
|
||||
self.features = None
|
||||
self.orig_h = None
|
||||
self.orig_w = None
|
||||
self.input_h = None
|
||||
self.input_w = None
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
import math
|
||||
from copy import deepcopy
|
||||
from itertools import product
|
||||
from typing import Any, Dict, Generator, ItemsView, List, Tuple
|
||||
|
||||
|
||||
class MaskData:
|
||||
"""
|
||||
A structure for storing masks and their related data in batched format.
|
||||
Implements basic filtering and concatenation.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
for v in kwargs.values():
|
||||
assert isinstance(
|
||||
v, (list, np.ndarray, torch.Tensor)
|
||||
), "MaskData only supports list, numpy arrays, and torch tensors."
|
||||
self._stats = dict(**kwargs)
|
||||
|
||||
def __setitem__(self, key: str, item: Any) -> None:
|
||||
assert isinstance(
|
||||
item, (list, np.ndarray, torch.Tensor)
|
||||
), "MaskData only supports list, numpy arrays, and torch tensors."
|
||||
self._stats[key] = item
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self._stats[key]
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self._stats[key]
|
||||
|
||||
def items(self) -> ItemsView[str, Any]:
|
||||
return self._stats.items()
|
||||
|
||||
def filter(self, keep: torch.Tensor) -> None:
|
||||
for k, v in self._stats.items():
|
||||
if v is None:
|
||||
self._stats[k] = None
|
||||
elif isinstance(v, torch.Tensor):
|
||||
self._stats[k] = v[torch.as_tensor(keep, device=v.device)]
|
||||
elif isinstance(v, np.ndarray):
|
||||
self._stats[k] = v[keep.detach().cpu().numpy()]
|
||||
elif isinstance(v, list) and keep.dtype == torch.bool:
|
||||
self._stats[k] = [a for i, a in enumerate(v) if keep[i]]
|
||||
elif isinstance(v, list):
|
||||
self._stats[k] = [v[i] for i in keep]
|
||||
else:
|
||||
raise TypeError(f"MaskData key {k} has an unsupported type {type(v)}.")
|
||||
|
||||
def cat(self, new_stats: "MaskData") -> None:
|
||||
for k, v in new_stats.items():
|
||||
if k not in self._stats or self._stats[k] is None:
|
||||
self._stats[k] = deepcopy(v)
|
||||
elif isinstance(v, torch.Tensor):
|
||||
self._stats[k] = torch.cat([self._stats[k], v], dim=0)
|
||||
elif isinstance(v, np.ndarray):
|
||||
self._stats[k] = np.concatenate([self._stats[k], v], axis=0)
|
||||
elif isinstance(v, list):
|
||||
self._stats[k] = self._stats[k] + deepcopy(v)
|
||||
else:
|
||||
raise TypeError(f"MaskData key {k} has an unsupported type {type(v)}.")
|
||||
|
||||
def to_numpy(self) -> None:
|
||||
for k, v in self._stats.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
self._stats[k] = v.detach().cpu().numpy()
|
||||
|
||||
|
||||
def is_box_near_crop_edge(
|
||||
boxes: torch.Tensor, crop_box: List[int], orig_box: List[int], atol: float = 20.0
|
||||
) -> torch.Tensor:
|
||||
"""Filter masks at the edge of a crop, but not at the edge of the original image."""
|
||||
crop_box_torch = torch.as_tensor(crop_box, dtype=torch.float, device=boxes.device)
|
||||
orig_box_torch = torch.as_tensor(orig_box, dtype=torch.float, device=boxes.device)
|
||||
boxes = uncrop_boxes_xyxy(boxes, crop_box).float()
|
||||
near_crop_edge = torch.isclose(boxes, crop_box_torch[None, :], atol=atol, rtol=0)
|
||||
near_image_edge = torch.isclose(boxes, orig_box_torch[None, :], atol=atol, rtol=0)
|
||||
near_crop_edge = torch.logical_and(near_crop_edge, ~near_image_edge)
|
||||
return torch.any(near_crop_edge, dim=1)
|
||||
|
||||
|
||||
def box_xyxy_to_xywh(box_xyxy: torch.Tensor) -> torch.Tensor:
|
||||
box_xywh = deepcopy(box_xyxy)
|
||||
box_xywh[2] = box_xywh[2] - box_xywh[0]
|
||||
box_xywh[3] = box_xywh[3] - box_xywh[1]
|
||||
return box_xywh
|
||||
|
||||
|
||||
def batch_iterator(batch_size: int, *args) -> Generator[List[Any], None, None]:
|
||||
assert len(args) > 0 and all(
|
||||
len(a) == len(args[0]) for a in args
|
||||
), "Batched iteration must have inputs of all the same size."
|
||||
n_batches = len(args[0]) // batch_size + int(len(args[0]) % batch_size != 0)
|
||||
for b in range(n_batches):
|
||||
yield [arg[b * batch_size : (b + 1) * batch_size] for arg in args]
|
||||
|
||||
|
||||
def mask_to_rle_pytorch(tensor: torch.Tensor) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Encodes masks to an uncompressed RLE, in the format expected by
|
||||
pycoco tools.
|
||||
"""
|
||||
# Put in fortran order and flatten h,w
|
||||
b, h, w = tensor.shape
|
||||
tensor = tensor.permute(0, 2, 1).flatten(1)
|
||||
|
||||
# Compute change indices
|
||||
diff = tensor[:, 1:] ^ tensor[:, :-1]
|
||||
change_indices = diff.nonzero()
|
||||
|
||||
# Encode run length
|
||||
out = []
|
||||
for i in range(b):
|
||||
cur_idxs = change_indices[change_indices[:, 0] == i, 1]
|
||||
cur_idxs = torch.cat(
|
||||
[
|
||||
torch.tensor([0], dtype=cur_idxs.dtype, device=cur_idxs.device),
|
||||
cur_idxs + 1,
|
||||
torch.tensor([h * w], dtype=cur_idxs.dtype, device=cur_idxs.device),
|
||||
]
|
||||
)
|
||||
btw_idxs = cur_idxs[1:] - cur_idxs[:-1]
|
||||
counts = [] if tensor[i, 0] == 0 else [0]
|
||||
counts.extend(btw_idxs.detach().cpu().tolist())
|
||||
out.append({"size": [h, w], "counts": counts})
|
||||
return out
|
||||
|
||||
|
||||
def rle_to_mask(rle: Dict[str, Any]) -> np.ndarray:
|
||||
"""Compute a binary mask from an uncompressed RLE."""
|
||||
h, w = rle["size"]
|
||||
mask = np.empty(h * w, dtype=bool)
|
||||
idx = 0
|
||||
parity = False
|
||||
for count in rle["counts"]:
|
||||
mask[idx : idx + count] = parity
|
||||
idx += count
|
||||
parity ^= True
|
||||
mask = mask.reshape(w, h)
|
||||
return mask.transpose() # Put in C order
|
||||
|
||||
|
||||
def area_from_rle(rle: Dict[str, Any]) -> int:
|
||||
return sum(rle["counts"][1::2])
|
||||
|
||||
|
||||
def calculate_stability_score(
|
||||
masks: torch.Tensor, mask_threshold: float, threshold_offset: float
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Computes the stability score for a batch of masks. The stability
|
||||
score is the IoU between the binary masks obtained by thresholding
|
||||
the predicted mask logits at high and low values.
|
||||
"""
|
||||
# One mask is always contained inside the other.
|
||||
# Save memory by preventing unnecessary cast to torch.int64
|
||||
intersections = (
|
||||
(masks > (mask_threshold + threshold_offset))
|
||||
.sum(-1, dtype=torch.int16)
|
||||
.sum(-1, dtype=torch.int32)
|
||||
)
|
||||
unions = (
|
||||
(masks > (mask_threshold - threshold_offset))
|
||||
.sum(-1, dtype=torch.int16)
|
||||
.sum(-1, dtype=torch.int32)
|
||||
)
|
||||
return intersections / unions
|
||||
|
||||
|
||||
def build_point_grid(n_per_side: int) -> np.ndarray:
|
||||
"""Generates a 2D grid of points evenly spaced in [0,1]x[0,1]."""
|
||||
offset = 1 / (2 * n_per_side)
|
||||
points_one_side = np.linspace(offset, 1 - offset, n_per_side)
|
||||
points_x = np.tile(points_one_side[None, :], (n_per_side, 1))
|
||||
points_y = np.tile(points_one_side[:, None], (1, n_per_side))
|
||||
points = np.stack([points_x, points_y], axis=-1).reshape(-1, 2)
|
||||
return points
|
||||
|
||||
|
||||
def build_all_layer_point_grids(
|
||||
n_per_side: int, n_layers: int, scale_per_layer: int
|
||||
) -> List[np.ndarray]:
|
||||
"""Generates point grids for all crop layers."""
|
||||
points_by_layer = []
|
||||
for i in range(n_layers + 1):
|
||||
n_points = int(n_per_side / (scale_per_layer**i))
|
||||
points_by_layer.append(build_point_grid(n_points))
|
||||
return points_by_layer
|
||||
|
||||
|
||||
def generate_crop_boxes(
|
||||
im_size: Tuple[int, ...], n_layers: int, overlap_ratio: float
|
||||
) -> Tuple[List[List[int]], List[int]]:
|
||||
"""
|
||||
Generates a list of crop boxes of different sizes. Each layer
|
||||
has (2**i)**2 boxes for the ith layer.
|
||||
"""
|
||||
crop_boxes, layer_idxs = [], []
|
||||
im_h, im_w = im_size
|
||||
short_side = min(im_h, im_w)
|
||||
|
||||
# Original image
|
||||
crop_boxes.append([0, 0, im_w, im_h])
|
||||
layer_idxs.append(0)
|
||||
|
||||
def crop_len(orig_len, n_crops, overlap):
|
||||
return int(math.ceil((overlap * (n_crops - 1) + orig_len) / n_crops))
|
||||
|
||||
for i_layer in range(n_layers):
|
||||
n_crops_per_side = 2 ** (i_layer + 1)
|
||||
overlap = int(overlap_ratio * short_side * (2 / n_crops_per_side))
|
||||
|
||||
crop_w = crop_len(im_w, n_crops_per_side, overlap)
|
||||
crop_h = crop_len(im_h, n_crops_per_side, overlap)
|
||||
|
||||
crop_box_x0 = [int((crop_w - overlap) * i) for i in range(n_crops_per_side)]
|
||||
crop_box_y0 = [int((crop_h - overlap) * i) for i in range(n_crops_per_side)]
|
||||
|
||||
# Crops in XYWH format
|
||||
for x0, y0 in product(crop_box_x0, crop_box_y0):
|
||||
box = [x0, y0, min(x0 + crop_w, im_w), min(y0 + crop_h, im_h)]
|
||||
crop_boxes.append(box)
|
||||
layer_idxs.append(i_layer + 1)
|
||||
|
||||
return crop_boxes, layer_idxs
|
||||
|
||||
|
||||
def uncrop_boxes_xyxy(boxes: torch.Tensor, crop_box: List[int]) -> torch.Tensor:
|
||||
x0, y0, _, _ = crop_box
|
||||
offset = torch.tensor([[x0, y0, x0, y0]], device=boxes.device)
|
||||
# Check if boxes has a channel dimension
|
||||
if len(boxes.shape) == 3:
|
||||
offset = offset.unsqueeze(1)
|
||||
return boxes + offset
|
||||
|
||||
|
||||
def uncrop_points(points: torch.Tensor, crop_box: List[int]) -> torch.Tensor:
|
||||
x0, y0, _, _ = crop_box
|
||||
offset = torch.tensor([[x0, y0]], device=points.device)
|
||||
# Check if points has a channel dimension
|
||||
if len(points.shape) == 3:
|
||||
offset = offset.unsqueeze(1)
|
||||
return points + offset
|
||||
|
||||
|
||||
def uncrop_masks(
|
||||
masks: torch.Tensor, crop_box: List[int], orig_h: int, orig_w: int
|
||||
) -> torch.Tensor:
|
||||
x0, y0, x1, y1 = crop_box
|
||||
if x0 == 0 and y0 == 0 and x1 == orig_w and y1 == orig_h:
|
||||
return masks
|
||||
# Coordinate transform masks
|
||||
pad_x, pad_y = orig_w - (x1 - x0), orig_h - (y1 - y0)
|
||||
pad = (x0, pad_x - x0, y0, pad_y - y0)
|
||||
return torch.nn.functional.pad(masks, pad, value=0)
|
||||
|
||||
|
||||
def remove_small_regions(
|
||||
mask: np.ndarray, area_thresh: float, mode: str
|
||||
) -> Tuple[np.ndarray, bool]:
|
||||
"""
|
||||
Removes small disconnected regions and holes in a mask. Returns the
|
||||
mask and an indicator of if the mask has been modified.
|
||||
"""
|
||||
import cv2 # type: ignore
|
||||
|
||||
assert mode in ["holes", "islands"]
|
||||
correct_holes = mode == "holes"
|
||||
working_mask = (correct_holes ^ mask).astype(np.uint8)
|
||||
n_labels, regions, stats, _ = cv2.connectedComponentsWithStats(working_mask, 8)
|
||||
sizes = stats[:, -1][1:] # Row 0 is background label
|
||||
small_regions = [i + 1 for i, s in enumerate(sizes) if s < area_thresh]
|
||||
if len(small_regions) == 0:
|
||||
return mask, False
|
||||
fill_labels = [0] + small_regions
|
||||
if not correct_holes:
|
||||
fill_labels = [i for i in range(n_labels) if i not in fill_labels]
|
||||
# If every region is below threshold, keep largest
|
||||
if len(fill_labels) == 0:
|
||||
fill_labels = [int(np.argmax(sizes)) + 1]
|
||||
mask = np.isin(regions, fill_labels)
|
||||
return mask, True
|
||||
|
||||
|
||||
def coco_encode_rle(uncompressed_rle: Dict[str, Any]) -> Dict[str, Any]:
|
||||
from pycocotools import mask as mask_utils # type: ignore
|
||||
|
||||
h, w = uncompressed_rle["size"]
|
||||
rle = mask_utils.frPyObjects(uncompressed_rle, h, w)
|
||||
rle["counts"] = rle["counts"].decode("utf-8") # Necessary to serialize with json
|
||||
return rle
|
||||
|
||||
|
||||
def batched_mask_to_box(masks: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Calculates boxes in XYXY format around masks. Return [0,0,0,0] for
|
||||
an empty mask. For input shape C1xC2x...xHxW, the output shape is C1xC2x...x4.
|
||||
"""
|
||||
# torch.max below raises an error on empty inputs, just skip in this case
|
||||
if torch.numel(masks) == 0:
|
||||
return torch.zeros(*masks.shape[:-2], 4, device=masks.device)
|
||||
|
||||
# Normalize shape to CxHxW
|
||||
shape = masks.shape
|
||||
h, w = shape[-2:]
|
||||
if len(shape) > 2:
|
||||
masks = masks.flatten(0, -3)
|
||||
else:
|
||||
masks = masks.unsqueeze(0)
|
||||
|
||||
# Get top and bottom edges
|
||||
in_height, _ = torch.max(masks, dim=-1)
|
||||
in_height_coords = in_height * torch.arange(h, device=in_height.device)[None, :]
|
||||
bottom_edges, _ = torch.max(in_height_coords, dim=-1)
|
||||
in_height_coords = in_height_coords + h * (~in_height)
|
||||
top_edges, _ = torch.min(in_height_coords, dim=-1)
|
||||
|
||||
# Get left and right edges
|
||||
in_width, _ = torch.max(masks, dim=-2)
|
||||
in_width_coords = in_width * torch.arange(w, device=in_width.device)[None, :]
|
||||
right_edges, _ = torch.max(in_width_coords, dim=-1)
|
||||
in_width_coords = in_width_coords + w * (~in_width)
|
||||
left_edges, _ = torch.min(in_width_coords, dim=-1)
|
||||
|
||||
# If the mask is empty the right edge will be to the left of the left edge.
|
||||
# Replace these boxes with [0, 0, 0, 0]
|
||||
empty_filter = (right_edges < left_edges) | (bottom_edges < top_edges)
|
||||
out = torch.stack([left_edges, top_edges, right_edges, bottom_edges], dim=-1)
|
||||
out = out * (~empty_filter).unsqueeze(-1)
|
||||
|
||||
# Return to original shape
|
||||
if len(shape) > 2:
|
||||
out = out.reshape(*shape[:-2], 4)
|
||||
else:
|
||||
out = out[0]
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
from ..modeling import Sam
|
||||
from .amg import calculate_stability_score
|
||||
|
||||
|
||||
class SamOnnxModel(nn.Module):
|
||||
"""
|
||||
This model should not be called directly, but is used in ONNX export.
|
||||
It combines the prompt encoder, mask decoder, and mask postprocessing of Sam,
|
||||
with some functions modified to enable model tracing. Also supports extra
|
||||
options controlling what information. See the ONNX export script for details.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: Sam,
|
||||
return_single_mask: bool,
|
||||
use_stability_score: bool = False,
|
||||
return_extra_metrics: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.mask_decoder = model.mask_decoder
|
||||
self.model = model
|
||||
self.img_size = model.image_encoder.img_size
|
||||
self.return_single_mask = return_single_mask
|
||||
self.use_stability_score = use_stability_score
|
||||
self.stability_score_offset = 1.0
|
||||
self.return_extra_metrics = return_extra_metrics
|
||||
|
||||
@staticmethod
|
||||
def resize_longest_image_size(
|
||||
input_image_size: torch.Tensor, longest_side: int
|
||||
) -> torch.Tensor:
|
||||
input_image_size = input_image_size.to(torch.float32)
|
||||
scale = longest_side / torch.max(input_image_size)
|
||||
transformed_size = scale * input_image_size
|
||||
transformed_size = torch.floor(transformed_size + 0.5).to(torch.int64)
|
||||
return transformed_size
|
||||
|
||||
def _embed_points(
|
||||
self, point_coords: torch.Tensor, point_labels: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
point_coords = point_coords + 0.5
|
||||
point_coords = point_coords / self.img_size
|
||||
point_embedding = self.model.prompt_encoder.pe_layer._pe_encoding(point_coords)
|
||||
point_labels = point_labels.unsqueeze(-1).expand_as(point_embedding)
|
||||
|
||||
point_embedding = point_embedding * (point_labels != -1)
|
||||
point_embedding = (
|
||||
point_embedding
|
||||
+ self.model.prompt_encoder.not_a_point_embed.weight * (point_labels == -1)
|
||||
)
|
||||
|
||||
for i in range(self.model.prompt_encoder.num_point_embeddings):
|
||||
point_embedding = (
|
||||
point_embedding
|
||||
+ self.model.prompt_encoder.point_embeddings[i].weight
|
||||
* (point_labels == i)
|
||||
)
|
||||
|
||||
return point_embedding
|
||||
|
||||
def _embed_masks(
|
||||
self, input_mask: torch.Tensor, has_mask_input: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
mask_embedding = has_mask_input * self.model.prompt_encoder.mask_downscaling(
|
||||
input_mask
|
||||
)
|
||||
mask_embedding = mask_embedding + (
|
||||
1 - has_mask_input
|
||||
) * self.model.prompt_encoder.no_mask_embed.weight.reshape(1, -1, 1, 1)
|
||||
return mask_embedding
|
||||
|
||||
def mask_postprocessing(
|
||||
self, masks: torch.Tensor, orig_im_size: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
masks = F.interpolate(
|
||||
masks,
|
||||
size=(self.img_size, self.img_size),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
|
||||
prepadded_size = self.resize_longest_image_size(orig_im_size, self.img_size).to(
|
||||
torch.int64
|
||||
)
|
||||
masks = masks[..., : prepadded_size[0], : prepadded_size[1]] # type: ignore
|
||||
|
||||
orig_im_size = orig_im_size.to(torch.int64)
|
||||
h, w = orig_im_size[0], orig_im_size[1]
|
||||
masks = F.interpolate(masks, size=(h, w), mode="bilinear", align_corners=False)
|
||||
return masks
|
||||
|
||||
def select_masks(
|
||||
self, masks: torch.Tensor, iou_preds: torch.Tensor, num_points: int
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
# Determine if we should return the multiclick mask or not from the number of points.
|
||||
# The reweighting is used to avoid control flow.
|
||||
score_reweight = torch.tensor(
|
||||
[[1000] + [0] * (self.model.mask_decoder.num_mask_tokens - 1)]
|
||||
).to(iou_preds.device)
|
||||
score = iou_preds + (num_points - 2.5) * score_reweight
|
||||
best_idx = torch.argmax(score, dim=1)
|
||||
masks = masks[torch.arange(masks.shape[0]), best_idx, :, :].unsqueeze(1)
|
||||
iou_preds = iou_preds[torch.arange(masks.shape[0]), best_idx].unsqueeze(1)
|
||||
|
||||
return masks, iou_preds
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
point_coords: torch.Tensor,
|
||||
point_labels: torch.Tensor,
|
||||
mask_input: torch.Tensor,
|
||||
has_mask_input: torch.Tensor,
|
||||
orig_im_size: torch.Tensor,
|
||||
):
|
||||
sparse_embedding = self._embed_points(point_coords, point_labels)
|
||||
dense_embedding = self._embed_masks(mask_input, has_mask_input)
|
||||
|
||||
masks, scores = self.model.mask_decoder.predict_masks(
|
||||
image_embeddings=image_embeddings,
|
||||
image_pe=self.model.prompt_encoder.get_dense_pe(),
|
||||
sparse_prompt_embeddings=sparse_embedding,
|
||||
dense_prompt_embeddings=dense_embedding,
|
||||
)
|
||||
|
||||
if self.use_stability_score:
|
||||
scores = calculate_stability_score(
|
||||
masks, self.model.mask_threshold, self.stability_score_offset
|
||||
)
|
||||
|
||||
if self.return_single_mask:
|
||||
masks, scores = self.select_masks(masks, scores, point_coords.shape[1])
|
||||
|
||||
upscaled_masks = self.mask_postprocessing(masks, orig_im_size)
|
||||
|
||||
if self.return_extra_metrics:
|
||||
stability_scores = calculate_stability_score(
|
||||
upscaled_masks, self.model.mask_threshold, self.stability_score_offset
|
||||
)
|
||||
areas = (upscaled_masks > self.model.mask_threshold).sum(-1).sum(-1)
|
||||
return upscaled_masks, scores, stability_scores, areas, masks
|
||||
|
||||
return upscaled_masks, scores, masks
|
||||
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
from torchvision.transforms.functional import resize, to_pil_image # type: ignore
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class ResizeLongestSide:
|
||||
"""
|
||||
Resizes images to the longest side 'target_length', as well as provides
|
||||
methods for resizing coordinates and boxes. Provides methods for
|
||||
transforming both numpy array and batched torch tensors.
|
||||
"""
|
||||
|
||||
def __init__(self, target_length: int) -> None:
|
||||
self.target_length = target_length
|
||||
|
||||
def apply_image(self, image: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Expects a numpy array with shape HxWxC in uint8 format.
|
||||
"""
|
||||
target_size = self.get_preprocess_shape(
|
||||
image.shape[0], image.shape[1], self.target_length
|
||||
)
|
||||
return np.array(resize(to_pil_image(image), target_size))
|
||||
|
||||
def apply_coords(
|
||||
self, coords: np.ndarray, original_size: Tuple[int, ...]
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Expects a numpy array of length 2 in the final dimension. Requires the
|
||||
original image size in (H, W) format.
|
||||
"""
|
||||
old_h, old_w = original_size
|
||||
new_h, new_w = self.get_preprocess_shape(old_h, old_w, self.target_length)
|
||||
new_coords = np.empty_like(coords)
|
||||
new_coords[..., 0] = coords[..., 0] * (new_w / old_w)
|
||||
new_coords[..., 1] = coords[..., 1] * (new_h / old_h)
|
||||
return new_coords
|
||||
|
||||
def apply_boxes(
|
||||
self, boxes: np.ndarray, original_size: Tuple[int, ...]
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Expects a numpy array shape Bx4. Requires the original image size
|
||||
in (H, W) format.
|
||||
"""
|
||||
boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size)
|
||||
return boxes.reshape(-1, 4)
|
||||
|
||||
def apply_image_torch(self, image: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Expects batched images with shape BxCxHxW and float format. This
|
||||
transformation may not exactly match apply_image. apply_image is
|
||||
the transformation expected by the model.
|
||||
"""
|
||||
# Expects an image in BCHW format. May not exactly match apply_image.
|
||||
target_size = self.get_preprocess_shape(
|
||||
image.shape[2], image.shape[3], self.target_length
|
||||
)
|
||||
return F.interpolate(
|
||||
image, target_size, mode="bilinear", align_corners=False, antialias=True
|
||||
)
|
||||
|
||||
def apply_coords_torch(
|
||||
self, coords: torch.Tensor, original_size: Tuple[int, ...]
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Expects a torch tensor with length 2 in the last dimension. Requires the
|
||||
original image size in (H, W) format.
|
||||
"""
|
||||
old_h, old_w = original_size
|
||||
new_h, new_w = self.get_preprocess_shape(
|
||||
original_size[0], original_size[1], self.target_length
|
||||
)
|
||||
coords = deepcopy(coords).to(torch.float)
|
||||
coords[..., 0] = coords[..., 0] * (new_w / old_w)
|
||||
coords[..., 1] = coords[..., 1] * (new_h / old_h)
|
||||
return coords
|
||||
|
||||
def apply_boxes_torch(
|
||||
self, boxes: torch.Tensor, original_size: Tuple[int, ...]
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Expects a torch tensor with shape Bx4. Requires the original image
|
||||
size in (H, W) format.
|
||||
"""
|
||||
boxes = self.apply_coords_torch(boxes.reshape(-1, 2, 2), original_size)
|
||||
return boxes.reshape(-1, 4)
|
||||
|
||||
@staticmethod
|
||||
def get_preprocess_shape(
|
||||
oldh: int, oldw: int, long_side_length: int
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Compute the output size given input size and target long side length.
|
||||
"""
|
||||
scale = long_side_length * 1.0 / max(oldh, oldw)
|
||||
newh, neww = oldh * scale, oldw * scale
|
||||
neww = int(neww + 0.5)
|
||||
newh = int(newh + 0.5)
|
||||
return (newh, neww)
|
||||
@@ -0,0 +1,269 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
# ====================== Self-Attention Block ======================
|
||||
class SelfAttention(nn.Module):
|
||||
def __init__(self, in_dim):
|
||||
super(SelfAttention, self).__init__()
|
||||
self.query_conv = nn.Conv2d(in_dim, max(1, in_dim // 8), kernel_size=1)
|
||||
self.key_conv = nn.Conv2d(in_dim, max(1, in_dim // 8), kernel_size=1)
|
||||
self.value_conv = nn.Conv2d(in_dim, in_dim, kernel_size=1)
|
||||
self.gamma = nn.Parameter(torch.zeros(1))
|
||||
|
||||
def forward(self, x):
|
||||
B, C, H, W = x.size()
|
||||
query = self.query_conv(x).view(B, -1, H * W).permute(0, 2, 1) # B, N, C'
|
||||
key = self.key_conv(x).view(B, -1, H * W) # B, C', N
|
||||
energy = torch.bmm(query, key) # B, N, N
|
||||
attention = F.softmax(energy, dim=-1)
|
||||
value = self.value_conv(x).view(B, -1, H * W) # B, C, N
|
||||
out = torch.bmm(value, attention.permute(0, 2, 1)) # B, C, N
|
||||
out = out.view(B, C, H, W)
|
||||
out = self.gamma * out + x
|
||||
return out
|
||||
|
||||
|
||||
# ====================== Attention Gate ======================
|
||||
class AttentionGate(nn.Module):
|
||||
def __init__(self, F_g, F_l, F_int):
|
||||
super(AttentionGate, self).__init__()
|
||||
self.W_g = nn.Sequential(
|
||||
nn.Conv2d(F_g, F_int, kernel_size=1, stride=1, padding=0, bias=True),
|
||||
nn.BatchNorm2d(F_int)
|
||||
)
|
||||
|
||||
self.W_x = nn.Sequential(
|
||||
nn.Conv2d(F_l, F_int, kernel_size=1, stride=1, padding=0, bias=True),
|
||||
nn.BatchNorm2d(F_int)
|
||||
)
|
||||
|
||||
self.psi = nn.Sequential(
|
||||
nn.Conv2d(F_int, 1, kernel_size=1, stride=1, padding=0, bias=True),
|
||||
nn.BatchNorm2d(1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
|
||||
def forward(self, g, x):
|
||||
# g: gating signal (deeper) ; x: skip connection (shallower)
|
||||
g1 = self.W_g(g)
|
||||
x1 = self.W_x(x)
|
||||
|
||||
# ensure same spatial size
|
||||
# if g1.size()[2:] != x1.size()[2:]:
|
||||
g1 = F.interpolate(g1, size=x1.size()[2:], mode='bilinear', align_corners=True)
|
||||
# g1 = F.interpolate(g1, size=(int(x1.size(2)), int(x1.size(3))), mode='bilinear', align_corners=True)
|
||||
|
||||
psi = self.relu(g1 + x1)
|
||||
psi = self.psi(psi)
|
||||
|
||||
# ensure psi matches x spatially
|
||||
# if psi.size()[2:] != x.size()[2:]:
|
||||
psi = F.interpolate(psi, size=x.size()[2:], mode='bilinear', align_corners=True)
|
||||
|
||||
return x * psi
|
||||
|
||||
|
||||
# ====================== Basic Conv Block ======================
|
||||
class conv_block(nn.Module):
|
||||
def __init__(self, in_ch, out_ch):
|
||||
super(conv_block, self).__init__()
|
||||
self.conv = nn.Sequential(
|
||||
nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1, bias=False),
|
||||
nn.BatchNorm2d(out_ch),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1, bias=False),
|
||||
nn.BatchNorm2d(out_ch),
|
||||
nn.ReLU(inplace=True)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.conv(x)
|
||||
|
||||
|
||||
# ====================== UNet3+ with Attention (fixed channels) ======================
|
||||
class UNet3Plus_Attention(nn.Module):
|
||||
def __init__(self, in_channels=3, num_classes=7, filters=[64, 128, 256, 512, 1024]):
|
||||
super(UNet3Plus_Attention, self).__init__()
|
||||
self.num_classes = num_classes
|
||||
F1, F2, F3, F4, F5 = filters # e.g. 64,128,256,512,1024
|
||||
|
||||
# ---------------- Encoder ----------------
|
||||
self.encoder1 = conv_block(in_channels, F1)
|
||||
self.pool1 = nn.MaxPool2d(2)
|
||||
self.encoder2 = conv_block(F1, F2)
|
||||
self.pool2 = nn.MaxPool2d(2)
|
||||
self.encoder3 = conv_block(F2, F3)
|
||||
self.pool3 = nn.MaxPool2d(2)
|
||||
self.encoder4 = conv_block(F3, F4)
|
||||
self.pool4 = nn.MaxPool2d(2)
|
||||
self.encoder5 = conv_block(F4, F5)
|
||||
|
||||
# ---------------- Self-Attention at bottleneck ----------------
|
||||
self.self_att = SelfAttention(F5)
|
||||
|
||||
# ---------------- 1x1 conv for multi-scale fusion ----------------
|
||||
# These compress encoder features to F1 channels for easier fusion where used.
|
||||
self.conv_1x1_1 = nn.Conv2d(F1, F1, kernel_size=1)
|
||||
self.conv_1x1_2 = nn.Conv2d(F2, F1, kernel_size=1)
|
||||
self.conv_1x1_3 = nn.Conv2d(F3, F1, kernel_size=1)
|
||||
self.conv_1x1_4 = nn.Conv2d(F4, F1, kernel_size=1)
|
||||
self.conv_1x1_5 = nn.Conv2d(F5, F1, kernel_size=1)
|
||||
|
||||
# ---------------- Decoder Blocks ----------------
|
||||
# IMPORTANT: set in_channels = actual concatenation channels at each level
|
||||
# Decoder4 concatenates upsampled conv_1x1 outputs (all compressed to F1) => 5*F1
|
||||
self.decoder4_cat_conv = conv_block(F1 * 5, F4) # output channels F4
|
||||
|
||||
# Decoder3 concatenation channels:
|
||||
# d1_up (conv_1x1_1) : F1
|
||||
# d2_up (conv_1x1_2) : F1
|
||||
# d3_up (conv_1x1_3) : F1
|
||||
# d4_up (decoder4 output) : F4
|
||||
# d5_up (conv_1x1_5) : F1
|
||||
dec3_in = F1 + F1 + F1 + F4 + F1 # = 4*F1 + F4
|
||||
self.decoder3_cat_conv = conv_block(dec3_in, F3)
|
||||
|
||||
# Decoder2 concatenation channels:
|
||||
# d1_up: F1
|
||||
# d2_up: F1
|
||||
# d3_up: decoder3 output F3
|
||||
# d4_up: decoder4 output F4
|
||||
# d5_up: F1
|
||||
dec2_in = F1 + F1 + F3 + F4 + F1
|
||||
self.decoder2_cat_conv = conv_block(dec2_in, F2)
|
||||
|
||||
# Decoder1 concatenation channels:
|
||||
# d1_up: F1
|
||||
# d2_up: decoder2 output F2
|
||||
# d3_up: decoder3 output F3
|
||||
# d4_up: decoder4 output F4
|
||||
# d5_up: F1
|
||||
dec1_in = F1 + F2 + F3 + F4 + F1
|
||||
self.decoder1_cat_conv = conv_block(dec1_in, F1)
|
||||
|
||||
# ---------------- Attention Gates ----------------
|
||||
self.att4 = AttentionGate(F_g=F5, F_l=F4, F_int=max(1, F4 // 2))
|
||||
self.att3 = AttentionGate(F_g=F4, F_l=F3, F_int=max(1, F3 // 2))
|
||||
self.att2 = AttentionGate(F_g=F3, F_l=F2, F_int=max(1, F2 // 2))
|
||||
self.att1 = AttentionGate(F_g=F2, F_l=F1, F_int=max(1, F1 // 2))
|
||||
|
||||
# ---------------- Final Fusion ----------------
|
||||
self.final_reduce = nn.Sequential(
|
||||
nn.Conv2d(F1 + F2 + F3 + F4 + F1, F1, kernel_size=1),
|
||||
nn.BatchNorm2d(F1),
|
||||
nn.ReLU(inplace=True)
|
||||
)
|
||||
# Note: final_reduce input channels use same sum as dec1 input (we fuse d1 + up(d2) + up(d3) + up(d4) + up(x5)
|
||||
# but to keep consistent we will recompute in forward and pass through this module (in_ch matches dec1_in)
|
||||
# For simplicity, we use a 1x1 to F1.
|
||||
|
||||
self.final_conv = nn.Conv2d(F1, self.num_classes, kernel_size=1)
|
||||
|
||||
self._init_weights()
|
||||
|
||||
def _init_weights(self):
|
||||
for m in self.modules():
|
||||
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
||||
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
|
||||
if getattr(m, "bias", None) is not None:
|
||||
nn.init.zeros_(m.bias)
|
||||
elif isinstance(m, nn.BatchNorm2d):
|
||||
nn.init.constant_(m.weight, 1)
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
# ---------------- Forward ----------------
|
||||
def forward(self, x):
|
||||
# Encoder
|
||||
x1 = self.encoder1(x) # [B, F1, H, W]
|
||||
x2 = self.encoder2(self.pool1(x1)) # [B, F2, H/2, W/2]
|
||||
x3 = self.encoder3(self.pool2(x2)) # [B, F3, H/4, W/4]
|
||||
x4 = self.encoder4(self.pool3(x3)) # [B, F4, H/8, W/8]
|
||||
x5 = self.encoder5(self.pool4(x4)) # [B, F5, H/16, W/16]
|
||||
|
||||
# Bottleneck self-attention
|
||||
x5 = self.self_att(x5)
|
||||
|
||||
# Attention gates on skip connections (ensure spatial alignment inside gate)
|
||||
x4 = self.att4(x5, x4)
|
||||
x3 = self.att3(x4, x3)
|
||||
x2 = self.att2(x3, x2)
|
||||
x1 = self.att1(x2, x1)
|
||||
|
||||
# ---- Prepare compressed feature maps (1x1) ----
|
||||
# These are used for multi-scale fusion where appropriate
|
||||
e1c = self.conv_1x1_1(x1) # [B, F1, H, W]
|
||||
e2c = self.conv_1x1_2(x2) # [B, F1, H/2, W/2]
|
||||
e3c = self.conv_1x1_3(x3) # [B, F1, H/4, W/4]
|
||||
e4c = self.conv_1x1_4(x4) # [B, F1, H/8, W/8]
|
||||
e5c = self.conv_1x1_5(x5) # [B, F1, H/16, W/16]
|
||||
|
||||
# ---------------- Decoder 4 (produce d4 with out channels F4) ----------------
|
||||
target4 = x4.size()[2:] # spatial of level 4
|
||||
d1_u4 = F.interpolate(e1c, size=target4, mode='bilinear', align_corners=True) # F1
|
||||
d2_u4 = F.interpolate(e2c, size=target4, mode='bilinear', align_corners=True) # F1
|
||||
d3_u4 = F.interpolate(e3c, size=target4, mode='bilinear', align_corners=True) # F1
|
||||
d4_c = e4c # F1
|
||||
d5_u4 = F.interpolate(e5c, size=target4, mode='bilinear', align_corners=True) # F1
|
||||
|
||||
d4_in = torch.cat([d1_u4, d2_u4, d3_u4, d4_c, d5_u4], dim=1) # 5*F1
|
||||
d4 = self.decoder4_cat_conv(d4_in) # out channels = F4
|
||||
|
||||
# ---------------- Decoder 3 (out channels F3) ----------------
|
||||
target3 = x3.size()[2:]
|
||||
d1_u3 = F.interpolate(e1c, size=target3, mode='bilinear', align_corners=True) # F1
|
||||
d2_u3 = F.interpolate(e2c, size=target3, mode='bilinear', align_corners=True) # F1
|
||||
d3_c = e3c # F1
|
||||
d4_u3 = F.interpolate(d4, size=target3, mode='bilinear', align_corners=True) # F4
|
||||
d5_u3 = F.interpolate(e5c, size=target3, mode='bilinear', align_corners=True) # F1
|
||||
|
||||
d3_in = torch.cat([d1_u3, d2_u3, d3_c, d4_u3, d5_u3], dim=1) # channels = 4*F1 + F4
|
||||
d3 = self.decoder3_cat_conv(d3_in) # out channels = F3
|
||||
|
||||
# ---------------- Decoder 2 (out channels F2) ----------------
|
||||
target2 = x2.size()[2:]
|
||||
d1_u2 = F.interpolate(e1c, size=target2, mode='bilinear', align_corners=True) # F1
|
||||
d2_c = e2c # F1
|
||||
d3_u2 = F.interpolate(d3, size=target2, mode='bilinear', align_corners=True) # F3
|
||||
d4_u2 = F.interpolate(d4, size=target2, mode='bilinear', align_corners=True) # F4
|
||||
d5_u2 = F.interpolate(e5c, size=target2, mode='bilinear', align_corners=True) # F1
|
||||
|
||||
d2_in = torch.cat([d1_u2, d2_c, d3_u2, d4_u2, d5_u2], dim=1) # channels = F1 + F1 + F3 + F4 + F1
|
||||
d2 = self.decoder2_cat_conv(d2_in) # out channels = F2
|
||||
|
||||
# ---------------- Decoder 1 (out channels F1) ----------------
|
||||
target1 = x1.size()[2:]
|
||||
d1_c = e1c # F1
|
||||
d2_u1 = F.interpolate(d2, size=target1, mode='bilinear', align_corners=True) # F2
|
||||
d3_u1 = F.interpolate(d3, size=target1, mode='bilinear', align_corners=True) # F3
|
||||
d4_u1 = F.interpolate(d4, size=target1, mode='bilinear', align_corners=True) # F4
|
||||
d5_u1 = F.interpolate(e5c, size=target1, mode='bilinear', align_corners=True) # F1
|
||||
|
||||
d1_in = torch.cat([d1_c, d2_u1, d3_u1, d4_u1, d5_u1], dim=1) # channels = F1 + F2 + F3 + F4 + F1
|
||||
d1 = self.decoder1_cat_conv(d1_in) # out channels = F1
|
||||
|
||||
# ---------------- Final feature fusion ----------------
|
||||
# Fuse d1 and upsampled coarser decoders + upsampled bottleneck
|
||||
# We will form a concat consistent with final_reduce 1x1 (which maps dec1_in -> F1)
|
||||
f_d2 = F.interpolate(d2, size=d1.size()[2:], mode='bilinear', align_corners=True)
|
||||
f_d3 = F.interpolate(d3, size=d1.size()[2:], mode='bilinear', align_corners=True)
|
||||
f_d4 = F.interpolate(d4, size=d1.size()[2:], mode='bilinear', align_corners=True)
|
||||
f_x5 = F.interpolate(e5c, size=d1.size()[2:], mode='bilinear', align_corners=True)
|
||||
|
||||
final_concat = torch.cat([d1, f_d2, f_d3, f_d4, f_x5], dim=1) # channels = F1 + F2 + F3 + F4 + F1
|
||||
# Reduce to F1
|
||||
final_feat = self.final_reduce(final_concat)
|
||||
out = self.final_conv(final_feat) # [B, num_classes, H, W]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# # quick sanity test (only run if file executed directly)
|
||||
# if __name__ == "__main__":
|
||||
# model = UNet3Plus_Attention(in_channels=3, num_classes=7).cuda()
|
||||
# x = torch.randn(2, 3, 512, 512).cuda()
|
||||
# y = model(x)
|
||||
# print("Output:", y.shape) # expect [2, 7, 512, 512]
|
||||
Reference in New Issue
Block a user