bruise-data 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,607 @@
1
+ Metadata-Version: 2.4
2
+ Name: bruise-data
3
+ Version: 1.2.0
4
+ Summary: PyTorch loaders for the BRUISE_DATA collection (NIJ, Fenwick, ALS, mimics).
5
+ Keywords: pytorch,dataset,dataloader,bruise,segmentation,medical-imaging
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
10
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: torch>=1.12
14
+ Requires-Dist: numpy
15
+ Requires-Dist: pandas>=1.3
16
+ Requires-Dist: Pillow>=9
17
+ Provides-Extra: augment
18
+ Requires-Dist: albumentations; extra == "augment"
19
+
20
+ # BRUISE_DATA
21
+
22
+ All bruise images (NIJ and Fenwick) and bruise look-alike images (mimics) in one consistent layout, plus a PyTorch loader that gives you `train`, `val` and `test` DataLoaders for any mix of them in one line. New datasets plug in by adding a folder with a `manifest.csv` ([section 11.1](#111-adding-a-new-dataset-plug-and-play)):
23
+
24
+ ```python
25
+ from bruise_data import get_dataloaders
26
+ train_dl, val_dl, test_dl = get_dataloaders(["nij", "fenwick", "mimics"])
27
+ ```
28
+
29
+ | Dataset | What it is | Images (train / val / test) | Masks | Skin tone |
30
+ |---|---|---|---|---|
31
+ | `nij` | NIJ white-light bruise photos, Labelbox full-consensus subset | 664 / 167 / 185 (subjects: 92 / 23 / 28) | yes (Paul; majority vote on test) | L\*a\*b\* + ITA per visit |
32
+ | `nij_als` | NIJ ALS photos (415–535 nm), same subjects and folds as `nij` ([3.5](#35-als-nij_als-fenwick_als)) | ≈ 6,868 train+val / 1,459 test (July 2026 export; built on the server) | yes (Paul; majority vote on test) | as `nij` |
33
+ | `fenwick` | Fenwick white-light bruise photos | 1,536 / 385 / 127 (subjects: 85 / 20 / 15) | yes (mzehra2; majority vote on test) | L\*a\*b\* + ITA per lesion |
34
+ | `fenwick_als` | Fenwick ALS photos (410/455 nm), same subjects and folds as `fenwick` | counted when downloaded | yes (mzehra2; majority vote on test) | as `fenwick` |
35
+ | `mimics` | Open-source images of conditions that look like bruises, in 9 taxonomy classes | 4,360 / 543 / 524 | no bruise (empty mask) | Fitzpatrick / Monk where the source gives it |
36
+ | `extra_bruise` | Web bruise photos (no masks, classification only) | 68 / 12 / 8 | none | none |
37
+
38
+ Hard negatives (healthy skin, generic rashes, moles, chronic ulcers) are left out on purpose.
39
+
40
+ ---
41
+
42
+ ## Contents
43
+
44
+ 1. [Quick start](#1-quick-start)
45
+ 2. [Folder layout](#2-folder-layout)
46
+ 3. [The datasets](#3-the-datasets)
47
+ 4. [Naming rules](#4-naming-rules)
48
+ 5. [manifest.csv columns](#5-manifestcsv-columns)
49
+ 6. [Using the dataloader](#6-using-the-dataloader)
50
+ 7. [Recipes](#7-recipes)
51
+ 8. [Skin tone (L\*a\*b\*)](#8-skin-tone-lab)
52
+ 9. [Command-line tools](#9-command-line-tools)
53
+ 10. [Running on the GPU server](#10-running-on-the-gpu-server)
54
+ 11. [Rebuilding or adding data](#11-rebuilding-or-adding-data) (incl. [11.1 adding a new dataset](#111-adding-a-new-dataset-plug-and-play), [11.2 ALS data](#112-adding-the-als-data-nij_als-fenwick_als))
55
+ 12. [Licences and data rules](#12-licences-and-data-rules)
56
+ 13. [Troubleshooting](#13-troubleshooting)
57
+
58
+ ---
59
+
60
+ ## 1. Quick start
61
+
62
+ **Requirements:** Python 3.9+, `torch`, `numpy`, `pandas`, `Pillow`. Nothing else; `albumentations` is optional.
63
+
64
+ ```bash
65
+ cd BRUISE_DATA
66
+ pip install -r requirements.txt # skip if torch/pandas/pillow are already installed
67
+
68
+ python -m bruise_data check # 1. confirm files are there and splits don't leak
69
+ python -m bruise_data summary # 2. see how many images each split has
70
+ python examples/quick_start.py # 3. load one batch and print what is inside
71
+ ```
72
+
73
+ In your own script:
74
+
75
+ ```python
76
+ import sys; sys.path.insert(0, "/path/to/BRUISE_DATA") # not needed if you run from inside BRUISE_DATA
77
+ from bruise_data import get_dataloaders
78
+
79
+ if __name__ == "__main__": # required on Windows when num_workers > 0
80
+ train_dl, val_dl, test_dl = get_dataloaders(["nij", "fenwick", "mimics"], image_size=512, batch_size=8)
81
+ for batch in train_dl:
82
+ images = batch["image"] # float [8, 3, 512, 512] (ImageNet-normalised)
83
+ masks = batch["mask"] # float [8, 1, 512, 512] 1 = bruise; all zeros for mimics
84
+ labels = batch["label"] # int [8] 0 = bruise, 1.. = mimic classes
85
+ break
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 2. Folder layout
91
+
92
+ ```
93
+ BRUISE_DATA/
94
+ ├── README.md this file
95
+ ├── requirements.txt
96
+ ├── bruise_data/ the Python loader package (import bruise_data)
97
+ ├── examples/ quick_start.py, train_segmentation.py
98
+ ├── tools/build_bruise_data.py rebuilds every folder below from the original sources
99
+ ├── tools/labelbox_als/ Labelbox ALS downloader for Hopper (notebook, Slurm job, README)
100
+ │
101
+ ├── nij/
102
+ │ ├── train/ images/ masks/ primary annotator (paul)
103
+ │ ├── val/ images/ masks/ primary annotator, fold 0 of the subject 5-fold split
104
+ │ ├── test/ images/ masks/ majority vote of paul + gbarimah + erik
105
+ │ │ masks_by_annotator/{paul,gbarimah,erik}/
106
+ │ ├── alt_annotators/{gbarimah,erik}/{images,masks}/ other annotators' train pools
107
+ │ ├── metadata/skin_tone_lab.csv L*a*b* per subject / visit / arm
108
+ │ ├── metadata/skin_tone_lab_subject_median.csv
109
+ │ ├── metadata/original/ the untouched source CSVs
110
+ │ ├── reports/build_logs/ how the dataset was built (consensus filter, leakage check)
111
+ │ └── manifest.csv one row per image (and per annotator)
112
+ │
113
+ ├── fenwick/ same layout; primary = mzehra2, alt = hliu36, nmousta5
114
+ │
115
+ ├── nij_als/, fenwick_als/ same layout (train/ val/ test/ + masks_by_annotator/), no alt_annotators;
116
+ │ server only (≈ 55 GB), see section 11.2
117
+ │
118
+ ├── mimics/
119
+ │ ├── <class>/<source_dataset>/<image> e.g. purpura/scin/..., burn/sunburn_roboflow/...
120
+ │ ├── manifest.csv
121
+ │ ├── mimic_class_map.csv every original diagnosis -> taxonomy class
122
+ │ └── README.md per-class counts and sources
123
+ │
124
+ └── extra_bruise/
125
+ ├── <source_dataset>/<image>
126
+ └── manifest.csv
127
+ ```
128
+
129
+ Every image and mask is a hard link to the original file, so this tree takes almost no extra disk space on the laptop. The zip archives hold real copies.
130
+
131
+ ---
132
+
133
+ ## 3. The datasets
134
+
135
+ ### 3.1 NIJ (`nij/`)
136
+
137
+ - **What it is:** white-light (`_N`) photos of experimentally induced bruises on the upper and lower arm, from GMU and partner sites. Only images where all three Labelbox raters said "bruise visible" (full consensus) are included.
138
+ - **Annotators:**
139
+ - `paul` is the primary annotator; his masks are used for `train`/`val`.
140
+ - `gbarimah` (862 images) and `erik` (763 images) have their own train pools under `alt_annotators/`.
141
+ - **Test set:** 185 images from 28 subjects. Nobody's train pool contains these subjects.
142
+ - Masks: `test/masks/` holds the majority vote (a pixel is bruise if at least 2 of the 3 annotators marked it).
143
+ - `test/masks_by_annotator/` holds each annotator's own mask.
144
+ - **Validation:** fold 0 of a subject-level 5-fold split, so a subject is never in both train and val. Any other fold can be used instead (`val_fold=`).
145
+ - **Image names:** `AB012_V03_UA_N` means subject AB012, visit 3, upper arm (`UA`; `LA` = lower arm), white light.
146
+
147
+ ### 3.2 Fenwick (`fenwick/`)
148
+
149
+ - **What it is:** white-light photos of bruises from the Fenwick study.
150
+ - **Annotators:** the top-3 labelers from the labeler-selection study.
151
+ - `mzehra2` is the primary annotator.
152
+ - `hliu36` (1,124 images) and `nmousta5` (507 images) are under `alt_annotators/`.
153
+ - **Test set:** 127 images from 15 subjects.
154
+ - Masks: majority vote of the three labelers in `test/masks/`, individual masks in `test/masks_by_annotator/`.
155
+ - **Validation:** fold 0 of the subject 5-fold split, same as NIJ.
156
+ - **Image names:** `123_F_2-4_WL_Bruise_...` means participant 123, lesion F (example IDs).
157
+
158
+ ### 3.3 Mimics (`mimics/`)
159
+
160
+ Classes follow the bruise taxonomy (`BRUISE_LOOKALIKE/TAXONOMY/Bruise Taxonomy.pdf`): its non-bruise leaf classes that can be mistaken for a bruise. Each class folder has one sub-folder per source dataset.
161
+
162
+ | Class | Taxonomy branch | Contains | Total images |
163
+ |---|---|---|---|
164
+ | `purpura` | Ecchymosis → Purpura / large ecchymosis | purpura, vasculitis, pigmented purpuric eruption, hemosiderin staining, erythema elevatum diutinum | 771 |
165
+ | `petechiae` | Ecchymosis → Petechiae (<3 mm) | petechiae | 10 (merged into `purpura` by default) |
166
+ | `hemangioma` | Vascular mimic → no blanching | hemangioma, port wine stain, pyogenic granuloma, Kaposi sarcoma, lymphangioma | 1,071 |
167
+ | `erythema` | Vascular mimic → blanches | cellulitis, erythema multiforme / nodosum / migrans, annular erythemas, urticaria, phytophotodermatitis | 1,197 |
168
+ | `pigmentary_lesion` | Pigmentary lesion (melanin, endogenous / exogenous) | melasma, hyperpigmentation, PIH, acanthosis nigricans, arsenical pigmentation, fixed drug eruption, dark circles | 1,521 |
169
+ | `burn` | Wound → Burn | thermal burns, sunburn, erythema ab igne | 589 |
170
+ | `abrasion` | Wound → Abrasion | scrapes, grazes | 138 |
171
+ | `laceration` | Wound → Laceration | blunt-force tears | 61 |
172
+ | `incised_wound` | Wound → Incised wound | cuts, stab wounds | 69 |
173
+
174
+ - **Splits:** train/val/test are stored in `manifest.csv`, not in folders, so the folders stay `class/source`. The split is about 80/10/10. Images from the same patient (SCIN case, PAD-UFES patient) and near-duplicate photos always share a split.
175
+ - **Filters applied:** short side ≥ 200 px, no cut-outs on white backgrounds, one image per near-duplicate group.
176
+ - **Exact mapping:** `mimic_class_map.csv` lists every original diagnosis name and its class. `mimics/README.md` gives counts per class, source and split.
177
+ - **Not filled yet:** `ecchymosis` (non-traumatic) and `surface_mark` (ink, paint, tattoo) have no open source yet, so they have no folder.
178
+ - **Mixed source to watch:** in `pigmentary_lesion`, 740 of 1,521 images are arsenicosis photos (`skin_diseases_normal_arsenic`), often palms with keratoses. Use `exclude_mimic_sources=["skin_diseases_normal_arsenic"]` if you want a cleaner class.
179
+
180
+ ### 3.4 Extra bruise (`extra_bruise/`)
181
+
182
+ 88 unique web photos of bruises from the Hugging Face "Clinical Wound Dataset" (`SaniaSalah/wound`) and the Kaggle wound dataset (`yasinpratomo`), with duplicates removed. They have no masks, so the loader only accepts them with `task="classification"`.
183
+
184
+ ### 3.5 ALS (`nij_als/`, `fenwick_als/`)
185
+
186
+ - **What it is:** the same studies photographed under an alternate light source (narrow-band light + barrier filter), downloaded from Labelbox with `tools/labelbox_als/` (section 11.2).
187
+ - NIJ: wavelengths 415, 450, 475, 495, 515, 535 nm. The filter letter (`Y`/`O`/`R`) is in `light_filter`. UV photos are left out.
188
+ - Fenwick: `ALS_410_455`.
189
+ - **Selection:** the same rule as white light.
190
+ - NIJ: Paul's mask, and every Labelbox rater said "bruise visible".
191
+ - Fenwick: mzehra2 drew a bruise mask.
192
+ - Only the primary annotator is in train/val (no `alt_annotators/`).
193
+ - **Same subjects, same folds:** every subject keeps its white-light fold, and the white-light test subjects are the only ALS test subjects. So `get_dataloaders(["nij", "nij_als"])` is leak-free, and `check` verifies it across the pair. Subjects that only have ALS photos are added to the lightest folds.
194
+ - **Test set:** majority vote of the three annotators (paul / gbarimah / erik, or mzehra2 / hliu36 / nmousta5), as for white light. Individual masks are in `test/masks_by_annotator/`.
195
+ - **Image names:** `GM059_V11_LA_415_Y` = subject GM059, visit 11, lower arm, 415 nm, yellow filter. `182_K_2-6_ALS_410_455_Bruise_...` = Fenwick participant 182, lesion K.
196
+ - **Where:** built on the GPU server only. The NIJ part is about 55 GB, too large for the laptop or a zip.
197
+
198
+ ---
199
+
200
+ ## 4. Naming rules
201
+
202
+ These are the same in every folder:
203
+
204
+ - **Splits** are always `train`, `val`, `test`. No "fixed", "holdout" or "minus".
205
+ - **Images** are in `images/`, **masks** in `masks/`. A mask has the image's stem plus `.png`. Mask pixels are 0 = background, 255 = bruise; the loader turns them into 0/1.
206
+ - **Folder and file names** are lower-case `snake_case`.
207
+ - **Dataset names** are the folder names: `nij`, `fenwick`, `mimics`, `extra_bruise`, plus any folder you add with a `manifest.csv`.
208
+ - **`image_id`** is `<dataset>__<original stem>`, unique across everything.
209
+ - **Skin tone** always lives in `metadata/skin_tone_lab.csv`.
210
+ - **Manifests:** each dataset has exactly one `manifest.csv`, and all manifests share the same columns (next section).
211
+ - **Paths** inside manifests are relative to `BRUISE_DATA/`, so the folder can be moved anywhere.
212
+
213
+ ---
214
+
215
+ ## 5. `manifest.csv` columns
216
+
217
+ | Column | Meaning |
218
+ |---|---|
219
+ | `image_id` | Unique ID: `<dataset>__<stem>` |
220
+ | `dataset` | `nij`, `fenwick`, `mimics`, `extra_bruise` |
221
+ | `split` | `train` / `val` / `test` (for NIJ/Fenwick, `val` = fold 0; the loader can re-cut with `val_fold`) |
222
+ | `fold` | Subject-level fold 0–4 for NIJ/Fenwick train/val rows; `-1` otherwise |
223
+ | `class_name` | `bruise` or a mimic class |
224
+ | `is_bruise` | True for NIJ, Fenwick, extra_bruise |
225
+ | `source_dataset` | Where the image came from (`nij`, `fenwick`, `scin`, `dermnet`, ...) |
226
+ | `source_class` | Original label from that source (e.g. "Pigmented purpuric eruption") |
227
+ | `annotator` | Whose mask `mask_path` points to: `paul`, `gbarimah`, `erik`, `mzehra2`, `hliu36`, `nmousta5`, `majority` (test only), `none` (mimics) |
228
+ | `is_primary_annotator` | True for the default rows (primary annotator for train/val, majority vote for test) |
229
+ | `image_path`, `mask_path` | Relative to `BRUISE_DATA/`. `mask_path` is empty for mimics and extra_bruise |
230
+ | `subject_id` | Participant / patient / case ID (for leak-free splits) |
231
+ | `lesion_id` | Fenwick lesion letter |
232
+ | `visit` | NIJ visit number |
233
+ | `near_dup_group` | Mimics: images whose perceptual hash matches |
234
+ | `width`, `height` | Original image size in pixels |
235
+ | `mask_positive_pixels` | Bruise pixels in the mask (5 NIJ train images have empty masks) |
236
+ | `skin_L`, `skin_a`, `skin_b`, `skin_ita` | Colorimeter CIE L\*a\*b\* of normal skin next to the bruise, and ITA° |
237
+ | `lesion_L`, `lesion_a`, `lesion_b` | Colorimeter L\*a\*b\* on the bruise itself |
238
+ | `ita_category` | Very Light / Light / Intermediate / Tan / Brown / Dark |
239
+ | `fitzpatrick`, `monk_tone` | Mimics, where the source provides it |
240
+ | `body_site` | NIJ `upper_arm` / `lower_arm`; mimics where known |
241
+ | `modality`, `lighting` | Photo type; `lighting` is `white_light`, or `als` for `nij_als` / `fenwick_als` |
242
+ | `licence`, `commercial_use` | See [section 12](#12-licences-and-data-rules) |
243
+ | `original_file` | File name or path in the original source |
244
+ | `wavelength`, `light_filter` | ALS only: NIJ `415`…`535` + filter `Y`/`O`/`R`; Fenwick `ALS_410_455`. Empty for white light |
245
+
246
+ NIJ and Fenwick manifests have one row per image **per annotator**, so an image's masks from different annotators sit side by side. The loader always picks one annotator per dataset, so you never get the same image twice.
247
+
248
+ ---
249
+
250
+ ## 6. Using the dataloader
251
+
252
+ ### 6.1 `get_dataloaders(...)` → `(train_dl, val_dl, test_dl)`
253
+
254
+ ```python
255
+ get_dataloaders(
256
+ datasets=("nij", "fenwick"), # any dataset folder: "nij", "fenwick", "mimics", "extra_bruise", ... (list_datasets())
257
+ task="segmentation", # or "classification"
258
+ # --- which mimics -------------------------------------------------------------
259
+ mimic_classes="all", # or a list, e.g. ["purpura", "hemangioma"]
260
+ mimic_sources=None, # keep only these sources, e.g. ["scin", "fitzpatrick17k"]
261
+ exclude_mimic_sources=None, # drop these sources, e.g. ["dermnet"]
262
+ merge=None, # class renames; default {"petechiae": "purpura"}; {} = no merging
263
+ min_class_images=50, # with mimic_classes="all", skip classes smaller than this
264
+ binary=False, # True: labels are 0 = non_bruise (all mimic classes in one), 1 = bruise
265
+ commercial_only=False, # True = only images whose licence allows commercial use
266
+ # --- which masks and folds -----------------------------------------------------
267
+ annotator="primary", # train/val masks: "primary", an annotator name, or a dict
268
+ test_annotator="majority", # test masks: "majority", "primary", a name, or a dict
269
+ val_fold=0, # which subject fold (0-4) is val for NIJ/Fenwick
270
+ # --- images ------------------------------------------------------------------
271
+ image_size=512, # output is image_size x image_size
272
+ resize="pad", # "pad" keeps aspect ratio (black borders), "stretch" squashes
273
+ augment=True, # light train augmentation (crop, flip, ±15° rotation, brightness/contrast/saturation)
274
+ train_transform=None, # your own transform (e.g. albumentations) replaces the built-in one
275
+ eval_transform=None,
276
+ # --- loader ------------------------------------------------------------------
277
+ batch_size=8, eval_batch_size=None, num_workers=4,
278
+ balance="dataset", # None, "dataset", "class" or "source" (see below)
279
+ seed=42,
280
+ root=None, # BRUISE_DATA folder; default = the folder holding bruise_data/
281
+ verbose=True, # prints a count table
282
+ )
283
+ ```
284
+
285
+ **What the loader does, step by step:**
286
+
287
+ 1. Reads `manifest.csv` of each dataset you asked for.
288
+ 2. Filters rows. Mimics are filtered by class, source and licence. NIJ/Fenwick rows are reduced to one annotator (`annotator` for train/val, `test_annotator` for test).
289
+ 3. Assigns splits. For NIJ/Fenwick, `fold == val_fold` becomes `val`, the other folds `train`, and the test set never changes. Mimic splits come straight from the manifest.
290
+ 4. Gives every class an integer label: `0 = bruise`, then the mimic classes in alphabetical order. The list is in `train_dl.dataset.class_names`. With `binary=True` there are only two, and bruise is the positive class: `0 = non_bruise`, `1 = bruise`.
291
+ 5. For each image:
292
+ 1. Loads it. Large JPEGs are decoded at reduced resolution, which is much faster for NIJ's 24 MP photos.
293
+ 2. Loads the mask, or uses an all-zero mask for mimics.
294
+ 3. Applies the transform and returns a dict.
295
+ 6. Draws training batches with `WeightedRandomSampler` when `balance` is set:
296
+ - `"dataset"`: NIJ, Fenwick and mimics are drawn equally often. This stops 5,000 mimics from drowning 664 NIJ images.
297
+ - `"class"`: every class (bruise and each mimic class) is drawn equally often.
298
+ - `"source"`: every source dataset is drawn equally often.
299
+ - `None`: plain shuffling.
300
+ Val and test are never re-weighted or shuffled.
301
+
302
+ ### 6.2 What one batch contains
303
+
304
+ | Key | Type / shape | Notes |
305
+ |---|---|---|
306
+ | `image` | float `[B, 3, S, S]` | ImageNet-normalised; `bruise_data.denormalize()` undoes it for plotting |
307
+ | `mask` | float `[B, 1, S, S]` | 1 = bruise. Segmentation only. All zeros for mimics |
308
+ | `label` | int `[B]` | index into `class_names` |
309
+ | `class_name`, `dataset`, `source_dataset`, `subject_id`, `image_id` | list of str | for analysis / per-group metrics |
310
+ | `is_bruise` | bool `[B]` | |
311
+ | `skin_L`, `skin_a`, `skin_b`, `skin_ita`, `lesion_L`, `lesion_a`, `lesion_b` | float `[B]` | NaN when unknown (mimics) |
312
+ | `fitzpatrick`, `monk_tone` | float `[B]` | NaN when unknown |
313
+
314
+ ### 6.3 Other functions
315
+
316
+ | Function | Use |
317
+ |---|---|
318
+ | `get_test_loaders(datasets, ...)` | `{"nij": dl, "fenwick": dl, "mimics": dl}`, one test loader per dataset, to report each one separately |
319
+ | `get_datasets(datasets, ...)` | Returns `(train_ds, val_ds, test_ds)` PyTorch Datasets, if you want to build DataLoaders yourself |
320
+ | `load_index(datasets, ...)` | The filtered table as a pandas DataFrame: no images loaded, good for counting or custom splits |
321
+ | `summary(df)` | Count table per dataset / class / split |
322
+ | `check_splits()` | Runs all leakage and file checks; returns a list of problems |
323
+ | `BruiseTransform(image_size, train=...)` | The built-in transform, if you want to tweak it (`vflip=0.5`, `rotate=30`, `jitter=0`, `normalize=False`, ...) |
324
+ | `read_manifest("nij")` | Raw manifest as a DataFrame (missing optional columns filled with defaults) |
325
+ | `list_datasets()` | Every dataset found: each folder of `BRUISE_DATA/` that has a `manifest.csv` |
326
+ | `dataset_info("nij")` | What a manifest holds: annotators, primary, masks, majority-vote test, folds |
327
+
328
+ ---
329
+
330
+ ## 7. Recipes
331
+
332
+ **Bruise segmentation on NIJ only, primary annotator (the setup of earlier experiments):**
333
+ ```python
334
+ train_dl, val_dl, test_dl = get_dataloaders(["nij"], balance=None)
335
+ ```
336
+
337
+ **NIJ + Fenwick + all mimics as negatives (segmentation):**
338
+ ```python
339
+ train_dl, val_dl, test_dl = get_dataloaders(["nij", "fenwick", "mimics"], balance="dataset")
340
+ ```
341
+ Mimic images come with empty masks, so the model learns not to fire on purpura, burns, erythema and the rest.
342
+
343
+ **Only the strongest mimics, without DermNet:**
344
+ ```python
345
+ get_dataloaders(["fenwick", "mimics"], mimic_classes=["purpura", "hemangioma", "pigmentary_lesion"],
346
+ exclude_mimic_sources=["dermnet"])
347
+ ```
348
+
349
+ **Classification (bruise vs. each mimic class), including the extra web bruises:**
350
+ ```python
351
+ train_dl, val_dl, test_dl = get_dataloaders(["nij", "fenwick", "mimics", "extra_bruise"],
352
+ task="classification", image_size=224, balance="class")
353
+ print(train_dl.dataset.class_names)
354
+ # ['bruise', 'abrasion', 'burn', 'erythema', 'hemangioma', 'incised_wound', 'laceration', 'pigmentary_lesion', 'purpura']
355
+ ```
356
+
357
+ **Keep petechiae as its own class:**
358
+ ```python
359
+ get_dataloaders(["mimics"], task="classification", merge={}, min_class_images=0)
360
+ ```
361
+
362
+ **Train on a different annotator; test against one annotator instead of the majority:**
363
+ ```python
364
+ get_dataloaders(["nij"], annotator="gbarimah", test_annotator="paul")
365
+ get_dataloaders(["nij", "fenwick"], annotator={"nij": "erik", "fenwick": "hliu36"})
366
+ ```
367
+
368
+ **5-fold cross-validation:**
369
+ ```python
370
+ for k in range(5):
371
+ train_dl, val_dl, test_dl = get_dataloaders(["nij", "fenwick"], val_fold=k, verbose=False)
372
+ ...
373
+ ```
374
+
375
+ **Your own albumentations pipeline** (the built-in transform is replaced; add Normalize + ToTensorV2 yourself):
376
+ ```python
377
+ import albumentations as A
378
+ from albumentations.pytorch import ToTensorV2
379
+ train_tf = A.Compose([A.LongestMaxSize(640), A.PadIfNeeded(640, 640), A.RandomCrop(512, 512),
380
+ A.HorizontalFlip(), A.Normalize(), ToTensorV2()])
381
+ eval_tf = A.Compose([A.LongestMaxSize(512), A.PadIfNeeded(512, 512), A.Normalize(), ToTensorV2()])
382
+ get_dataloaders(["nij", "mimics"], train_transform=train_tf, eval_transform=eval_tf)
383
+ ```
384
+ With a custom transform, images are decoded at full resolution, so your transform should do the resizing.
385
+
386
+ **Per-dataset test scores:**
387
+ ```python
388
+ for name, dl in get_test_loaders(["nij", "fenwick", "mimics"], image_size=512).items():
389
+ print(name, evaluate(model, dl)) # mimics: count false-positive pixels / images
390
+ ```
391
+
392
+ **Results by skin tone:**
393
+ ```python
394
+ for batch in test_dl:
395
+ pred = model(batch["image"])
396
+ # batch["skin_ita"] / batch["skin_L"] / batch["subject_id"] let you group the metric
397
+ ```
398
+
399
+ **ALS (server only, section 3.5):**
400
+ ```python
401
+ get_dataloaders(["nij", "nij_als", "fenwick", "fenwick_als"]) # white light + ALS, leak-free (shared folds)
402
+ get_dataloaders(["nij_als", "fenwick_als"]) # ALS only
403
+ tests = get_test_loaders(["nij", "nij_als"]) # report white light and ALS separately
404
+
405
+ # one wavelength only (e.g. 415 nm): filter the index, then build the Datasets yourself
406
+ from bruise_data import load_index, BruiseDataset, BruiseTransform
407
+ df = load_index(["nij_als"])
408
+ df = df[df.wavelength.astype(str) == "415"]
409
+ parts = {s: df[df.split == s].copy() for s in ("train", "val", "test")}
410
+ for part in parts.values():
411
+ part.attrs = dict(df.attrs) # keeps class names and root
412
+ train_ds = BruiseDataset(parts["train"], transform=BruiseTransform(512, train=True), fast_decode=1024)
413
+ val_ds = BruiseDataset(parts["val"], transform=BruiseTransform(512, train=False), fast_decode=1024)
414
+ test_ds = BruiseDataset(parts["test"], transform=BruiseTransform(512, train=False), fast_decode=1024)
415
+ ```
416
+
417
+ **Only the table (no images):**
418
+ ```python
419
+ from bruise_data import load_index
420
+ df = load_index(["nij", "fenwick", "mimics"])
421
+ df.groupby(["split", "ita_category"]).size()
422
+ ```
423
+
424
+ A full, runnable training loop is in `examples/train_segmentation.py`.
425
+
426
+ ---
427
+
428
+ ## 8. Skin tone (L\*a\*b\*)
429
+
430
+ Both bruise datasets carry colorimeter readings measured during the study (CIE L\*a\*b\*). ITA° = arctan((L\* − 50) / b\*) × 180/π, and `ita_category` uses the standard bands: >55 Very Light, 41–55 Light, 28–41 Intermediate, 10–28 Tan, −30–10 Brown, < −30 Dark.
431
+
432
+ | | NIJ | Fenwick |
433
+ |---|---|---|
434
+ | File | `nij/metadata/skin_tone_lab.csv` | `fenwick/metadata/skin_tone_lab.csv` |
435
+ | Key | `subject_id, visit, arm` (UA/LA) | `subject_id, lesion_id` |
436
+ | Skin (`skin_*`) | "around" reading next to the bruise | `cie_skin_l/a/b` |
437
+ | Lesion (`lesion_*`) | "bruise" reading on the bruise | `cie_lesion_l/a/b` |
438
+ | Source | `final_upperarm_dataset.csv`, `final_lowerarm_dataset.csv` | REDCap export `IAL_Fenwick_10_12_2026.csv` |
439
+ | Fallback | subject median ("around", same arm) when the visit has no reading | none needed |
440
+ | Coverage | 99.7% of images (skin), 97.9% (lesion) | 100% |
441
+
442
+ The untouched source files are in each dataset's `metadata/original/`. Skin-tone values are already joined into every manifest row and returned in every batch.
443
+
444
+ ---
445
+
446
+ ## 9. Command-line tools
447
+
448
+ Run from inside `BRUISE_DATA/`:
449
+
450
+ ```bash
451
+ python -m bruise_data datasets # every dataset found, its annotators / masks / folds
452
+ python -m bruise_data check # every dataset: files exist + no subject/near-duplicate leakage
453
+ python -m bruise_data check --deep 50 # also open 50 images per dataset, compare mask size
454
+ python -m bruise_data summary --datasets nij fenwick mimics
455
+ python -m bruise_data summary --datasets nij fenwick mimics extra_bruise --task classification
456
+ python -m bruise_data classes # mimic counts per class x source x split
457
+ python -m bruise_data preview --datasets nij mimics --out preview.jpg # 16 training images, bruise mask in red
458
+ ```
459
+
460
+ ---
461
+
462
+ ## 10. Running on the GPU server
463
+
464
+ 1. Copy the data: unzip all four archives into the **same** folder, e.g. `/home/data/bruise/BRUISE_DATA/`. Every path in the manifests is relative, so nothing has to be edited.
465
+ | Archive | Contains | Size |
466
+ |---|---|---|
467
+ | `bruise_data_code.zip` | `README.md`, `requirements.txt`, `bruise_data/`, `examples/`, `tools/` | < 1 MB |
468
+ | `nij.zip` | `nij/` | 6.6 GB (4 GB of it is `alt_annotators/`) |
469
+ | `fenwick.zip` | `fenwick/` | 0.6 GB |
470
+ | `mimics.zip` | `mimics/` and `extra_bruise/` | 2.6 GB |
471
+ ```bash
472
+ # on the laptop (Git Bash): send the zips (~10.4 GB)
473
+ scp C:/BRUISE_ORGANIZED/KIYARASH_DATASET/BRUISE_DATA_zips/*.zip tbommawa@mlidl:/home/data/bruise/
474
+
475
+ # on the server: unzip (needs ~11 GB more free space), make it readable for everyone, remove the zips
476
+ df -h /home/data
477
+ mkdir -p /home/data/bruise/BRUISE_DATA && cd /home/data/bruise/BRUISE_DATA
478
+ for z in bruise_data_code nij fenwick mimics; do unzip -q /home/data/bruise/$z.zip; done
479
+ chmod -R a+rX /home/data/bruise/BRUISE_DATA
480
+ rm /home/data/bruise/bruise_data_code.zip /home/data/bruise/nij.zip /home/data/bruise/fenwick.zip /home/data/bruise/mimics.zip
481
+ ```
482
+ 2. Activate your environment and check:
483
+ ```bash
484
+ conda activate /home/tbommawa/conda_envs/bruise # or any env with torch, pandas, pillow
485
+ cd /home/data/bruise/BRUISE_DATA
486
+ python -m bruise_data check
487
+ python -m bruise_data summary --datasets nij fenwick mimics
488
+ python examples/quick_start.py
489
+ ```
490
+ 3. Use it from a training script anywhere:
491
+ ```bash
492
+ export BRUISE_DATA_ROOT=/home/data/bruise/BRUISE_DATA
493
+ export PYTHONPATH=$BRUISE_DATA_ROOT:$PYTHONPATH
494
+ python my_train.py
495
+ ```
496
+ or in Python: `get_dataloaders(..., root="/home/data/bruise/BRUISE_DATA")`.
497
+ 4. Example training run:
498
+ ```bash
499
+ python examples/train_segmentation.py --datasets nij fenwick mimics --epochs 2 --image-size 512 --batch-size 8
500
+ ```
501
+
502
+ Use `num_workers=8` or more on the server. Decoding the large NIJ JPEGs is the slowest step.
503
+
504
+ The ALS datasets (`nij_als`, `fenwick_als`) are not in these zips. They are built directly on the server: see [11.2](#112-adding-the-als-data-nij_als-fenwick_als).
505
+
506
+ ---
507
+
508
+ ## 11. Rebuilding or adding data
509
+
510
+ `tools/build_bruise_data.py` rebuilds every folder and manifest from the original sources in `C:/BRUISE_ORGANIZED/KIYARASH_DATASET`. It is safe to re-run: existing links are kept and manifests are rewritten.
511
+
512
+ ```bash
513
+ python tools/build_bruise_data.py # everything
514
+ python tools/build_bruise_data.py --only mimics # one part
515
+ python -m bruise_data check # always re-check afterwards
516
+ ```
517
+
518
+ - **Change a mimic class mapping:** edit `CLASS_MAP` at the top of the builder, re-run `--only mimics`.
519
+ - **Add Roboflow "Basic Wound Classify"** (needs a Roboflow API key): export it in *folder* format into `BRUISE_LOOKALIKE/downloads/basic_wound_classify/` (`<split>/<class>/*.jpg`), then re-run `--only mimics`. Abrasion, burn and laceration go to mimics, bruise to `extra_bruise`, and duplicates of existing images are removed automatically.
520
+ - **Splits are fixed.** NIJ/Fenwick folds come from the original subject split. Mimic splits come from the look-alike package. New web images get a split from a hash of their near-duplicate group, so re-running never moves an image to another split.
521
+
522
+ ### 11.1 Adding a new dataset (plug-and-play)
523
+
524
+ The loader does not hard-code any dataset. **Every sub-folder of `BRUISE_DATA/` that holds a `manifest.csv` is a dataset**, and the loader treats it according to what its manifest contains. To add one:
525
+
526
+ 1. Put the files in `BRUISE_DATA/<name>/`. Any layout works; `train|val|test/images|masks/` matches the other datasets.
527
+ 2. Write `BRUISE_DATA/<name>/manifest.csv`.
528
+ 3. Run `python -m bruise_data datasets` (it should appear in the list) and `python -m bruise_data check` (everything should PASS).
529
+ 4. Use it: `get_dataloaders(["nij", "fenwick", "<name>"])`. No code changes are needed, including in the SegFormer script (`--datasets nij fenwick <name>`).
530
+
531
+ **Required columns** (only these five): `image_id`, `split` (`train`/`val`/`test`), `class_name`, `is_bruise`, `image_path` (relative to `BRUISE_DATA/`). Every other column in [section 5](#5-manifestcsv-columns) is optional; missing ones get defaults:
532
+
533
+ | Column | Default |
534
+ |---|---|
535
+ | `mask_path` | `""` (no mask) |
536
+ | `annotator` | `none` |
537
+ | `is_primary_annotator` | `True` |
538
+ | `fold` | `-1` |
539
+ | `subject_id` | `""` |
540
+ | `source_dataset` | the folder name |
541
+ | skin-tone columns | NaN |
542
+
543
+ **How the manifest decides the behaviour** (`dataset_info("<name>")` shows the result):
544
+
545
+ | The manifest has… | It is treated like | What the loader does |
546
+ |---|---|---|
547
+ | bruise rows with real annotator names (one flagged `is_primary_annotator=True`) | `nij`, `fenwick` | Trains on the chosen annotator's masks (`annotator=`, default the primary). If train/val rows have `fold` 0–4, val is re-cut with `val_fold`; otherwise the manifest's split is kept. Test uses rows with `annotator == "majority"` if present, otherwise the primary (with a warning). |
548
+ | bruise rows, no annotators | `extra_bruise` | Uses the rows as listed. Segmentation needs a `mask_path`, otherwise it is classification only. |
549
+ | no bruise rows (`is_bruise` all False) | `mimics` | Each image is a negative with an empty mask. `mimic_classes`, `mimic_sources`, `exclude_mimic_sources`, `merge` and `min_class_images` apply to all negative datasets together. |
550
+
551
+ **Rules that keep it leak-free:**
552
+ - For annotated bruise datasets, give every row a `subject_id`, and keep each subject in one fold and out of test. `check` verifies this.
553
+ - For negatives, set `near_dup_group` (and `subject_id` if known) so that duplicates stay in one split.
554
+ - Use `class_name="bruise"` for bruise images, and the taxonomy names for mimics so that classes merge across sources.
555
+
556
+ Minimal example: a new negative set with only the required columns.
557
+ ```python
558
+ import pandas as pd
559
+ rows = [dict(image_id="newneg__001", split="train", class_name="purpura", is_bruise=False,
560
+ image_path="newneg/purpura/001.jpg")] # ... one dict per image
561
+ pd.DataFrame(rows).to_csv("BRUISE_DATA/newneg/manifest.csv", index=False)
562
+ ```
563
+ An annotated bruise set adds `mask_path`, `annotator`, `is_primary_annotator`, `fold` and `subject_id`. It has one row per image per annotator, plus `annotator="majority"` rows on test if you have a consensus mask. For a dataset that is rebuilt from raw sources, add a `build_<name>()` function to `tools/build_bruise_data.py` (copy `build_fenwick`), so the manifest can be regenerated.
564
+
565
+ ### 11.2 Adding the ALS data (`nij_als`, `fenwick_als`)
566
+
567
+ The ALS photos are downloaded from Labelbox on the Hopper cluster and built into BRUISE_DATA on mlidl. They are never on the laptop, which has too little space. Full steps with every command are in `tools/labelbox_als/README.md`. In short:
568
+
569
+ ```bash
570
+ # Hopper (/scratch/tbommawa/als_download, after the one-time setup in that README)
571
+ sbatch --export=ALL,STUDY=nij run_als_download.slurm
572
+ sbatch --export=ALL,STUDY=fenwick run_als_download.slurm
573
+ # mlidl: pull the finished download, then build and check
574
+ mkdir -p /home/data/bruise/als_download && cd /home/data/bruise/als_download
575
+ ssh tbommawa@hopper.orc.gmu.edu 'cd /scratch/tbommawa/als_download && tar cf - labelbox_*_als/train_*_als_minus_test_subjects labelbox_*_als/fixed_consensus_test_als labelbox_*_als/parsed_tables' | tar xf -
576
+ cd /home/data/bruise/BRUISE_DATA
577
+ python tools/build_bruise_data.py --only nij_als fenwick_als --als-src /home/data/bruise/als_download
578
+ python -m bruise_data check
579
+ ```
580
+
581
+ The builder needs `nij/` and `fenwick/` in the same BRUISE_DATA: it reads the subject folds, test subjects and skin tone from them. It does not rebuild them.
582
+
583
+ ---
584
+
585
+ ## 12. Licences and data rules
586
+
587
+ - **NIJ and Fenwick** are private study data. Never upload them to GitHub, a public bucket or a model hub.
588
+ - **Mimics** come from open datasets with different licences (`licence` and `commercial_use` columns). Non-commercial: DermNet, Fitzpatrick17k, SCIN, arsenic set. Commercial use allowed: Skin Burn (CC0), the Roboflow sets and MEMI-DS (CC BY 4.0), Clinical Wound (Apache-2.0). The Kaggle wound set (`wound_yasinpratomo`) has no stated licence. Use `commercial_only=True` to restrict.
589
+ - **Citations:** cite the source datasets in papers. Their URLs are in `BRUISE_LOOKALIKE/bruise_lookalike_negatives/sources.csv`.
590
+
591
+ ---
592
+
593
+ ## 13. Troubleshooting
594
+
595
+ | Problem | Fix |
596
+ |---|---|
597
+ | `FileNotFoundError: .../manifest.csv` | `root` is wrong. Pass `root="/path/to/BRUISE_DATA"` or set `BRUISE_DATA_ROOT` |
598
+ | `ModuleNotFoundError: bruise_data` | Run from inside `BRUISE_DATA/` or add it to `PYTHONPATH` / `sys.path` |
599
+ | Windows: `RuntimeError ... freeze_support` or the script restarts | Put your code under `if __name__ == "__main__":`, or use `num_workers=0` |
600
+ | Training is slow | Raise `num_workers`; keep the built-in transform (it decodes big JPEGs at reduced scale); use a smaller `image_size` |
601
+ | `'extra_bruise' has no masks` | Use `task="classification"`, or drop `extra_bruise` (same for any bruise dataset without `mask_path`) |
602
+ | `unknown dataset(s)` | The name must be a folder of `BRUISE_DATA/` with a `manifest.csv`; `python -m bruise_data datasets` lists them |
603
+ | `missing required column(s)` | A new manifest needs `image_id, split, class_name, is_bruise, image_path` |
604
+ | `annotators [...] but none has is_primary_annotator=True` | Flag exactly one annotator's train/val rows as primary |
605
+ | Warning "mimic classes with < 50 images left out" | Expected for small classes. Name them in `mimic_classes`, or set `min_class_images=0` |
606
+ | `unknown mimic class` | Run `python -m bruise_data classes` to see valid names |
607
+ | A batch has NaN `skin_L` | Normal for mimics and the 0.3% of NIJ images without a colorimeter reading |