BEDINGTE Freigabe der macOS Version Sequoia für tomedo®
Alle Hinweise und Informationen finden Sie unter folgendem Link.
Hi Zusammen,

KI im Input Management ist schon länger ein großes Thema bei vielen Versicherungen - also kurz gesagt: Den Informationseingang vollständig erfassen und bedarfsgerecht zu kanalisieren...

Wir bekommen leider Gottes von vielen Leistungserbringern immer noch massig Briefe per Post. Diese werden heute in die Akte des Patienten eingescannt. Das Problem: Die Briefe müssen ja gelesen werden und hin und wieder leiten sich daraus Folgeaktivitäten ab. Und genau hier könnte man ansetzen.

Die Idee: Brief kommt rein, wird eingescannt, eine KI checkt den Text und extrahiert wichtige Informationen und stellt bei Bedarf dem Arzt eine Aufgabe, erstellt einen Karteieintrag oder schreibt dem Patienten eine E-Mail mit der Bitte um Wiedervorstellung oder Termin zur Befundbesprechung. So könnte immens viel Zeit gespart gewerden. Technisch sollte das machbar sein, die Frage ist, wie weit ist Zollsoft (oder andere Partner), wenns um solche Themen geht? Mir ist klar, das Thema "vertrauenswürdige KI" ist heikel, also vertrau ich der KI wirklich, dass die im Brief nichts übersehen oder falsch interpretiert hat...aber trotzdem wäre das m.E. ein cooler UseCase.

Hat jemand von euch Erfahrung damit oder ähnliche Ideen?
Gefragt in Frage von (150 Punkte)
+1 Punkt

4 Antworten

Hi,

da gibt es bereits viele Threads und einige Lösungen, zB von dem Kollegen Baumann oder Klaproth...

https://forum.tomedo.de/index.php/82980/chatgpt-aus-cke-futtern-mit-standard-prompt-applescript-hurden?show=82980#q82980

Aktuell gab es seitens Zollsoft auch eine Umfrage, welche use case sinnvoll sein könnten....

 

-js
Beantwortet von (5.4k Punkte)
0 Punkte
Habe Anfang der Woche ein Webmeeting mit Herrn Findeisen gehabt, der mir entsprechende Entwicklungen von Zollsoft demonstriert hat. Die zielen genau auf so etwas ab und gehen teilweise sogar noch darüber hinaus.

Allerdings wäre noch mehr möglich, was allerdings nicht ohne weiteres durch Zollsoft zur Verfügung gestellt werden kann.  Vielleicht kann ich da demnächst mehr berichten.
Beantwortet von (36.5k Punkte)
Bearbeitet von
0 Punkte
Habe am Wochenende nochmal mit Python zum Thema automatisierte Erkennung von eingescannten Dokumenten gespielt. Herr Klaproth hat da ja schon extrem viel an Problemen gelöst.

Ich habe für mich einen Ablauf realisieren können, der mir erst einmal zeigen soll, welche Schritte (besonders OCR) in welcher Form in ein Python-Programm zu integrieren sind. Mittels Python sind auch sehr komplexe, vollständig automatisch ablaufende Prozesse vorstellbar, weshalb ich diesen Weg ausprobieren wollte.

Ziel ist (wie im Thread oben formuliert):

1. Dokument kommt per Scan oder als Mail-Anhang (drag & drop) in einen Lausch-Ordner

2. Python-Programm liest den Text aus dem PDF per OCR aus (bisher setzt die Logik einzelne PDFs für einzelne Dokumente voraus, Aufsplitten von einem Sammel-Befund-PDF-Monster mit x verschiedenen Befunden in einer PDF-Datei wäre ein Riesenproblem)

3. OCR-Ergebnis wird mittels Vergleich einer CSV-Patienten-Datei erkannt und die Patienten-ID ermittelt. Anschließend wird der OCR-Text anonymisiert (Dummy statt Name und Vorname etc.; Grund: Datenschutz)

4. Anonymisiertes OCR-Ergebnis wird per openAI-API an ChatGPT gesandt mit zunächst einem prompt zum Erkennen, um welche Art von Text es sich handelt (Klinikbrief, Arztbrief ambulant, CT-/MRT-/...-Befund, Behördenanfrage). Dieser Schritt muss natürlich gut funktionieren, damit er automatisch und sinnvoll ablaufen kann.

5. Wenn der Text von ChatGPT korrekt zugeordnet ist (klappt meiner Erfahrung nach sehr gut), kann mit einem auf dem Dokumententyp basierenden, zweiten Prompt nochmal zu ChatGPT gesandt werden, um dokumentenspezifisch ausgewertet werden: "Fasse den Arztbericht in dieser spez. Form zusammen: ..." Vielleicht gelingt es auch, beide Schritte in einem "Mega"-Prompt zusammenzufassen, glaube ich aber nicht.

6. Das Ergebnis dieser Abfrage kann dann in eine Datei (mit Patienten-ID als Namen, somit für tomedo dem passenden Patienten zuzuordnen) in einen tomedo-Lauschordner abgelegt werden.

7. Wenn der Text, der das Ergebnis des Pythonprogrammes darstellt, sinnvolle Code-Wörter enthält, kann wiederum mittels Aktionskettenauslöser/bedingung (Karteieintrag vom Typ XXX enthält Codewort XYZ) eine gewünschte Reaktion ausgelöst werden.

Der gleich folgende Code zeigt einige, aber noch nicht alle der oben genannten Schritte, es ging zunächst darum zeigen zu können, dass die OCR-Funktion alltagstauglich funktioniert (ist wirklich der Fall). Erste Versuche mit den Python-Modulen tesseract und easyOCR waren unbefriedigend, da entweder sehr ungenau oder sehr langsam. Durch die (etwas mühsam zu realisierende) Verwendung vom Apple-eigenen OCR-Modul (Vision Framework, kommt auch in der Vorschau zum Einsatz) gelang die sehr gute und schnelle OCR-Funktion.

Der Rest ist (wie immer) sehr viel Fleißarbeit, um die übrigen Schritte sinnvoll aufeinander abzustimmen und das korrekte Funktionieren auch der GPT-prompts zu prüfen.

Ich wollte für Interessierte vorab zeigen, was auf diesem Wege grundsätzlich möglich ist (code ist zu lang für eine Antwort, daher gleich noch eine Antwort hinterher).
Beantwortet von (7.1k Punkte)
0 Punkte
import numpy as np
import os
import csv
import fitz  # PyMuPDF
from tkinter import Tk, Canvas, Label, Button, filedialog, PhotoImage, messagebox
from PIL import Image
from PyPDF2 import PdfReader, PdfWriter
from objc import loadBundleFunctions, loadBundle
import Quartz
import CoreText
import Vision
import re


class PDFSplitterGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("PDF-Seiten teilen und Patienteninformationen auslesen")
        self.pdf_path = None
        self.output_dir = os.path.dirname(os.path.abspath(__file__))  # Projektordner
        self.images = []
        self.page_selection = []
        self.patients = []  # Liste zum Speichern der Patientendaten

        # Benutzeroberfläche
        self.label = Label(root, text="PDF-Teiler mit OCR und Patientenabgleich")
        self.label.pack(pady=10)

        self.select_pdf_button = Button(root, text="1. PDF auswählen", command=self.select_pdf)
        self.select_pdf_button.pack(pady=5)

        self.load_button = Button(root, text="2. PDF laden und anzeigen", command=self.load_pdf, state="disabled")
        self.load_button.pack(pady=5)

        self.split_button = Button(root, text="3. PDF exportieren", command=self.export_pdf, state="disabled")
        self.split_button.pack(pady=10)

        self.ocr_button = Button(root, text="4. OCR auf ausgewählten Seiten", command=self.ocr_selected_pages, state="disabled")
        self.ocr_button.pack(pady=10)

        self.canvas = Canvas(root, width=800, height=600, bg="white")
        self.canvas.pack()

        # Patientendaten laden
        self.load_patient_data()

    def select_pdf(self):
        self.pdf_path = filedialog.askopenfilename(filetypes=[("PDF-Dateien", "*.pdf")])
        if self.pdf_path:
            messagebox.showinfo("PDF ausgewählt", f"PDF-Datei ausgewählt:\n{self.pdf_path}")
            self.load_button.config(state="normal")

    def load_pdf(self):
        if not self.pdf_path:
            messagebox.showwarning("Keine PDF-Datei", "Bitte wählen Sie zuerst eine PDF-Datei aus.")
            return
        self.images = self.extract_thumbnails(self.pdf_path)
        self.display_thumbnails()

    def extract_thumbnails(self, pdf_path):
        images = []
        doc = fitz.open(pdf_path)

        # Zielgröße der Thumbnails
        thumbnail_width = 150
        thumbnail_height = 200

        for page_num in range(len(doc)):
            page = doc[page_num]
            mat = fitz.Matrix(thumbnail_width / page.rect.width, thumbnail_height / page.rect.height)
            pix = page.get_pixmap(matrix=mat)
            img = PhotoImage(data=pix.tobytes("ppm"))
            images.append((page_num + 1, img))
        return images

    def display_thumbnails(self):
        self.canvas.delete("all")
        self.page_selection = [False] * len(self.images)

        for i, (page_num, img) in enumerate(self.images):
            x = (i % 4) * 200 + 10
            y = (i // 4) * 250 + 10

            image_id = self.canvas.create_image(x, y, anchor="nw", image=img)
            text_id = self.canvas.create_text(x + 75, y + 220, text=f"Seite {page_num}", fill="black")
            rect_id = self.canvas.create_rectangle(x, y, x + 150, y + 200, outline="blue", width=2)

            self.canvas.tag_bind(image_id, "<Button-1>", lambda event, idx=i, rect=rect_id: self.toggle_selection(idx, rect))
            self.canvas.tag_bind(text_id, "<Button-1>", lambda event, idx=i, rect=rect_id: self.toggle_selection(idx, rect))
            self.canvas.tag_bind(rect_id, "<Button-1>", lambda event, idx=i, rect=rect_id: self.toggle_selection(idx, rect))

        self.split_button.config(state="normal")
        self.ocr_button.config(state="normal")

    def toggle_selection(self, idx, rect_id):
        self.page_selection[idx] = not self.page_selection[idx]
        color = "green" if self.page_selection[idx] else "blue"
        self.canvas.itemconfig(rect_id, outline=color)

    def export_pdf(self):
        selected_pages = [i for i, selected in enumerate(self.page_selection) if selected]
        if not selected_pages:
            messagebox.showwarning("Keine Seiten ausgewählt", "Bitte wählen Sie mindestens eine Seite aus.")
            return

        reader = PdfReader(self.pdf_path)
        writer = PdfWriter()

        for page_num in selected_pages:
            writer.add_page(reader.pages[page_num])

        output_file = os.path.join(self.output_dir, "Exportierte_Seiten.pdf")
        with open(output_file, "wb") as f:
            writer.write(f)

        messagebox.showinfo("Export abgeschlossen", f"Das PDF wurde gespeichert unter:\n{output_file}")

    def load_patient_data(self):
        file_path = os.path.join(self.output_dir, "patients.csv")
        try:
            with open(file_path, "r", encoding="utf-8") as file:
                reader = csv.DictReader(file, delimiter=";")
                required_columns = {"patientid", "vorname", "nachname", "geburtsdatum"}
                if not required_columns.issubset(reader.fieldnames):
                    raise ValueError("Die Datei 'patients.csv' enthält nicht alle erforderlichen Spalten.")
                self.patients = [row for row in reader]
        except FileNotFoundError:
            messagebox.showwarning("Patientendaten fehlen", f"Die Datei '{file_path}' wurde nicht gefunden.")
            self.patients = []
        except ValueError as e:
            messagebox.showerror("Ungültige CSV-Datei", str(e))
            self.patients = []

    def anonymize_patient_data(self, text):
        """
        Ersetzt Patientendaten im Text durch Dummy-Werte.
        - Nachname -> "Name"
        - Vorname -> "Vorname"
        - Geburtsdatum -> "01.01." + Geburtsjahr
        - Wohnort und Straße -> Entfernen
        """
        for patient in self.patients:
            if patient["nachname"] in text:
                text = text.replace(patient["nachname"], "Name")
            if patient["vorname"] in text:
                text = text.replace(patient["vorname"], "Vorname")
            if patient["geburtsdatum"] in text:
                geburtsjahr = patient["geburtsdatum"].split(".")[-1]
                dummy_datum = f"01.01.{geburtsjahr}"
                text = text.replace(patient["geburtsdatum"], dummy_datum)
            wohnort = patient.get("wohnort", "")
            if wohnort and wohnort in text:
                text = text.replace(wohnort, "")
            strasse = patient.get("strasse", "")
            if strasse and strasse in text:
                text = text.replace(strasse, "")
        text = re.sub(r"\s{2,}", " ", text).strip()
        return text

    def find_patient_pid(self, text):
        for patient in self.patients:
            if (patient["nachname"] in text and
                patient["vorname"] in text and
                (patient["geburtsdatum"] in text or patient["geburtsdatum"].replace(".", ".20") in text)):
                return patient["patientid"]
        return None

    def ocr_selected_pages(self):
        selected_pages = [i for i, selected in enumerate(self.page_selection) if selected]
        if not selected_pages:
            messagebox.showwarning("Keine Seiten ausgewählt", "Bitte wählen Sie mindestens eine Seite aus.")
            return

        doc = fitz.open(self.pdf_path)
        results = []
        success_messages = []

        for page_num in selected_pages:
            pix = doc[page_num].get_pixmap(dpi=300)
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

            # OCR mit Vision
            ocr_result = self.perform_mac_ocr(img)
            text = "\n".join(ocr_result)

            # Anonymisiere Patientendaten
            anonymized_text = self.anonymize_patient_data(text)

            pid = self.find_patient_pid(text)
            if pid:
                for patient in self.patients:
                    if patient["patientid"] == pid:
                        success_messages.append(
                            f"Patient {patient['vorname']} {patient['nachname']}, geb. {patient['geburtsdatum']}, ID {pid}"
                        )
                        break

            page_results = f"Seite {page_num + 1}:\n"
            page_results += f"Patient-ID: {pid if pid else 'Nicht gefunden'}\n"
            page_results += "OCR-Text:\n"
            page_results += f"{anonymized_text}\n"
            results.append(page_results)

        self.save_ocr_results("\n".join(results))

        success_text = "\n".join(success_messages) if success_messages else "Keine Patienten gefunden."
        messagebox.showinfo("OCR abgeschlossen", f"Die Ergebnisse wurden gespeichert.\n\n{success_text}")

    def perform_mac_ocr(self, image):
        import Vision
        from io import BytesIO

        print(f"Bildgröße: {image.size}, Modus: {image.mode}")

        image = image.convert("RGBA")
        buffer = BytesIO()
        image.save(buffer, format="PNG")
        png_data = buffer.getvalue()

        request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(
            lambda req, err: None
        )
        request.setRecognitionLanguages_(["de", "en"])

        handler = Vision.VNImageRequestHandler.alloc().initWithData_options_(
            png_data, None
        )
        success, error = handler.performRequests_error_([request], None)
        if not success:
            raise ValueError(f"OCR konnte nicht ausgeführt werden: {error}")

        results = request.results()
        if not results:
            return []

        recognized_texts = []
        for res in results:
            if isinstance(res, Vision.VNRecognizedTextObservation):
                top_candidate = res.topCandidates_(1)
                if top_candidate:
                    recognized_texts.append(top_candidate[0].string())
        return recognized_texts

    def save_ocr_results(self, results):
        output_file = os.path.join(self.output_dir, "ocr_results_with_pid.txt")
        with open(output_file, "w", encoding="utf-8") as f:
            f.write(results)


def run_pdf_splitter():
    root = Tk()
    app = PDFSplitterGUI(root)
    root.mainloop()


if __name__ == "__main__":
    run_pdf_splitter()

 

Beantwortet von (7.1k Punkte)
0 Punkte

Hallo Hans Jörg,

Vielen Dank für deinen engagierten Einsatz! Meine Erfahrung zeigt jedoch, dass die Extraktion von Dokumenten mittels OCR oft problematisch ist – insbesondere bei Sonderzeichen oder handschriftlichen Notizen. In solchen Fällen werden häufig ungewollte Sonderzeichen ausgegeben, die von Scripts falsch interpretiert werden. Das führt nicht nur zu Störungen im gesamten Ablauf, sondern birgt auch das Risiko von Fehlinformationen, die in manchen Dokumenten enthalten sein können.

Aus diesem Grund sehe ich die Arbeit mit einem "Superprompt" kritisch. Ich gehe mittlerweile so vor, dass ich jedes Dokument zunächst sorgfältig überprüfe, bevor ich es weiter auswerte. Nur so können wirklich belastbare Ergebnisse erzielt werden. Aus denselben Überlegungen heraus betrachte ich die ePA mit Skepsis. Ich würde mich niemals allein auf die Richtigkeit der darin enthaltenen Daten verlassen und arbeite ausschließlich mit selbst erhobenen und geprüften Daten.

Der Aufwand, eine solche Vorgehensweise in Tomedo zu implementieren, ist enorm und grenzt fast an Masochismus. Ich sehe aktuell auch nicht, dass Zollsoft eine Lösung präsentieren wird, die den gehobenen Ansprüchen gerecht wird. Zudem besteht das Risiko, dass Tomedo als Medizinprodukt zugelassen werden müsste, was weitere Herausforderungen mit sich bringt.

Bleibt also nur die individulle Konfiguration. Der Normaluser dürfte damit übefordert werden oder müßte monatelang Arbeit investieren so wie Du es tust.

Moin Christian,

zugegebenermaßen ist das auch viel Hobby & Spieltrieb, um zu sehen, wie weit man mit diesen Instrumenten kommt. Andere lösen Sudoku...

Die Fehlertoleranz solcher Systeme ist sicher immer ein relevantes Thema, dass auch mit (nahezu) perfektem OCR dazu zwingen wird, dass der Mensch den letzten Überblick behalten muss (human in the loop). Es wird immer Ausnahmen / Überraschungen geben, die vom Ablauf nicht erwartet/vorgesehen waren und zu sinnfreien Ergebnissen führen können/werden. Das Vorbereiten und "Vorkauen" der meisten Informationen sehe ich aber dennoch als großen Nutzen an. Mich würde es nicht stören, wenn von 100 Dokumenten, die durch solche eine (vollautomatische) Routine geschickt würden, bei der menschlichen Überprüfung 10 als fehlerhaft aussortiert und der generierte Karteieintrag z.B. wieder gelöscht werden müsste (Dokumentenstruktur falsch erkannt, dann falscher Prompt auf den OCR-Text angewandt: Resultat = Müll). Ich bin nach den bisherigen Erfahrungen eher optimistisch, dass eine kritisch begleitete und immer menschlich bestätigte Auswertung eine Hilfe sein kann, nicht unbedingt um die Abläufe vollständig automatisch zu erledigen, sondern eher um die anflutenden Daten in strukturierte Form gießen und dadurch im Verlauf besser nutzen zu können. Als Beispiel: Briefe aus dem Schlaflabor mit vielen Detailinformationen hatte ich ohne Auswertungsfunktion gelesen und dann in der Kartei abgelegt. Im Fliesstext wurden dann "menschliche" Notizen gemacht. Mittels automatisierter Auswertung erlaube ich mir den Luxus, die kompletten Daten (CPAP-Druck, Masken-Typ, Beginn der Therapie, viele weitere Details) zu extrahieren und strukturiert abzulegen. Dies erlaubt dann, wie Du nur zu gut weißt, die sinnvolle Nutzung in anderen Zusammenhängen. Die Vidierfunktion ist zwingendes Element der KI-generierten Beiträge...

Zusammengefasst sehe ich den Sinn meiner Bemühungen eher im Befehl: "Schreibe aus dem Brief/Befund, den ich gerade aufmerksam durchgelesen habe, die Informationen an die korrekte Stelle in der Kartei, damit ich/wir später damit effektiv weiterarbeiten kann/können."
Moin Hans Jörg

du hast völlig Recht. Ich wollte nur auf die verbundenen Probleme aufmerksam machen und betonen, dass die Konfiguration extrem aufwändig ist. Gern mal wieder  Austausch per TV.
18,370 Beiträge
26,631 Antworten
47,744 Kommentare
28,957 Nutzer