-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathByCSV.vue
More file actions
177 lines (148 loc) · 4.99 KB
/
Copy pathByCSV.vue
File metadata and controls
177 lines (148 loc) · 4.99 KB
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
<template>
<div class="grid grid-cols-1 lg:grid-cols-2 cards-gap">
<div>
<IconCard title-placement="center">
<template #title>
Connect the CSV file containing a mapping between images and labels
</template>
<FileSelection v-model="csvFiles" type="tabular" no-upload>
<b>
The CSV file must contain a header with only two columns (filename,
label) </b
>. The file name must NOT include the file extension. You can find an
example of how to create a CSV
<a
class="underline text-blue-400"
target="_blank"
href="https://github.com/epfml/disco/blob/develop/docs/examples/dataset_csv_creation.ipynb"
>here</a
>.
</FileSelection>
</IconCard>
</div>
<div
class="relative"
:class="{ 'border-disco-cyan rounded-xl border-2': cannotUploadFiles }"
>
<div
v-if="cannotUploadFiles"
class="w-full h-full absolute z-20 cursor-not-allowed grid place-content-center"
>
<div class="text-disco-cyan font-bold text-2xl">Upload CSV first</div>
</div>
<IconCard
title-placement="center"
:class="{ 'opacity-10': cannotUploadFiles }"
>
<template #title> Connect the images </template>
<FileSelection v-model="images" type="image" multiple no-upload>
Drag and drop or browse for the images referenced in the connected CSV
file.
<br />
</FileSelection>
</IconCard>
</div>
</div>
</template>
<script lang="ts" setup>
import * as d3 from "d3";
import { Map, Set } from "immutable";
import { computed, ref, watch } from "vue";
import { Dataset } from "@epfml/discojs";
import { loadImage } from "@epfml/discojs-web";
import IconCard from "@/components/containers/IconCard.vue";
import { useToaster } from "@/composables/toaster";
import FileSelection from "../FileSelection.vue";
import type { NamedLabeledImageDataset } from "../types.js";
const toaster = useToaster();
const dataset = defineModel<NamedLabeledImageDataset>();
watch(dataset, (dataset: NamedLabeledImageDataset | undefined) => {
if (dataset === undefined) csvFiles.value = undefined; // trickles down
});
const csvFiles = ref<Set<File>>();
const filenameToLabel = ref<Map<string, string>>();
const images = ref<Set<File>>();
const cannotUploadFiles = computed(() => filenameToLabel.value === undefined);
watch(csvFiles, async (files) => {
if (files === undefined) {
filenameToLabel.value = undefined;
images.value = undefined;
return;
}
const file = files.first();
if (file === undefined)
throw new Error("no file should be checked by FileSelection");
const text = await file.text();
const csv = d3.csvParse(text);
class MalformedCSVError extends Error {}
try {
const updatedFilenameToLabel = Map(
csv.map(({ filename, label }) => {
if (filename === undefined || label === undefined)
throw new MalformedCSVError(
"The CSV file should have a header with these exact 2 column names: filename & label.",
);
return [filename, label];
}),
);
if (updatedFilenameToLabel.size !== csv.length)
throw new MalformedCSVError(
"The CSV file matches multiple label to the same filename.",
);
filenameToLabel.value = updatedFilenameToLabel;
} catch (e) {
if (!(e instanceof MalformedCSVError)) throw e;
csvFiles.value = undefined;
toaster.error(e.message);
}
});
// match the images to labels via the parsed CSV
watch(images, (files) => {
if (files === undefined) {
dataset.value = undefined;
return;
}
// create a map from filename to file to speed up the search
const filenameToFile = Map(
files.map((file) => {
const filename = file.name.split(".").slice(0, -1).join(".");
return [filename, file] as const;
}),
);
if (filenameToLabel.value === undefined)
throw new Error("csvFiles should have been called");
if (files.size !== filenameToLabel.value.size)
toaster.warning(
"Some inputted images we not found in the CSV: " +
filenameToFile
.keySeq()
.toSet()
.subtract(filenameToLabel.value.keySeq())
.join(", "),
);
const missingImages = new Error();
let fileToLabel: Iterable<[File, string]>;
try {
fileToLabel = filenameToLabel.value.mapEntries(([filename, label]) => {
const file = filenameToFile.get(filename);
if (file === undefined) throw missingImages;
return [file, label];
});
} catch (e) {
images.value = undefined;
if (e === missingImages) {
toaster.error(
"Some images listed in the CSV file are missing, " +
"make sure the CSV filenames don't include file extensions.",
);
return;
}
throw e;
}
dataset.value = new Dataset(fileToLabel).map(async ([file, label]) => ({
filename: file.name,
image: await loadImage(file),
label,
}));
});
</script>