The Refusals Are the Product Building an AI radiology platform for a hospital that cannot assume an internet connection

The Refusals Are the Product Building an AI radiology platform for a hospital that cannot assume an internet connection

A radiology AI platform is easy to demonstrate and hard to trust. Ours runs fourteen models across CT, MRI, radiography, ultrasound and ECG — and the part that took the real engineering was not getting them to run. It was deciding, case by case, when they should refuse to answer.

Medicine Center is a medical imaging platform built around an Orthanc DICOM archive. Scanners push studies to it; a backend ingests the metadata, classifies each series, and runs real segmentation and detection models against them; a browser dashboard gives the radiologist an embedded DICOM viewer, a review workflow, and a signed Arabic report at the end of it.

It was written for a hospital on the Syrian coast, and that context shaped almost every technical decision in it. Bandwidth is not assumed. A cloud API is not assumed. The imaging centres in the surrounding towns are largely single-physician practices running on paper — no worklist, no online booking, often no web presence at all. Software that phones home, licenses per-study, or expects a stable link to a data centre is not software those clinics can use.

14AI models across five modalities
117anatomical structures measured per CT
762backend tests, all passing
0outbound network calls at runtime

What it actually does

The pipeline is deliberately one-directional. A modality sends a study over DICOM to the archive. Ingestion reads it through the archive’s REST API — never off its file tree — and records patients, studies, series and instances, matched on DICOM UIDs rather than on whatever identifiers happen to be in the file. On the way in, each series is classified: body region, slice thickness, contrast phase, and the reconstruction kernel it was built with. Those four facts decide, later, which models are even allowed to look at it.

Architecture diagram: acquisition, archive, application and clinic layers, with session authentication, an append-only audit log and no outbound internet.
Figure 1. The browser never talks to the PACS, and the backend never reads its storage directly. Every image request goes through an authenticated proxy that writes an audit row naming the patient.

Analysis is asynchronous, because the machine has one GPU and a segmentation run takes minutes. Every run is queued in the database — not in Redis or Celery, because with a single GPU the queue’s job is to serialise, not to distribute — and executed by a worker in a fresh subprocess. That subprocess isolation is not defensive excess: an earlier in-process design left fragmented GPU memory behind after a crash, which then broke unrelated later requests in the long-lived server. A process exit reclaims CUDA memory reliably; nothing else does.

The radiologist reviews each finding, records a diagnosis, and signs. Signing re-checks the password — a logged-in but unattended workstation must not be enough to issue a diagnostic document — and the signer’s name and licence number are snapshotted onto the report, so a later rename or account deletion cannot rewrite something already issued.

Fourteen models, and where each one came from

Breadth here is not the achievement; provenance is. Every checkpoint in the system is either vendored as local files or prefetched at container build time, verified against the checksum its publisher distributes, and carries its licence and its paper in the code that loads it.

Model inventory grouped by modality: seven CT models, three radiography, three MR, two ultrasound and four ECG, each with its licence.
Figure 2. Thirteen of the fourteen are published, externally validated checkpoints. The fourteenth was trained in-house — and is the one the system trusts least.

The CT path measures 117 anatomical structures with volumes and mean attenuation, then runs a set of opportunistic screens over the same voxels the segmentation already loaded: coronary calcium, aortic diameter, vertebral bone density, hepatic steatosis, emphysema, renal calculi, and a morphological screen for displaced fractures of the ribs, hip and skull. These cost about thirty seconds of CPU and no extra GPU time, because the images are already in memory — the whole-body run had been throwing that information away.

The ECG path is the newest and the most unusual. Machines at the site cannot export DICOM waveforms, so the system accepts a photograph of the printout: it recovers the grid pitch by autocorrelation, splits the page into lead rows, follows each trace, and reconstructs a twelve-lead signal. When a machine can export DICOM, that path is taken instead, and the report says which one was used — because a digitised printout and a digital export are not the same evidence.

The part that took the longest: deciding what to show

The ECG model emits 77 diagnostic classes. Its authors publish, for each one, the threshold they chose and the accuracy it reached — and those numbers say plainly that most of the label set is unusable as an alert. The median per-class F1 is 0.388.

So the labels are tiered. Twelve are strong enough to state as findings. Twenty-three are shown with their accuracy printed beside them, so two rows at the same probability are not read as equal claims. Twenty-one are computed and stored but never displayed. And twenty-one sit in a fourth tier that only exists because the first attempt at this was wrong.

The four display tiers for 77 ECG labels: 12 reported, 23 secondary, 21 screening, 21 suppressed, with the acute MI and complete heart block cases explained.
Figure 3. Acute myocardial infarction scored an F1 of 0.329 and was discarded. The same published numbers, read differently, show a detector that catches 92.8% of cases.

An F1 threshold is a prevalence filter wearing a quality filter’s clothes. It throws away exactly the rare, dangerous conditions you most wanted to catch.

F1 collapses for a rare condition however well a model ranks it. Derive the operating point from the same three published figures and acute MI is 92.8% sensitive at 20.0% precision — a 32-fold lift over chance. Across the 41 labels the original gate had suppressed, the median sensitivity was 93.1%.

The fix was not to lower the bar. It was to add a tier with a precision floor, and to mark everything in it as one-way: a prompt to look, never a finding and never reassurance. That floor is what keeps the tier honest — complete heart block is 89.8% sensitive and 1.7% precise, so a sensitivity-only rule would have admitted it and put a heart-block alert on one ECG in eight. It stays suppressed. A channel that fires on everything is one people learn to dismiss, and then it fails on the day it matters.

The refusals

Roughly forty rules in this codebase exist to stop a model answering a question it cannot answer. Each one was written after the opposite was tried, measured, or shipped — and produced output that looked entirely ordinary while meaning nothing.

Nine refusal rules: contrast gating, median-beat ECG rejection, bone-kernel rejection, patient identity, calibration, inference limits, negative results, evidence strength and clinical safety.
Figure 4. Nine of about forty. Each is enforced in code and documented at the line that enforces it.

Two are worth expanding, because they are the ones that would never show up in a demo.

A report about nothing

A head CT arrived here exported as a bone reconstruction only — the scanner’s soft-tissue series was never sent. Haemorrhage detection ran on it and produced a perfectly ordinary report, flagging 50 of 434 slices, complete with subtypes and percentages. Nothing in the output suggested anything was wrong.

Measured on that series, brain noise is 11.3 Hounsfield units against a grey-to-white contrast of roughly 7 to 10. The noise exceeds the signal the model reads. Neither a positive nor a negative from it means anything. The report was not so much wrong as not about anything, which is worse, because it looks the same. The reconstruction kernel is now read at ingestion and checked before that model is offered — and the refusal message names the series the scanner almost certainly still has, so the person reading it can act.

“Overall: normal”

The report’s organ summary panel had two states. It counted abnormal organs, and if there were none it printed “Overall: normal” in green at the top of page one. An ECG report has no organ table. Neither does a bone-age report, a fracture report, or a haemorrhage report. Every one of them carried an explicit, positive, green claim of normality about a region nothing had ever looked at — 79 documents in this installation would have.

There are three states now, and the third says, in as many words, that there is no verdict here in either direction. That distinction — between “we looked and it was fine” and “we did not look” — runs through the whole system. Screens that find nothing emit an explicit negative row rather than staying silent, because silence and a clean result render identically, and only one of them is true.

Patient data stays where it is

The platform holds real patient data, so the security model is not a checklist item. Every route requires a server-side session — opaque rows, revocable the instant someone leaves, with only the hash stored, chosen over a stateless token precisely because a token cannot be revoked before it expires. Roles are enforced in the API, never in the interface; hiding a button is a courtesy, and the same request issued by hand is still refused.

Every access to patient data lands in an append-only audit log — semantic events from the routers, plus a middleware backstop that records every state-changing request, so nothing can happen untraced even if a future route forgets. The log deliberately records the DICOM patient identifier and never the name: it exists to record that patient data was touched, not to become a second copy of it.

In the packaged deployment, the database, the archive and the backend publish no ports at all. Only the web front door and the DICOM port a scanner needs. TLS is mandatory and there is no flag to turn it off. Nightly backups cover all three data stores together — an index without its files restores nothing useful — and are encrypted as they are created, piped straight into GPG so plaintext never touches the disk in the first place.

It runs with no internet access. Every model weight is either vendored in the repository or fetched during the container build, and the running container makes no outbound calls. That is a reproducibility property before it is a security one: each deployment gets the weights the image was built and tested with, not whatever a registry happens to serve that day. It also happens to be the difference between a system a Syrian hospital can install and one it cannot.

Written in Arabic, and that is harder than it sounds

The reports are Arabic and right-to-left, rendered through HTML and CSS rather than a lower-level PDF library, because the browser engine handles Arabic shaping and bidirectional layout natively.

Three separate times, in three unrelated modules, the Unicode bidirectional algorithm published a sentence that said the opposite of its source. The characters <, >, and are defined as mirrored: in a right-to-left run the renderer is required to draw each as its opposite. So the definition of an abdominal aortic aneurysm — “≥ 30 mm” — printed as “≤ 30 mm”. A rule stating that volumes under 50 ml are not reported printed as “over 50 ml”. An ECG printout layout of “12×1”, which exists on every machine, printed as “1×12”, which exists on none.

None of this is findable by reading the code, and none of it is findable by extracting text from the PDF, which hands back the character the author typed. It is visible only by rendering the page to an image and looking at the glyphs. There is now one module that owns the rule, and an automated sweep over every Arabic string in the codebase that fails the build if an unprotected mirrored operator reappears.

What it is not

This is the part most vendors leave out, so it goes in the middle of the page rather than the bottom of a footer.

  • It is an assistive tool, not a diagnostic device. There is no CE mark and no FDA clearance, and none is claimed.
  • Nothing here has been clinically validated on this population. Every accuracy figure in the system is the model authors’, measured on their data. Not one number was measured on a Syrian patient.
  • Several models are explicitly research-grade — their own metadata says “not to be used for diagnostic purposes”, and the system prints that sentence next to every finding they produce.
  • The fracture screen is not a fracture detector. It sees displaced discontinuities in a bone mask. A hairline fracture is invisible to it — completely, not merely less reliably — and every result it emits says so, negative results included.
  • No cardiologist has reviewed the ECG tiering, the preventive pathway table, or the risk estimates. No radiologist has reviewed the reference thresholds.

Each of those limits is written into the product, not just into the documentation. The scope note that says which patients and which bones the fracture model never looked at is printed above the result, not below it — because a reader who meets an empty table first has already concluded “no fracture” before reaching any caveat underneath.

Why build it this way

The temptation in medical AI is to ship the demo: run the model, print the number, let the confidence interval be someone else’s problem. It demos beautifully. It also produces exactly the failure this whole system is organised against — a document that looks like every other document, carries a plausible finding, and is about nothing at all.

A wrong answer is recoverable. A confident answer about nothing is not, because nobody goes back to check it.

So the platform is built out of gates as much as models: contrast phase gating attenuation rules, reconstruction kernel gating haemorrhage detection, calibration tags gating millimetre measurements, publication-backed thresholds gating every clinical cutoff, and a citation next to each one in the source. Where no citable threshold exists, the system reports the measurement and declines to grade it, rather than inventing a cutoff that would look identical to a real one.

That is not caution for its own sake. It is the only version of this software a physician can build a habit around — and a tool nobody trusts on the fifth study is worth nothing on the first.


Medicine Center is roughly 28,000 lines of Python and 15,000 lines of TypeScript, with about 11 GB of vendored model weights, running on a single 8 GB GPU — and the ECG path, measured, runs on an ordinary desktop with no GPU at all. If you run an imaging centre or a hospital department and want to talk about what this could look like on your equipment, get in touch.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *