lica-raw 0.1.1__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.
- lica_raw-0.1.1/LICENSE +21 -0
- lica_raw-0.1.1/PKG-INFO +368 -0
- lica_raw-0.1.1/README.md +346 -0
- lica_raw-0.1.1/pyproject.toml +40 -0
- lica_raw-0.1.1/pyproject.toml.orig +48 -0
- lica_raw-0.1.1/src/lica_raw/__init__.py +46 -0
- lica_raw-0.1.1/src/lica_raw/adapters/__init__.py +3 -0
- lica_raw-0.1.1/src/lica_raw/adapters/exif.py +222 -0
- lica_raw-0.1.1/src/lica_raw/adapters/factory.py +33 -0
- lica_raw-0.1.1/src/lica_raw/adapters/fits.py +252 -0
- lica_raw-0.1.1/src/lica_raw/errors.py +49 -0
- lica_raw-0.1.1/src/lica_raw/models.py +37 -0
- lica_raw-0.1.1/src/lica_raw/ports.py +56 -0
- lica_raw-0.1.1/src/lica_raw/rect.py +109 -0
- lica_raw-0.1.1/src/lica_raw/types.py +48 -0
- lica_raw-0.1.1/src/lica_raw/utils.py +45 -0
lica_raw-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guaix UCM
|
|
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.
|
lica_raw-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lica-raw
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: LICA RGB Raw loader for DSLR images
|
|
5
|
+
Author: Rafael González
|
|
6
|
+
Author-email: Rafael González <rafael08@ucm.es>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
14
|
+
Requires-Dist: numpy>=2.5
|
|
15
|
+
Requires-Dist: exifread>=3.5 ; extra == 'exif'
|
|
16
|
+
Requires-Dist: rawpy>=0.27 ; extra == 'exif'
|
|
17
|
+
Requires-Dist: astropy>=8.0 ; extra == 'fits'
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Provides-Extra: exif
|
|
20
|
+
Provides-Extra: fits
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# lica-raw
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
`lica-raw` is a lightweight utility for reading metadata and raw Bayer pixel data from astronomical camera images. It defines minimal reader interfaces using `typing.Protocol` and provides adapters for FITS images and camera RAW files with EXIF metadata.
|
|
28
|
+
|
|
29
|
+
The package supports Bayer colour-filter-array layouts, per-channel black levels, optional rectangular regions of interest, and automatic selection of a reader from the image file extension. Application code depends only on the `ImageReader`, `ImageReaderFactory`, and `RawPixels` protocols, while the concrete image-format behaviour is supplied by adapters.
|
|
30
|
+
|
|
31
|
+
Objects returned by `read_pixels()` own the underlying image resource. They can be used as context managers, or closed explicitly with `close()` when batch processing makes a `with` block inconvenient.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install `lica-raw` directly from the `lica-ng` Git repository:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install lica-raw
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
or add it to your `pyproject.toml`:
|
|
42
|
+
|
|
43
|
+
```toml
|
|
44
|
+
[project]
|
|
45
|
+
dependencies = [
|
|
46
|
+
"lica-raw",
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For FITS image support, `lica-raw[fits]` installs the needed dependency on `astropy`:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
[project]
|
|
54
|
+
dependencies = [
|
|
55
|
+
"lica-raw[fits]",
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For camera RAW file support, `lica-raw[exif]` installs the needed dependencies `rawpy`, `exifread`, and `numpy`:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[project]
|
|
63
|
+
dependencies = [
|
|
64
|
+
"lica-raw[exif]",
|
|
65
|
+
]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Interfaces
|
|
69
|
+
|
|
70
|
+
`lica-raw` defines its image-access contracts using `typing.Protocol`. Adapters do not need to inherit from these protocols explicitly; they only need to provide the required methods and attributes.
|
|
71
|
+
|
|
72
|
+
### Image reader
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from typing import Protocol
|
|
76
|
+
|
|
77
|
+
from lica_raw import HeaderType, ImageMetadata
|
|
78
|
+
from lica_raw.ports import RawPixels
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ImageReader(Protocol):
|
|
82
|
+
def header_type(self) -> HeaderType: ...
|
|
83
|
+
|
|
84
|
+
def read_metadata(self, path: str) -> ImageMetadata: ...
|
|
85
|
+
|
|
86
|
+
def read_pixels(self, path: str) -> RawPixels: ...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Raw pixels
|
|
90
|
+
|
|
91
|
+
`RawPixels` objects support the context-manager protocol. Entering the context returns the pixel-data object; leaving it closes the underlying image resource, even if an exception occurs.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from collections.abc import Generator
|
|
95
|
+
from types import TracebackType
|
|
96
|
+
from typing import Protocol, Self
|
|
97
|
+
|
|
98
|
+
from lica_raw import IntArray2D, Rect
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class RawPixels(Protocol):
|
|
102
|
+
def r(self, section: Rect | None) -> IntArray2D: ...
|
|
103
|
+
def gr(self, section: Rect | None) -> IntArray2D: ...
|
|
104
|
+
def gb(self, section: Rect | None) -> IntArray2D: ...
|
|
105
|
+
def b(self, section: Rect | None) -> IntArray2D: ...
|
|
106
|
+
|
|
107
|
+
def r_black(self) -> int: ...
|
|
108
|
+
def gr_black(self) -> int: ...
|
|
109
|
+
def gb_black(self) -> int: ...
|
|
110
|
+
def b_black(self) -> int: ...
|
|
111
|
+
|
|
112
|
+
def close(self) -> None: ...
|
|
113
|
+
|
|
114
|
+
def __enter__(self) -> Self: ...
|
|
115
|
+
|
|
116
|
+
def __exit__(
|
|
117
|
+
self,
|
|
118
|
+
exc_type: type[BaseException] | None,
|
|
119
|
+
exc_value: BaseException | None,
|
|
120
|
+
traceback: TracebackType | None,
|
|
121
|
+
) -> None: ...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`close()` is idempotent and can be called explicitly. This is useful in batch-processing loops where wrapping every image in a `with` block would make the client code unnecessarily nested.
|
|
125
|
+
|
|
126
|
+
### Reader factory
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from typing import Protocol
|
|
130
|
+
|
|
131
|
+
from lica_raw.ports import ImageReader
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ImageReaderFactory(Protocol):
|
|
135
|
+
def get_reader(self, path: str) -> ImageReader: ...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Adapters
|
|
139
|
+
|
|
140
|
+
| Adapter | Module | Extra | Description |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `FitsReader` | `lica_raw.adapters.fits` | `astropy` | Reads metadata and raw Bayer pixels from FITS images. |
|
|
143
|
+
| `ExifReader` | `lica_raw.adapters.exif` | `rawpy`, `exifread`, `numpy` | Reads metadata and raw Bayer pixels from camera RAW files using EXIF metadata and `rawpy`. |
|
|
144
|
+
| `ImageReaderFactoryImpl` | `lica_raw.adapters` | — | Selects `FitsReader` for FITS extensions and `ExifReader` otherwise. |
|
|
145
|
+
|
|
146
|
+
The factory recognises the following extensions as FITS:
|
|
147
|
+
|
|
148
|
+
- `.fit`
|
|
149
|
+
- `.fits`
|
|
150
|
+
- `.fts`
|
|
151
|
+
|
|
152
|
+
All other files are handled by `ExifReader`.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from lica_raw.adapters import ImageReaderFactoryImpl
|
|
156
|
+
|
|
157
|
+
factory = ImageReaderFactoryImpl()
|
|
158
|
+
reader = factory.get_reader("image_001.fits")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Image metadata
|
|
162
|
+
|
|
163
|
+
`read_metadata()` returns an `ImageMetadata` dataclass:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from datetime import datetime
|
|
167
|
+
|
|
168
|
+
from lica_raw import BayerPattern, HeaderType, ImageType
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class ImageMetadata:
|
|
173
|
+
width: int
|
|
174
|
+
height: int
|
|
175
|
+
make: str | None
|
|
176
|
+
model: str | None
|
|
177
|
+
exptime: float | None
|
|
178
|
+
iso: int | None
|
|
179
|
+
gain: float | None
|
|
180
|
+
black_level: int | None
|
|
181
|
+
bayer: BayerPattern
|
|
182
|
+
imagetyp: ImageType | None
|
|
183
|
+
timestamp: datetime | None
|
|
184
|
+
focal_len: float | None
|
|
185
|
+
f_number: float | None
|
|
186
|
+
header_type: HeaderType
|
|
187
|
+
extension: str
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The supported Bayer patterns are defined in the `BayerPattern` enum as follows:
|
|
191
|
+
|
|
192
|
+
- `RGGB`
|
|
193
|
+
- `BGGR`
|
|
194
|
+
- `GRBG`
|
|
195
|
+
- `GBRG`
|
|
196
|
+
|
|
197
|
+
The supported image types are defined in the `ImageType` enum as follows:
|
|
198
|
+
|
|
199
|
+
- `BIAS`
|
|
200
|
+
- `DARK`
|
|
201
|
+
- `FLAT`
|
|
202
|
+
- `LIGHT`
|
|
203
|
+
|
|
204
|
+
For non-FITS images, `image_type_by_path()` infers the image type from the file name or immediate parent-directory name. Names containing `FLAT`, `DARK` or `OSCURO`, or `BIAS` are classified accordingly; all other images are treated as `LIGHT`.
|
|
205
|
+
|
|
206
|
+
## FITS support
|
|
207
|
+
|
|
208
|
+
`FitsReader` requires the following mandatory FITS keywords:
|
|
209
|
+
|
|
210
|
+
- `NAXIS1`
|
|
211
|
+
- `NAXIS2`
|
|
212
|
+
- `BAYERPAT`
|
|
213
|
+
|
|
214
|
+
It also reads optional metadata from these keywords:
|
|
215
|
+
|
|
216
|
+
| Metadata | FITS keyword |
|
|
217
|
+
|---|---|
|
|
218
|
+
| Instrument or camera model | `INSTRUME` |
|
|
219
|
+
| Exposure time | `EXPTIME` |
|
|
220
|
+
| Gain | `LOG-GAIN` |
|
|
221
|
+
| Black level | `PEDESTAL` |
|
|
222
|
+
| Image type | `IMAGETYP` |
|
|
223
|
+
| Observation date | `DATE-OBS` |
|
|
224
|
+
| Focal length | `FOCALLEN` |
|
|
225
|
+
| Aperture diameter | `APTDIA` |
|
|
226
|
+
|
|
227
|
+
If `FOCALLEN` and `APTDIA` are both present, the focal ratio is calculated as `FOCALLEN / APTDIA`.
|
|
228
|
+
|
|
229
|
+
The observation timestamp is interpreted in UTC. `FitsRawPixels` uses `PEDESTAL` as the black level for all four Bayer channels.
|
|
230
|
+
|
|
231
|
+
## Camera RAW support
|
|
232
|
+
|
|
233
|
+
`ExifReader` reads EXIF metadata with `exifread` and raw image data with `rawpy`. It extracts manufacturer, model, focal length, focal ratio, ISO, exposure time, observation date, image dimensions, black levels, and the Bayer pattern.
|
|
234
|
+
|
|
235
|
+
The adapter currently requires a `RGBG` colour-filter-array description from `rawpy`. Other CFA layouts raise `UnsupportedCFAError`.
|
|
236
|
+
|
|
237
|
+
The observation date is read from `Image DateTime` or `EXIF DateTimeOriginal`, interpreted in UTC, and accepts the following formats:
|
|
238
|
+
|
|
239
|
+
- `%Y:%m:%d %H:%M:%S`
|
|
240
|
+
- `%Y-%m-%dT%H:%M:%S`
|
|
241
|
+
- `%Y-%m-%dT%H:%M:%S%z`
|
|
242
|
+
|
|
243
|
+
## Regions of interest
|
|
244
|
+
|
|
245
|
+
The `r()`, `gr()`, `gb()`, and `b()` methods return a two-dimensional NumPy array containing the pixels of one Bayer colour channel.
|
|
246
|
+
|
|
247
|
+
If `section` is `None`, the full image is used. Otherwise, `section` must be a `Rect` expressed in output-channel coordinates; it is validated against the available image area and converted internally to the corresponding full-resolution Bayer coordinates.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from lica_raw import Rect
|
|
251
|
+
|
|
252
|
+
section = Rect(0, 0, 100, 100)
|
|
253
|
+
|
|
254
|
+
red = pixels.r(section)
|
|
255
|
+
green_red = pixels.gr(section)
|
|
256
|
+
green_blue = pixels.gb(section)
|
|
257
|
+
blue = pixels.b(section)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A section outside the valid area raises `OutOfBoundsError`.
|
|
261
|
+
|
|
262
|
+
## Resource management
|
|
263
|
+
|
|
264
|
+
`read_pixels()` returns an object that owns an open image resource. Prefer a context manager when the lifetime of the pixels is limited to a local block:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
with reader.read_pixels(path) as pixels:
|
|
268
|
+
red = pixels.r(None)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
For batch processing, explicit closing is equally valid:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
for path in image_paths:
|
|
275
|
+
pixels = reader.read_pixels(path)
|
|
276
|
+
try:
|
|
277
|
+
red = pixels.r(None)
|
|
278
|
+
...
|
|
279
|
+
finally:
|
|
280
|
+
pixels.close()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
After `close()`, the pixel object must not be used to access image data.
|
|
284
|
+
|
|
285
|
+
## Errors
|
|
286
|
+
|
|
287
|
+
All package-specific errors derive from `RawBaseError`.
|
|
288
|
+
|
|
289
|
+
| Error | Description |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `UnsupportedCFAError` | The camera RAW file uses an unsupported colour-filter-array layout. |
|
|
292
|
+
| `ReadMetadataError` | EXIF metadata could not be read. |
|
|
293
|
+
| `UnknownDateTimeFormatError` | The observation-date string uses an unsupported format. |
|
|
294
|
+
| `OutOfBoundsError` | The requested rectangular section lies outside the image. |
|
|
295
|
+
| `MissingImageError` | The image path does not exist. |
|
|
296
|
+
| `MissingMandatoryFitsError` | One or more mandatory FITS keywords are missing. |
|
|
297
|
+
| `AlreadyClosedError` | Attempt to close an already closed FITS/EXIF image. |
|
|
298
|
+
|
|
299
|
+
## Usage example
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from lica_raw import ImageReaderFactory, Rect
|
|
303
|
+
from lica_raw.adapters import ImageReaderFactoryImpl
|
|
304
|
+
|
|
305
|
+
path = "light_001.fits"
|
|
306
|
+
|
|
307
|
+
factory: ImageReaderFactory = ImageReaderFactoryImpl()
|
|
308
|
+
reader = factory.get_reader(path)
|
|
309
|
+
|
|
310
|
+
metadata = reader.read_metadata(path)
|
|
311
|
+
print(metadata.model, metadata.width, metadata.height)
|
|
312
|
+
print(metadata.bayer, metadata.exptime, metadata.timestamp)
|
|
313
|
+
|
|
314
|
+
with reader.read_pixels(path) as pixels:
|
|
315
|
+
# Gets the whole channel when section is None.
|
|
316
|
+
red = pixels.r(None)
|
|
317
|
+
green_red = pixels.gr(Rect(0, 0, 256, 256))
|
|
318
|
+
green_blue = pixels.gb(Rect(0, 0, 256, 256))
|
|
319
|
+
blue = pixels.b(Rect(0, 0, 256, 256))
|
|
320
|
+
|
|
321
|
+
print(
|
|
322
|
+
pixels.r_black(),
|
|
323
|
+
pixels.gr_black(),
|
|
324
|
+
pixels.gb_black(),
|
|
325
|
+
pixels.b_black(),
|
|
326
|
+
)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Usage example, with lica-cli package
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
import sys
|
|
333
|
+
from argparse import ArgumentParser, Namespace
|
|
334
|
+
|
|
335
|
+
from lica_cli import parse_args, run_cli
|
|
336
|
+
from lica_raw import ImageReaderFactory
|
|
337
|
+
from lica_raw.adapters import ImageReaderFactoryImpl
|
|
338
|
+
|
|
339
|
+
from myprog import __version__
|
|
340
|
+
|
|
341
|
+
DESCRIPTION = "My program short description"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def add_args(parser: ArgumentParser) -> None:
|
|
345
|
+
parser.add_argument("image", help="FITS or camera RAW image path")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def cli_main(args: Namespace) -> None:
|
|
349
|
+
factory: ImageReaderFactory = ImageReaderFactoryImpl()
|
|
350
|
+
reader = factory.get_reader(args.image)
|
|
351
|
+
|
|
352
|
+
metadata = reader.read_metadata(args.image)
|
|
353
|
+
print(metadata)
|
|
354
|
+
|
|
355
|
+
with reader.read_pixels(args.image) as pixels:
|
|
356
|
+
# Gets the whole channel when section is None.
|
|
357
|
+
print("Red channel shape:", pixels.r(None).shape)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def main():
|
|
361
|
+
args = parse_args(add_args, __name__, __version__, DESCRIPTION)
|
|
362
|
+
return_code = run_cli(cli_main, args, __name__, __version__)
|
|
363
|
+
sys.exit(return_code)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
main()
|
|
368
|
+
```
|