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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """Reproducible image-recognition experiments for autonomous-vehicle research."""
2
+
3
+ from .model import build_paper_cnn
4
+ from .safety import create_protocol, summarize
5
+
6
+ __all__ = ["build_paper_cnn", "create_protocol", "summarize"]
7
+ __version__ = "0.1.0"
8
+
@@ -0,0 +1,6 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
6
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ image-recognition = image_recognition.cli:main
@@ -0,0 +1,4 @@
1
+ tensorflow<2.17,>=2.16
2
+ numpy<2,>=1.26
3
+ matplotlib<3.9,>=3.8
4
+ Pillow<11,>=10.3