-
Notifications
You must be signed in to change notification settings - Fork 432
/
TestTableDetection.java
336 lines (268 loc) · 12.6 KB
/
TestTableDetection.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package technology.tabula;
import com.google.gson.Gson;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import technology.tabula.detectors.NurminenDetectionAlgorithm;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Created by matt on 2015-12-14.
*/
@RunWith(Parameterized.class)
public class TestTableDetection {
private static int numTests = 0;
private static int numPassingTests = 0;
private static int totalExpectedTables = 0;
private static int totalCorrectlyDetectedTables = 0;
private static int totalErroneouslyDetectedTables = 0;
private static Level defaultLogLevel;
private static final class TestStatus {
public int numExpectedTables;
public int numCorrectlyDetectedTables;
public int numErroneouslyDetectedTables;
public boolean expectedFailure;
private transient boolean firstRun;
private transient String pdfFilename;
public TestStatus(String pdfFilename) {
this.numExpectedTables = 0;
this.numCorrectlyDetectedTables = 0;
this.expectedFailure = false;
this.pdfFilename = pdfFilename;
}
public static TestStatus load(String pdfFilename) {
TestStatus status;
try {
String json = UtilsForTesting.loadJson(jsonFilename(pdfFilename));
status = new Gson().fromJson(json, TestStatus.class);
status.pdfFilename = pdfFilename;
} catch (IOException ioe) {
status = new TestStatus(pdfFilename);
status.firstRun = true;
}
return status;
}
public void save() {
try (FileWriter w = new FileWriter(jsonFilename(this.pdfFilename))) {
Gson gson = new Gson();
w.write(gson.toJson(this));
w.close();
} catch (Exception e) {
throw new Error(e);
}
}
public boolean isFirstRun() {
return this.firstRun;
}
private static String jsonFilename(String pdfFilename) {
return pdfFilename.replace(".pdf", ".json");
}
}
@BeforeClass
public static void disableLogging() {
Logger pdfboxLogger = Logger.getLogger("org.apache.pdfbox");
defaultLogLevel = pdfboxLogger.getLevel();
pdfboxLogger.setLevel(Level.OFF);
}
@AfterClass
public static void enableLogging() {
Logger.getLogger("org.apache.pdfbox").setLevel(defaultLogLevel);
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
String[] regionCodes = {"eu", "us"};
ArrayList<Object[]> data = new ArrayList<>();
for (String regionCode : regionCodes) {
String directoryName = "src/test/resources/technology/tabula/icdar2013-dataset/competition-dataset-" + regionCode + "/";
File dir = new File(directoryName);
File[] pdfs = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".pdf"));
for (File pdf : pdfs) {
data.add(new Object[]{pdf});
}
}
return data;
}
private File pdf;
private DocumentBuilder builder;
private TestStatus status;
private int numCorrectlyDetectedTables = 0;
private int numErroneouslyDetectedTables = 0;
public TestTableDetection(File pdf) {
this.pdf = pdf;
this.status = TestStatus.load(pdf.getAbsolutePath());
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
this.builder = factory.newDocumentBuilder();
} catch (Exception e) {
// ignored
}
}
private void printTables(Map<Integer, List<Rectangle>> tables) {
for (Integer page : tables.keySet()) {
System.out.println("Page " + page.toString());
for (Rectangle table : tables.get(page)) {
System.out.println(table);
}
}
}
@Test
public void testDetectionOfTables() throws Exception {
numTests++;
// xml parsing stuff for ground truth
Document regionDocument = this.builder.parse(this.pdf.getAbsolutePath().replace(".pdf", "-reg.xml"));
NodeList tables = regionDocument.getElementsByTagName("table");
// tabula extractors
PDDocument pdfDocument = Loader.loadPDF(this.pdf);
ObjectExtractor extractor = new ObjectExtractor(pdfDocument);
// parse expected tables from the ground truth dataset
Map<Integer, List<Rectangle>> expectedTables = new HashMap<>();
int numExpectedTables = 0;
for (int i = 0; i < tables.getLength(); i++) {
Element table = (Element) tables.item(i);
Element region = (Element) table.getElementsByTagName("region").item(0);
Element boundingBox = (Element) region.getElementsByTagName("bounding-box").item(0);
// we want to know where tables appear in the document - save the page and areas where tables appear
Integer page = Integer.decode(region.getAttribute("page"));
float x1 = Float.parseFloat(boundingBox.getAttribute("x1"));
float y1 = Float.parseFloat(boundingBox.getAttribute("y1"));
float x2 = Float.parseFloat(boundingBox.getAttribute("x2"));
float y2 = Float.parseFloat(boundingBox.getAttribute("y2"));
List<Rectangle> pageTables = expectedTables.get(page);
if (pageTables == null) {
pageTables = new ArrayList<>();
expectedTables.put(page, pageTables);
}
// have to invert y co-ordinates
// unfortunately the ground truth doesn't contain page dimensions
// do some extra work to extract the page with tabula and get the dimensions from there
Page extractedPage = extractor.extractPage(page);
float top = (float) extractedPage.getHeight() - y2;
float left = x1;
float width = x2 - x1;
float height = y2 - y1;
pageTables.add(new Rectangle(top, left, width, height));
numExpectedTables++;
}
// now find tables detected by tabula-java
Map<Integer, List<Rectangle>> detectedTables = new HashMap<>();
// the algorithm we're going to be testing
NurminenDetectionAlgorithm detectionAlgorithm = new NurminenDetectionAlgorithm();
PageIterator pages = extractor.extract();
while (pages.hasNext()) {
Page page = pages.next();
List<Rectangle> tablesOnPage = detectionAlgorithm.detect(page);
if (!tablesOnPage.isEmpty()) {
detectedTables.put(page.getPageNumber(), tablesOnPage);
}
}
// now compare
System.out.println("Testing " + this.pdf.getName());
List<String> errors = new ArrayList<>();
this.status.numExpectedTables = numExpectedTables;
totalExpectedTables += numExpectedTables;
for (Integer page : expectedTables.keySet()) {
List<Rectangle> expectedPageTables = expectedTables.get(page);
List<Rectangle> detectedPageTables = detectedTables.get(page);
if (detectedPageTables == null) {
errors.add("Page " + page.toString() + ": " + expectedPageTables.size() + " expected tables not found");
continue;
}
errors.addAll(this.comparePages(page, detectedPageTables, expectedPageTables));
detectedTables.remove(page);
}
// leftover pages means we detected extra tables
for (Integer page : detectedTables.keySet()) {
List<Rectangle> detectedPageTables = detectedTables.get(page);
errors.add("Page " + page.toString() + ": " + detectedPageTables.size() + " tables detected where there are none");
this.numErroneouslyDetectedTables += detectedPageTables.size();
totalErroneouslyDetectedTables += detectedPageTables.size();
}
boolean failed = errors.size() > 0;
if (failed) {
System.out.println("==== CURRENT TEST ERRORS ====");
for (String error : errors) {
System.out.println(error);
}
} else {
numPassingTests++;
}
System.out.println("==== CUMULATIVE TEST STATISTICS ====");
System.out.println(numPassingTests + " out of " + numTests + " currently passing");
System.out.println(totalCorrectlyDetectedTables + " out of " + totalExpectedTables + " expected tables detected");
System.out.println(totalErroneouslyDetectedTables + " tables incorrectly detected");
if (this.status.isFirstRun()) {
// make the baseline
this.status.expectedFailure = failed;
this.status.numCorrectlyDetectedTables = this.numCorrectlyDetectedTables;
this.status.numErroneouslyDetectedTables = this.numErroneouslyDetectedTables;
this.status.save();
} else {
// compare to baseline
if (this.status.expectedFailure) {
// make sure the failure didn't get worse
assertTrue("This test is an expected failure, but it now detects even fewer tables.", this.numCorrectlyDetectedTables >= this.status.numCorrectlyDetectedTables);
assertTrue("This test is an expected failure, but it now detects more bad tables.", this.numErroneouslyDetectedTables <= this.status.numErroneouslyDetectedTables);
assertTrue("This test used to fail but now it passes! Hooray! Please update the test's JSON file accordingly.", failed);
} else {
assertFalse("Table detection failed. Please see the error messages for more information.", failed);
}
}
}
private List<String> comparePages(Integer page, List<Rectangle> detected, List<Rectangle> expected) {
ArrayList<String> errors = new ArrayList<>();
// go through the detected tables and try to match them with expected tables
// from http://www.orsigiorgio.net/wp-content/papercite-data/pdf/gho*12.pdf (comparing regions):
// for other (e.g.“black-box”) algorithms, bounding boxes and content are used. A region is correct if it
// contains the minimal bounding box of the ground truth without intersecting additional content.
for (Iterator<Rectangle> detectedIterator = detected.iterator(); detectedIterator.hasNext(); ) {
Rectangle detectedTable = detectedIterator.next();
for (int i = 0; i < expected.size(); i++) {
if (detectedTable.contains(expected.get(i))) {
// we have a candidate for the detected table, make sure it doesn't intersect any others
boolean intersectsOthers = false;
for (int j = 0; j < expected.size(); j++) {
if (i == j) continue;
if (detectedTable.intersects(expected.get(j))) {
intersectsOthers = true;
break;
}
}
if (!intersectsOthers) {
// success
detectedIterator.remove();
expected.remove(i);
this.numCorrectlyDetectedTables++;
totalCorrectlyDetectedTables++;
break;
}
}
}
}
// any expected tables left over weren't detected
for (Rectangle expectedTable : expected) {
errors.add("Page " + page.toString() + ": " + expectedTable.toString() + " not detected");
}
// any detected tables left over were detected erroneously
for (Rectangle detectedTable : detected) {
errors.add("Page " + page.toString() + ": " + detectedTable.toString() + " detected where there is no table");
this.numErroneouslyDetectedTables++;
totalErroneouslyDetectedTables++;
}
return errors;
}
}