Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .DS_Store +0 -0
- .gitattributes +4 -35
- .ipynb_checkpoints/DGFF_full-checkpoint.ipynb +0 -0
- .ipynb_checkpoints/DGFF_full-v2-checkpoint.ipynb +0 -0
- .ipynb_checkpoints/DGFF_full_v1-checkpoint.ipynb +0 -0
- .ipynb_checkpoints/LOL_load-checkpoint.ipynb +0 -0
- .ipynb_checkpoints/Untitled-checkpoint.ipynb +6 -0
- .ipynb_checkpoints/additional_revew_test-checkpoint.ipynb +6 -0
- .ipynb_checkpoints/confusion_matrix_normalized-checkpoint.png +3 -0
- .ipynb_checkpoints/dgff-checkpoint.py +367 -0
- .ipynb_checkpoints/dgff_results-checkpoint.png +3 -0
- .ipynb_checkpoints/dgff_v2-checkpoint.py +178 -0
- .ipynb_checkpoints/gate_maps-checkpoint.png +3 -0
- .ipynb_checkpoints/llen-checkpoint.py +62 -0
- .ipynb_checkpoints/lol_dataset-checkpoint.py +226 -0
- .ipynb_checkpoints/train-checkpoint.ipynb +6 -0
- .ipynb_checkpoints/val_batch0_pred-checkpoint.jpg +3 -0
- DGFF_full-v2.ipynb +0 -0
- DGFF_full.ipynb +3 -0
- DGFF_full_v1.ipynb +0 -0
- Dataset/README.md +40 -0
- Dataset/Thumbnails.png +3 -0
- Dataset/exdarkimg.gif +3 -0
- ExDark/.DS_Store +0 -0
- ExDark/Bicycle/2015_00001.png +3 -0
- ExDark/Bicycle/2015_00002.png +3 -0
- ExDark/Bicycle/2015_00003.png +3 -0
- ExDark/Bicycle/2015_00004.jpg +0 -0
- ExDark/Bicycle/2015_00005.jpg +0 -0
- ExDark/Bicycle/2015_00006.jpg +3 -0
- ExDark/Bicycle/2015_00007.jpg +0 -0
- ExDark/Bicycle/2015_00008.jpg +0 -0
- ExDark/Bicycle/2015_00009.jpg +3 -0
- ExDark/Bicycle/2015_00010.jpg +3 -0
- ExDark/Bicycle/2015_00011.jpg +0 -0
- ExDark/Bicycle/2015_00012.jpg +0 -0
- ExDark/Bicycle/2015_00013.jpg +0 -0
- ExDark/Bicycle/2015_00014.jpg +3 -0
- ExDark/Bicycle/2015_00015.jpg +3 -0
- ExDark/Bicycle/2015_00016.jpg +3 -0
- ExDark/Bicycle/2015_00017.jpg +0 -0
- ExDark/Bicycle/2015_00018.jpg +0 -0
- ExDark/Bicycle/2015_00019.jpg +3 -0
- ExDark/Bicycle/2015_00020.jpg +3 -0
- ExDark/Bicycle/2015_00021.jpg +0 -0
- ExDark/Bicycle/2015_00022.jpg +3 -0
- ExDark/Bicycle/2015_00023.jpg +3 -0
- ExDark/Bicycle/2015_00024.JPG +0 -0
- ExDark/Bicycle/2015_00025.jpg +3 -0
- ExDark/Bicycle/2015_00026.jpg +3 -0
.DS_Store
ADDED
|
Binary file (28.7 kB). View file
|
|
|
.gitattributes
CHANGED
|
@@ -1,35 +1,4 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
DGFF_full.ipynb filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
Dataset/exdarkimg.gif filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.ipynb_checkpoints/DGFF_full-checkpoint.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
.ipynb_checkpoints/DGFF_full-v2-checkpoint.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
.ipynb_checkpoints/DGFF_full_v1-checkpoint.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
.ipynb_checkpoints/LOL_load-checkpoint.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
.ipynb_checkpoints/Untitled-checkpoint.ipynb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [],
|
| 3 |
+
"metadata": {},
|
| 4 |
+
"nbformat": 4,
|
| 5 |
+
"nbformat_minor": 5
|
| 6 |
+
}
|
.ipynb_checkpoints/additional_revew_test-checkpoint.ipynb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [],
|
| 3 |
+
"metadata": {},
|
| 4 |
+
"nbformat": 4,
|
| 5 |
+
"nbformat_minor": 5
|
| 6 |
+
}
|
.ipynb_checkpoints/confusion_matrix_normalized-checkpoint.png
ADDED
|
Git LFS Details
|
.ipynb_checkpoints/dgff-checkpoint.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Detection-Guided Feature Feedback (DGFF) Module
|
| 3 |
+
================================================
|
| 4 |
+
The DGFF module is the core architectural contribution of this thesis.
|
| 5 |
+
|
| 6 |
+
During training it:
|
| 7 |
+
1. Hooks into the YOLOv8 backbone to extract intermediate feature maps
|
| 8 |
+
at P4 (stride-16) and P5 (stride-32) — the semantically richest layers.
|
| 9 |
+
2. Projects each feature map to match the channel depth of the corresponding
|
| 10 |
+
LLEN decoder stage via lightweight 1×1 convolutional adapters.
|
| 11 |
+
3. Returns the projected tensors so LLEN.forward() can inject them via
|
| 12 |
+
element-wise addition at decoder stages D4 and D3.
|
| 13 |
+
|
| 14 |
+
At inference the DGFF module is NOT used — LLEN runs alone with no overhead.
|
| 15 |
+
|
| 16 |
+
YOLOv8n backbone channel dimensions (verified for 640×640 input):
|
| 17 |
+
P4: (B, 256, H/16, W/16) stride-16, semantically rich
|
| 18 |
+
P5: (B, 512, H/32, W/32) stride-32, most semantic
|
| 19 |
+
|
| 20 |
+
LLEN decoder injection targets:
|
| 21 |
+
dec4 output: (B, 128, H/8, W/8) ← receives projected P5
|
| 22 |
+
dec3 output: (B, 64, H/4, W/4) ← receives projected P4
|
| 23 |
+
|
| 24 |
+
Data flow during training:
|
| 25 |
+
low_img
|
| 26 |
+
│
|
| 27 |
+
▼
|
| 28 |
+
LLEN encoder → bottleneck
|
| 29 |
+
│ YOLOv8 backbone
|
| 30 |
+
│ enhanced_img ──────────────────────► P4 (256ch)
|
| 31 |
+
│ ▲ ► P5 (512ch)
|
| 32 |
+
│ │ │
|
| 33 |
+
│ LLEN head DGFF adapters
|
| 34 |
+
│ ▲ P4→64ch, P5→128ch
|
| 35 |
+
│ │ │
|
| 36 |
+
└── dec1 ◄ dec2 ◄ dec3 ◄──────── dgff_p4 │
|
| 37 |
+
dec4 ◄──────────────── dgff_p5
|
| 38 |
+
(element-wise +)
|
| 39 |
+
|
| 40 |
+
Usage:
|
| 41 |
+
from dgff import DGFFModule
|
| 42 |
+
dgff = DGFFModule()
|
| 43 |
+
|
| 44 |
+
# During joint training forward pass:
|
| 45 |
+
enhanced = llen(low_img) # first pass: no feedback
|
| 46 |
+
dgff_p4, dgff_p5 = dgff(enhanced) # extract + project features
|
| 47 |
+
enhanced = llen(low_img, dgff_p5, dgff_p4) # second pass: with feedback
|
| 48 |
+
loss = detection_loss + l1_loss(enhanced, high_img)
|
| 49 |
+
loss.backward()
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
import torch
|
| 53 |
+
import torch.nn as nn
|
| 54 |
+
import torch.nn.functional as F
|
| 55 |
+
from ultralytics import YOLO
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# ─────────────────────────────────────────────────────────────
|
| 59 |
+
# Adapter — lightweight 1×1 projection
|
| 60 |
+
# ─────────────────────────────────────────────────────────────
|
| 61 |
+
|
| 62 |
+
class FeatureAdapter(nn.Module):
|
| 63 |
+
"""
|
| 64 |
+
Projects a YOLOv8 feature map to the channel depth expected by LLEN.
|
| 65 |
+
|
| 66 |
+
1×1 Conv keeps spatial size unchanged and adds negligible parameter cost.
|
| 67 |
+
BN + ReLU stabilise training when detection gradients flow back.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
in_ch : channels from YOLOv8 backbone (P4=256, P5=512)
|
| 71 |
+
out_ch : channels expected by LLEN decoder (dec3=64, dec4=128)
|
| 72 |
+
"""
|
| 73 |
+
def __init__(self, in_ch: int, out_ch: int):
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.adapt = nn.Sequential(
|
| 76 |
+
nn.Conv2d(in_ch, out_ch, kernel_size=1, bias=False),
|
| 77 |
+
nn.BatchNorm2d(out_ch),
|
| 78 |
+
nn.ReLU(inplace=True),
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def forward(self, x):
|
| 82 |
+
return self.adapt(x)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ─────────────────────────────────────────────────────────────
|
| 86 |
+
# DGFF Module
|
| 87 |
+
# ─────────────────────────────────────────────────────────────
|
| 88 |
+
|
| 89 |
+
class DGFFModule(nn.Module):
|
| 90 |
+
"""
|
| 91 |
+
Detection-Guided Feature Feedback module.
|
| 92 |
+
|
| 93 |
+
Wraps a frozen-or-trainable YOLOv8 backbone and attaches forward hooks
|
| 94 |
+
to capture P4 and P5 feature maps. Lightweight adapters project them to
|
| 95 |
+
match the LLEN decoder channel depths.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
yolo_weights : YOLOv8 model weights string (e.g. 'yolov8n.pt')
|
| 99 |
+
Downloads automatically on first use.
|
| 100 |
+
llen_base_ch : base channel count of the LLEN (default 32).
|
| 101 |
+
dec4 expects llen_base_ch*4, dec3 expects llen_base_ch*2.
|
| 102 |
+
freeze_backbone : if True, YOLOv8 backbone weights are frozen and
|
| 103 |
+
only the adapter layers are trained in this module.
|
| 104 |
+
Set False to fine-tune backbone end-to-end.
|
| 105 |
+
|
| 106 |
+
YOLOv8n P-layer channels (hard-coded for yolov8n):
|
| 107 |
+
P4 → 128 channels (layer 6 C2f output)
|
| 108 |
+
P5 → 256 channels (layer 9 C2f output)
|
| 109 |
+
If you switch to yolov8s/m/l/x, update P4_CH and P5_CH accordingly:
|
| 110 |
+
yolov8s: P4=256, P5=512
|
| 111 |
+
yolov8m: P4=384, P5=576
|
| 112 |
+
yolov8l: P4=512, P5=512
|
| 113 |
+
yolov8x: P4=640, P5=640
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
# YOLOv8n backbone output channels at P4 and P5
|
| 117 |
+
# Verified by probing layer outputs on 640×640 input:
|
| 118 |
+
# layer 6 (C2f) → 128ch = P4
|
| 119 |
+
# layer 9 (C2f) → 256ch = P5
|
| 120 |
+
P4_CH = 128
|
| 121 |
+
P5_CH = 256
|
| 122 |
+
|
| 123 |
+
# YOLOv8 layer indices where P4 and P5 are produced
|
| 124 |
+
# (these are the C2f output layers in the YOLOv8n backbone)
|
| 125 |
+
P4_LAYER_IDX = 6 # stride-16 feature map
|
| 126 |
+
P5_LAYER_IDX = 9 # stride-32 feature map
|
| 127 |
+
|
| 128 |
+
def __init__(
|
| 129 |
+
self,
|
| 130 |
+
yolo_weights: str = 'yolov8n.pt',
|
| 131 |
+
llen_base_ch: int = 32,
|
| 132 |
+
freeze_backbone: bool = True,
|
| 133 |
+
):
|
| 134 |
+
super().__init__()
|
| 135 |
+
|
| 136 |
+
# ── Load YOLOv8 ──────────────────────────────────────
|
| 137 |
+
yolo = YOLO(yolo_weights)
|
| 138 |
+
self.backbone = yolo.model.model # nn.Sequential of backbone layers
|
| 139 |
+
|
| 140 |
+
if freeze_backbone:
|
| 141 |
+
for p in self.backbone.parameters():
|
| 142 |
+
p.requires_grad_(False)
|
| 143 |
+
|
| 144 |
+
# ── Verify actual backbone channel depths ────────────
|
| 145 |
+
# Run a tiny probe so we catch channel mismatches at init time
|
| 146 |
+
# rather than deep inside a training loop.
|
| 147 |
+
with torch.no_grad():
|
| 148 |
+
_probe = torch.zeros(1, 3, 64, 64)
|
| 149 |
+
_out = _probe
|
| 150 |
+
for _i, _layer in enumerate(list(self.backbone)[:self.P5_LAYER_IDX + 1]):
|
| 151 |
+
_out = _layer(_out)
|
| 152 |
+
if _i == self.P4_LAYER_IDX:
|
| 153 |
+
_actual_p4 = _out.shape[1]
|
| 154 |
+
if _i == self.P5_LAYER_IDX:
|
| 155 |
+
_actual_p5 = _out.shape[1]
|
| 156 |
+
if _actual_p4 != self.P4_CH or _actual_p5 != self.P5_CH:
|
| 157 |
+
raise ValueError(
|
| 158 |
+
f"YOLOv8 backbone channel mismatch!\n"
|
| 159 |
+
f" P4: expected {self.P4_CH}, got {_actual_p4}\n"
|
| 160 |
+
f" P5: expected {self.P5_CH}, got {_actual_p5}\n"
|
| 161 |
+
f"Update P4_CH and P5_CH class attributes to match."
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# ── Hook storage ─────────────────────────────────────
|
| 165 |
+
self._p4_feat = None
|
| 166 |
+
self._p5_feat = None
|
| 167 |
+
self._hooks = []
|
| 168 |
+
self._register_hooks()
|
| 169 |
+
|
| 170 |
+
# ── Adapter layers ───────────────────────────────────
|
| 171 |
+
# Project YOLOv8 channels → LLEN decoder channels
|
| 172 |
+
dec4_ch = llen_base_ch * 4 # 128 (dec4 out_ch)
|
| 173 |
+
dec3_ch = llen_base_ch * 2 # 64 (dec3 out_ch)
|
| 174 |
+
|
| 175 |
+
self.adapter_p5 = FeatureAdapter(self.P5_CH, dec4_ch) # 256 → 128
|
| 176 |
+
self.adapter_p4 = FeatureAdapter(self.P4_CH, dec3_ch) # 128 → 64
|
| 177 |
+
|
| 178 |
+
# ----------------------------------------------------------
|
| 179 |
+
def _register_hooks(self):
|
| 180 |
+
"""Attach forward hooks to the P4 and P5 backbone layers."""
|
| 181 |
+
|
| 182 |
+
def make_hook(name):
|
| 183 |
+
def hook(module, input, output):
|
| 184 |
+
if name == 'p4':
|
| 185 |
+
self._p4_feat = output
|
| 186 |
+
else:
|
| 187 |
+
self._p5_feat = output
|
| 188 |
+
return hook
|
| 189 |
+
|
| 190 |
+
layer_list = list(self.backbone)
|
| 191 |
+
h4 = layer_list[self.P4_LAYER_IDX].register_forward_hook(make_hook('p4'))
|
| 192 |
+
h5 = layer_list[self.P5_LAYER_IDX].register_forward_hook(make_hook('p5'))
|
| 193 |
+
self._hooks = [h4, h5]
|
| 194 |
+
|
| 195 |
+
# ----------------------------------------------------------
|
| 196 |
+
def remove_hooks(self):
|
| 197 |
+
"""Call this if you want to detach hooks (e.g. at inference)."""
|
| 198 |
+
for h in self._hooks:
|
| 199 |
+
h.remove()
|
| 200 |
+
self._hooks = []
|
| 201 |
+
|
| 202 |
+
# ----------------------------------------------------------
|
| 203 |
+
def forward(self, enhanced_img: torch.Tensor):
|
| 204 |
+
"""
|
| 205 |
+
Run the enhanced image through the YOLOv8 backbone to extract
|
| 206 |
+
P4 and P5 features, then project them via adapter layers.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
enhanced_img : (B, 3, H, W) enhanced output from LLEN, [0,1]
|
| 210 |
+
Will be resized to 640×640 for YOLOv8 if needed.
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
dgff_p4 : (B, llen_base_ch*2, H/4, W/4) — for LLEN dec3
|
| 214 |
+
dgff_p5 : (B, llen_base_ch*4, H/8, W/8) — for LLEN dec4
|
| 215 |
+
"""
|
| 216 |
+
B, C, H, W = enhanced_img.shape
|
| 217 |
+
|
| 218 |
+
# YOLOv8 expects 640×640 input; resize if necessary
|
| 219 |
+
if H != 640 or W != 640:
|
| 220 |
+
x = F.interpolate(
|
| 221 |
+
enhanced_img, size=(640, 640),
|
| 222 |
+
mode='bilinear', align_corners=False
|
| 223 |
+
)
|
| 224 |
+
else:
|
| 225 |
+
x = enhanced_img
|
| 226 |
+
|
| 227 |
+
# Run backbone layer-by-layer up to P5_LAYER_IDX.
|
| 228 |
+
# We cannot call self.backbone(x) directly because layers after
|
| 229 |
+
# the pure backbone use Concat ops that expect list inputs — they
|
| 230 |
+
# crash when called sequentially. The pure backbone layers 0-9
|
| 231 |
+
# are all simple Conv/C2f/SPPF blocks that accept a single tensor.
|
| 232 |
+
# YOLOv8 also saves certain intermediate outputs for skip routes;
|
| 233 |
+
# we replicate that here by keeping a `saved` dict keyed by index.
|
| 234 |
+
layer_list = list(self.backbone)
|
| 235 |
+
saved = {} # stores outputs of layers that feed into later Concat ops
|
| 236 |
+
# Layer save indices for YOLOv8n backbone (layers 4 and 6 are reused)
|
| 237 |
+
SAVE_LAYERS = {4, 6}
|
| 238 |
+
out = x
|
| 239 |
+
for i, layer in enumerate(layer_list[:self.P5_LAYER_IDX + 1]):
|
| 240 |
+
out = layer(out)
|
| 241 |
+
if i in SAVE_LAYERS:
|
| 242 |
+
saved[i] = out
|
| 243 |
+
# hooks already captured p4 (layer 6) and p5 (layer 9) via register_forward_hook
|
| 244 |
+
|
| 245 |
+
p4_raw = self._p4_feat # (B, 128, 40, 40) for 640×640 input
|
| 246 |
+
p5_raw = self._p5_feat # (B, 256, 20, 20) for 640×640 input
|
| 247 |
+
|
| 248 |
+
if p4_raw is None or p5_raw is None:
|
| 249 |
+
raise RuntimeError(
|
| 250 |
+
"DGFF hooks did not capture features. "
|
| 251 |
+
"Check P4_LAYER_IDX and P5_LAYER_IDX match your YOLOv8 version."
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Project to LLEN decoder channel depths
|
| 255 |
+
dgff_p4_proj = self.adapter_p4(p4_raw) # (B, 64, 40, 40) 128→64
|
| 256 |
+
dgff_p5_proj = self.adapter_p5(p5_raw) # (B, 128, 20, 20) 256→128
|
| 257 |
+
|
| 258 |
+
# Resize projected features to match LLEN decoder spatial sizes
|
| 259 |
+
# LLEN decoder spatial sizes depend on input resolution (H, W)
|
| 260 |
+
# dec4 operates at H/8, W/8 — matches P5 after resize
|
| 261 |
+
# dec3 operates at H/4, W/4 — matches P4 after resize
|
| 262 |
+
dgff_p5_out = F.interpolate(
|
| 263 |
+
dgff_p5_proj, size=(H // 8, W // 8),
|
| 264 |
+
mode='bilinear', align_corners=False
|
| 265 |
+
)
|
| 266 |
+
dgff_p4_out = F.interpolate(
|
| 267 |
+
dgff_p4_proj, size=(H // 4, W // 4),
|
| 268 |
+
mode='bilinear', align_corners=False
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
return dgff_p4_out, dgff_p5_out
|
| 272 |
+
|
| 273 |
+
# ----------------------------------------------------------
|
| 274 |
+
def count_parameters(self):
|
| 275 |
+
total = sum(p.numel() for p in self.parameters())
|
| 276 |
+
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 277 |
+
return total, trainable
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
# ─────────────────────────────────────────────────────────────
|
| 281 |
+
# Smoke Test
|
| 282 |
+
# ─────────────────────────────────────────────────────────────
|
| 283 |
+
|
| 284 |
+
if __name__ == '__main__':
|
| 285 |
+
import sys
|
| 286 |
+
from pathlib import Path
|
| 287 |
+
|
| 288 |
+
print("=" * 55)
|
| 289 |
+
print(" DGFF Module Smoke Test")
|
| 290 |
+
print("=" * 55)
|
| 291 |
+
|
| 292 |
+
device = (
|
| 293 |
+
torch.device('mps') if torch.backends.mps.is_available() else
|
| 294 |
+
torch.device('cuda') if torch.cuda.is_available() else
|
| 295 |
+
torch.device('cpu')
|
| 296 |
+
)
|
| 297 |
+
print(f" Device : {device}")
|
| 298 |
+
|
| 299 |
+
# ── Load DGFF ────────────────────────────────────────────
|
| 300 |
+
print(" Loading YOLOv8n backbone ...")
|
| 301 |
+
dgff = DGFFModule(
|
| 302 |
+
yolo_weights = 'yolov8n.pt',
|
| 303 |
+
llen_base_ch = 32,
|
| 304 |
+
freeze_backbone = True,
|
| 305 |
+
).to(device)
|
| 306 |
+
|
| 307 |
+
total, trainable = dgff.count_parameters()
|
| 308 |
+
print(f" Total parameters : {total:,}")
|
| 309 |
+
print(f" Trainable (adapters): {trainable:,}")
|
| 310 |
+
|
| 311 |
+
# ── Forward pass test ────────────────────────────────────
|
| 312 |
+
dummy_enhanced = torch.rand(2, 3, 256, 256).to(device)
|
| 313 |
+
|
| 314 |
+
print(f"\n Input : {tuple(dummy_enhanced.shape)}")
|
| 315 |
+
dgff_p4, dgff_p5 = dgff(dummy_enhanced)
|
| 316 |
+
print(f" dgff_p4 (→ dec3) : {tuple(dgff_p4.shape)}") # expect (2, 64, 64, 64)
|
| 317 |
+
print(f" dgff_p5 (→ dec4) : {tuple(dgff_p5.shape)}") # expect (2, 128, 32, 32)
|
| 318 |
+
|
| 319 |
+
assert dgff_p4.shape == (2, 64, 64, 64), f"P4 shape wrong: {dgff_p4.shape}"
|
| 320 |
+
assert dgff_p5.shape == (2, 128, 32, 32), f"P5 shape wrong: {dgff_p5.shape}"
|
| 321 |
+
print(" Shape assertions passed ✓")
|
| 322 |
+
|
| 323 |
+
# ── Full pipeline test: LLEN + DGFF ─────────────────────
|
| 324 |
+
print("\n Testing LLEN + DGFF combined forward pass ...")
|
| 325 |
+
sys.path.insert(0, str(Path(__file__).parent if '__file__' in dir() else Path('.')))
|
| 326 |
+
from llen import LLEN
|
| 327 |
+
|
| 328 |
+
llen = LLEN(base_ch=32).to(device)
|
| 329 |
+
|
| 330 |
+
# Simulate the two-pass training forward:
|
| 331 |
+
# Pass 1: LLEN enhances without feedback (cold start)
|
| 332 |
+
enhanced = llen(dummy_enhanced)
|
| 333 |
+
print(f" Pass 1 enhanced : {tuple(enhanced.shape)}")
|
| 334 |
+
|
| 335 |
+
# Pass 2: DGFF extracts features from enhanced image
|
| 336 |
+
dgff_p4, dgff_p5 = dgff(enhanced)
|
| 337 |
+
|
| 338 |
+
# Pass 3: LLEN re-enhances with detection feedback injected
|
| 339 |
+
enhanced_guided = llen(dummy_enhanced, dgff_p5=dgff_p5, dgff_p4=dgff_p4)
|
| 340 |
+
print(f" Pass 2 guided : {tuple(enhanced_guided.shape)}")
|
| 341 |
+
|
| 342 |
+
assert enhanced_guided.shape == dummy_enhanced.shape
|
| 343 |
+
print(" Combined pipeline ✓")
|
| 344 |
+
|
| 345 |
+
# ── Gradient flow test ─��─────────────────────────────────
|
| 346 |
+
print("\n Testing gradient flow through DGFF adapters ...")
|
| 347 |
+
import torch.nn.functional as F_test
|
| 348 |
+
|
| 349 |
+
llen.train()
|
| 350 |
+
dgff.train()
|
| 351 |
+
|
| 352 |
+
enhanced = llen(dummy_enhanced)
|
| 353 |
+
dgff_p4, dgff_p5 = dgff(enhanced)
|
| 354 |
+
enhanced_guided = llen(dummy_enhanced, dgff_p5=dgff_p5, dgff_p4=dgff_p4)
|
| 355 |
+
|
| 356 |
+
dummy_target = torch.rand_like(enhanced_guided)
|
| 357 |
+
loss = F_test.l1_loss(enhanced_guided, dummy_target)
|
| 358 |
+
loss.backward()
|
| 359 |
+
|
| 360 |
+
# Check adapter grads received signal
|
| 361 |
+
for name, param in dgff.adapter_p5.named_parameters():
|
| 362 |
+
assert param.grad is not None, f"adapter_p5.{name} has no gradient!"
|
| 363 |
+
for name, param in dgff.adapter_p4.named_parameters():
|
| 364 |
+
assert param.grad is not None, f"adapter_p4.{name} has no gradient!"
|
| 365 |
+
|
| 366 |
+
print(" Adapter gradients flowing ✓")
|
| 367 |
+
print("\n All DGFF tests passed ✓")
|
.ipynb_checkpoints/dgff_results-checkpoint.png
ADDED
|
Git LFS Details
|
.ipynb_checkpoints/dgff_v2-checkpoint.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
dgff_v2.py — DGFF Module with Channel Gating
|
| 3 |
+
=============================================
|
| 4 |
+
Improvement over v1: 1×1 adapter + sigmoid gate replaces plain
|
| 5 |
+
element-wise addition. The gate learns which detector channels
|
| 6 |
+
to incorporate per-spatial-location, making the feedback selective
|
| 7 |
+
and more theoretically principled.
|
| 8 |
+
|
| 9 |
+
Change from v1:
|
| 10 |
+
v1: x_decoder = x_decoder + adapter(f_det)
|
| 11 |
+
v2: gate = sigmoid(gate_conv(f_det))
|
| 12 |
+
x_decoder = x_decoder + gate * adapter(f_det)
|
| 13 |
+
|
| 14 |
+
The gate suppresses irrelevant detector features (e.g. background
|
| 15 |
+
texture channels that don't help enhancement) while amplifying
|
| 16 |
+
object-boundary and semantic channels.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
from ultralytics import YOLO
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class GatedAdapter(nn.Module):
|
| 25 |
+
"""
|
| 26 |
+
Projects YOLOv8 backbone features into LLEN decoder space
|
| 27 |
+
with a learned sigmoid gate.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
in_ch : YOLOv8 feature channels (256 for P4, 512 for P5 on yolov8n)
|
| 31 |
+
out_ch : LLEN decoder channels at the injection point
|
| 32 |
+
"""
|
| 33 |
+
def __init__(self, in_ch: int, out_ch: int):
|
| 34 |
+
super().__init__()
|
| 35 |
+
# Value path: projects detector features to decoder space
|
| 36 |
+
self.proj = nn.Sequential(
|
| 37 |
+
nn.Conv2d(in_ch, out_ch, kernel_size=1, bias=False),
|
| 38 |
+
nn.BatchNorm2d(out_ch),
|
| 39 |
+
nn.ReLU(inplace=True),
|
| 40 |
+
)
|
| 41 |
+
# Gate path: learns a channel-wise attention mask in [0, 1]
|
| 42 |
+
self.gate = nn.Sequential(
|
| 43 |
+
nn.Conv2d(in_ch, out_ch, kernel_size=1, bias=False),
|
| 44 |
+
nn.Sigmoid(),
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
def forward(self, f_det, target_size):
|
| 48 |
+
"""
|
| 49 |
+
Args:
|
| 50 |
+
f_det : (B, in_ch, H', W') raw backbone feature
|
| 51 |
+
target_size : (H, W) spatial size of the decoder stage
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
(B, out_ch, H, W) gated feature feedback
|
| 55 |
+
"""
|
| 56 |
+
# Resize to match decoder spatial resolution
|
| 57 |
+
f_det = nn.functional.interpolate(
|
| 58 |
+
f_det, size=target_size, mode='bilinear', align_corners=False
|
| 59 |
+
)
|
| 60 |
+
return self.gate(f_det) * self.proj(f_det) # element-wise gated projection
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class DGFFModule(nn.Module):
|
| 64 |
+
"""
|
| 65 |
+
Detection-Guided Feature Feedback module.
|
| 66 |
+
|
| 67 |
+
Wraps a frozen YOLOv8 backbone and two GatedAdapters that project
|
| 68 |
+
P4 and P5 backbone features into the LLEN decoder spatial dimensions.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
yolo_weights : path to YOLOv8 .pt file
|
| 72 |
+
llen_base_ch : base channel width of the LLEN (default 32)
|
| 73 |
+
freeze_backbone : if True, backbone weights are not updated
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
# YOLOv8n backbone channel counts at P4 and P5
|
| 77 |
+
# These depend on the variant (n/s/m/l/x). yolov8n:
|
| 78 |
+
YOLO_P4_CH = 256
|
| 79 |
+
YOLO_P5_CH = 512
|
| 80 |
+
|
| 81 |
+
def __init__(
|
| 82 |
+
self,
|
| 83 |
+
yolo_weights: str = 'yolov8n.pt',
|
| 84 |
+
llen_base_ch: int = 32,
|
| 85 |
+
freeze_backbone: bool = True,
|
| 86 |
+
):
|
| 87 |
+
super().__init__()
|
| 88 |
+
|
| 89 |
+
# ── Load YOLOv8 backbone ──────────────────────────────
|
| 90 |
+
yolo = YOLO(yolo_weights)
|
| 91 |
+
# Extract the backbone (model.model.model[:10] for YOLOv8)
|
| 92 |
+
# This gives us the CSP + C2f feature extraction up to P5
|
| 93 |
+
self.backbone = yolo.model.model[:10]
|
| 94 |
+
|
| 95 |
+
if freeze_backbone:
|
| 96 |
+
for p in self.backbone.parameters():
|
| 97 |
+
p.requires_grad_(False)
|
| 98 |
+
|
| 99 |
+
# ── Gated adapters ───────────────────────────────────
|
| 100 |
+
c = llen_base_ch
|
| 101 |
+
# P5 feedback → decoder stage D4 (out_ch = c*4 = 128)
|
| 102 |
+
self.adapter_p5 = GatedAdapter(self.YOLO_P5_CH, c * 4)
|
| 103 |
+
# P4 feedback → decoder stage D3 (out_ch = c*2 = 64)
|
| 104 |
+
self.adapter_p4 = GatedAdapter(self.YOLO_P4_CH, c * 2)
|
| 105 |
+
|
| 106 |
+
self._llen_base_ch = c
|
| 107 |
+
|
| 108 |
+
# ----------------------------------------------------------
|
| 109 |
+
def forward(self, enhanced_img):
|
| 110 |
+
"""
|
| 111 |
+
Args:
|
| 112 |
+
enhanced_img : (B, 3, H, W) enhanced image from LLEN Pass 1
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
(p4_feedback, p5_feedback) — tensors ready to inject into LLEN decoder
|
| 116 |
+
"""
|
| 117 |
+
# Run backbone — collect intermediate feature maps
|
| 118 |
+
x = enhanced_img
|
| 119 |
+
p4_raw = None
|
| 120 |
+
p5_raw = None
|
| 121 |
+
|
| 122 |
+
for i, layer in enumerate(self.backbone):
|
| 123 |
+
x = layer(x)
|
| 124 |
+
# YOLOv8n: P4 at layer 6, P5 at layer 9 (backbone output)
|
| 125 |
+
if i == 6:
|
| 126 |
+
p4_raw = x
|
| 127 |
+
if i == 9:
|
| 128 |
+
p5_raw = x
|
| 129 |
+
|
| 130 |
+
if p4_raw is None or p5_raw is None:
|
| 131 |
+
raise RuntimeError(
|
| 132 |
+
"Could not extract P4/P5 from backbone. "
|
| 133 |
+
"Check layer indices for your YOLOv8 variant."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
c = self._llen_base_ch
|
| 137 |
+
H_input, W_input = enhanced_img.shape[-2:]
|
| 138 |
+
|
| 139 |
+
# Compute decoder target sizes (matching LLEN decoder stages)
|
| 140 |
+
p5_size = (H_input // 8, W_input // 8) # D4 spatial dim
|
| 141 |
+
p4_size = (H_input // 4, W_input // 4) # D3 spatial dim
|
| 142 |
+
|
| 143 |
+
p5_feedback = self.adapter_p5(p5_raw, p5_size) # (B, c*4, H/8, W/8)
|
| 144 |
+
p4_feedback = self.adapter_p4(p4_raw, p4_size) # (B, c*2, H/4, W/4)
|
| 145 |
+
|
| 146 |
+
return p4_feedback, p5_feedback
|
| 147 |
+
|
| 148 |
+
# ----------------------------------------------------------
|
| 149 |
+
def count_parameters(self):
|
| 150 |
+
total = sum(p.numel() for p in self.parameters())
|
| 151 |
+
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 152 |
+
return total, trainable
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ─────────────────────────────────────────────────────────────
|
| 156 |
+
# Smoke test
|
| 157 |
+
# ─────────────────────────────────────────────────────────────
|
| 158 |
+
|
| 159 |
+
if __name__ == '__main__':
|
| 160 |
+
import sys
|
| 161 |
+
|
| 162 |
+
print("DGFFv2 Smoke Test")
|
| 163 |
+
device = torch.device('cpu')
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
dgff = DGFFModule(yolo_weights='yolov8n.pt', llen_base_ch=32).to(device)
|
| 167 |
+
except Exception as e:
|
| 168 |
+
print(f"[SKIP] Could not load YOLOv8: {e}")
|
| 169 |
+
sys.exit(0)
|
| 170 |
+
|
| 171 |
+
total, trainable = dgff.count_parameters()
|
| 172 |
+
print(f" Params total / trainable: {total:,} / {trainable:,}")
|
| 173 |
+
|
| 174 |
+
dummy = torch.rand(2, 3, 256, 256)
|
| 175 |
+
p4, p5 = dgff(dummy)
|
| 176 |
+
print(f" P4 feedback shape: {tuple(p4.shape)} (expected: 2, 64, 64, 64)")
|
| 177 |
+
print(f" P5 feedback shape: {tuple(p5.shape)} (expected: 2, 128, 32, 32)")
|
| 178 |
+
print(" Smoke test passed ✓")
|
.ipynb_checkpoints/gate_maps-checkpoint.png
ADDED
|
Git LFS Details
|
.ipynb_checkpoints/llen-checkpoint.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from ultralytics import YOLO
|
| 5 |
+
|
| 6 |
+
# ── PASTE THIS into your DGFF smoke-test cell, replacing the import block ──
|
| 7 |
+
#
|
| 8 |
+
# The problem: your notebook has all code in cells, so there is no llen.py
|
| 9 |
+
# file on disk to import. LLEN is already defined in a previous cell though,
|
| 10 |
+
# so we just skip the import entirely and use it directly.
|
| 11 |
+
#
|
| 12 |
+
# BEFORE (broken):
|
| 13 |
+
# sys.path.insert(0, str(Path(__file__).parent if '__file__' in dir() else Path('.')))
|
| 14 |
+
# from llen import LLEN
|
| 15 |
+
#
|
| 16 |
+
# AFTER (fixed) — replace those two lines with:
|
| 17 |
+
|
| 18 |
+
# Check if LLEN is already defined in this notebook session
|
| 19 |
+
if 'LLEN' not in dir():
|
| 20 |
+
# Only needed if running dgff.py as a standalone script
|
| 21 |
+
import sys
|
| 22 |
+
sys.path.insert(0, '.') # assumes llen.py is in the same folder as notebook
|
| 23 |
+
from llen import LLEN
|
| 24 |
+
|
| 25 |
+
# Everything below stays exactly the same
|
| 26 |
+
llen_model = LLEN(base_ch=32).to(device)
|
| 27 |
+
|
| 28 |
+
# Pass 1: LLEN enhances without feedback
|
| 29 |
+
enhanced = llen_model(dummy_enhanced)
|
| 30 |
+
print(f" Pass 1 enhanced : {tuple(enhanced.shape)}")
|
| 31 |
+
|
| 32 |
+
# Pass 2: DGFF extracts features from enhanced image
|
| 33 |
+
dgff_p4, dgff_p5 = dgff(enhanced)
|
| 34 |
+
|
| 35 |
+
# Pass 3: LLEN re-enhances with detection feedback injected
|
| 36 |
+
enhanced_guided = llen_model(dummy_enhanced, dgff_p5=dgff_p5, dgff_p4=dgff_p4)
|
| 37 |
+
print(f" Pass 2 guided : {tuple(enhanced_guided.shape)}")
|
| 38 |
+
|
| 39 |
+
assert enhanced_guided.shape == dummy_enhanced.shape
|
| 40 |
+
print(" Combined pipeline ✓")
|
| 41 |
+
|
| 42 |
+
# ── Gradient flow test ──────────────────────────────────────
|
| 43 |
+
print("\n Testing gradient flow through DGFF adapters ...")
|
| 44 |
+
|
| 45 |
+
llen_model.train()
|
| 46 |
+
dgff.train()
|
| 47 |
+
|
| 48 |
+
enhanced = llen_model(dummy_enhanced)
|
| 49 |
+
dgff_p4, dgff_p5 = dgff(enhanced)
|
| 50 |
+
enhanced_guided = llen_model(dummy_enhanced, dgff_p5=dgff_p5, dgff_p4=dgff_p4)
|
| 51 |
+
|
| 52 |
+
dummy_target = torch.rand_like(enhanced_guided)
|
| 53 |
+
loss = torch.nn.functional.l1_loss(enhanced_guided, dummy_target)
|
| 54 |
+
loss.backward()
|
| 55 |
+
|
| 56 |
+
for name, param in dgff.adapter_p5.named_parameters():
|
| 57 |
+
assert param.grad is not None, f"adapter_p5.{name} has no gradient!"
|
| 58 |
+
for name, param in dgff.adapter_p4.named_parameters():
|
| 59 |
+
assert param.grad is not None, f"adapter_p4.{name} has no gradient!"
|
| 60 |
+
|
| 61 |
+
print(" Adapter gradients flowing ✓")
|
| 62 |
+
print("\n All DGFF tests passed ✓")
|
.ipynb_checkpoints/lol_dataset-checkpoint.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
# ─────────────────────────────────────────────────────────────
|
| 7 |
+
# 1. Core Dataset Class
|
| 8 |
+
# ─────────────────────────────────────────────────────────────
|
| 9 |
+
|
| 10 |
+
class LOLDataset(Dataset):
|
| 11 |
+
"""
|
| 12 |
+
Loads paired (low-light, normal-light) images from the LOL dataset.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
root : path to the LOL root folder
|
| 16 |
+
split : 'train' → uses our485/
|
| 17 |
+
'val' → uses eval15/
|
| 18 |
+
crop_size : if not None, randomly crops both images to this size
|
| 19 |
+
during training (e.g. 256 or 384).
|
| 20 |
+
Set to None to use full images (600×400 for LOL).
|
| 21 |
+
augment : apply random horizontal flip during training
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
SPLIT_DIRS = {
|
| 25 |
+
'train': 'our485',
|
| 26 |
+
'val': 'eval15',
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
root: str,
|
| 32 |
+
split: str = 'train',
|
| 33 |
+
crop_size: int | None = 256,
|
| 34 |
+
augment: bool = True,
|
| 35 |
+
):
|
| 36 |
+
super().__init__()
|
| 37 |
+
|
| 38 |
+
assert split in self.SPLIT_DIRS, f"split must be 'train' or 'val', got '{split}'"
|
| 39 |
+
|
| 40 |
+
self.split = split
|
| 41 |
+
self.crop_size = crop_size
|
| 42 |
+
self.augment = augment and (split == 'train')
|
| 43 |
+
|
| 44 |
+
split_dir = Path(root) / self.SPLIT_DIRS[split]
|
| 45 |
+
self.low_dir = split_dir / 'low'
|
| 46 |
+
self.high_dir = split_dir / 'high'
|
| 47 |
+
|
| 48 |
+
self._verify_dirs()
|
| 49 |
+
|
| 50 |
+
# Collect filenames — sorted so low/high pairs always align
|
| 51 |
+
# Filter to image files only (excludes .DS_Store and other macOS metadata)
|
| 52 |
+
_IMG_EXTS = {'.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff'}
|
| 53 |
+
self.filenames = sorted(
|
| 54 |
+
f for f in os.listdir(self.low_dir)
|
| 55 |
+
if Path(f).suffix.lower() in _IMG_EXTS
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Sanity check: every low image has a matching high image
|
| 59 |
+
missing = [f for f in self.filenames if not (self.high_dir / f).exists()]
|
| 60 |
+
if missing:
|
| 61 |
+
raise FileNotFoundError(
|
| 62 |
+
f"No matching high-light image for: {missing[:5]} ..."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Simple normalisation: PIL image [0,255] → tensor [0,1]
|
| 66 |
+
self.to_tensor = T.ToTensor()
|
| 67 |
+
|
| 68 |
+
# ----------------------------------------------------------
|
| 69 |
+
def _verify_dirs(self):
|
| 70 |
+
for d in (self.low_dir, self.high_dir):
|
| 71 |
+
if not d.is_dir():
|
| 72 |
+
raise FileNotFoundError(
|
| 73 |
+
f"Directory not found: {d}\n"
|
| 74 |
+
f"Make sure your LOL dataset is at the correct path."
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# ----------------------------------------------------------
|
| 78 |
+
def __len__(self):
|
| 79 |
+
return len(self.filenames)
|
| 80 |
+
|
| 81 |
+
# ----------------------------------------------------------
|
| 82 |
+
def __getitem__(self, idx):
|
| 83 |
+
fname = self.filenames[idx]
|
| 84 |
+
|
| 85 |
+
# Load images as RGB PIL
|
| 86 |
+
low_img = Image.open(self.low_dir / fname).convert('RGB')
|
| 87 |
+
high_img = Image.open(self.high_dir / fname).convert('RGB')
|
| 88 |
+
|
| 89 |
+
# ── Random crop (same crop for both images) ──
|
| 90 |
+
if self.crop_size is not None:
|
| 91 |
+
low_img, high_img = self._paired_random_crop(low_img, high_img)
|
| 92 |
+
|
| 93 |
+
# ── Random horizontal flip ──
|
| 94 |
+
if self.augment and random.random() > 0.5:
|
| 95 |
+
low_img = TF.hflip(low_img)
|
| 96 |
+
high_img = TF.hflip(high_img)
|
| 97 |
+
|
| 98 |
+
# ── PIL → float tensor [0, 1] ──
|
| 99 |
+
low_tensor = self.to_tensor(low_img) # shape: (3, H, W)
|
| 100 |
+
high_tensor = self.to_tensor(high_img) # shape: (3, H, W)
|
| 101 |
+
|
| 102 |
+
return low_tensor, high_tensor, fname
|
| 103 |
+
|
| 104 |
+
# ----------------------------------------------------------
|
| 105 |
+
def _paired_random_crop(self, low_pil, high_pil):
|
| 106 |
+
"""Apply the SAME random crop to both images."""
|
| 107 |
+
w, h = low_pil.size # PIL gives (width, height)
|
| 108 |
+
crop = self.crop_size
|
| 109 |
+
|
| 110 |
+
if w < crop or h < crop:
|
| 111 |
+
# If image is smaller than desired crop, just resize both
|
| 112 |
+
low_pil = TF.resize(low_pil, [crop, crop])
|
| 113 |
+
high_pil = TF.resize(high_pil, [crop, crop])
|
| 114 |
+
return low_pil, high_pil
|
| 115 |
+
|
| 116 |
+
# Random top-left corner
|
| 117 |
+
top = random.randint(0, h - crop)
|
| 118 |
+
left = random.randint(0, w - crop)
|
| 119 |
+
|
| 120 |
+
low_pil = TF.crop(low_pil, top, left, crop, crop)
|
| 121 |
+
high_pil = TF.crop(high_pil, top, left, crop, crop)
|
| 122 |
+
return low_pil, high_pil
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ─────────────────────────────────────────────────────────────
|
| 126 |
+
# 2. Convenience Factory Function
|
| 127 |
+
# ──────────────────────────────────────────────��──────────────
|
| 128 |
+
|
| 129 |
+
def get_lol_dataloaders(
|
| 130 |
+
root: str,
|
| 131 |
+
batch_size: int = 8,
|
| 132 |
+
crop_size: int | None = 256,
|
| 133 |
+
num_workers: int = 0, # 0 = safe for Jupyter notebooks on Mac/Windows
|
| 134 |
+
pin_memory: bool = False, # False = safe for MPS (Apple Silicon) and CPU
|
| 135 |
+
) -> tuple[DataLoader, DataLoader]:
|
| 136 |
+
"""
|
| 137 |
+
Build train and validation DataLoaders for the LOL dataset.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
root : path to the LOL root folder (contains our485/ and eval15/)
|
| 141 |
+
batch_size : number of image pairs per batch
|
| 142 |
+
crop_size : random crop size during training. None = full image.
|
| 143 |
+
num_workers : parallel workers for data loading
|
| 144 |
+
pin_memory : speeds up CPU→GPU transfer (set True when using CUDA)
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
(train_loader, val_loader)
|
| 148 |
+
"""
|
| 149 |
+
train_set = LOLDataset(root=root, split='train', crop_size=crop_size, augment=True)
|
| 150 |
+
val_set = LOLDataset(root=root, split='val', crop_size=None, augment=False)
|
| 151 |
+
|
| 152 |
+
train_loader = DataLoader(
|
| 153 |
+
train_set,
|
| 154 |
+
batch_size = batch_size,
|
| 155 |
+
shuffle = True, # shuffle every epoch
|
| 156 |
+
num_workers = num_workers,
|
| 157 |
+
pin_memory = pin_memory,
|
| 158 |
+
drop_last = True, # keeps batch size consistent
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
val_loader = DataLoader(
|
| 162 |
+
val_set,
|
| 163 |
+
batch_size = 1, # validate one image at a time (full resolution)
|
| 164 |
+
shuffle = False,
|
| 165 |
+
num_workers = num_workers,
|
| 166 |
+
pin_memory = pin_memory,
|
| 167 |
+
drop_last = False,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
return train_loader, val_loader
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# ─────────────────────────────────────────────────────────────
|
| 174 |
+
# 3. Quick Sanity Check (run this file directly to test)
|
| 175 |
+
# ─────────────────────────────────────────────────────────────
|
| 176 |
+
|
| 177 |
+
if __name__ == '__main__':
|
| 178 |
+
import sys
|
| 179 |
+
import matplotlib.pyplot as plt
|
| 180 |
+
|
| 181 |
+
# ── Point this at your LOL folder ──
|
| 182 |
+
LOL_ROOT = './LOL' # <-- change this if your path is different
|
| 183 |
+
|
| 184 |
+
print(f"Loading LOL dataset from: {LOL_ROOT}")
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
train_loader, val_loader = get_lol_dataloaders(
|
| 188 |
+
root = LOL_ROOT,
|
| 189 |
+
batch_size = 4,
|
| 190 |
+
crop_size = 256,
|
| 191 |
+
num_workers = 0, # always 0 when running directly as __main__
|
| 192 |
+
pin_memory = False, # False for MPS (Apple Silicon) and CPU
|
| 193 |
+
)
|
| 194 |
+
except FileNotFoundError as e:
|
| 195 |
+
print(f"\n[ERROR] {e}")
|
| 196 |
+
sys.exit(1)
|
| 197 |
+
|
| 198 |
+
print(f" Train batches : {len(train_loader)} "
|
| 199 |
+
f"({len(train_loader.dataset)} image pairs)")
|
| 200 |
+
print(f" Val batches : {len(val_loader)} "
|
| 201 |
+
f"({len(val_loader.dataset)} image pairs)")
|
| 202 |
+
|
| 203 |
+
# ── Grab one batch and print tensor info ──
|
| 204 |
+
low_batch, high_batch, fnames = next(iter(train_loader))
|
| 205 |
+
|
| 206 |
+
print(f"\nBatch shapes : low={tuple(low_batch.shape)} "
|
| 207 |
+
f"high={tuple(high_batch.shape)}")
|
| 208 |
+
print(f"Value range : [{low_batch.min():.3f}, {low_batch.max():.3f}]")
|
| 209 |
+
print(f"Filenames : {list(fnames)}")
|
| 210 |
+
|
| 211 |
+
# ── Visualise the first pair ──
|
| 212 |
+
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
|
| 213 |
+
# Convert tensor (C,H,W) → numpy (H,W,C) for matplotlib
|
| 214 |
+
axes[0].imshow(low_batch[0].permute(1, 2, 0).numpy())
|
| 215 |
+
axes[0].set_title(f'Low-light input\n{fnames[0]}')
|
| 216 |
+
axes[0].axis('off')
|
| 217 |
+
|
| 218 |
+
axes[1].imshow(high_batch[0].permute(1, 2, 0).numpy())
|
| 219 |
+
axes[1].set_title(f'Normal-light reference\n{fnames[0]}')
|
| 220 |
+
axes[1].axis('off')
|
| 221 |
+
|
| 222 |
+
plt.tight_layout()
|
| 223 |
+
plt.savefig('lol_sample.png', dpi=120)
|
| 224 |
+
print("\nSaved sample visualisation → lol_sample.png")
|
| 225 |
+
plt.show()
|
| 226 |
+
|
.ipynb_checkpoints/train-checkpoint.ipynb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [],
|
| 3 |
+
"metadata": {},
|
| 4 |
+
"nbformat": 4,
|
| 5 |
+
"nbformat_minor": 5
|
| 6 |
+
}
|
.ipynb_checkpoints/val_batch0_pred-checkpoint.jpg
ADDED
|
Git LFS Details
|
DGFF_full-v2.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
DGFF_full.ipynb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd1fa7b3b356e59e7defe3ed590d3e456a6764df8553ee537768707b1a72bf8a
|
| 3 |
+
size 56188001
|
DGFF_full_v1.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Dataset/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Description
|
| 2 |
+
|
| 3 |
+
The ExDark dataset can be downloaded at [this https URL](https://drive.google.com/file/d/1BHmPgu8EsHoFDDkMGLVoXIlCth2dW6Yx/view?usp=sharing) (size = 1.5Gb).
|
| 4 |
+
|
| 5 |
+

|
| 6 |
+
|
| 7 |
+
## Dataset Information
|
| 8 |
+
|
| 9 |
+
The images are kept in individual class folders following the image class labels:
|
| 10 |
+
1. Bicycle - 652 images
|
| 11 |
+
2. Boat - 679 images
|
| 12 |
+
3. Bottle - 547 images
|
| 13 |
+
4. Bus - 527 images
|
| 14 |
+
5. Car - 638 images
|
| 15 |
+
6. Cat - 735 images
|
| 16 |
+
7. Chair - 648 images
|
| 17 |
+
8. Cup - 519 images
|
| 18 |
+
9. Dog - 801 images
|
| 19 |
+
10. Motorbike - 503 images
|
| 20 |
+
11. People - 609 images
|
| 21 |
+
12. Table - 505 images
|
| 22 |
+
|
| 23 |
+
Total : 7,363 images
|
| 24 |
+
|
| 25 |
+

|
| 26 |
+
|
| 27 |
+
For the experiments in our paper that involves training, the data are split as follows:
|
| 28 |
+
|
| 29 |
+
(a) Training - 3,000 images (250 images per class)
|
| 30 |
+
|
| 31 |
+
(b) Validation - 1,800 images (150 images per class)
|
| 32 |
+
|
| 33 |
+
(c) Testing - 2,563 images
|
| 34 |
+
|
| 35 |
+
* See [annotation](https://github.com/cs-chan/Exclusively-Dark-Image-Dataset/tree/master/Groundtruth) for further details.
|
| 36 |
+
|
| 37 |
+
Note:
|
| 38 |
+
1. The ExDark dataset can be used only for non-commercial research purpose.
|
| 39 |
+
|
| 40 |
+
2. For commercial purpose usage, please contact Dr. Chee Seng Chan at `cs.chan at um.edu.my`
|
Dataset/Thumbnails.png
ADDED
|
|
Git LFS Details
|
Dataset/exdarkimg.gif
ADDED
|
Git LFS Details
|
ExDark/.DS_Store
ADDED
|
Binary file (10.2 kB). View file
|
|
|
ExDark/Bicycle/2015_00001.png
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00002.png
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00003.png
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00004.jpg
ADDED
|
ExDark/Bicycle/2015_00005.jpg
ADDED
|
ExDark/Bicycle/2015_00006.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00007.jpg
ADDED
|
ExDark/Bicycle/2015_00008.jpg
ADDED
|
ExDark/Bicycle/2015_00009.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00010.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00011.jpg
ADDED
|
ExDark/Bicycle/2015_00012.jpg
ADDED
|
ExDark/Bicycle/2015_00013.jpg
ADDED
|
ExDark/Bicycle/2015_00014.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00015.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00016.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00017.jpg
ADDED
|
ExDark/Bicycle/2015_00018.jpg
ADDED
|
ExDark/Bicycle/2015_00019.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00020.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00021.jpg
ADDED
|
ExDark/Bicycle/2015_00022.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00023.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00024.JPG
ADDED
|
|
ExDark/Bicycle/2015_00025.jpg
ADDED
|
Git LFS Details
|
ExDark/Bicycle/2015_00026.jpg
ADDED
|
Git LFS Details
|