lidar-camera-image-recognition 0.1.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.
- lidar_camera_image_recognition-0.1.0/LICENSE +21 -0
- lidar_camera_image_recognition-0.1.0/PKG-INFO +182 -0
- lidar_camera_image_recognition-0.1.0/README.md +156 -0
- lidar_camera_image_recognition-0.1.0/pyproject.toml +52 -0
- lidar_camera_image_recognition-0.1.0/setup.cfg +4 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/__init__.py +8 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/__main__.py +6 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/cli.py +45 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/mnist.py +147 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/model.py +41 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/safety.py +167 -0
- lidar_camera_image_recognition-0.1.0/src/image_recognition/vgg16.py +65 -0
- lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/PKG-INFO +182 -0
- lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/SOURCES.txt +16 -0
- lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/dependency_links.txt +1 -0
- lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/entry_points.txt +2 -0
- lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/requires.txt +4 -0
- lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benjamin Quito
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lidar-camera-image-recognition
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reproducible CNN, VGG16, and AV safety-framework experiments
|
|
5
|
+
Author: Benjamin Quito, Larbi Esmahi
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/benjaminquito/ImageRecognition
|
|
8
|
+
Project-URL: Repository, https://github.com/benjaminquito/ImageRecognition
|
|
9
|
+
Project-URL: Paper, https://doi.org/10.4236/ojsst.2023.133006
|
|
10
|
+
Keywords: computer-vision,image-classification,lidar,autonomous-vehicles,reproducible-research
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: tensorflow<2.17,>=2.16
|
|
22
|
+
Requires-Dist: numpy<2,>=1.26
|
|
23
|
+
Requires-Dist: matplotlib<3.9,>=3.8
|
|
24
|
+
Requires-Dist: Pillow<11,>=10.3
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# LiDAR and Camera Image Recognition
|
|
28
|
+
|
|
29
|
+
Reproducible code companion for:
|
|
30
|
+
|
|
31
|
+
> Quito, B. and Esmahi, L. (2023). “Compare and Contrast LiDAR and Non-LiDAR
|
|
32
|
+
> Technology in an Autonomous Vehicle: Developing a Safety Framework.”
|
|
33
|
+
> *Open Journal of Safety Science and Technology*, 13, 101–131.
|
|
34
|
+
> [https://doi.org/10.4236/ojsst.2023.133006](https://doi.org/10.4236/ojsst.2023.133006)
|
|
35
|
+
|
|
36
|
+
The project reconstructs the executable parts of the paper and separates them
|
|
37
|
+
from the proposed (not yet completed) vehicle experiment.
|
|
38
|
+
|
|
39
|
+
## What is reproduced
|
|
40
|
+
|
|
41
|
+
| Paper component | Package command | Output |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| 28 × 28 CNN on MNIST | `image-recognition train-mnist` | Saved model, metrics, history, and accuracy/loss plots |
|
|
44
|
+
| ImageNet VGG16 inference | `image-recognition predict-vgg16` | Top-k labels and probabilities for an input image |
|
|
45
|
+
| LiDAR/camera safety framework | `image-recognition safety` | Blank 25-run protocol and aggregated weather results |
|
|
46
|
+
|
|
47
|
+
The MNIST network follows the architecture in sections 3.6.1–3.6.4: two
|
|
48
|
+
5 × 5 convolution layers (32 and 64 filters), max pooling, a 1,024-unit dense
|
|
49
|
+
layer, and a 10-class output. It contains 3,274,634 trainable parameters when
|
|
50
|
+
dropout is disabled, matching the paper.
|
|
51
|
+
|
|
52
|
+
The VGG16 script follows sections 3.7–3.8: resize to 224 × 224, apply Keras'
|
|
53
|
+
VGG16 preprocessing, use ImageNet weights, and decode the ten most likely
|
|
54
|
+
classes.
|
|
55
|
+
|
|
56
|
+
## Important interpretation
|
|
57
|
+
|
|
58
|
+
The paper reports several different numbers:
|
|
59
|
+
|
|
60
|
+
- **99.29%** is the reported MNIST evaluation accuracy after 25 epochs.
|
|
61
|
+
- **94.63%** is the top prediction probability for one bee photograph using
|
|
62
|
+
pretrained VGG16. It is a confidence score for one sample, not a dataset
|
|
63
|
+
accuracy and not a comparison of LiDAR against camera data.
|
|
64
|
+
- The LiDAR/camera weather tables are an experimental proposal. No completed
|
|
65
|
+
sensor dataset or table values are published in the paper.
|
|
66
|
+
|
|
67
|
+
Accordingly, this repository does not claim to reproduce a measured 94.63%
|
|
68
|
+
LiDAR-vs-camera result. Exact floating-point results can vary by platform,
|
|
69
|
+
TensorFlow version, initialization, and the input image.
|
|
70
|
+
|
|
71
|
+
## Setup
|
|
72
|
+
|
|
73
|
+
The paper used Python 3.10.4 and TensorFlow. A clean Python 3.10 environment is
|
|
74
|
+
recommended.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python3.10 -m venv .venv
|
|
78
|
+
source .venv/bin/activate
|
|
79
|
+
python -m pip install --upgrade pip
|
|
80
|
+
python -m pip install .
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For development, use an editable installation so source changes are immediately
|
|
84
|
+
available:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python -m pip install --editable .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Verify the installed package and command:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -c "import image_recognition; print(image_recognition.__version__)"
|
|
94
|
+
image-recognition --help
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Run the MNIST experiment
|
|
98
|
+
|
|
99
|
+
Full 25-epoch reproduction:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
image-recognition train-mnist --epochs 25 --output-dir artifacts/mnist
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Optional dropout enhancement described in section 3.6.5:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
image-recognition train-mnist --epochs 25 --dropout 0.5 \
|
|
109
|
+
--output-dir artifacts/mnist-dropout
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For a quick pipeline check:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
image-recognition train-mnist --epochs 1 --train-limit 2048 --test-limit 512 \
|
|
116
|
+
--output-dir artifacts/smoke
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each run writes `model.keras`, `metrics.json`, `history.csv`,
|
|
120
|
+
`training_curves.png`, and `model_summary.txt`.
|
|
121
|
+
|
|
122
|
+
## Run VGG16 inference
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
image-recognition predict-vgg16 path/to/image.jpg --top 10 \
|
|
126
|
+
--output artifacts/vgg16-prediction.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The ImageNet weights download automatically on first use. To revisit the bee
|
|
130
|
+
example, supply a bee image whose reuse rights you have; the original image is
|
|
131
|
+
not distributed with the article.
|
|
132
|
+
|
|
133
|
+
## Use the safety-framework scaffold
|
|
134
|
+
|
|
135
|
+
Create a protocol with 25 runs for each combination of driver mode,
|
|
136
|
+
technology, and weather condition:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
image-recognition safety init data/safety_runs.csv --runs 25
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Fill the measurement columns in the CSV, then aggregate results:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
image-recognition safety summarize data/safety_runs.csv \
|
|
146
|
+
--output artifacts/safety_summary.csv
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The long-form protocol captures accuracy, confidence, latency, obstacle
|
|
150
|
+
detection, stopping distance, and notes. These fields support the paper's
|
|
151
|
+
proposed extensions while keeping missing measurements blank.
|
|
152
|
+
|
|
153
|
+
The prose names six conditions: sunny (`S`), cloudy (`C`), daytime rain
|
|
154
|
+
(`DR`), fog (`F`), nighttime rain (`NR`), and snow (`SW`). Tables 3–6 also
|
|
155
|
+
contain an unexplained `R` column. The scaffold preserves `R` as `rain` so the
|
|
156
|
+
published table layout can be represented without silently discarding it.
|
|
157
|
+
|
|
158
|
+
## Tests
|
|
159
|
+
|
|
160
|
+
The lightweight tests do not download datasets or TensorFlow weights:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
python -m unittest -v
|
|
164
|
+
python -m compileall -q src
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
GitHub Actions runs these packaging checks automatically on every push and pull
|
|
168
|
+
request. The original root-level Python scripts remain available as
|
|
169
|
+
backward-compatible wrappers.
|
|
170
|
+
|
|
171
|
+
## Published reproduction
|
|
172
|
+
|
|
173
|
+
The verified 25-epoch run reached 99.20% test accuracy. See [RESULTS.md](RESULTS.md)
|
|
174
|
+
for the environment, comparison with the paper, raw metrics, and curves.
|
|
175
|
+
|
|
176
|
+
## Reproducibility boundary
|
|
177
|
+
|
|
178
|
+
This code reproduces the published software procedures as closely as the paper
|
|
179
|
+
allows. A direct LiDAR-versus-camera safety comparison still requires paired,
|
|
180
|
+
time-synchronized sensor captures, ground-truth labels, weather metadata, and
|
|
181
|
+
the 25 physical runs per condition proposed in section 4. Those data are not
|
|
182
|
+
included in the publication.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# LiDAR and Camera Image Recognition
|
|
2
|
+
|
|
3
|
+
Reproducible code companion for:
|
|
4
|
+
|
|
5
|
+
> Quito, B. and Esmahi, L. (2023). “Compare and Contrast LiDAR and Non-LiDAR
|
|
6
|
+
> Technology in an Autonomous Vehicle: Developing a Safety Framework.”
|
|
7
|
+
> *Open Journal of Safety Science and Technology*, 13, 101–131.
|
|
8
|
+
> [https://doi.org/10.4236/ojsst.2023.133006](https://doi.org/10.4236/ojsst.2023.133006)
|
|
9
|
+
|
|
10
|
+
The project reconstructs the executable parts of the paper and separates them
|
|
11
|
+
from the proposed (not yet completed) vehicle experiment.
|
|
12
|
+
|
|
13
|
+
## What is reproduced
|
|
14
|
+
|
|
15
|
+
| Paper component | Package command | Output |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 28 × 28 CNN on MNIST | `image-recognition train-mnist` | Saved model, metrics, history, and accuracy/loss plots |
|
|
18
|
+
| ImageNet VGG16 inference | `image-recognition predict-vgg16` | Top-k labels and probabilities for an input image |
|
|
19
|
+
| LiDAR/camera safety framework | `image-recognition safety` | Blank 25-run protocol and aggregated weather results |
|
|
20
|
+
|
|
21
|
+
The MNIST network follows the architecture in sections 3.6.1–3.6.4: two
|
|
22
|
+
5 × 5 convolution layers (32 and 64 filters), max pooling, a 1,024-unit dense
|
|
23
|
+
layer, and a 10-class output. It contains 3,274,634 trainable parameters when
|
|
24
|
+
dropout is disabled, matching the paper.
|
|
25
|
+
|
|
26
|
+
The VGG16 script follows sections 3.7–3.8: resize to 224 × 224, apply Keras'
|
|
27
|
+
VGG16 preprocessing, use ImageNet weights, and decode the ten most likely
|
|
28
|
+
classes.
|
|
29
|
+
|
|
30
|
+
## Important interpretation
|
|
31
|
+
|
|
32
|
+
The paper reports several different numbers:
|
|
33
|
+
|
|
34
|
+
- **99.29%** is the reported MNIST evaluation accuracy after 25 epochs.
|
|
35
|
+
- **94.63%** is the top prediction probability for one bee photograph using
|
|
36
|
+
pretrained VGG16. It is a confidence score for one sample, not a dataset
|
|
37
|
+
accuracy and not a comparison of LiDAR against camera data.
|
|
38
|
+
- The LiDAR/camera weather tables are an experimental proposal. No completed
|
|
39
|
+
sensor dataset or table values are published in the paper.
|
|
40
|
+
|
|
41
|
+
Accordingly, this repository does not claim to reproduce a measured 94.63%
|
|
42
|
+
LiDAR-vs-camera result. Exact floating-point results can vary by platform,
|
|
43
|
+
TensorFlow version, initialization, and the input image.
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
The paper used Python 3.10.4 and TensorFlow. A clean Python 3.10 environment is
|
|
48
|
+
recommended.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python3.10 -m venv .venv
|
|
52
|
+
source .venv/bin/activate
|
|
53
|
+
python -m pip install --upgrade pip
|
|
54
|
+
python -m pip install .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For development, use an editable installation so source changes are immediately
|
|
58
|
+
available:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m pip install --editable .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Verify the installed package and command:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -c "import image_recognition; print(image_recognition.__version__)"
|
|
68
|
+
image-recognition --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Run the MNIST experiment
|
|
72
|
+
|
|
73
|
+
Full 25-epoch reproduction:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
image-recognition train-mnist --epochs 25 --output-dir artifacts/mnist
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Optional dropout enhancement described in section 3.6.5:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
image-recognition train-mnist --epochs 25 --dropout 0.5 \
|
|
83
|
+
--output-dir artifacts/mnist-dropout
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For a quick pipeline check:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
image-recognition train-mnist --epochs 1 --train-limit 2048 --test-limit 512 \
|
|
90
|
+
--output-dir artifacts/smoke
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Each run writes `model.keras`, `metrics.json`, `history.csv`,
|
|
94
|
+
`training_curves.png`, and `model_summary.txt`.
|
|
95
|
+
|
|
96
|
+
## Run VGG16 inference
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
image-recognition predict-vgg16 path/to/image.jpg --top 10 \
|
|
100
|
+
--output artifacts/vgg16-prediction.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The ImageNet weights download automatically on first use. To revisit the bee
|
|
104
|
+
example, supply a bee image whose reuse rights you have; the original image is
|
|
105
|
+
not distributed with the article.
|
|
106
|
+
|
|
107
|
+
## Use the safety-framework scaffold
|
|
108
|
+
|
|
109
|
+
Create a protocol with 25 runs for each combination of driver mode,
|
|
110
|
+
technology, and weather condition:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
image-recognition safety init data/safety_runs.csv --runs 25
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Fill the measurement columns in the CSV, then aggregate results:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
image-recognition safety summarize data/safety_runs.csv \
|
|
120
|
+
--output artifacts/safety_summary.csv
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The long-form protocol captures accuracy, confidence, latency, obstacle
|
|
124
|
+
detection, stopping distance, and notes. These fields support the paper's
|
|
125
|
+
proposed extensions while keeping missing measurements blank.
|
|
126
|
+
|
|
127
|
+
The prose names six conditions: sunny (`S`), cloudy (`C`), daytime rain
|
|
128
|
+
(`DR`), fog (`F`), nighttime rain (`NR`), and snow (`SW`). Tables 3–6 also
|
|
129
|
+
contain an unexplained `R` column. The scaffold preserves `R` as `rain` so the
|
|
130
|
+
published table layout can be represented without silently discarding it.
|
|
131
|
+
|
|
132
|
+
## Tests
|
|
133
|
+
|
|
134
|
+
The lightweight tests do not download datasets or TensorFlow weights:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python -m unittest -v
|
|
138
|
+
python -m compileall -q src
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
GitHub Actions runs these packaging checks automatically on every push and pull
|
|
142
|
+
request. The original root-level Python scripts remain available as
|
|
143
|
+
backward-compatible wrappers.
|
|
144
|
+
|
|
145
|
+
## Published reproduction
|
|
146
|
+
|
|
147
|
+
The verified 25-epoch run reached 99.20% test accuracy. See [RESULTS.md](RESULTS.md)
|
|
148
|
+
for the environment, comparison with the paper, raw metrics, and curves.
|
|
149
|
+
|
|
150
|
+
## Reproducibility boundary
|
|
151
|
+
|
|
152
|
+
This code reproduces the published software procedures as closely as the paper
|
|
153
|
+
allows. A direct LiDAR-versus-camera safety comparison still requires paired,
|
|
154
|
+
time-synchronized sensor captures, ground-truth labels, weather metadata, and
|
|
155
|
+
the 25 physical runs per condition proposed in section 4. Those data are not
|
|
156
|
+
included in the publication.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
name = "lidar-camera-image-recognition"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "Reproducible CNN, VGG16, and AV safety-framework experiments"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Benjamin Quito" },
|
|
14
|
+
{ name = "Larbi Esmahi" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"computer-vision",
|
|
18
|
+
"image-classification",
|
|
19
|
+
"lidar",
|
|
20
|
+
"autonomous-vehicles",
|
|
21
|
+
"reproducible-research",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Education",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"tensorflow>=2.16,<2.17",
|
|
34
|
+
"numpy>=1.26,<2",
|
|
35
|
+
"matplotlib>=3.8,<3.9",
|
|
36
|
+
"Pillow>=10.3,<11",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
image-recognition = "image_recognition.cli:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/benjaminquito/ImageRecognition"
|
|
44
|
+
Repository = "https://github.com/benjaminquito/ImageRecognition"
|
|
45
|
+
Paper = "https://doi.org/10.4236/ojsst.2023.133006"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools]
|
|
48
|
+
package-dir = { "" = "src" }
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Unified command-line interface for the research package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
COMMANDS = {
|
|
12
|
+
"train-mnist": "Train and evaluate the paper's MNIST CNN",
|
|
13
|
+
"predict-vgg16": "Classify an image with ImageNet-pretrained VGG16",
|
|
14
|
+
"safety": "Create or summarize the AV safety-study protocol",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="image-recognition",
|
|
21
|
+
description="Reproduce the paper's image-recognition experiments.",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
24
|
+
parser.add_argument("command", nargs="?", choices=COMMANDS)
|
|
25
|
+
parser.add_argument("arguments", nargs=argparse.REMAINDER)
|
|
26
|
+
return parser
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
30
|
+
parser = build_parser()
|
|
31
|
+
args = parser.parse_args(argv)
|
|
32
|
+
if args.command is None:
|
|
33
|
+
parser.print_help()
|
|
34
|
+
print("\ncommands:")
|
|
35
|
+
for command, description in COMMANDS.items():
|
|
36
|
+
print(f" {command:<15} {description}")
|
|
37
|
+
return 0
|
|
38
|
+
if args.command == "train-mnist":
|
|
39
|
+
from .mnist import main as command_main
|
|
40
|
+
elif args.command == "predict-vgg16":
|
|
41
|
+
from .vgg16 import main as command_main
|
|
42
|
+
else:
|
|
43
|
+
from .safety import main as command_main
|
|
44
|
+
return command_main(args.arguments)
|
|
45
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Training workflow for the paper's MNIST convolutional network."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import random
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Sequence
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .model import build_paper_cnn
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
21
|
+
parser.add_argument("--epochs", type=int, default=25)
|
|
22
|
+
parser.add_argument("--batch-size", type=int, default=128)
|
|
23
|
+
parser.add_argument("--dropout", type=float, default=0.0)
|
|
24
|
+
parser.add_argument("--seed", type=int, default=42)
|
|
25
|
+
parser.add_argument("--train-limit", type=int)
|
|
26
|
+
parser.add_argument("--test-limit", type=int)
|
|
27
|
+
parser.add_argument("--output-dir", type=Path, default=Path("artifacts/mnist"))
|
|
28
|
+
return parser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def set_reproducible_seed(seed: int) -> None:
|
|
32
|
+
os.environ.setdefault("TF_DETERMINISTIC_OPS", "1")
|
|
33
|
+
random.seed(seed)
|
|
34
|
+
np.random.seed(seed)
|
|
35
|
+
import tensorflow as tf
|
|
36
|
+
|
|
37
|
+
tf.random.set_seed(seed)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def prepare_mnist(train_limit: int | None, test_limit: int | None):
|
|
41
|
+
from tensorflow import keras
|
|
42
|
+
|
|
43
|
+
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
|
|
44
|
+
if train_limit is not None:
|
|
45
|
+
x_train, y_train = x_train[:train_limit], y_train[:train_limit]
|
|
46
|
+
if test_limit is not None:
|
|
47
|
+
x_test, y_test = x_test[:test_limit], y_test[:test_limit]
|
|
48
|
+
|
|
49
|
+
x_train = x_train.astype("float32")[..., np.newaxis] / 255.0
|
|
50
|
+
x_test = x_test.astype("float32")[..., np.newaxis] / 255.0
|
|
51
|
+
y_train = keras.utils.to_categorical(y_train, 10)
|
|
52
|
+
y_test = keras.utils.to_categorical(y_test, 10)
|
|
53
|
+
return (x_train, y_train), (x_test, y_test)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_history(history: dict[str, list[float]], path: Path) -> None:
|
|
57
|
+
columns = list(history)
|
|
58
|
+
with path.open("w", newline="", encoding="utf-8") as stream:
|
|
59
|
+
writer = csv.DictWriter(stream, fieldnames=["epoch", *columns])
|
|
60
|
+
writer.writeheader()
|
|
61
|
+
for epoch in range(len(history[columns[0]])):
|
|
62
|
+
row = {name: history[name][epoch] for name in columns}
|
|
63
|
+
writer.writerow({"epoch": epoch + 1, **row})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_curves(history: dict[str, list[float]], path: Path) -> None:
|
|
67
|
+
import matplotlib
|
|
68
|
+
|
|
69
|
+
matplotlib.use("Agg")
|
|
70
|
+
import matplotlib.pyplot as plt
|
|
71
|
+
|
|
72
|
+
epochs = range(1, len(history["loss"]) + 1)
|
|
73
|
+
figure, axes = plt.subplots(1, 2, figsize=(11, 4.5))
|
|
74
|
+
axes[0].plot(epochs, history["accuracy"], label="training")
|
|
75
|
+
axes[0].plot(epochs, history["val_accuracy"], label="validation")
|
|
76
|
+
axes[0].set(title="Accuracy", xlabel="Epoch", ylabel="Accuracy")
|
|
77
|
+
axes[0].legend()
|
|
78
|
+
axes[0].grid(alpha=0.25)
|
|
79
|
+
axes[1].plot(epochs, history["loss"], label="training")
|
|
80
|
+
axes[1].plot(epochs, history["val_loss"], label="validation")
|
|
81
|
+
axes[1].set(title="Loss", xlabel="Epoch", ylabel="Categorical cross-entropy")
|
|
82
|
+
axes[1].legend()
|
|
83
|
+
axes[1].grid(alpha=0.25)
|
|
84
|
+
figure.tight_layout()
|
|
85
|
+
figure.savefig(path, dpi=160)
|
|
86
|
+
plt.close(figure)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run(args: argparse.Namespace) -> dict[str, object]:
|
|
90
|
+
if args.epochs < 1 or args.batch_size < 1:
|
|
91
|
+
raise ValueError("epochs and batch size must be positive")
|
|
92
|
+
|
|
93
|
+
set_reproducible_seed(args.seed)
|
|
94
|
+
(x_train, y_train), (x_test, y_test) = prepare_mnist(
|
|
95
|
+
args.train_limit, args.test_limit
|
|
96
|
+
)
|
|
97
|
+
model = build_paper_cnn(dropout=args.dropout)
|
|
98
|
+
|
|
99
|
+
output_dir = args.output_dir
|
|
100
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
summary = io.StringIO()
|
|
102
|
+
model.summary(print_fn=lambda line: summary.write(line + "\n"))
|
|
103
|
+
(output_dir / "model_summary.txt").write_text(summary.getvalue(), encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
trained = model.fit(
|
|
106
|
+
x_train,
|
|
107
|
+
y_train,
|
|
108
|
+
validation_data=(x_test, y_test),
|
|
109
|
+
epochs=args.epochs,
|
|
110
|
+
batch_size=args.batch_size,
|
|
111
|
+
verbose=2,
|
|
112
|
+
)
|
|
113
|
+
test_loss, test_accuracy = model.evaluate(x_test, y_test, verbose=0)
|
|
114
|
+
model.save(output_dir / "model.keras")
|
|
115
|
+
save_history(trained.history, output_dir / "history.csv")
|
|
116
|
+
save_curves(trained.history, output_dir / "training_curves.png")
|
|
117
|
+
|
|
118
|
+
metrics = {
|
|
119
|
+
"test_loss": float(test_loss),
|
|
120
|
+
"test_accuracy": float(test_accuracy),
|
|
121
|
+
"epochs": args.epochs,
|
|
122
|
+
"batch_size": args.batch_size,
|
|
123
|
+
"dropout": args.dropout,
|
|
124
|
+
"seed": args.seed,
|
|
125
|
+
"training_samples": int(len(x_train)),
|
|
126
|
+
"test_samples": int(len(x_test)),
|
|
127
|
+
"trainable_parameters": int(model.count_params()),
|
|
128
|
+
}
|
|
129
|
+
(output_dir / "metrics.json").write_text(
|
|
130
|
+
json.dumps(metrics, indent=2) + "\n", encoding="utf-8"
|
|
131
|
+
)
|
|
132
|
+
return metrics
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
136
|
+
args = build_parser().parse_args(argv)
|
|
137
|
+
try:
|
|
138
|
+
metrics = run(args)
|
|
139
|
+
except ValueError as error:
|
|
140
|
+
build_parser().error(str(error))
|
|
141
|
+
print(json.dumps(metrics, indent=2))
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
raise SystemExit(main())
|
|
147
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Neural-network definitions reconstructed from the paper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_paper_cnn(dropout: float = 0.0):
|
|
7
|
+
"""Build the 28x28 CNN described in sections 3.6.1-3.6.4.
|
|
8
|
+
|
|
9
|
+
TensorFlow is imported lazily so metadata and safety utilities remain
|
|
10
|
+
usable without initializing the ML runtime.
|
|
11
|
+
"""
|
|
12
|
+
if not 0.0 <= dropout < 1.0:
|
|
13
|
+
raise ValueError("dropout must be in the interval [0, 1)")
|
|
14
|
+
|
|
15
|
+
from tensorflow import keras
|
|
16
|
+
|
|
17
|
+
layers = [
|
|
18
|
+
keras.layers.Input(shape=(28, 28, 1), name="image"),
|
|
19
|
+
keras.layers.Conv2D(
|
|
20
|
+
32, kernel_size=(5, 5), padding="same", activation="relu"
|
|
21
|
+
),
|
|
22
|
+
keras.layers.MaxPooling2D(pool_size=(2, 2)),
|
|
23
|
+
keras.layers.Conv2D(
|
|
24
|
+
64, kernel_size=(5, 5), padding="same", activation="relu"
|
|
25
|
+
),
|
|
26
|
+
keras.layers.MaxPooling2D(pool_size=(2, 2)),
|
|
27
|
+
keras.layers.Flatten(),
|
|
28
|
+
keras.layers.Dense(1024, activation="relu"),
|
|
29
|
+
]
|
|
30
|
+
if dropout:
|
|
31
|
+
layers.append(keras.layers.Dropout(dropout))
|
|
32
|
+
layers.append(keras.layers.Dense(10, activation="softmax"))
|
|
33
|
+
|
|
34
|
+
model = keras.Sequential(layers, name="paper_mnist_cnn")
|
|
35
|
+
model.compile(
|
|
36
|
+
optimizer="adam",
|
|
37
|
+
loss="categorical_crossentropy",
|
|
38
|
+
metrics=["accuracy"],
|
|
39
|
+
)
|
|
40
|
+
return model
|
|
41
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Create and summarize the paper's proposed AV safety-study protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
import statistics
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Sequence
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
WEATHER = {
|
|
14
|
+
"S": "sunny",
|
|
15
|
+
"C": "cloudy",
|
|
16
|
+
"R": "rain (table-only code)",
|
|
17
|
+
"F": "foggy",
|
|
18
|
+
"DR": "daytime rainy",
|
|
19
|
+
"NR": "nighttime rainy",
|
|
20
|
+
"SW": "snowy",
|
|
21
|
+
}
|
|
22
|
+
TECHNOLOGIES = ("lidar", "camera")
|
|
23
|
+
DRIVER_MODES = ("human", "autonomous")
|
|
24
|
+
FIELDS = (
|
|
25
|
+
"driver_mode",
|
|
26
|
+
"technology",
|
|
27
|
+
"weather_code",
|
|
28
|
+
"weather",
|
|
29
|
+
"run",
|
|
30
|
+
"expected_label",
|
|
31
|
+
"predicted_label",
|
|
32
|
+
"confidence",
|
|
33
|
+
"latency_ms",
|
|
34
|
+
"obstacle_detected",
|
|
35
|
+
"stopping_distance_m",
|
|
36
|
+
"notes",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_protocol(path: Path, runs: int = 25) -> int:
|
|
41
|
+
"""Write a blank long-form experimental protocol and return its row count."""
|
|
42
|
+
if runs < 1:
|
|
43
|
+
raise ValueError("runs must be positive")
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
count = 0
|
|
46
|
+
with path.open("w", newline="", encoding="utf-8") as stream:
|
|
47
|
+
writer = csv.DictWriter(stream, fieldnames=FIELDS)
|
|
48
|
+
writer.writeheader()
|
|
49
|
+
for driver_mode in DRIVER_MODES:
|
|
50
|
+
for technology in TECHNOLOGIES:
|
|
51
|
+
for code, weather in WEATHER.items():
|
|
52
|
+
for run_number in range(1, runs + 1):
|
|
53
|
+
writer.writerow(
|
|
54
|
+
{
|
|
55
|
+
"driver_mode": driver_mode,
|
|
56
|
+
"technology": technology,
|
|
57
|
+
"weather_code": code,
|
|
58
|
+
"weather": weather,
|
|
59
|
+
"run": run_number,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
count += 1
|
|
63
|
+
return count
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _optional_float(value: str) -> float | None:
|
|
67
|
+
value = value.strip()
|
|
68
|
+
return float(value) if value else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _optional_correct(expected: str, predicted: str) -> float | None:
|
|
72
|
+
expected, predicted = expected.strip(), predicted.strip()
|
|
73
|
+
if not expected or not predicted:
|
|
74
|
+
return None
|
|
75
|
+
return float(expected == predicted)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _mean(values: list[float]) -> str:
|
|
79
|
+
return f"{statistics.fmean(values):.6f}" if values else ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def summarize(source: Path, destination: Path) -> int:
|
|
83
|
+
"""Aggregate populated measurements and return the number of groups."""
|
|
84
|
+
grouped: dict[tuple[str, str, str], dict[str, list[float]]] = defaultdict(
|
|
85
|
+
lambda: defaultdict(list)
|
|
86
|
+
)
|
|
87
|
+
with source.open(newline="", encoding="utf-8") as stream:
|
|
88
|
+
reader = csv.DictReader(stream)
|
|
89
|
+
missing = set(FIELDS) - set(reader.fieldnames or ())
|
|
90
|
+
if missing:
|
|
91
|
+
raise ValueError(f"missing columns: {', '.join(sorted(missing))}")
|
|
92
|
+
for row in reader:
|
|
93
|
+
key = (row["driver_mode"], row["technology"], row["weather_code"])
|
|
94
|
+
values = {
|
|
95
|
+
"accuracy": _optional_correct(
|
|
96
|
+
row["expected_label"], row["predicted_label"]
|
|
97
|
+
),
|
|
98
|
+
"confidence": _optional_float(row["confidence"]),
|
|
99
|
+
"latency_ms": _optional_float(row["latency_ms"]),
|
|
100
|
+
"stopping_distance_m": _optional_float(row["stopping_distance_m"]),
|
|
101
|
+
}
|
|
102
|
+
for name, value in values.items():
|
|
103
|
+
if value is not None:
|
|
104
|
+
grouped[key][name].append(value)
|
|
105
|
+
|
|
106
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
output_fields = (
|
|
108
|
+
"driver_mode",
|
|
109
|
+
"technology",
|
|
110
|
+
"weather_code",
|
|
111
|
+
"weather",
|
|
112
|
+
"labeled_samples",
|
|
113
|
+
"accuracy",
|
|
114
|
+
"mean_confidence",
|
|
115
|
+
"mean_latency_ms",
|
|
116
|
+
"mean_stopping_distance_m",
|
|
117
|
+
)
|
|
118
|
+
with destination.open("w", newline="", encoding="utf-8") as stream:
|
|
119
|
+
writer = csv.DictWriter(stream, fieldnames=output_fields)
|
|
120
|
+
writer.writeheader()
|
|
121
|
+
for key in sorted(grouped):
|
|
122
|
+
driver_mode, technology, code = key
|
|
123
|
+
values = grouped[key]
|
|
124
|
+
writer.writerow(
|
|
125
|
+
{
|
|
126
|
+
"driver_mode": driver_mode,
|
|
127
|
+
"technology": technology,
|
|
128
|
+
"weather_code": code,
|
|
129
|
+
"weather": WEATHER.get(code, "unknown"),
|
|
130
|
+
"labeled_samples": len(values["accuracy"]),
|
|
131
|
+
"accuracy": _mean(values["accuracy"]),
|
|
132
|
+
"mean_confidence": _mean(values["confidence"]),
|
|
133
|
+
"mean_latency_ms": _mean(values["latency_ms"]),
|
|
134
|
+
"mean_stopping_distance_m": _mean(
|
|
135
|
+
values["stopping_distance_m"]
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
return len(grouped)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
143
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
144
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
145
|
+
initialize = commands.add_parser("init", help="create a blank study protocol")
|
|
146
|
+
initialize.add_argument("output", type=Path)
|
|
147
|
+
initialize.add_argument("--runs", type=int, default=25)
|
|
148
|
+
aggregate = commands.add_parser("summarize", help="aggregate completed runs")
|
|
149
|
+
aggregate.add_argument("source", type=Path)
|
|
150
|
+
aggregate.add_argument("--output", type=Path, required=True)
|
|
151
|
+
return parser
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
155
|
+
args = build_parser().parse_args(argv)
|
|
156
|
+
if args.command == "init":
|
|
157
|
+
rows = create_protocol(args.output, args.runs)
|
|
158
|
+
print(f"Created {rows} protocol rows at {args.output}")
|
|
159
|
+
else:
|
|
160
|
+
groups = summarize(args.source, args.output)
|
|
161
|
+
print(f"Wrote {groups} populated groups to {args.output}")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
raise SystemExit(main())
|
|
167
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""ImageNet-pretrained VGG16 inference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def predict(image_path: Path, top: int = 10) -> list[dict[str, object]]:
|
|
14
|
+
"""Return the top ImageNet predictions for one image."""
|
|
15
|
+
if not image_path.is_file():
|
|
16
|
+
raise FileNotFoundError(image_path)
|
|
17
|
+
if not 1 <= top <= 1000:
|
|
18
|
+
raise ValueError("top must be between 1 and 1000")
|
|
19
|
+
|
|
20
|
+
from tensorflow.keras.applications.vgg16 import (
|
|
21
|
+
VGG16,
|
|
22
|
+
decode_predictions,
|
|
23
|
+
preprocess_input,
|
|
24
|
+
)
|
|
25
|
+
from tensorflow.keras.utils import img_to_array, load_img
|
|
26
|
+
|
|
27
|
+
image = load_img(image_path, target_size=(224, 224))
|
|
28
|
+
batch = np.expand_dims(img_to_array(image), axis=0)
|
|
29
|
+
probabilities = VGG16(weights="imagenet").predict(
|
|
30
|
+
preprocess_input(batch), verbose=0
|
|
31
|
+
)
|
|
32
|
+
decoded = decode_predictions(probabilities, top=top)[0]
|
|
33
|
+
return [
|
|
34
|
+
{"synset": synset, "label": label, "probability": float(probability)}
|
|
35
|
+
for synset, label, probability in decoded
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
40
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
41
|
+
parser.add_argument("image", type=Path)
|
|
42
|
+
parser.add_argument("--top", type=int, default=10)
|
|
43
|
+
parser.add_argument("--output", type=Path)
|
|
44
|
+
return parser
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
48
|
+
args = build_parser().parse_args(argv)
|
|
49
|
+
results = {
|
|
50
|
+
"image": str(args.image),
|
|
51
|
+
"model": "VGG16",
|
|
52
|
+
"weights": "ImageNet",
|
|
53
|
+
"predictions": predict(args.image, args.top),
|
|
54
|
+
}
|
|
55
|
+
rendered = json.dumps(results, indent=2) + "\n"
|
|
56
|
+
if args.output:
|
|
57
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
args.output.write_text(rendered, encoding="utf-8")
|
|
59
|
+
print(rendered, end="")
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
raise SystemExit(main())
|
|
65
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lidar-camera-image-recognition
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reproducible CNN, VGG16, and AV safety-framework experiments
|
|
5
|
+
Author: Benjamin Quito, Larbi Esmahi
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/benjaminquito/ImageRecognition
|
|
8
|
+
Project-URL: Repository, https://github.com/benjaminquito/ImageRecognition
|
|
9
|
+
Project-URL: Paper, https://doi.org/10.4236/ojsst.2023.133006
|
|
10
|
+
Keywords: computer-vision,image-classification,lidar,autonomous-vehicles,reproducible-research
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: tensorflow<2.17,>=2.16
|
|
22
|
+
Requires-Dist: numpy<2,>=1.26
|
|
23
|
+
Requires-Dist: matplotlib<3.9,>=3.8
|
|
24
|
+
Requires-Dist: Pillow<11,>=10.3
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# LiDAR and Camera Image Recognition
|
|
28
|
+
|
|
29
|
+
Reproducible code companion for:
|
|
30
|
+
|
|
31
|
+
> Quito, B. and Esmahi, L. (2023). “Compare and Contrast LiDAR and Non-LiDAR
|
|
32
|
+
> Technology in an Autonomous Vehicle: Developing a Safety Framework.”
|
|
33
|
+
> *Open Journal of Safety Science and Technology*, 13, 101–131.
|
|
34
|
+
> [https://doi.org/10.4236/ojsst.2023.133006](https://doi.org/10.4236/ojsst.2023.133006)
|
|
35
|
+
|
|
36
|
+
The project reconstructs the executable parts of the paper and separates them
|
|
37
|
+
from the proposed (not yet completed) vehicle experiment.
|
|
38
|
+
|
|
39
|
+
## What is reproduced
|
|
40
|
+
|
|
41
|
+
| Paper component | Package command | Output |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| 28 × 28 CNN on MNIST | `image-recognition train-mnist` | Saved model, metrics, history, and accuracy/loss plots |
|
|
44
|
+
| ImageNet VGG16 inference | `image-recognition predict-vgg16` | Top-k labels and probabilities for an input image |
|
|
45
|
+
| LiDAR/camera safety framework | `image-recognition safety` | Blank 25-run protocol and aggregated weather results |
|
|
46
|
+
|
|
47
|
+
The MNIST network follows the architecture in sections 3.6.1–3.6.4: two
|
|
48
|
+
5 × 5 convolution layers (32 and 64 filters), max pooling, a 1,024-unit dense
|
|
49
|
+
layer, and a 10-class output. It contains 3,274,634 trainable parameters when
|
|
50
|
+
dropout is disabled, matching the paper.
|
|
51
|
+
|
|
52
|
+
The VGG16 script follows sections 3.7–3.8: resize to 224 × 224, apply Keras'
|
|
53
|
+
VGG16 preprocessing, use ImageNet weights, and decode the ten most likely
|
|
54
|
+
classes.
|
|
55
|
+
|
|
56
|
+
## Important interpretation
|
|
57
|
+
|
|
58
|
+
The paper reports several different numbers:
|
|
59
|
+
|
|
60
|
+
- **99.29%** is the reported MNIST evaluation accuracy after 25 epochs.
|
|
61
|
+
- **94.63%** is the top prediction probability for one bee photograph using
|
|
62
|
+
pretrained VGG16. It is a confidence score for one sample, not a dataset
|
|
63
|
+
accuracy and not a comparison of LiDAR against camera data.
|
|
64
|
+
- The LiDAR/camera weather tables are an experimental proposal. No completed
|
|
65
|
+
sensor dataset or table values are published in the paper.
|
|
66
|
+
|
|
67
|
+
Accordingly, this repository does not claim to reproduce a measured 94.63%
|
|
68
|
+
LiDAR-vs-camera result. Exact floating-point results can vary by platform,
|
|
69
|
+
TensorFlow version, initialization, and the input image.
|
|
70
|
+
|
|
71
|
+
## Setup
|
|
72
|
+
|
|
73
|
+
The paper used Python 3.10.4 and TensorFlow. A clean Python 3.10 environment is
|
|
74
|
+
recommended.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python3.10 -m venv .venv
|
|
78
|
+
source .venv/bin/activate
|
|
79
|
+
python -m pip install --upgrade pip
|
|
80
|
+
python -m pip install .
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For development, use an editable installation so source changes are immediately
|
|
84
|
+
available:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python -m pip install --editable .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Verify the installed package and command:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -c "import image_recognition; print(image_recognition.__version__)"
|
|
94
|
+
image-recognition --help
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Run the MNIST experiment
|
|
98
|
+
|
|
99
|
+
Full 25-epoch reproduction:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
image-recognition train-mnist --epochs 25 --output-dir artifacts/mnist
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Optional dropout enhancement described in section 3.6.5:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
image-recognition train-mnist --epochs 25 --dropout 0.5 \
|
|
109
|
+
--output-dir artifacts/mnist-dropout
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For a quick pipeline check:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
image-recognition train-mnist --epochs 1 --train-limit 2048 --test-limit 512 \
|
|
116
|
+
--output-dir artifacts/smoke
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each run writes `model.keras`, `metrics.json`, `history.csv`,
|
|
120
|
+
`training_curves.png`, and `model_summary.txt`.
|
|
121
|
+
|
|
122
|
+
## Run VGG16 inference
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
image-recognition predict-vgg16 path/to/image.jpg --top 10 \
|
|
126
|
+
--output artifacts/vgg16-prediction.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The ImageNet weights download automatically on first use. To revisit the bee
|
|
130
|
+
example, supply a bee image whose reuse rights you have; the original image is
|
|
131
|
+
not distributed with the article.
|
|
132
|
+
|
|
133
|
+
## Use the safety-framework scaffold
|
|
134
|
+
|
|
135
|
+
Create a protocol with 25 runs for each combination of driver mode,
|
|
136
|
+
technology, and weather condition:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
image-recognition safety init data/safety_runs.csv --runs 25
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Fill the measurement columns in the CSV, then aggregate results:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
image-recognition safety summarize data/safety_runs.csv \
|
|
146
|
+
--output artifacts/safety_summary.csv
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The long-form protocol captures accuracy, confidence, latency, obstacle
|
|
150
|
+
detection, stopping distance, and notes. These fields support the paper's
|
|
151
|
+
proposed extensions while keeping missing measurements blank.
|
|
152
|
+
|
|
153
|
+
The prose names six conditions: sunny (`S`), cloudy (`C`), daytime rain
|
|
154
|
+
(`DR`), fog (`F`), nighttime rain (`NR`), and snow (`SW`). Tables 3–6 also
|
|
155
|
+
contain an unexplained `R` column. The scaffold preserves `R` as `rain` so the
|
|
156
|
+
published table layout can be represented without silently discarding it.
|
|
157
|
+
|
|
158
|
+
## Tests
|
|
159
|
+
|
|
160
|
+
The lightweight tests do not download datasets or TensorFlow weights:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
python -m unittest -v
|
|
164
|
+
python -m compileall -q src
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
GitHub Actions runs these packaging checks automatically on every push and pull
|
|
168
|
+
request. The original root-level Python scripts remain available as
|
|
169
|
+
backward-compatible wrappers.
|
|
170
|
+
|
|
171
|
+
## Published reproduction
|
|
172
|
+
|
|
173
|
+
The verified 25-epoch run reached 99.20% test accuracy. See [RESULTS.md](RESULTS.md)
|
|
174
|
+
for the environment, comparison with the paper, raw metrics, and curves.
|
|
175
|
+
|
|
176
|
+
## Reproducibility boundary
|
|
177
|
+
|
|
178
|
+
This code reproduces the published software procedures as closely as the paper
|
|
179
|
+
allows. A direct LiDAR-versus-camera safety comparison still requires paired,
|
|
180
|
+
time-synchronized sensor captures, ground-truth labels, weather metadata, and
|
|
181
|
+
the 25 physical runs per condition proposed in section 4. Those data are not
|
|
182
|
+
included in the publication.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/image_recognition/__init__.py
|
|
5
|
+
src/image_recognition/__main__.py
|
|
6
|
+
src/image_recognition/cli.py
|
|
7
|
+
src/image_recognition/mnist.py
|
|
8
|
+
src/image_recognition/model.py
|
|
9
|
+
src/image_recognition/safety.py
|
|
10
|
+
src/image_recognition/vgg16.py
|
|
11
|
+
src/lidar_camera_image_recognition.egg-info/PKG-INFO
|
|
12
|
+
src/lidar_camera_image_recognition.egg-info/SOURCES.txt
|
|
13
|
+
src/lidar_camera_image_recognition.egg-info/dependency_links.txt
|
|
14
|
+
src/lidar_camera_image_recognition.egg-info/entry_points.txt
|
|
15
|
+
src/lidar_camera_image_recognition.egg-info/requires.txt
|
|
16
|
+
src/lidar_camera_image_recognition.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
lidar_camera_image_recognition-0.1.0/src/lidar_camera_image_recognition.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
image_recognition
|