Advanced Usage
Using custom models
Users who want to customize AIgarMIC or use advanced configurations are best setting up a custom python script and inheriting from AIgarMIC classes. For example, let’s say that we wanted to predict images based on a very simple mean pixel value. Note that we will only be using this to demonstrate how AIgarMIC can be extended and customised (this model will not make good MIC predictions).
Firstly we will import the necessary classes and functions from AIgarMIC:
>>> from aigarmic import Plate, PlateSet, Model
>>> from aigarmic import get_paths_from_directory, get_concentration_from_path
Next, we will define a custom model that inherits from aigarmic.Model. To make MIC predictions, models must have a predict method that takes an opencv image as an input (in numpy array format), and returns a dict as an output. As a minimum, the dict should contain a growth_code key, whose value is an integer corresponding to growth code. Since this is a binary model, the code will be 0 or 1. It is also useful to provide an accuracy value (between 0 and 1) to indicate the confidence of the model’s prediction. This can then be used by AIgarMIC to flag up quality issues with predictions. Here, we will not provide one, so AIgarMIC will assume the model is 100% accurate (accuracy=1.0).
>>> class MeanModel(Model):
... def __init__(self, key=None):
... super().__init__(key=key)
... def predict(self, image):
... return {'growth_code': int(image.mean() > 150)}
Now we will use aigarmic.get_paths_from_directory() to get the paths of images of agar plates, and use these to create a list of aigarmic.Plate objects. The function aigarmic.get_concentration_from_path() is used to extract the concentration of the antibiotic from the image path.
>>> antibiotic = "amikacin"
>>> model = MeanModel(key=['no growth', 'growth'])
>>> paths = get_paths_from_directory("../images/antimicrobials/")
>>> plates = [Plate(antibiotic, get_concentration_from_path(path), model=model, image=path, n_row=8, n_col=12) for path in paths[antibiotic]]
Note also that we provided an (optional) key to MeanModel on construction, to help with the interpretation of the growth code. Since we provided both an image and a valid model to the aigarmic.Plate constructor, the agar plate image has been split into smaller colony images, and each colony image converted to a growth code (using the linked model). We can confirm this by inspecting the growth_code_matrix (here we will look at the first row of the first plate):
>>> plates[0].growth_code_matrix[0]
[0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0]
Finally, we can convert the list of plates to a aigarmic.PlateSet object, and calculate the MICs:
>>> plate_set = PlateSet(plates_list=plates)
>>> _ = plate_set.calculate_mic(no_growth_key_items=tuple([0]))
>>> plate_set.convert_mic_matrix(mic_format='string').tolist()[0]
['32.0', '32.0', '32.0', '>64.0', '>64.0', '>64.0', '64.0', '32.0', '16.0', '16.0', '16.0', '16.0']
We now also have access to other AIgarMIC features such as aigarmic.PlateSet.generate_qc().
Alternative approaches
Alternatively, to train a keras convolutional neural network please see Modular model training script (allows training of binary or softmax model from annotate colony images using a fixed CNN structure described in http://dx.doi.org/10.1128/spectrum.04209-23), aigarmic.train_binary() and aigarmic.train_softmax(). A custom keras Sequential CNN can be defined for the latter two functions, but the training process is simplified, if a binary or softmax model is sufficient.
Using custom image splitters
A key step in calculating MICs using AIgarMIC is the splitting of agar plate images into small images covering an inoculation position. By default, AIgarMIC uses a grid-based splitting algorithm, and applies this by default on initialisation of a aigarmic.Plate object. Users may prefer to implement their own splitting function, or input small images directly. To demonstrate how this could be implemented, we will explicitly call the splitting function on a aigarmic.Plate object:
>>> from aigarmic.process_plate_image import split_by_grid
>>> import cv2
>>> plates = [Plate(antibiotic, get_concentration_from_path(path), model=model, n_row=8, n_col=12) for path in paths[antibiotic]]
>>> for plate, path in zip(plates, paths[antibiotic]):
... image = cv2.imread(path)
... plate.image_matrix = split_by_grid(image, n_rows=8, n_cols=12)
>>> for i in plates:
... _ = i.annotate_images() # explicit call needed
>>> plate_set = PlateSet(plates_list=plates)
>>> _ = plate_set.calculate_mic(no_growth_key_items=tuple([0]))
>>> plate_set.convert_mic_matrix(mic_format='string').tolist()[0]
['32.0', '32.0', '32.0', '>64.0', '>64.0', '>64.0', '64.0', '32.0', '16.0', '16.0', '16.0', '16.0']
The code here is similar to previous examples, except that we are not passing an image path on initialisation of aigarmic.Plate. Instead, we are reading the image (using opencv) and splitting (using aigarmic.process_plate_image.split_by_grid()) into small images. The key point here is that we then overwrite the attribute image_matrix of the aigarmic.Plate object with the split images. Note also that we have to explicitly call aigarmic.Plate.annotate_images() after adding the plate image, since we did not provide the image on object construction. From this point onwards, the code is the same as before.
To use a different splitting function, simply replace the call to aigarmic.process_plate_image.split_by_grid() with a custom function. The function must have a return type of list[list[ndarray]], i.e., a 2D matrix of images read using opencv.