Skip to content

Commit

Permalink
preparing 0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincent Berenz committed Oct 10, 2024
1 parent 4c7025a commit 09eb4cf
Show file tree
Hide file tree
Showing 48 changed files with 4,153 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: unit tests

on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-20.04

strategy:
matrix:
python-version: [3.9.2]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry install --with test
- name: Run tests
run: |
poetry run pytest
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
[![Python package](https://github.com/MPI-IS/nightskycam-images/actions/workflows/tests.yml/badge.svg)](https://github.com/MPI-IS/nightskycam-images/actions/workflows/tests.yml)
[![PyPI version](https://img.shields.io/pypi/v/nightskycam-images.svg)](https://pypi.org/project/nightskycam-images/)

> 🚧 **Under Construction**
> This project is currently under development. Please check back later for updates.

# Nightskycam Images

Expand Down
Empty file added nightskycam_images/__init__.py
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
36 changes: 36 additions & 0 deletions nightskycam_images/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Definition of project-level constants.
"""

from typing import Tuple

# Files in general.
FILE_PERMISSIONS: int = 0o755 # Octal integer.

# HD image.
IMAGE_FILE_FORMATS: Tuple[str, ...] = ("npy", "tiff", "jpg", "jpeg")

# Thumbnail image.
THUMBNAIL_DIR_NAME: str = "thumbnails"
THUMBNAIL_FILE_FORMAT: str = "jpeg"
THUMBNAIL_WIDTH: int = 200 # In pixels.

# Zip.
ZIP_DIR_NAME: str = "zip"

# Video.
VIDEO_FILE_NAME: str = "day_summary.webm"

# Weather summary.
WEATHER_SUMMARY_FILE_NAME: str = "weathers.toml"

# Format patterns.

# formats for filename, e.g. nightskycamX_2024_09_26_13_57_50.jpeg
DATE_FORMAT_FILE: str = "%Y_%m_%d"
TIME_FORMAT_FILE: str = "%H_%M_%S"
DATETIME_FORMATS: Tuple[str, ...] = ("%d_%m_%Y_%H_%M_%S", "%Y_%m_%d_%H_%M_%S")

# formats for displaying on the website
DATE_FORMAT: str = "%Y-%m-%d"
TIME_FORMAT: str = "%H:%M:%S"
44 changes: 44 additions & 0 deletions nightskycam_images/convert_npy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from pathlib import Path
import tempfile

import PIL.Image as PILImage
import cv2
import numpy as np
import numpy.typing as npt


def _bits_reduction(data: npt.NDArray, target: type) -> npt.NDArray:
original_max = np.iinfo(data.dtype).max
target_max = np.iinfo(target).max
ratio = target_max / original_max
return (data * ratio).astype(target)


def _to_8bits(image: npt.NDArray) -> npt.NDArray:
return _bits_reduction(image, np.uint8)


def npy_file_to_pil(image_path: Path) -> PILImage.Image:
"""
Read the file (expected to be an .npy file) and
return a corresponding instance of PIL Image.
It first converts the image to 8 bits.
"""
img_array = np.load(image_path)
img_array = _to_8bits(img_array)

# Create a temporary directory for intermediary tiff file.
with tempfile.TemporaryDirectory() as tmp_dir:
tiff_file_path = Path(tmp_dir) / f"{image_path.stem}.tiff"
cv2.imwrite(str(tiff_file_path), img_array, [cv2.IMWRITE_TIFF_COMPRESSION, 1])
return PILImage.open(str(tiff_file_path))


def npy_file_to_numpy(image_path: Path) -> np.ndarray:
"""
Read the file (expected to be an .npy file) and
return a corresponding numpy array, converted to 8 bits.
"""
img_array = np.load(image_path)
img_array = _to_8bits(img_array)
return img_array
42 changes: 42 additions & 0 deletions nightskycam_images/folder_change.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os
from pathlib import Path
from typing import Optional


def folder_has_changed(
folder: Path, history: Optional[dict[Path, Optional[float]]]
) -> bool:
"""
Check whether the last-modified-time of the directory changed
since the previous check.
If there is no history of a previous check:
return True and initialise history.
Parameters
----------
folder
Path to directory.
history
Last modification times found in previous check:
- key:
Path to directory.
- value:
Time of last modification of directory.
"""
history = history if history else {}

# Float: number of seconds since epoch.
last_modified_time = os.path.getmtime(folder)

# Set to dummy value when key does NOT exist.
previous_modified_time = history.get(folder, None)

# If the folder has NO previous history
# OR was modified since previous history,
# update the history.
if not previous_modified_time or previous_modified_time < last_modified_time:
history[folder] = last_modified_time
return True

return False
226 changes: 226 additions & 0 deletions nightskycam_images/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import datetime as dt
from pathlib import Path
from typing import Any, Dict, Optional, cast

import toml

from .constants import (
DATE_FORMAT,
DATE_FORMAT_FILE,
IMAGE_FILE_FORMATS,
THUMBNAIL_DIR_NAME,
THUMBNAIL_FILE_FORMAT,
VIDEO_FILE_NAME,
)


class Image:
def __init__(self) -> None:

# Stem of file name (without extension).
self.filename_stem: Optional[str] = None
# Datetime instance.
# NOT named `datetime` to avoid confusion with package/class.
self.date_and_time: Optional[dt.datetime] = None
# System name.
self.system: Optional[str] = None

# Absolute path to directory containing the HD images.
# Also contains sub-directory containing the thumbnail images.
self.dir_path: Optional[Path] = None

@property
def filename(self) -> Optional[str]:
"""
Alias for filename_stem
"""
return self.filename_stem

@property
def date(self) -> Optional[dt.datetime]:
"""
Alias for date_and_time
"""
return self.date_and_time

@property
def thumbnail(self) -> Optional[Path]:
"""
Absolute path to thumbnail image file.
"""
if self.dir_path is None:
return None

file_path = (
self.dir_path
/ THUMBNAIL_DIR_NAME
/ f"{self.filename_stem}.{THUMBNAIL_FILE_FORMAT}"
)
if file_path.is_file():
return file_path

return None

@property
def video(self) -> Optional[Path]:
"""
Absolute path to video file.
"""
if self.dir_path is None:
return None

file_path = self.dir_path / THUMBNAIL_DIR_NAME / VIDEO_FILE_NAME
if file_path.is_file():
return file_path

return None

@property
def hd(self) -> Optional[Path]:
"""
Absolute path to HD image file.
"""
if self.dir_path is None:
return None

for f in IMAGE_FILE_FORMATS:
file_path = self.dir_path / f"{self.filename_stem}.{f}"
if file_path.is_file():
return file_path

return None

@property
def path(self) -> Optional[Path]:
"""
Alias for hd
"""
return self.hd

@property
def meta_path(self) -> Optional[Path]:
"""
Absolute path to meta data file.
"""
if self.dir_path is None:
return None

file_path = self.dir_path / f"{self.filename_stem}.toml"
if file_path.is_file():
return file_path

return None

@property
def meta(self) -> Dict[str, Any]:

if self.dir_path is None or self.filename_stem is None:
return {}
meta_file = self.meta_path
if not meta_file:
return {}
if not meta_file.is_file():
return {}
try:
meta = toml.load(meta_file)
except toml.decoder.TomlDecodeError as e:
meta = {"error": f"failed to read {meta_file}: {e}"}
return meta

@property
def nightstart_date(self) -> Optional[dt.date]:
"""
Start date of the night.
Purpose:
Utility for bundling images per night instead of date.
Context:
The date switches at midnight.
"""
if self.date_and_time is None:
return None
hour = self.date_and_time.hour

# Before noon,
# therefore assign to date of previous day.
if hour < 12:
return (self.date_and_time - dt.timedelta(days=1)).date()

return self.date_and_time.date()

@property
def day(self) -> Optional[dt.date]:
"""
Alias for nightstart_date
"""
return self.nightstart_date

@property
def day_as_str(self) -> Optional[str]:
if self.day is None:
return None
return self.day.strftime(DATE_FORMAT_FILE)

@property
def nightstart_date_as_str(self) -> Optional[str]:
"""
String representation of the property `nightstart_date`.
"""
nightstart_date = self.nightstart_date
if nightstart_date is None:
return None
return nightstart_date.strftime(DATE_FORMAT)

@staticmethod
def date_from_str(date: str) -> dt.date:
"""
Get date instance from its string representation.
"""
return dt.datetime.strptime(date, DATE_FORMAT_FILE).date()

@staticmethod
def day_from_str(day: str) -> dt.date:
"""
Alias for date_from_str
"""
return dt.datetime.strptime(day, DATE_FORMAT_FILE).date()

@property
def datetime_as_str(
self, datetime_format: str = "%b. %d, %Y, %-I:%M %p"
) -> Optional[str]:
"""
String representation of the attribute `date_and_time`.
"""
if self.date is None:
return None
return self.date.strftime(datetime_format)

def to_dict(self) -> Dict[str, str]:
"""
Get dictionary representation of image instance.
"""
r: Dict[str, str] = {}
r["path"] = str(self.hd)
r["thumbnail"] = str(self.thumbnail)
r["video"] = str(self.video)
r["date_as_str"] = str(self.datetime_as_str)
r["day_as_str"] = str(self.nightstart_date_as_str)
r["system"] = str(self.system)
r["meta"] = repr(self.meta)
r["meta_path"] = str(self.meta_path)
return r

def __eq__(self, other: object) -> bool:
return self.date_and_time == Image.date_and_time

def __gt__(self, other: object) -> bool:
other_ = cast("Image", other)
if self.date_and_time is None:
return False
if other_.date_and_time is None:
return True
if self.date_and_time >= other_.date_and_time:
return True
return False
Loading

0 comments on commit 09eb4cf

Please sign in to comment.