3
$\begingroup$

I want to evaluate a questionnaire made at school. The survey is made on paper and I will scan it. So I have an image with all the questions (44 questions). What I am looking for is which number was chosen for which question. I attached an instance. It is only a small part of the questionnaire, but the principle is repeated. The cross color may vary. Also how it is filled by cross or by a filling the square, not filled at all or... . At the end I would like to have a list with chosen numbers for each question to make plots... . I want to do it as automated as possible. Currently I am lacking a good idea to detect what number was chosen and assign the question number and chosen number. What would be your strategy to evaluate the survey?

enter image description here

$\endgroup$
4
  • 1
    $\begingroup$ see if this helps how-to-detect-crosses-and-circles-in-60x60-raster-images $\endgroup$
    – Nasser
    Commented Jun 9, 2023 at 6:44
  • $\begingroup$ Is your survey already finished with the results on paper? Then the techniques suggested by @Nasser will be useful, but there will be quite a bit of adaptation for your example. If you are starting from scratch, the easiest thing to do would be to use FormPage and Databin. If you need to use paper and can redesign the form it will be easier if you introduce fiduciary marks on the form and use a color that you can easily identify on an image for your form's questions. $\endgroup$ Commented Jun 9, 2023 at 11:41
  • $\begingroup$ Unfortunately, the survey is finished and on paper. @Nasser example is one idea. Yet I struggle to assign a number 1..5 if an according cross is set in a box. $\endgroup$
    – Eisbär
    Commented Jun 11, 2023 at 7:46
  • 2
    $\begingroup$ An alternative method would be to scan a form with no boxes filled as a target template. The use ImageCorrelate to align the target template to each scanned form. From the target template you already know the rows (question #) and columns (response #). From the located occurrence you have an "oversampled" matrix that you can downsample and simply read the location of the "filled" boxes $\endgroup$
    – OpticsMan
    Commented Aug 10, 2023 at 21:54

1 Answer 1

4
$\begingroup$

First I start by identifying the vertical position of each question number. You can see that the answer boxes on the right for each question line up nicely with the bulleted question number on the left.

img = Import["https://i.sstatic.net/DjGwH.png"];

questionNumbers = 
 ImageTake[img, {40, 243}, {1, 30}] // RemoveBackground

enter image description here

Then I find the centroids of each of the components in questionNumbers, and grab only the vertical positions. I then define a Nearest Function to calculate which question number a given vertical coordinate is closest to:

numberCentroids = 
 ComponentMeasurements[questionNumbers, "Centroid"] // Values;
numberVerticalPos = numberCentroids[[All, 2]];
returnNearestNum := First@Nearest[numberVerticalPos -> "Index", #] &

Now let's look at only the boxes part of the image:

boxes = ImageTake[img, {40, 243}, {900, 1100}]

enter image description here

I just determined by eye where each box column lies horizontally using the coordinates tool. It looks like they lie at horizontal image positions {40,70,100,130,160}. I then define another NearestFunction to return the box column number a horizontal coordinate is closest to:

horizontalComponents = Range[40, 160, 30];
returnNearestBoxColumn := 
 First@Nearest[horizontalComponents -> "Index", #] &

Let's now Binarize and ColorNegate our box image. We can delete small, erratic components too while we're at it:

bin = Binarize[boxes] // ColorNegate;
bin = DeleteSmallComponents[bin, 30]

enter image description here

We need to now find a sufficient ComponentMeasurement that splits our components into two groupings: one of boxes with checks, and the other of boxes without checks.

A good candidate is "Rectangularity". The definition of "Rectangularity" (in the ComponentMeasurements documentation) is the fraction of pixels within the minimal bounding box. We expect a filled in box (filled with an X, a check mark, anything) to have a higher fraction of pixels within its bounding box compared to an empty box. If we histogram the Rectangularity of the components in bin, we indeed see it splits the components into two separable groupings:

ComponentMeasurements[bin, "Rectangularity"] // Values // Histogram

enter image description here

It looks like a Rectangularity of 0.3 is a good threshold to split our components by. The ones above this threshold should be the filled in boxes. Let's use SelectComponents to find the components that have Rectangularity greater than 0.3:

filledBoxes = SelectComponents[bin, #Rectangularity > 0.3 &]

enter image description here

And now we can get the corresponding question number and box column number for each filled-in box by grabbing the Centroids of the components in filledBoxes and using our Nearest functions:

xPos = ComponentMeasurements[filledBoxes, "Centroid"] // Values;
xQuestionNumber = returnNearestNum /@ xPos[[All, 2]];
xBoxNumber = returnNearestBoxColumn /@ xPos[[All, 1]];
Thread[{xQuestionNumber, xBoxNumber}]

{{1, 2}, {2, 4}, {3, 4}, {4, 2}}

The xQuestionNumber part isn't really necessary if we know there will only be one box filled per question, but it is a nice check as it confirms that we are getting an answer for each question.

Hope this helps. Please validate this with other examples to make sure the parameters are robust.

Add-on As an alternative for using Rectangularity, it appears DeleteSmallComponents[bin, 200] works quite well, while also being simple. This makes sense that DeleteSmallComponents works, because filled in boxes should consist of more elements compared to empty boxes. My only concern is that if a box is filled with a small X that doesn't touch the edges, it may be treated as its own component (separate from the box edges) and fall below the DeleteSmallComponents threshold.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.