# Copyright 2021, Blue Brain Project, EPFL
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tools to deal with brain hierarchy from the AIBS."""
from __future__ import annotations
import numpy as np
[docs]class RegionData:
"""Extract and process brain region data.
Parameters
----------
brain_regions : dict
The brain regions dictionary. Can be obtained from the "msg" key of
the `brain_regions.json` (`1.json`) file.
"""
def __init__(self, brain_regions):
# id to region name
self.id_to_region_dictionary = {}
# dictionary from region id to region complete name
self.id_to_region_dictionary_ALLNAME = {}
# name to id
self.region_dictionary_to_id = {}
# dictionary from region complete name to region id
self.region_dictionary_to_id_ALLNAME = {}
# dictionary from region complete name to its parent complete name
self.region_dictionary_to_id_ALLNAME_parent = {}
self.region_dictionary_to_id_parent = {} # name to name parent
self.allname2name = {} # complete name to name
# dictionary from region name to region complete name
self.name2allname = {}
self.region_keys = [] # list of regions names
self.regions_ALLNAME_list = [] # list of complete regions names
# dictionary from region complete name to boolean, True if the region
# is a leaf region
self.is_leaf = {} # full name to int (! if is leaf, else 0)
self.id_to_color = {} # region id to color in RGB
self.region_to_color = {} # complete name to color in RGB
self.id_to_abv = {}
self.region_dictionary_to_abv = {}
dict_corrections = {}
old_regions_layer23 = [
41,
113,
163,
180,
201,
211,
219,
241,
251,
269,
288,
296,
304,
328,
346,
412,
427,
430,
434,
492,
556,
561,
582,
600,
643,
657,
667,
670,
694,
755,
806,
821,
838,
854,
888,
905,
943,
962,
965,
973,
1053,
1066,
1106,
1127,
12994,
182305697,
]
for reg in old_regions_layer23:
dict_corrections[reg] = [reg + 20000, reg + 30000]
# Change of id when L2 and L2/3 existed
dict_corrections[195] = [20304]
dict_corrections[747] = [20556]
dict_corrections[524] = [20582]
dict_corrections[606] = [20430]
inv_corrections = {}
for k, v in dict_corrections.items():
for conv in v:
inv_corrections[conv] = k
self.search_children(brain_regions)
[docs] def find_unique_regions(
self,
annotation,
top_region_name="Basic cell groups and regions",
):
"""Find unique regions.
Finds unique regions ids that are present in an annotation file and
are contained in the top_region_name Adds also to the list each parent
of the regions present in the annotation file. Dictionaries parameters
Parameters
----------
annotation : np.ndarray
3D numpy ndarray of integers ids of the regions.
top_region_name : str
Name of the most broader region included in the uniques.
Returns
-------
uniques : np.ndarray
Array of unique regions id in the annotation file that are
included in top_region_name.
"""
# Take the parent of the top region to stop the loop
root_allname = self.region_dictionary_to_id_ALLNAME_parent[
self.name2allname[top_region_name]
]
uniques = []
for uniq in np.unique(annotation)[1:]: # Cell regions without outside
allname = self.id_to_region_dictionary_ALLNAME[uniq]
if (
top_region_name in self.id_to_region_dictionary_ALLNAME[uniq]
and uniq not in uniques
):
uniques.append(uniq)
parent_allname = self.region_dictionary_to_id_ALLNAME_parent[allname]
id_parent = self.region_dictionary_to_id_ALLNAME[parent_allname]
while id_parent not in uniques and parent_allname != root_allname:
uniques.append(id_parent)
parent_allname = self.region_dictionary_to_id_ALLNAME_parent[
parent_allname
]
if parent_allname == "":
break
id_parent = self.region_dictionary_to_id_ALLNAME[parent_allname]
return np.array(uniques)
[docs] def find_children(self, uniques):
"""Find children.
Finds the children regions of each region id in uniques and its
distance from a leaf region in the hierarchy tree. Non leaf regions
are included in the children list.
Parameters
----------
uniques : np.ndarray
List of unique region ids
Returns
-------
children : dict
Dictionary of region complete name to list of child region ids.
order_ : np.ndarray
Array of distances from a leaf region in the hierarchy tree for
each region in uniques.
"""
children: dict[str, list[int]] = {}
order_ = np.zeros(uniques.shape)
for id_reg, allname in self.id_to_region_dictionary_ALLNAME.items():
if self.is_leaf[allname]:
inc = 0
ids_reg = [id_reg]
parentname = self.region_dictionary_to_id_ALLNAME_parent[allname]
while parentname != "":
if parentname not in children:
children[parentname] = []
children[parentname] += ids_reg
inc += 1
id_parent = self.region_dictionary_to_id_ALLNAME[parentname]
if id_parent in uniques:
ids_reg.append(id_parent)
place_ = np.where(uniques == id_parent)
order_[place_] = max(order_[place_], inc)
allname = parentname
parentname = self.region_dictionary_to_id_ALLNAME_parent[allname]
for parent, child in children.items():
children[parent] = np.unique(child)
return children, order_
[docs] def filter_region(self, annotation, allname, children):
"""Filter a region.
Computes a 3d boolean mask to filter a region and its subregion from
the annotations.
Parameters
----------
annotation : np.ndarray
3D numpy ndarray of integers ids of the regions.
allname : str
Complete name of the region.
children : dict
Dictionary of region complete name to list of child region ids.
Returns
-------
filter_ : np.ndarray
3D numpy ndarray of boolean, boolean mask with all the voxels of a
region and its children set to True.
"""
if not self.is_leaf[allname]:
filter_ = np.in1d(
annotation,
np.concatenate(
(children[allname], [self.region_dictionary_to_id_ALLNAME[allname]])
),
).reshape(annotation.shape)
else:
filter_ = annotation == self.region_dictionary_to_id_ALLNAME[allname]
return filter_
[docs] def return_ids_containing_str_list(self, str_list):
"""Return IDs containing all keywords.
Retrieve the list of region id which complete name contains all the
keywords in str_list.
Parameters
----------
str_list : iterable
List of keyword that the region complete name.
Returns
-------
id_list : list
List of region id matching condition
"""
id_list = []
for kk in self.id_to_region_dictionary_ALLNAME:
region_is_in = True
for str1 in str_list:
# if any of the regions is not there, do not take
if (self.id_to_region_dictionary_ALLNAME[kk].lower()).find(
str1.lower()
) < 0:
region_is_in = False
break
if region_is_in:
id_list.append(kk)
return id_list
[docs] @staticmethod
def hex_to_rgb(value):
"""Convert a hexadecimal color into its RGB value counterpart.
Parameters
----------
value : str
Hexadecimal color to convert.
Returns
-------
tuple
List of the Red, Green, and Blue components of the color.
"""
value = value.lstrip("#")
lv = len(value)
return tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
[docs] def search_children(self, object_, lastname_ALL="", lastname="", darken=True):
"""Explores the hierarchy dictionary to extract brain regions.
Explores the hierarchy dictionary to extract its brain regions and
fills internal dictionaries.
Parameters
----------
object_ : dict
Dictionary of regions properties. See
https://bbpteam.epfl.ch/documentation/projects/voxcell/latest/atlas.html#brain-region-hierarchy
lastname_ALL : str
Complete name of the parent of the current brain region.
lastname : str
Name of the parent of the current brain region.
darken : bool
If True, darkens the region colors too high.
"""
self.regions_ALLNAME_list.append(lastname_ALL + "|" + object_["name"])
self.name2allname[object_["name"]] = lastname_ALL + "|" + object_["name"]
self.allname2name[lastname_ALL + "|" + object_["name"]] = object_["name"]
self.id_to_region_dictionary[object_["id"]] = object_["name"]
self.id_to_abv[object_["id"]] = object_["acronym"]
self.id_to_region_dictionary_ALLNAME[object_["id"]] = (
lastname_ALL + "|" + object_["name"]
)
self.region_dictionary_to_id[object_["name"]] = object_["id"]
self.region_dictionary_to_abv[object_["name"]] = object_["acronym"]
self.region_dictionary_to_id_ALLNAME[
lastname_ALL + "|" + object_["name"]
] = object_["id"]
self.region_dictionary_to_id_ALLNAME_parent[
lastname_ALL + "|" + object_["name"]
] = lastname_ALL
self.region_dictionary_to_id_parent[object_["name"]] = lastname
clrTMP = np.float32(
np.array(list(self.hex_to_rgb(object_["color_hex_triplet"])))
)
if np.sum(clrTMP) > 255.0 * 3.0 * 0.75 and darken:
clrTMP *= 255.0 * 3.0 * 0.75 / np.sum(clrTMP)
self.region_to_color[lastname_ALL + "|" + object_["name"]] = list(clrTMP)
self.id_to_color[object_["id"]] = list(clrTMP)
self.region_keys.append(object_["name"])
try:
self.is_leaf[lastname_ALL + "|" + object_["name"]] = 1
# ~ region_dictionary_to_id_ALLNAME_child[
# lastname_ALL + "|" + object_["name"]
# ] = children
# ~ id_children[object_["id"]] = object_["children"]
for children in object_["children"]:
self.search_children(
children,
lastname_ALL + "|" + object_["name"],
object_["name"],
darken=darken,
)
self.is_leaf[lastname_ALL + "|" + object_["name"]] = 0
except KeyError:
print("No children of object")