Skip to content

Commit

Permalink
Increase the flexibility and modularity of the package. Implement bas…
Browse files Browse the repository at this point in the history
…e classes (#35)

Increase the flexibility and modularity of the package and reduce the need to manipulate collections of primitives. Provide the basis for being able to implement different types of plate and colony analysis (see #38).

- Adds base classes to provide core functionality
- Create a Plate class (and collection) to store information related to plates and hold a collection of Colony class instances
- Create an ImageFile class (and collection) to store image file paths and timestamp information
- Refactored most of the functions from `main` as static methods in the `plate` or `image_file` modules

- Adds a command line argument, `plate_labels`, to allow labels to be attached to specific plates
- Rename `save_plots` CLA to `plots`

Closes #30
Closes #36
  • Loading branch information
Erik-White authored Feb 8, 2020
1 parent c875f3f commit ec0c59c
Show file tree
Hide file tree
Showing 21 changed files with 2,264 additions and 503 deletions.
12 changes: 11 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Full unit test coverage
- Type annotations on all functions

## [0.4.0] - 2020-01-19
## [0.4.0] - 2020-02-08
### Added
- `plate_labels` command line argument
- `base` module with base classes to provide core functionality throughout the package
- `geometry` module with base shape classes
- `plate` module with `Plate` and `PlateCollection` classes
- `image_file` module with `ImageFile` and `ImageFileCollection` classes
### Changed
- Cached data is now not used by default
- `use_saved` command line argument renamed to `use_cached_data`
- Compressed serialised data filename changed to `cached_data`
- `save_plots` command line argument renamed to `plots`
- Refactored most of the functions from `main` as static methods in the `plate` or `image_file` modules
### Fixed
- A rare error when opening images using skimage.io.imread

## [0.3.4] - 2020-01-18
### Added
Expand Down
62 changes: 37 additions & 25 deletions docs/command_line_arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ A full list of available arguments, along with their default values
-h
--help
```
### Information output
The level of information output to the command line. Default level is `1`, increase to see more information. Output can be silenced with `0`

- input: integer
```
-v
--verbose
```
### Image density
The image density your scanner uses, this can usually be found in your scanner settings. It is important to set this correctly as it enables the program to acurately convert the plate size in millimeters to pixels.

Expand All @@ -31,12 +23,23 @@ The image density your scanner uses, this can usually be found in your scanner s
-dpi
--dots_per_inch
```
### Plate size
The diameter of the plates used, in millimeters. It is important to set this correctly otherwise the plates may be located incorrectly in the images.
### Multiprocessing
This technique utilises all of the available processors that your computer has to analyse images in parallel. Since most computers now have at least 2 or 4 processors, this can greatly reduce the time needed to process a set of images.

- input: integer
This technique is however quite resource intensive for your computer so you may wish to disable it.

- input: boolean
```
--plate_size
-mp
--multiprocessing
```
### Plot images output
The level of detail required when saving plot images after analysis. At the default level (`1`), a few summary plots are saved to give a quick overview of the data. If the output level is increased, individual plots for each plate will be saved.

- input: boolean
```
-p
--plots
```
### Plate edge cut
The radius, in pixels, to remove from the edge of the plate image. This ensures that the image is clear of reflections, shadows and writing that are typically present near the edge of the plate image.
Expand All @@ -45,6 +48,19 @@ The radius, in pixels, to remove from the edge of the plate image. This ensures
```
--plate_edge_cut
```
### Plate labels
A list of labels to identify each plate. The label is used in file names and the plate map.

Plates are ordered from top left, in rows, and labels must be provided in that order.

Labels are separated with spaces. To use a space within a label, wrap that label in quotes

Example: `--plate_labels first second third "label with spaces" fifth sixth`

- input: list
```
--plate_labels
```
### Plate holder shape
The layout of the plates in the image in rows and columns. The default is `3` rows and `2` columns.

Expand All @@ -54,14 +70,12 @@ A square grid of 9 plates would be entered as `--plate_lattice 3 3`
```
--plate_lattice
```
### Plot images output
The level of detail required when saving plot images after analysis. At the default level (`1`), a few summary plots are saved to give a quick overview of the data. If the output level is increased, individual plots for each plate will be saved.

Warning: increasing the number of plots can greatly increase the time taken for the image analysis
### Plate size
The diameter of the plates used, in millimeters. It is important to set this correctly otherwise the plates may be located incorrectly in the images.

- input: boolean
- input: integer
```
--save_plots
--plate_size
```
### Cached data
The package saves a compressed serialised version of its output, along with the uncompressed CSV data. This allows it to quickly generate the CSV files and plot images again, without the need for reanalysing the original images. This is disabled by default to prevent confusing situation where outdated information is output from new or altered image sets.
Expand All @@ -70,14 +84,12 @@ The package saves a compressed serialised version of its output, along with the
```
--use_cached_data
```
### Multiprocessing
This technique utilises all of the available processors that your computer has to analyse images in parallel. Since most computers now have at least 2 or 4 processors, this can greatly reduce the time needed to process a set of images.

This technique is however quite resource intensive for your computer so you may wish to disable it.
### Information output
The level of information output to the command line. Default level is `1`, increase to see more information. Output can be silenced with `0`

- input: boolean
- input: integer
```
-mp
--multiprocessing
-v
--verbose
```

2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ColonyScanalyser can provide information on:
* Colony appearance time
* Colony growth over time
* Growth and appearance time distribution
* Colony colour (e.g. staining or other visual
* Colony colour (e.g. staining or other visual indicator)

## Install
```
Expand Down
242 changes: 242 additions & 0 deletions src/colonyscanalyser/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
from typing import Type, TypeVar, Optional, List
from collections.abc import Collection
from datetime import datetime, timedelta


class Identified:
"""
An object with a integer ID number
"""
def __init__(self, id: int):
self.id = id

@property
def id(self) -> int:
return self.__id

@id.setter
def id(self, val: int):
if self.__id_is_valid(val):
self.__id = val
else:
raise ValueError(f"'{val}' is not a valid id. An id must be a non-negative integer'")

@staticmethod
def __id_exists(collection: Collection, id: int) -> bool:
"""
Verifies if an object in a collection matches the specified ID number
:param collection: a collection of objects (List, Dict etc)
:param id: an ID number to locate
:returns: True if an object with id exists in the collection
"""
return any(id == existing.id for existing in collection)

@staticmethod
def __id_is_valid(id: int) -> bool:
"""
Verifies if a value conforms to the requirements for an ID number
An ID number is an integer with a value greater than zero
:param id: an ID number to verify
:returns: True if the value conforms to the requirements for an ID number
"""
return isinstance(id, int) and id > 0


class IdentifiedCollection:
"""
An collection of Identified objects with generic methods for modifying the collection
"""
T = TypeVar("T", bound = Identified)

def __init__(self, items: Collection = None):
self.items = items

@property
def count(self) -> int:
return len(self.items)

@property
def items(self) -> List["T"]:
"""
Returns a sorted list of items from the collection
A copy is returned, preventing direct changes to the collection
"""
return sorted(self.__items, key = lambda item: item.id)

@items.setter
def items(self, val: Collection):
if isinstance(val, dict):
val = list(val.values())

if val is None:
self.__items = list()
elif isinstance(val, Collection) and not isinstance(val, str):
self.__items = val.copy()
else:
raise ValueError(f"Items must be supplied as a valid Collection, not {type(val)}")

def add(self, id: int) -> "T":
"""
Create a new instance of T and append it to the collection
:param id: a valid Identified ID number
:returns: a new instance of T
"""
item = Identified(id = id)

self.append(item)

return item

def append(self, item: Type[T]):
"""
Append an item to the collection
:param item: the object to append to the collection
"""
if not self.exists(item):
self.__items.append(item)
else:
raise ValueError(f"An item with ID #{item.id} already exists")

def exists(self, item: Type[T]) -> bool:
"""
Check if an item exists in the item collection
:param item: an instance of T
:returns: True if an item is found with matching ID
"""
return self.id_exists(item.id)

def id_exists(self, id: int) -> bool:
"""
Check if an item with the specified ID number exists in the item collection
:param id: a valid Identified id number
:returns: True if an item is found with matching ID
"""
return Identified._Identified__id_exists(self.items, id)

def get_item(self, id: int) -> Optional["T"]:
"""
Returns an item with the specified ID number from the item collection
:param id: a valid Identified ID number
:returns: an item from the collection, if found
"""
for item in self.items:
if item.id == id:
return item

return None

def remove(self, id: int):
"""
Remove an item from the collection
:param id: a valid Identified ID number
"""
if self.id_exists(id):
for item in self.items:
if item.id == id:
self.__items.remove(item)
else:
raise KeyError(f"No item with ID #{id} could be found")


class Named:
"""
An object with a string identifier
"""
def __init__(self, name: str):
self.name = name

@property
def name(self) -> str:
return self.__name

@name.setter
def name(self, val: str):
self.__name = str(val)


class Unique(Identified):
"""
An object with a auto incremented integer ID number
"""
id_count = 0

def __init__(self):
self._Identified__id = self.id_increment()

@Identified.id.setter
def id(self, val: int):
"""
Overrides base method to make id read-only
"""
pass

def id_increment(self) -> int:
"""
Increments the built-in ID counter
:returns: the auto incremented ID number
"""
Unique.id_count += 1

return Unique.id_count


class TimeStamped:
def __init__(self, timestamp: datetime = None):
if timestamp is None:
timestamp = datetime.now()

self.timestamp = timestamp

@property
def timestamp(self) -> datetime:
return self.__timestamp

@timestamp.setter
def timestamp(self, val: datetime):
self.__timestamp = val


class TimeStampElapsed(TimeStamped):
def __init__(self, timestamp: datetime = None, timestamp_initial: datetime = None):
if timestamp is None:
timestamp = datetime.now()
if timestamp_initial is None:
timestamp_initial = timestamp

self._TimeStamped__timestamp = timestamp
self.timestamp_initial = timestamp_initial

@property
def timestamp_elapsed(self) -> timedelta:
return self.timestamp - self.timestamp_initial

@property
def timestamp_elapsed_hours(self) -> float:
return (self.timestamp_elapsed_seconds / 60) / 60

@property
def timestamp_elapsed_minutes(self) -> int:
return int(self.timestamp_elapsed_seconds / 60)

@property
def timestamp_elapsed_seconds(self) -> int:
return self.timestamp_elapsed.total_seconds()

@property
def timestamp_initial(self) -> datetime:
return self.__timestamp_initial

@timestamp_initial.setter
def timestamp_initial(self, val: datetime):
self.__timestamp_initial = val
Loading

0 comments on commit ec0c59c

Please sign in to comment.