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 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.
@@ -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
+ ```