Introduction

The eyesuite software has an export function that creates a csv of visual fields that can be selected according to several criteria.

Our version of Eyesuite, installed in Germany, uses a latin1-encoding and the date format “%d.%m.%Y”. I am not sure, if this is handled differently in other installation and would be grateful for feedback in this regard.

NB: Problems may arise when semicolons are used in the “note” field in Eyesuite. We have trained our technicians to avoid this, but it has to be kept in mind when problems arise.

The csv-file is quite extensive. Besides the sensitivities, it also contains the coordinates and the normal values for the given age for all locations. Often, it contains different types of visual fields.

Therefore, I have decided to implement the loadoctopus function as a function that parses the csv-file into a list. Here, I show how information can be extracted from this list.

NB: This is the first, preliminary version.

Loading the package and parsing the csv-file

# library(devtools)
# install_github(...)

library(visualFields)

octopus_list <- loadoctopus("EyesuiteTestfile.csv")

str(octopus_list)
## List of 9
##  $ patients         :'data.frame':   4 obs. of  4 variables:
##   ..$ id       : int [1:4] 2 2 3 1
##   ..$ firstname: chr [1:4] "Peter" "Peter" "Norrin" "Hüsker"
##   ..$ lastname : chr [1:4] "Parker" "Parker" "Radd" "Dü"
##   ..$ dob      : POSIXlt[1:4], format: "1977-12-18" "1977-12-18" ...
##  $ vf_types         :'data.frame':   3 obs. of  5 variables:
##   ..$ vfID      : int [1:3] 1 2 3
##   ..$ tperimetry: Factor w/ 2 levels "sap","swap": 1 1 1
##   ..$ pattern   : Factor w/ 3 levels "G","LVP","M": 1 2 3
##   ..$ locnum    : int [1:3] 59 76 81
##   ..$ strategy  : Factor w/ 7 levels "normal","dynamic",..: 2 4 2
##  $ locmaps          :List of 3
##   ..$ :'data.frame': 59 obs. of  3 variables:
##   .. ..$ loc_ID: int [1:59] 1 2 3 4 5 6 7 8 9 10 ...
##   .. ..$ xod   : num [1:59] -26 -26 -20 -20 -20 -20 -20 -20 -14 -14 ...
##   .. ..$ yod   : num [1:59] -4 4 -20 -12 -4 4 12 20 -4 4 ...
##   ..$ :'data.frame': 76 obs. of  3 variables:
##   .. ..$ loc_ID: int [1:76] 1 2 3 4 5 6 7 8 9 10 ...
##   .. ..$ xod   : num [1:76] -60 -60 -45 -45 -45 -45 -45 -30 -30 -30 ...
##   .. ..$ yod   : num [1:76] 0 15 -30 -15 0 15 30 -45 -30 -15 ...
##   ..$ :'data.frame': 81 obs. of  3 variables:
##   .. ..$ loc_ID: int [1:81] 1 2 3 4 5 6 7 8 9 10 ...
##   .. ..$ xod   : num [1:81] -9.5 -9.5 -7.5 -7.5 -7.5 -7.5 -5.5 -5.5 -5.5 -5.5 ...
##   .. ..$ yod   : num [1:81] -3.5 3.5 -7.5 -2.5 2.5 7.5 -5.5 -1.5 1.5 5.5 ...
##  $ sensitivities    :List of 3
##   ..$ : num [1:2, 1:59] 23 31 25 26 25.5 23 26 30 27 27 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   .. .. ..$ : chr [1:2] "1" "2"
##   .. .. ..$ : chr [1:59] "l1" "l2" "l3" "l4" ...
##   ..$ : num [1, 1:76] 0 0 0 20 2 0 0 10 26 20 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   .. .. ..$ : chr "3"
##   .. .. ..$ : chr [1:76] "l1" "l2" "l3" "l4" ...
##   ..$ : num [1, 1:81] 31 31 30 31 31 30 27 32 31 32 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   .. .. ..$ : chr "4"
##   .. .. ..$ : chr [1:81] "l1" "l2" "l3" "l4" ...
##  $ defects          :List of 3
##   ..$ : num [1:2, 1:59] -3.9 4.1 -1.8 -0.8 -0.6 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   .. .. ..$ : chr [1:2] "1" "2"
##   .. .. ..$ : chr [1:59] "l1" "l2" "l3" "l4" ...
##   ..$ : num [1, 1:76] -2.1 -3.3 -0.8 7.4 -12.8 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   .. .. ..$ : chr "3"
##   .. .. ..$ : chr [1:76] "l1" "l2" "l3" "l4" ...
##   ..$ : num [1, 1:81] 0.6 0.7 -0.3 0.2 0.3 ...
##   .. ..- attr(*, "dimnames")=List of 2
##   .. .. ..$ : chr "4"
##   .. .. ..$ : chr [1:81] "l1" "l2" "l3" "l4" ...
##  $ fields           :'data.frame':   4 obs. of  10 variables:
##   ..$ id  : int [1:4] 2 2 3 1
##   ..$ eye : chr [1:4] "OD" "OS" "OD" "OD"
##   ..$ date: Date[1:4], format: "2020-01-02" "2020-01-02" ...
##   ..$ time: chr [1:4] "09:19:13" "09:26:24" "14:26:06" "12:50:20"
##   ..$ age : num [1:4] 42 42 85 32
##   ..$ type: logi [1:4] NA NA NA NA
##   ..$ fpr : num [1:4] 0 0 0 0
##   ..$ fnr : num [1:4] 0 0.125 0.091 0
##   ..$ fl  : int [1:4] 0 0 0 2
##   ..$ vfID: int [1:4] 1 1 2 3
##  $ create_locmap    :function (vf_id)  
##   ..- attr(*, "srcref")= 'srcref' int [1:8] 367 5 376 5 5 5 1924 1933
##   .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55c831fd9a50> 
##  $ get_sensitivities:function (vf_id)  
##   ..- attr(*, "srcref")= 'srcref' int [1:8] 379 5 382 5 5 5 1936 1939
##   .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55c831fd9a50> 
##  $ get_defects      :function (vf_id)  
##   ..- attr(*, "srcref")= 'srcref' int [1:8] 385 5 388 5 5 5 1942 1945
##   .. ..- attr(*, "srcfile")=Classes 'srcfilealias', 'srcfile' <environment: 0x55c831fd9a50>

Patients

The visualFields packages identifies each patients with a unique id.

When the csv-file is parsed, an id is created and data.frame is created to save the first name, last name, and date of birth for each patient.

print(octopus_list$patients)
##   id firstname lastname        dob
## 1  2     Peter   Parker 1977-12-18
## 2  2     Peter   Parker 1977-12-18
## 3  3    Norrin     Radd 1934-09-27
## 4  1    Hüsker       Dü 1987-12-26

Visual field types

Different visual field patterns result in different column numbers in the visual field objects. Therefore, different lists are created. These are identified by a visual field id (vfID). The data.frame vf_types keeps track of these field types.

print(octopus_list$vf_types)
##   vfID tperimetry pattern locnum   strategy
## 1    1        sap       G     59    dynamic
## 3    2        sap     LVP     76 low vision
## 4    3        sap       M     81    dynamic

Getting sensitivities

With this id, a visual field object can be created. In order to avoid redundancy, sensitivities and further information on the visual field are saved in different lists:

# G pattern, dynamic strategy: vfID = 1

print(octopus_list$fields[octopus_list$fields$vfID == 1, ])
##   id eye       date     time age type fpr   fnr fl vfID
## 1  2  OD 2020-01-02 09:19:13  42   NA   0 0.000  0    1
## 2  2  OS 2020-01-02 09:26:24  42   NA   0 0.125  0    1
print(octopus_list$sensitivities[[1]])
##   l1 l2   l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13  l14 l15 l16 l17 l18  l19  l20
## 1 23 25 25.5 26 27 25 23 25 26  27  25  23  29 25.0  18  29  26  26 28.5 25.5
## 2 31 26 23.0 30 27 25 27 25 30  29  28  27  29 25.5  27  31  29  32 29.5 24.0
##   l21 l22 l23 l24 l25  l26 l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39
## 1  25  28  32  23  28 25.5  31  27  30  26  29  32  30  27  23  23  28  30  30
## 2  28  26  34  29  27 21.0  28  31  30  28  30  32  28  26  27  27  30  30  28
##    l40 l41 l42  l43 l44 l45 l46 l47 l48 l49 l50 l51 l52 l53 l54  l55 l56  l57
## 1 27.0  21  22 28.5  21  29  30  18  23  26  26  21  25  27  21 22.0  26 23.0
## 2 26.5  22  26 28.5  27  28  25  25  21  27  27  26  26  32  25 26.5  26 25.5
##   l58 l59
## 1  25  23
## 2  27  23

In order to simplify getting this information, the list contains a functions that returns a visual field object.

# G pattern, dynamic strategy: vfID = 1

sens <- octopus_list$get_sensitivities(1)

print(sens)
##   id eye       date     time age type fpr   fnr fl vfID l1 l2   l3 l4 l5 l6 l7
## 1  2  OD 2020-01-02 09:19:13  42   NA   0 0.000  0    1 23 25 25.5 26 27 25 23
## 2  2  OS 2020-01-02 09:26:24  42   NA   0 0.125  0    1 31 26 23.0 30 27 25 27
##   l8 l9 l10 l11 l12 l13  l14 l15 l16 l17 l18  l19  l20 l21 l22 l23 l24 l25  l26
## 1 25 26  27  25  23  29 25.0  18  29  26  26 28.5 25.5  25  28  32  23  28 25.5
## 2 25 30  29  28  27  29 25.5  27  31  29  32 29.5 24.0  28  26  34  29  27 21.0
##   l27 l28 l29 l30 l31 l32 l33 l34 l35 l36 l37 l38 l39  l40 l41 l42  l43 l44 l45
## 1  31  27  30  26  29  32  30  27  23  23  28  30  30 27.0  21  22 28.5  21  29
## 2  28  31  30  28  30  32  28  26  27  27  30  30  28 26.5  22  26 28.5  27  28
##   l46 l47 l48 l49 l50 l51 l52 l53 l54  l55 l56  l57 l58 l59
## 1  30  18  23  26  26  21  25  27  21 22.0  26 23.0  25  23
## 2  25  25  21  27  27  26  26  32  25 26.5  26 25.5  27  23

Getting defects

Since the csv file also contains age-related normal values for each subject and each location, we can also directly calculate a defect list, without an explicit model for normal values.

# M pattern (M = macula): vfID = 3

def <- octopus_list$get_defects(3)

print(def)
##   id eye       date     time age type fpr fnr fl vfID  l1  l2   l3  l4  l5 l6
## 4  1  OD 2020-01-08 12:50:20  32   NA   0   0  2    3 0.6 0.7 -0.3 0.2 0.3  0
##     l7  l8   l9 l10  l11 l12 l13 l14  l15 l16  l17 l18  l19 l20 l21 l22 l23 l24
## 4 -3.8 0.8 -0.1 1.5 -0.3 0.7 1.4 3.4 -0.2 0.1 -0.7 0.8 -2.8 1.2 3.2 1.3   1 0.6
##    l25  l26 l27 l28 l29 l30  l31 l32 l33  l34 l35 l36  l37 l38 l39 l40  l41
## 4 -1.2 -0.8  -1 1.9   1 1.2 -0.6 1.1 1.4 -0.8  -1 1.7 -5.2 1.1 3.4 3.6 -1.4
##    l42  l43  l44  l45 l46 l47 l48 l49  l50 l51 l52 l53 l54 l55 l56 l57  l58
## 4 -0.7 -0.9 -0.1 -2.3 2.8   2 3.3 2.6 -1.2 0.2  -1 1.9   2 3.2 0.4 1.1 -0.8
##    l59 l60  l61  l62 l63 l64  l65 l66  l67 l68  l69 l70  l71  l72 l73 l74 l75
## 4 -1.1 0.3 -0.8 -2.7 0.4 3.1 -0.4 0.7 -1.3 0.4 -0.5 0.9 -0.8 -0.7  -1 0.1 0.6
##   l76  l77 l78 l79 l80 l81
## 4 1.9 -0.5 0.6 0.2 0.9   2

Extracting locmaps and graphic parameters from the parsed file

For many functions of the visualFields package, explicit information on locations and graphical parameters are needed. The locmap functions helps to create such tables. These are then loaded in the environment with special set functions.

# M pattern (M = macula): vfID = 3

m_locmap <- octopus_list$create_locmap(3)

print(m_locmap)
## $name
## [1] "M 81"
## 
## $desc
## [1] "This locmap was automatically created from the csv exported by Eyesuite. NB: The locations are not numbered according to the standard!"
## 
## $coord
##       x    y
## 1  -9.5 -3.5
## 2  -9.5  3.5
## 3  -7.5 -7.5
## 4  -7.5 -2.5
## 5  -7.5  2.5
## 6  -7.5  7.5
## 7  -5.5 -5.5
## 8  -5.5 -1.5
## 9  -5.5  1.5
## 10 -5.5  5.5
## 11 -3.5 -9.5
## 12 -3.5 -3.5
## 13 -3.5 -0.5
## 14 -3.5  0.5
## 15 -3.5  3.5
## 16 -3.5  9.5
## 17 -2.5 -7.5
## 18 -2.5 -2.5
## 19 -2.5 -1.5
## 20 -2.5 -0.5
## 21 -2.5  0.5
## 22 -2.5  1.5
## 23 -2.5  2.5
## 24 -2.5  7.5
## 25 -1.5 -5.5
## 26 -1.5 -2.5
## 27 -1.5 -1.5
## 28 -1.5 -0.5
## 29 -1.5  0.5
## 30 -1.5  1.5
## 31 -1.5  2.5
## 32 -1.5  5.5
## 33 -0.5 -3.5
## 34 -0.5 -2.5
## 35 -0.5 -1.5
## 36 -0.5 -0.5
## 37 -0.5  0.5
## 38 -0.5  1.5
## 39 -0.5  2.5
## 40 -0.5  3.5
## 41  0.0  0.0
## 42  0.5 -3.5
## 43  0.5 -2.5
## 44  0.5 -1.5
## 45  0.5 -0.5
## 46  0.5  0.5
## 47  0.5  1.5
## 48  0.5  2.5
## 49  0.5  3.5
## 50  1.5 -5.5
## 51  1.5 -2.5
## 52  1.5 -1.5
## 53  1.5 -0.5
## 54  1.5  0.5
## 55  1.5  1.5
## 56  1.5  2.5
## 57  1.5  5.5
## 58  2.5 -7.5
## 59  2.5 -2.5
## 60  2.5 -1.5
## 61  2.5 -0.5
## 62  2.5  0.5
## 63  2.5  1.5
## 64  2.5  2.5
## 65  2.5  7.5
## 66  3.5 -9.5
## 67  3.5 -3.5
## 68  3.5 -0.5
## 69  3.5  0.5
## 70  3.5  3.5
## 71  3.5  9.5
## 72  5.5 -5.5
## 73  5.5 -1.5
## 74  5.5  1.5
## 75  5.5  5.5
## 76  7.5 -7.5
## 77  7.5 -2.5
## 78  7.5  2.5
## 79  7.5  7.5
## 80  9.5 -3.5
## 81  9.5  3.5
## 
## $bs
## [1] NA
setlocmap(m_locmap)

For plotting fields, a gpar list is needed. This is not fully implemented, yet. However, a rudimentary list can be easily created.

test_gpar <- list()
test_gpar$coord <- getlocmap()$coord
test_gpar$tess <- vftess(getlocmap()$coord, floor = 0, delta = 3)
## 
##      PLEASE NOTE:  The components "delsgs" and "summary" of the
##  object returned by deldir() are now DATA FRAMES rather than
##  matrices (as they were prior to release 0.0-18).
##  See help("deldir").
##  
##      PLEASE NOTE: The process that deldir() uses for determining
##  duplicated points has changed from that used in version
##  0.0-9 of this package (and previously). See help("deldir").
setgpar(test_gpar)

Now, we can plot sensitivities. Note that the blind spot is outside the field for the macula test pattern.

vfM <- octopus_list$get_sensitivities(3)

# I'm not sure why this step is necessary? Otherwise, validity check fails.
vfM$date <- as.Date(vfM$date) 

vfplot(vfM[1, ], type = "s")

To Do

Many functions can still not be used. Especially, an explicit model for age related normal values is necessary to plot defects. I will implement more functions as soon as I find the time.