Eyespot

5 minute read

Published:

What are these spots?

In a recent survey doctors were required to take pictures of patients’ eyes and look for brigh local spots in the middle region of the cornea. There can be about one/tow hundreds possible spots in a single cornea image; these spots can be of very different size, and more over they have not the same color. screenshotA sample image of a patient right eye It has been seen that a well-trained doctor can find all the spots in about 40 minutes; this Java tool can mark them in 2 seconds.

The problem

Essentially this is a classical pattern-matching assignment, with the exception that no neuronal-networks, kNN models and neither statistical analysis solutions can be employed: not enough data is available since there are about one hundred patients, and furthermore the images provided by the medical facility is to be cropped edited so that only the area where the spots can be, is left. So a different solution (iterative in this case) must be found. Essentially, you would go pixel by pixel (column by column and row by row) and get an analysis of the neighbourhood: if the average brightness of the neighbour pixels is less than the brightness of the current pixel, then it is a spot, mark it.

The solution

This is easy to say, but a bit tricky to get to work: you’ve to crop the picture and only analyse the pixels inside red lines.

private int[] getCenterArea(ImageProcessor ip) {
    Rectangle roi = ip.getRoi();
    int width = roi.width;
    int height = roi.height;
    boolean hasStartCoordinateXBeenFound = false;
    int start_x = 0;  // cropping coordinates
    int end_x = width;
    int start_y = height;
    int end_y = 0;
    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            int rawPixel = ip.getPixel(x, y);
            int red = (rawPixel >> 16) & 0xff;
            int green = (rawPixel >> 8) & 0xff;
            int blue = rawPixel & 0xff;
            Color color = new Color(red, green, blue);
            boolean isApproximatelyRed = (Math.abs(red - 255) < 5) && (Math.abs(green - 0) < 5) && (Math.abs(blue - 0) < 5);
            if (isApproximatelyRed) {
                // update x coordinates
                if (hasStartCoordinateXBeenFound) {
                    // already found start x coordinate
                    end_x = x;
                } else {
                    // found start x coordinate
                    start_x = x;
                    hasStartCoordinateXBeenFound = true;
                }

                // update y coordinates
                if (start_y > y) {
                    start_y = y;
                }

                if (end_y < y) {
                    end_y = y;
                }
            }
        }
    }
    return new int[] {start_x, start_y, end_x, end_y};
}

screenshotCropped cornea image Then you go about discarding pixels that are relative maxima, but are too dark to be considerd white spots.

int pixelColor = ip.getPixel(x, y);
int red = (pixelColor >> 16) & 0xff;
int green = (pixelColor >> 8) & 0xff;
int blue = pixelColor & 0xff;
int blackThreshold = 50;  // threshold to be considered black
if(!(red < blackThreshold && green < blackThreshold && blue < blackThreshold)) {
    xyVector.addElement(new int[]{x, y});
}

Finally you can’t mark all the spots that are near each other: they are the same big spot.

boolean maxPossible = true;         //it may be a true maximum
double xEqual = x0;                 //for creating a single point: determine average over the
double yEqual = y0;                 //  coordinates of contiguous equal-height points
int nEqual = 1;                     //counts xEqual/yEqual points that we use for averaging
do {                                //while neigbor list is not fully processed (to listLen)
    int offset = pList[listI];
    int x = offset % width;
    int y = offset / width;
    //if(x==18&&y==20)IJ.write("x0,y0="+x0+","+y0+"@18,20;v0="+v0+" sortingError="+sortingError);
    boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin
    for (int d=0; d<8; d++) {       //analyze all neighbors (in 8 directions) at the same level
        int offset2 = offset+dirOffset[d];
        if ((isInner || isWithin(x, y, d)) && (types[offset2]&LISTED)==0) {
            if (isEDM && edmPixels[offset2]<=0) continue;   //ignore the background (non-particles)
            if ((types[offset2]&PROCESSED)!=0) {
                maxPossible = false; //we have reached a point processed previously, thus it is no maximum now
                //if(x0<25&&y0<20)IJ.write("x0,y0="+x0+","+y0+":stop at processed neighbor from x,y="+x+","+y+", dir="+d);
                break;
            }
            int x2 = x+DIR_X_OFFSET[d];
            int y2 = y+DIR_Y_OFFSET[d];
            float v2 = isEDM ? trueEdmHeight(x2, y2, ip) : ip.getPixelValue(x2, y2);
            if (v2 > v0 + maxSortingError) {
                maxPossible = false;    //we have reached a higher point, thus it is no maximum
                //if(x0<25&&y0<20)IJ.write("x0,y0="+x0+","+y0+":stop at higher neighbor from x,y="+x+","+y+", dir="+d+",value,value2,v2-v="+v0+","+v2+","+(v2-v0));
                break;
            } else if (v2 >= v0-(float)tolerance) {
                if (v2 > v0) {          //maybe this point should have been treated earlier
                    sortingError = true;
                    offset0 = offset2;
                    v0 = v2;
                    x0 = x2;
                    y0 = y2;
                }
                pList[listLen] = offset2;
                listLen++;              //we have found a new point within the tolerance
                types[offset2] |= LISTED;
                if (x2==0 || x2==width-1 || y2==0 || y2==height-1) {
                    isEdgeMaximum = true;
                    if (excludeEdgesNow) {
                        maxPossible = false;
                        break;          //we have an edge maximum;
                    }
                }
                if (v2==v0) {           //prepare finding center of equal points (in case single point needed)
                    types[offset2] |= EQUAL;
                    xEqual += x2;
                    yEqual += y2;
                    nEqual ++;
                }
            }
        } // if isWithin & not LISTED
    } // for directions d
    listI++;
} while (listI < listLen);

screenshotA simple usage can produce a result like this

Further notice

There are many improvements that can be done:

  • prompt the user for a size of the spot to search for
  • automate further the process by prompting the user for the folder where images are store, then process all of them automatically
  • bug fixes: please, open an isse, and I’ll try to patch it as soon as possible