Please use this identifier to cite or link to this item: https://er.chdtu.edu.ua/handle/ChSTU/7051
Title: Програмний комплекс іридодіагностичного моніторингу
Authors: Голуб, Сергій Васильович
Нестеренко, Владислав Вячеславович
Keywords: іридодіагностика;програмний комплекс;веб-додаток;комп’ютерний зір;сегментація райдужки;іридознаки;моніторинг;Next.js;React;CASIA-Iris-Thousand;iridology;software;web application;computer vision;iris segmentation;iridosigns;monitoring;Next.js;React
Issue Date: 29-Jan-2026
Abstract: АНОТАЦІЯ Прізвище та ініціали студента: Нестеренко В.В. Назва КРМ: Програмний комплекс іридодіагностичного моніторингу Спеціальність (шифр і назва): 121 Інженерія програмного забезпечення Установа: Черкаський державний технологічний університет Місто, рік: Черкаси, 2025 В кваліфікаційній роботі розглянуто проблему створення сучасного програмного комплексу іридодіагностичного моніторингу як веб-орієнтованої інформаційної системи медичного призначення. Наявні програмні засоби іридодіагностики не забезпечують обробку зображень високої роздільної здатності, автоматичну сегментацію райдужки та кількісний моніторинг у часі, що зменшує об’єктивність і відтворюваність діагностики. На основі аналізу методів інженерії програмного забезпечення, алгоритмів комп’ютерного зору та вимог міжнародних стандартів до медичних інформаційних систем обґрунтовано доцільність поєднання модульної веб-архітектури з гібридними алгоритмічними підходами до обробки зображень. Метою роботи є розробка та впровадження програмного комплексу іридодіагностичного моніторингу на базі сучасних веб-технологій з інтегрованими алгоритмами комп’ютерного зору для автоматичної сегментації райдужкової оболонки, детекції іридознаків та кількісної оцінки динаміки змін у часі. В роботі сформульовано та формально описано задачі сегментації зіниці й райдужки, класифікації локальних текстурних утворень (іридознаків) та моніторингу динамічних параметрів; наведено математичні моделі, що забезпечують інваріантність до масштабу й повороту, а також стійкість до шумів і варіацій освітлення. Практичним результатом роботи є веб-орієнтований програмний комплекс, який реалізує повний цикл обробки іридозображень в браузері.
Student: Nesterenko V.V. Qualifying work title: Software Complex for Iridodiagnostic Monitoring Specialty: 121 Software Engineering Defense institution: Cherkasy state technological university Location and year: Cherkasy, 2025 The qualification work addresses the problem of developing a modern software complex for iridodiagnostic monitoring as a web-oriented medical information system. Existing iridodiagnostic software tools generally do not support processing of high-resolution images, automatic iris segmentation, or quantitative monitoring of patient condition over time, which significantly reduces the objectivity and reproducibility of diagnostic conclusions. Based on the analysis of software engineering methods, computer vision algorithms, and requirements of international standards for medical information systems, the feasibility of combining a modular web architecture with hybrid algorithmic approaches to image processing has been substantiated. The aim of the work is to develop and implement a software complex for iridodiagnostic monitoring based on modern web technologies with integrated computer vision algorithms for automatic segmentation of the iris, detection of iris signs (iridosigns), and quantitative assessment of dynamic changes over time. The work formulates and formally describes the tasks of pupil and iris segmentation, classification of local textural formations (iris signs), and monitoring of dynamic parameters. Mathematical models ensuring scale and rotation invariance, as well as robustness to noise and lighting variations, are presented. The practical result of the work is a web-based software suite that implements the complete cycle of iris image processing directly in the browser.
URI: https://er.chdtu.edu.ua/handle/ChSTU/7051
Appears in Collections:121 Інженерія програмного забезпечення (Інженерія програмного забезпечення)



Items in DSpace are protected by copyright, with all rights reserved, unless otherwise indicated.

Extracted text
 
ДОДАТОК Б  
  
  
  
  
  
  
  
  
Програмний комплекс іридодіагностичного моніторингу 
   
  
  
  
  
Текст програми  
482.ЧДТУ. 25 2487 12 01 
Листів 31 
  
  
  
Розробник    ________________  Нестеренко В.В. 
 
  
 
 
 
 
2025 
 
 
import React, { useState, useEffect, useRef } from 'react' 
import ImageUploadPanel from './components/ImageUploadPanel' 
import ControlsPanel from './components/ControlsPanel' 
import ImagePreview from './components/ImagePreview' 
import OverlaysCanvas from './components/OverlaysCanvas' 
import MonitoringPanel from './components/MonitoringPanel' 
import ThemeToggle from './components/ThemeToggle' 
import EyeAnalysisPanel from './components/EyeAnalysisPanel' 
  
import { applyAdjustmentsToImage, cropImageToRegion, normalizeImageToBox, 
EYE_CROP_PADDING } from './services/imageProcessing' 
import { detectEyesOnImage, type DetectedEyeRegion } from './services/faceDetection' 
import { segmentIrisOnImage } from './services/irisSegmentation' 
import { 
  findOrCreatePatient, 
  createSessionForPatient, 
  addMeasurementToSession, 
  clearMonitoringState, 
  getFullMonitoringSnapshot, 
  getAnnotationsForMeasurement, 
  addAnnotationForMeasurement, 
} from './services/monitoring' 
  
import { IRIS_SIGNS } from './config/irisSigns' 
import { exportMeasurementReport } from './services/reportExport' 
import { detectAutoIrisSigns, type AutoSignCandidate } from './services/autoIrisSigns' 
  
import type { 
  ImageItem, 
  UploadMode, 
  ImageAdjustments, 
  IrisSegmentationInfo, 
  MonitoringSession, 
  IrisSignAnnotation, 
105 
 
 
  TestSampleResult, 
  EyeAnalysis, 
} from './types' 
  
const App: React.FC = () => { 
  const [uploadMode, setUploadMode] = useState<UploadMode>('single') 
  const [images, setImages] = useState<ImageItem[]>([]) 
  const [selectedImageId, setSelectedImageId] = useState<string | null>(null) 
  
  const [adjustments, setAdjustments] = useState<ImageAdjustments>({ 
    contrast: 0, 
    gamma: 100, 
  }) 
  
  const [eyeRegions, setEyeRegions] = useState<DetectedEyeRegion[]>([]) 
  const [eyeAnalyses, setEyeAnalyses] = useState<Map<'left' | 'right', EyeAnalysis>>(new Map()) 
  const [selectedEyeSide, setSelectedEyeSide] = useState<'left' | 'right' | null>(null) 
  const [irisSegmentation, setIrisSegmentation] = useState<IrisSegmentationInfo | null>(null) 
  
  const [isDetecting, setIsDetecting] = useState(false) 
  const [isSegmenting, setIsSegmenting] = useState(false) 
  const [isDetectingSigns, setIsDetectingSigns] = useState(false) 
  
  const [patientName, setPatientName] = useState<string>('') 
  const [currentSession, setCurrentSession] = useState<MonitoringSession | null>(null) 
  const [snapshot, setSnapshot] = useState(getFullMonitoringSnapshot()) 
  const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null) 
  
  const [currentMeasurementId, setCurrentMeasurementId] = useState<string | null>(null) 
  const [currentAnnotations, setCurrentAnnotations] = useState<IrisSignAnnotation[]>([]) 
  
  const [selectedSignId, setSelectedSignId] = useState<string>('') 
  const [markingMode, setMarkingMode] = useState<boolean>(false) 
  
106 
 
 
  const analysisBlockRef = useRef<HTMLDivElement | null>(null) 
  
  const [autoSigns, setAutoSigns] = useState<AutoSignCandidate[]>([]) 
  
  const [showIrisMap, setShowIrisMap] = useState<boolean>(false) 
  
  const [testResults, setTestResults] = useState<TestSampleResult[]>([]) 
  const [isTestingBatch, setIsTestingBatch] = useState<boolean>(false) 
  
  useEffect(() => { 
    setSnapshot(getFullMonitoringSnapshot()) 
  }, []) 
  
  const handleFilesSelected = async (files: FileList) => { 
    const arr = Array.from(files) 
    const readers = arr.map( 
      file => 
        new Promise<ImageItem>((resolve, reject) => { 
          const reader = new FileReader() 
          reader.onload = async e => { 
            const result = e.target?.result 
            if (typeof result === 'string') { 
              try { 
                const { normalizedSrc, metadata } = await normalizeImageToBox(result) 
                const img = new Image() 
                img.onload = () => { 
                  resolve({ 
                    id: `${Date.now()}_${Math.random().toString(16).slice(2)}`, 
                    name: file.name, 
                    src: normalizedSrc, 
                    originalSrc: result, 
                    width: img.width, 
                    height: img.height, 
                    normalizationMetadata: metadata, 
107 
 
 
                  }) 
                } 
                img.onerror = reject 
                img.src = normalizedSrc 
              } catch (err) { 
                reject(err) 
              } 
            } else { 
              reject(new Error('Непідтримуваний формат зображення')) 
            } 
          } 
          reader.onerror = reject 
          reader.readAsDataURL(file) 
        }) 
    ) 
  
    Promise.all(readers) 
      .then(loaded => { 
        setImages(loaded) 
        setTestResults([]) 
        if (loaded.length > 0) { 
          setSelectedImageId(loaded[0].id) 
          setEyeRegions([]) 
          setEyeAnalyses(new Map()) 
          setSelectedEyeSide(null) 
          setIrisSegmentation(null) 
          setCurrentMeasurementId(null) 
          setCurrentAnnotations([]) 
          setAutoSigns([]) 
          setShowIrisMap(false) 
        } 
      }) 
      .catch(err => { 
        console.error(err) 
108 
 
 
        alert('Помилка при завантаженні зображень') 
      }) 
  } 
  
  const selectedImage = images.find(img => img.id === selectedImageId) ?? null 
  
  const handleApplyAdjustments = async () => { 
    if (!selectedImage) return 
    try { 
      const sourceImage = selectedImage.originalSrc || selectedImage.src 
      const processedSrc = await applyAdjustmentsToImage(sourceImage, adjustments) 
      const { normalizedSrc, metadata } = await normalizeImageToBox(processedSrc) 
      const normalizedImg = new Image() 
      await new Promise<void>((resolve, reject) => { 
        normalizedImg.onload = () => resolve() 
        normalizedImg.onerror = reject 
        normalizedImg.src = normalizedSrc 
      }) 
      setImages(prev => 
        prev.map(img => 
          img.id === selectedImage.id 
            ? { 
                ...img, 
                src: normalizedSrc, 
                originalSrc: img.originalSrc || img.src, 
                width: normalizedImg.width, 
                height: normalizedImg.height, 
                normalizationMetadata: metadata, 
              } 
            : img 
        ) 
      ) 
      setEyeRegions([]) 
      setIrisSegmentation(null) 
109 
 
 
      setCurrentMeasurementId(null) 
      setCurrentAnnotations([]) 
      setAutoSigns([]) 
      setShowIrisMap(false) 
    } catch (e) { 
      console.error(e) 
      alert('Помилка під час обробки зображення') 
    } 
  } 
  
  const handleDetectEyes = async () => { 
    if (!selectedImage) return 
    try { 
      setIsDetecting(true) 
      const regions = await detectEyesOnImage(selectedImage.src) 
       
      if (!regions.length) { 
        alert('Очі не виявлено на цьому зображенні.') 
        setIsDetecting(false) 
        return 
      } 
  
      const newAnalyses = new Map<'left' | 'right', EyeAnalysis>() 
  
      const sortedRegions = [...regions].sort((a, b) => a.x - b.x) 
  
      for (let i = 0; i < Math.min(sortedRegions.length, 2); i++) { 
        const region = sortedRegions[i] 
        const eyeSide: 'left' | 'right' = i === 0 ? 'right' : 'left' 
         
        const croppedSrc = await cropImageToRegion(selectedImage.src, region, 
EYE_CROP_PADDING) 
        const croppedImg = new Image() 
        croppedImg.src = croppedSrc 
110 
 
 
        await new Promise<void>((resolve, reject) => { 
          croppedImg.onload = () => resolve() 
          croppedImg.onerror = reject 
        }) 
  
        const cropX = Math.max(0, region.x - EYE_CROP_PADDING) 
        const cropY = Math.max(0, region.y - EYE_CROP_PADDING) 
  
        const adjustedRegionX = region.x - cropX 
        const adjustedRegionY = region.y - cropY 
  
        // Keep landmarks relative to the adjusted region box (consistent with original detection) 
        // Landmarks remain relative to region box, just the region box position changes 
        const adjustedLandmarks = region.landmarks 
          ? region.landmarks.map(l => ({ 
              x: l.x, 
              y: l.y, 
            })) 
          : undefined 
  
        const adjustedIrisLandmarks = region.irisLandmarks 
          ? region.irisLandmarks.map(l => ({ 
              x: l.x, 
              y: l.y, 
            })) 
          : undefined 
  
        const adjustedPupilCenter = region.pupilCenter 
          ? { 
              x: region.pupilCenter.x, 
              y: region.pupilCenter.y, 
            } 
          : undefined 
  
111 
 
 
        const adjustedRegion: DetectedEyeRegion = { 
          x: adjustedRegionX, 
          y: adjustedRegionY, 
          width: region.width, 
          height: region.height, 
          landmarks: adjustedLandmarks, 
          irisLandmarks: adjustedIrisLandmarks, 
          pupilCenter: adjustedPupilCenter, 
        } 
  
        newAnalyses.set(eyeSide, { 
          eyeSide, 
          croppedSrc, 
          croppedWidth: croppedImg.width, 
          croppedHeight: croppedImg.height, 
          region: adjustedRegion, 
          segmentation: null, 
          annotations: [], 
          autoSigns: [], 
        }) 
      } 
  
      setEyeAnalyses(newAnalyses) 
      setEyeRegions(regions) 
      setSelectedEyeSide(newAnalyses.size > 0 ? Array.from(newAnalyses.keys())[0] : null) 
      setIrisSegmentation(null) 
      setCurrentMeasurementId(null) 
      setCurrentAnnotations([]) 
      setAutoSigns([]) 
      setShowIrisMap(false) 
    } catch (e) { 
      console.error(e) 
      alert('Сталася помилка під час детекції очей.') 
    } finally { 
112 
 
 
      setIsDetecting(false) 
    } 
  } 
  
  const handleSegmentIris = async (eyeSide: 'left' | 'right') => { 
    if (!selectedImage || !selectedEyeSide) { 
      alert('Спочатку виконайте детекцію очей та оберіть око для аналізу.') 
      return 
    } 
  
    const eyeAnalysis = eyeAnalyses.get(eyeSide) 
    if (!eyeAnalysis) { 
      alert('Дані для обраного ока не знайдено.') 
      return 
    } 
  
    try { 
      setIsSegmenting(true) 
      const seg = await segmentIrisOnImage(eyeAnalysis.croppedSrc, eyeAnalysis.region) 
      if (!seg) { 
        alert('Не вдалося сегментувати райдужку/зіницю.') 
        const updated = new Map(eyeAnalyses) 
        updated.set(eyeSide, { ...eyeAnalysis, segmentation: null }) 
        setEyeAnalyses(updated) 
        if (selectedEyeSide === eyeSide) { 
          setIrisSegmentation(null) 
          setCurrentAnnotations([]) 
          setAutoSigns([]) 
          setShowIrisMap(false) 
        } 
        return 
      } 
  
      const segWithSide = { ...seg, eyeSide } 
113 
 
 
      const updated = new Map(eyeAnalyses) 
      updated.set(eyeSide, { ...eyeAnalysis, segmentation: segWithSide }) 
      setEyeAnalyses(updated) 
  
      if (selectedEyeSide === eyeSide) { 
        setIrisSegmentation(segWithSide) 
        setCurrentAnnotations([]) 
        setAutoSigns([]) 
        setShowIrisMap(false) 
      } 
    } catch (e) { 
      console.error(e) 
      alert('Сталася помилка під час сегментації.') 
      const updated = new Map(eyeAnalyses) 
      updated.set(eyeSide, { ...eyeAnalysis, segmentation: null }) 
      setEyeAnalyses(updated) 
      if (selectedEyeSide === eyeSide) { 
        setIrisSegmentation(null) 
        setCurrentAnnotations([]) 
        setAutoSigns([]) 
        setShowIrisMap(false) 
      } 
    } finally { 
      setIsSegmenting(false) 
    } 
  } 
  
  const handleDetectIrisSigns = async (eyeSide: 'left' | 'right') => { 
    if (!selectedEyeSide) { 
      alert('Спочатку виконайте детекцію очей та оберіть око для аналізу.') 
      return 
    } 
  
    const eyeAnalysis = eyeAnalyses.get(eyeSide) 
114 
 
 
    if (!eyeAnalysis || !eyeAnalysis.segmentation) { 
      alert('Спочатку виконайте сегментацію райдужки/зіниці для обраного ока.') 
      return 
    } 
  
    try { 
      setIsDetectingSigns(true) 
      const candidates = await detectAutoIrisSigns(eyeAnalysis.croppedSrc, 
eyeAnalysis.segmentation) 
       
      const previewAnnotations: IrisSignAnnotation[] = candidates.map((c, idx) => ({ 
        id: `auto_preview_${eyeSide}_${idx}`, 
        measurementId: '__preview__', 
        signId: c.signId, 
        x: c.x, 
        y: c.y, 
        radius: c.radius, 
      })) 
  
      const updated = new Map(eyeAnalyses) 
      updated.set(eyeSide, { 
        ...eyeAnalysis, 
        autoSigns: candidates, 
        annotations: previewAnnotations, 
      }) 
      setEyeAnalyses(updated) 
  
      if (selectedEyeSide === eyeSide) { 
        setAutoSigns(candidates) 
        setCurrentAnnotations(previewAnnotations) 
        setShowIrisMap(false) 
      } 
    } catch (e) { 
      console.error(e) 
115 
 
 
      alert('Сталася помилка під час виявлення іридознаків.') 
      const updated = new Map(eyeAnalyses) 
      updated.set(eyeSide, { ...eyeAnalysis, autoSigns: [], annotations: [] }) 
      setEyeAnalyses(updated) 
      if (selectedEyeSide === eyeSide) { 
        setAutoSigns([]) 
        setCurrentAnnotations([]) 
      } 
    } finally { 
      setIsDetectingSigns(false) 
    } 
  } 
  
  const handleToggleIrisMap = () => { 
    const hasAnySegmentation = Array.from(eyeAnalyses.values()).some( 
      analysis => analysis.segmentation !== null 
    ) 
    if (!hasAnySegmentation) { 
      alert('Спочатку виконайте сегментацію райдужки/зіниці хоча б для одного ока.') 
      return 
    } 
  
    setShowIrisMap(prev => !prev) 
  } 
  
  const handleSelectEye = (eyeSide: 'left' | 'right') => { 
    setSelectedEyeSide(eyeSide) 
    const eyeAnalysis = eyeAnalyses.get(eyeSide) 
    if (eyeAnalysis) { 
      setIrisSegmentation(eyeAnalysis.segmentation) 
      setCurrentAnnotations(eyeAnalysis.annotations) 
      setAutoSigns(eyeAnalysis.autoSigns) 
      setShowIrisMap(false) 
    } 
116 
 
 
  } 
  
  const handleStartNewSession = () => { 
    if (!patientName.trim()) { 
      alert("Введіть, будь ласка, ім'я пацієнта для створення сесії.") 
      return 
    } 
    const patient = findOrCreatePatient(patientName.trim()) 
    const label = `Сесія від ${new Date().toLocaleString()}` 
    const session = createSessionForPatient(patient.id, label) 
    setCurrentSession(session) 
    setSelectedSessionId(session.id) 
    setSnapshot(getFullMonitoringSnapshot()) 
    setCurrentMeasurementId(null) 
    setCurrentAnnotations([]) 
    setAutoSigns([]) 
    setShowIrisMap(false) 
  } 
  
  const handleSaveMeasurement = () => { 
    if (!selectedImage || !irisSegmentation) { 
      alert('Немає результатів сегментації для збереження.') 
      return 
    } 
    if (!currentSession) { 
      alert('Спочатку створіть або оберіть сесію моніторингу.') 
      return 
    } 
  
    const meas = addMeasurementToSession(currentSession.id, { 
      imageName: selectedImage.name, 
      pupilRadius: irisSegmentation.pupilRadius, 
      irisRadius: irisSegmentation.irisRadius, 
      pupilToIrisRatio: 
117 
 
 
        irisSegmentation.irisRadius > 0 
          ? irisSegmentation.pupilRadius / irisSegmentation.irisRadius 
          : 0, 
    }) 
  
    if (autoSigns.length > 0) { 
      autoSigns.forEach(c => { 
        addAnnotationForMeasurement(meas.id, c.signId, c.x, c.y, c.radius) 
      }) 
    } 
  
    setCurrentMeasurementId(meas.id) 
    setCurrentAnnotations(getAnnotationsForMeasurement(meas.id)) 
    setSnapshot(getFullMonitoringSnapshot()) 
  } 
  
  const handleClearHistory = () => { 
    if ( 
      window.confirm( 
        'Очистити всіх пацієнтів, сесії, вимірювання та розмітку іридознаків?' 
      ) 
    ) { 
      clearMonitoringState() 
      setSnapshot(getFullMonitoringSnapshot()) 
      setCurrentSession(null) 
      setSelectedSessionId(null) 
      setCurrentMeasurementId(null) 
      setCurrentAnnotations([]) 
      setAutoSigns([]) 
      setShowIrisMap(false) 
    } 
  } 
  
  const handleCanvasClick = (x: number, y: number) => { 
118 
 
 
    if (markingMode &amp;&amp; selectedSignId) { 
      alert( 
        'Зараз авто-розмітка працює автоматично після сегментації. ' + 
          'За потреби можна повернути ручне додавання міток у handleCanvasClick.' 
      ) 
    } 
  } 
  
  const handleExportReport = async () => { 
    if (!analysisBlockRef.current) { 
      alert('Немає області для формування звіту.') 
      return 
    } 
    if (!irisSegmentation || !selectedImage) { 
      alert('Немає результатів сегментації для формування звіту.') 
      return 
    } 
  
    const ratio = 
      irisSegmentation.irisRadius > 0 
        ? irisSegmentation.pupilRadius / irisSegmentation.irisRadius 
        : 0 
  
    const annotationsMeta = 
      currentAnnotations?.map(a => { 
        const def = IRIS_SIGNS.find(s => s.id === a.signId) 
        return { 
          label: def?.label ?? a.signId, 
          x: a.x, 
          y: a.y, 
          radius: a.radius, 
        } 
      }) ?? [] 
  
119 
 
 
    const meta = { 
      patientName: patientName || undefined, 
      sessionLabel: currentSession?.label, 
      measurementDate: new Date().toLocaleString(), 
      imageName: selectedImage.name, 
      pupilRadius: irisSegmentation.pupilRadius, 
      irisRadius: irisSegmentation.irisRadius, 
      ratio, 
      annotations: annotationsMeta, 
    } 
  
    const baseName = patientName.trim() || 'iridol_report' 
    const fileName = `${baseName}_${new Date() 
      .toISOString() 
      .slice(0, 19) 
      .replace(/[:T]/g, '-')}.pdf` 
  
    try { 
      await exportMeasurementReport(analysisBlockRef.current, meta, fileName) 
    } catch (e) { 
      console.error(e) 
      alert('Сталася помилка під час формування PDF-звіту.') 
    } 
  } 
  
  const handleRunCasiaBatchTest = async () => { 
    if (!images.length) { 
      alert('Спочатку завантажте набір зображень (наприклад, із CASIA) у режимі batch.') 
      return 
    } 
  
    if (!window.confirm('Запустити послідовне тестування всіх завантажених зображень?')) { 
      return 
    } 
120 
 
 
  
    setIsTestingBatch(true) 
    setTestResults([]) 
  
    const results: TestSampleResult[] = [] 
  
    for (const img of images) { 
      try { 
        const t0 = performance.now() 
        const eyes = await detectEyesOnImage(img.src) 
        const t1 = performance.now() 
        const detectTimeMs = t1 - t0 
        const detectSuccess = eyes.length > 0 
  
        let segmentSuccess = false 
        let segmentTimeMs = 0 
  
        if (detectSuccess) { 
          const t2 = performance.now() 
          const seg = await segmentIrisOnImage(img.src, eyes[0]) 
          const t3 = performance.now() 
          segmentTimeMs = t3 - t2 
          segmentSuccess = !!seg 
        } 
  
        results.push({ 
          imageName: img.name, 
          detectSuccess, 
          segmentSuccess, 
          detectTimeMs, 
          segmentTimeMs, 
        }) 
      } catch (e) { 
        console.error('Помилка при обробці зображення', img.name, e) 
121 
 
 
        results.push({ 
          imageName: img.name, 
          detectSuccess: false, 
          segmentSuccess: false, 
          detectTimeMs: 0, 
          segmentTimeMs: 0, 
        }) 
      } 
    } 
  
    setTestResults(results) 
    setIsTestingBatch(false) 
  } 
  
  const handleExportCasiaResultsCsv = () => { 
    if (!testResults.length) { 
      alert('Немає результатів тестування для експорту.') 
      return 
    } 
  
    const header = 'imageName,detectSuccess,segmentSuccess,detectTimeMs,segmentTimeMs\n' 
    const rows = testResults 
      .map(r => 
        [ 
          r.imageName, 
          r.detectSuccess ? '1' : '0', 
          r.segmentSuccess ? '1' : '0', 
          r.detectTimeMs.toFixed(1), 
          r.segmentTimeMs.toFixed(1), 
        ].join(',') 
      ) 
      .join('\n') 
  
    const csvContent = header + rows 
122 
 
 
    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) 
    const url = URL.createObjectURL(blob) 
    const a = document.createElement('a') 
    a.href = url 
    a.download = 'casia_test_results.csv' 
    document.body.appendChild(a) 
    a.click() 
    document.body.removeChild(a) 
    URL.revokeObjectURL(url) 
  } 
  
  const totalSamples = testResults.length 
  const detectSuccessCount = testResults.filter(r => r.detectSuccess).length 
  const segmentSuccessCount = testResults.filter(r => r.segmentSuccess).length 
  const avgDetectTime = 
    totalSamples > 0 
      ? testResults.reduce((sum, r) => sum + r.detectTimeMs, 0) / totalSamples 
      : 0 
  const avgSegmentTime = 
    totalSamples > 0 
      ? testResults.reduce((sum, r) => sum + r.segmentTimeMs, 0) / totalSamples 
      : 0 
  
  const contrastFactor = 1 + adjustments.contrast / 100 
  const gammaValue = adjustments.gamma > 0 ? adjustments.gamma / 100 : 1 
  const previewFilterStyle: React.CSSProperties = { 
    filter: `contrast(${contrastFactor}) brightness(${1 / gammaValue})`, 
  } 
  
  return ( 
    <div className="medical-container"> 
      <ThemeToggle /> 
      <div className="medical-section-header"> 
123 
 
 
        <h1 className="medical-title">Програмний комплекс іридодіагностичного 
моніторингу</h1> 
      </div> 
  
      <div className="medical-card"> 
        <label className="medical-label">Пацієнт</label> 
        <div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}> 
          <input 
            className="medical-input" 
            type="text" 
            value={patientName} 
            onChange={e => setPatientName(e.target.value)} 
            placeholder="Введіть ім'я пацієнта" 
            style={{ flex: 1 }} 
          /> 
          <button className="medical-button medical-button-primary" 
onClick={handleStartNewSession}> 
            Почати нову сесію моніторингу 
          </button> 
        </div> 
        {currentSession &amp;&amp; ( 
          <div className="medical-notification medical-notification-info" style={{ marginTop: '1rem' 
}}> 
            <strong>Поточна сесія:</strong> {currentSession.label} 
          </div> 
        )} 
      </div> 
  
        <ImageUploadPanel 
          mode={uploadMode} 
          onModeChange={setUploadMode} 
          onFilesSelected={handleFilesSelected} 
        /> 
  
      {images.length > 0 &amp;&amp; ( 
124 
 
 
        <div className="medical-card"> 
          <label className="medical-label">Вибір зображення</label> 
          <select 
            className="medical-input" 
            value={selectedImageId ?? ''} 
            onChange={e => { 
              setSelectedImageId(e.target.value) 
              setEyeRegions([]) 
              setEyeAnalyses(new Map()) 
              setSelectedEyeSide(null) 
              setIrisSegmentation(null) 
              setCurrentMeasurementId(null) 
              setCurrentAnnotations([]) 
              setAutoSigns([]) 
              setShowIrisMap(false) 
            }} 
            style={{ width: '100%', padding: '0.625rem 0.875rem' }} 
          > 
            {images.map(img => ( 
              <option key={img.id} value={img.id}> 
                {img.name} 
              </option> 
            ))} 
          </select> 
        </div> 
      )} 
  
        <ControlsPanel adjustments={adjustments} onChange={setAdjustments} /> 
  
      <div className="medical-card"> 
        <label className="medical-label">Операції обробки</label> 
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}> 
          <button 
            className="medical-button" 
125 
 
 
            onClick={handleApplyAdjustments} 
            disabled={!selectedImage} 
            style={{ 
              backgroundColor: 'var(--medical-bg-tertiary)', 
              color: 'var(--medical-text-primary)', 
            }} 
          > 
            Застосувати корекцію 
          </button> 
          <button 
            className="medical-button medical-button-primary" 
            onClick={handleDetectEyes} 
            disabled={!selectedImage || isDetecting} 
          > 
            {isDetecting ? 'Виконується детекція...' : 'Детектувати очі'} 
          </button> 
          <button 
            className="medical-button medical-button-success" 
            onClick={handleSaveMeasurement} 
            disabled={!selectedImage || !irisSegmentation || !currentSession} 
          > 
            Зберегти вимірювання в поточну сесію 
          </button> 
          <button 
            className="medical-button medical-button-danger" 
            onClick={handleExportReport} 
            disabled={!selectedImage || !irisSegmentation} 
          > 
            Експортувати звіт у PDF 
          </button> 
        </div> 
      </div> 
  
      <div className="medical-card"> 
126 
 
 
        <h3 className="medical-subtitle">Розмітка іридознаків</h3> 
        <div style={{ display: 'flex', gap: '1.5rem', alignItems: 'flex-end' }}> 
          <div style={{ flex: 1 }}> 
            <label className="medical-label">Тип знака</label> 
            <select 
              className="medical-input" 
              value={selectedSignId} 
              onChange={e => setSelectedSignId(e.target.value)} 
              style={{ width: '100%', padding: '0.625rem 0.875rem' }} 
            > 
              <option value="">— оберіть тип —</option> 
              {IRIS_SIGNS.map(sign => ( 
                <option key={sign.id} value={sign.id}> 
                  {sign.label} 
                </option> 
              ))} 
            </select> 
          </div> 
          <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}> 
            <input 
              type="checkbox" 
              checked={markingMode} 
              onChange={e => setMarkingMode(e.target.checked)} 
              style={{ width: '1.25rem', height: '1.25rem', cursor: 'pointer' }} 
            /> 
            <span style={{ color: 'var(--medical-text-secondary)', fontSize: '0.9375rem' }}> 
              Режим розмітки (ручні мітки можна додати пізніше за потреби) 
            </span> 
          </label> 
        </div> 
      </div> 
  
      <div className="medical-card" ref={analysisBlockRef}> 
        {eyeAnalyses.size > 0 ? ( 
127 
 
 
          <EyeAnalysisPanel 
            eyeAnalyses={eyeAnalyses} 
            selectedEyeSide={selectedEyeSide} 
            onSelectEye={handleSelectEye} 
            onSegmentEye={handleSegmentIris} 
            onDetectSigns={handleDetectIrisSigns} 
            onToggleIrisMap={handleToggleIrisMap} 
            isSegmenting={isSegmenting} 
            isDetectingSigns={isDetectingSigns} 
            showIrisMap={showIrisMap} 
          /> 
        ) : ( 
          <div className="medical-notification" style={{ backgroundColor: 'var(--medical-bg-
tertiary)', borderLeft: '4px solid var(--medical-border)' }}> 
            <p style={{ margin: 0, color: 'var(--medical-text-secondary)' }}> 
              Виконайте детекцію очей для відображення аналізу. 
            </p> 
          </div> 
        )} 
      </div> 
  
      <details className="medical-card" style={{ marginTop: '1.5rem' }}> 
        <summary 
          style={{ 
            cursor: 'pointer', 
            fontWeight: 600, 
            fontSize: '1rem', 
            color: 'var(--medical-text-primary)', 
            padding: '0.5rem 0', 
            userSelect: 'none', 
          }} 
        > 
          Порівняння: оригінал та оброблене зображення 
        </summary> 
128 
 
 
        <div style={{ marginTop: '1rem' }}> 
          {selectedImage &amp;&amp; ( 
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> 
              <div> 
                <p style={{ marginBottom: '0.5rem', fontWeight: 600, fontSize: '0.875rem', color: 'var(--
medical-text-primary)', textAlign: 'center' }}> 
                  Оригінал 
                </p> 
                <div style={previewFilterStyle}> 
                  <ImagePreview  
                    image={{ 
                      ...selectedImage, 
                      src: selectedImage.originalSrc || selectedImage.src 
                    }}  
                  /> 
                </div> 
              </div> 
              <div> 
                <p style={{ marginBottom: '0.5rem', fontWeight: 600, fontSize: '0.875rem', color: 'var(--
medical-text-primary)', textAlign: 'center' }}> 
                  З корекцією, детекцією та сегментацією 
                </p> 
                <div style={{ position: 'relative', display: 'inline-block', width: '100%' }}> 
                  <img 
                    src={selectedImage.src} 
                    alt={selectedImage.name} 
                    style={{ 
                      ...previewFilterStyle, 
                      maxWidth: '100%', 
                      width: '100%', 
                      height: 'auto', 
                      display: 'block', 
                      border: '1px solid var(--medical-border)', 
                      borderRadius: '8px', 
129 
 
 
                    }} 
                  /> 
                  <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', 
pointerEvents: 'auto' }}> 
                    <OverlaysCanvas 
                      image={selectedImage} 
                      eyeRegions={eyeRegions} 
                      irisSegmentation={irisSegmentation} 
                      annotations={currentAnnotations} 
                      showIrisMap={showIrisMap} 
                      onCanvasClick={handleCanvasClick} 
                      drawImage={false} 
                    /> 
                  </div> 
                </div> 
              </div> 
            </div> 
          )} 
        </div> 
      </details> 
  
      <div className="medical-card"> 
        <div className="medical-section-header"> 
          <h2 className="medical-subtitle">Режим тестування CASIA / тестових даних</h2> 
        </div> 
        <p style={{ color: 'var(--medical-text-secondary)', marginBottom: '1.5rem', lineHeight: '1.6' 
}}> 
          Для оцінювання якості роботи алгоритмів завантажте кілька зображень ока з тестового 
          набору (наприклад, CASIA Iris Image Database) у режимі batch та запустіть 
          автоматичне тестування. Програма послідовно виконає детекцію очей і сегментацію 
          райдужки/зіниці для кожного зображення, зафіксує успішність та час обробки. 
        </p> 
        <div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1.5rem' }}> 
          <button 
130 
 
 
            className="medical-button medical-button-primary" 
            onClick={handleRunCasiaBatchTest} 
            disabled={isTestingBatch || !images.length} 
          > 
            {isTestingBatch ? 'Виконується тестування...' : 'Запустити тестування на наборі 
(CASIA)'} 
          </button> 
          <button 
            className="medical-button medical-button-primary" 
            onClick={handleExportCasiaResultsCsv} 
            disabled={!testResults.length} 
          > 
            Експортувати результати в CSV 
          </button> 
        </div> 
  
        {totalSamples > 0 &amp;&amp; ( 
          <div className="medical-notification medical-notification-info"> 
            <p style={{ fontWeight: 600, marginBottom: '0.75rem', fontSize: '0.9375rem' }}> 
              Зведена статистика: 
            </p> 
            <ul style={{ margin: 0, paddingLeft: '1.25rem' }}> 
              <li style={{ marginBottom: '0.5rem' }}> 
                <strong>Кількість зразків:</strong>{' '} 
                <span className="medical-tag" style={{ backgroundColor: 'var(--medical-bg-tertiary)', 
color: 'var(--medical-text-primary)' }}> 
                  {totalSamples} 
                </span> 
              </li> 
              <li style={{ marginBottom: '0.5rem' }}> 
                <strong>Успішна детекція очей:</strong>{' '} 
                <span className="medical-tag medical-tag-success"> 
                  {detectSuccessCount} ({((detectSuccessCount / totalSamples) * 100).toFixed(1)}%) 
                </span> 
131 
 
 
              </li> 
              <li style={{ marginBottom: '0.5rem' }}> 
                <strong>Успішна сегментація райдужки/зіниці:</strong>{' '} 
                <span className="medical-tag medical-tag-success"> 
                  {segmentSuccessCount} ({((segmentSuccessCount / totalSamples) * 
100).toFixed(1)}%) 
                </span> 
              </li> 
              <li style={{ marginBottom: '0.5rem' }}> 
                <strong>Середній час детекції:</strong>{' '} 
                <span className="medical-tag medical-tag-info">{avgDetectTime.toFixed(1)} 
мс</span> 
              </li> 
              <li> 
                <strong>Середній час сегментації:</strong>{' '} 
                <span className="medical-tag medical-tag-info">{avgSegmentTime.toFixed(1)} 
мс</span> 
              </li> 
            </ul> 
          </div> 
        )} 
  
        {testResults.length > 0 &amp;&amp; ( 
          <div style={{ marginTop: '1.5rem', overflowX: 'auto' }}> 
            <table className="medical-table"> 
              <thead> 
                <tr> 
                  <th>Файл</th> 
                  <th>Детекція очей</th> 
                  <th>Сегментація</th> 
                  <th>Час детекції, мс</th> 
                  <th>Час сегментації, мс</th> 
                </tr> 
              </thead> 
              <tbody> 
132 
 
 
                {testResults.map((r, idx) => ( 
                  <tr key={`${r.imageName}_${idx}`}> 
                    <td>{r.imageName}</td> 
                    <td> 
                      {r.detectSuccess ? ( 
                        <span className="medical-tag medical-tag-success">успішно</span> 
                      ) : ( 
                        <span className="medical-tag medical-tag-danger">ні</span> 
                      )} 
                    </td> 
                    <td> 
                      {r.segmentSuccess ? ( 
                        <span className="medical-tag medical-tag-success">успішно</span> 
                      ) : ( 
                        <span className="medical-tag medical-tag-danger">ні</span> 
                      )} 
                    </td> 
                    <td>{r.detectTimeMs.toFixed(1)}</td> 
                    <td>{r.segmentSuccess ? r.segmentTimeMs.toFixed(1) : '—'}</td> 
                  </tr> 
                ))} 
              </tbody> 
            </table> 
          </div> 
        )} 
      </div> 
  
      <div className="medical-card"> 
        <div className="medical-section-header"> 
          <h2 className="medical-subtitle">Журнал сесій моніторингу</h2> 
          <button 
            className="medical-button medical-button-danger" 
            onClick={handleClearHistory} 
            disabled={!snapshot.length} 
133 
 
 
          > 
            Очистити всі дані моніторингу 
          </button> 
        </div> 
        <MonitoringPanel 
          snapshot={snapshot} 
          selectedSessionId={selectedSessionId} 
          onSelectSession={setSelectedSessionId} 
        /> 
      </div> 
    </div> 
  ) 
} 
  
export default App 
 
134