OSCR

Nuclear Enlargement as a Histological Hallmark of Skeletal Muscle Aging, Revealed by Deep Learning-Driven Analysis and Validated in Inflammatory Myopathies.

Code ↔ Paper

The paper beside its authors' code: matches between them have not been computed for this paper yet.

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 627 lines · 22 KB · BSD-3-Clause

  1. #!/usr/bin/env python3
  2. #
  3. # Deep Zoom Tools
  4. #
  5. # Copyright (c) 2008-2019, Daniel Gasienica <[email hidden]>
  6. # Copyright (c) 2008-2011, OpenZoom <http://openzoom.org>
  7. # Copyright (c) 2010, Boris Bluntschli <[email hidden]>
  8. # Copyright (c) 2008, Kapil Thangavelu <[email hidden]>
  9. # All rights reserved.
  10. #
  11. # Redistribution and use in source and binary forms, with or without modification,
  12. # are permitted provided that the following conditions are met:
  13. #
  14. # 1. Redistributions of source code must retain the above copyright notice,
  15. # this list of conditions and the following disclaimer.
  16. #
  17. # 2. Redistributions in binary form must reproduce the above copyright
  18. # notice, this list of conditions and the following disclaimer in the
  19. # documentation and/or other materials provided with the distribution.
  20. #
  21. # 3. Neither the name of OpenZoom nor the names of its contributors may be used
  22. # to endorse or promote products derived from this software without
  23. # specific prior written permission.
  24. #
  25. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  26. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  27. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  28. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  29. # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  30. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  31. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  32. # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  33. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  34. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  35. #
  36. import io
  37. import math
  38. import optparse
  39. import os
  40. import shutil
  41. from urllib.parse import urlparse
  42. import sys
  43. import time
  44. import urllib.request
  45. import warnings
  46. import xml.dom.minidom
  47. import PIL.Image
  48. from collections import deque
  49. NS_DEEPZOOM = "http://schemas.microsoft.com/deepzoom/2008"
  50. DEFAULT_RESIZE_FILTER = PIL.Image.LANCZOS
  51. DEFAULT_IMAGE_FORMAT = "jpg"
  52. RESIZE_FILTERS = {
  53. "bilinear": PIL.Image.BILINEAR,
  54. "bicubic": PIL.Image.BICUBIC,
  55. "nearest": PIL.Image.NEAREST,
  56. "lanczos": PIL.Image.LANCZOS,
  57. }
  58. IMAGE_FORMATS = {
  59. "jpg": "jpg",
  60. "png": "png",
  61. }
  62. class DeepZoomImageDescriptor(object):
  63. def __init__(
  64. self, width=None, height=None, tile_size=254, tile_overlap=1, tile_format="jpg"
  65. ):
  66. self.width = width
  67. self.height = height
  68. self.tile_size = tile_size
  69. self.tile_overlap = tile_overlap
  70. self.tile_format = tile_format
  71. self._num_levels = None
  72. def open(self, source):
  73. """Intialize descriptor from an existing descriptor file."""
  74. doc = xml.dom.minidom.parse(safe_open(source))
  75. image = doc.getElementsByTagName("Image")[0]
  76. size = doc.getElementsByTagName("Size")[0]
  77. self.width = int(size.getAttribute("Width"))
  78. self.height = int(size.getAttribute("Height"))
  79. self.tile_size = int(image.getAttribute("TileSize"))
  80. self.tile_overlap = int(image.getAttribute("Overlap"))
  81. self.tile_format = image.getAttribute("Format")
  82. def save(self, destination):
  83. """Save descriptor file."""
  84. file = open(destination, "wb")
  85. doc = xml.dom.minidom.Document()
  86. image = doc.createElementNS(NS_DEEPZOOM, "Image")
  87. image.setAttribute("xmlns", NS_DEEPZOOM)
  88. image.setAttribute("TileSize", str(self.tile_size))
  89. image.setAttribute("Overlap", str(self.tile_overlap))
  90. image.setAttribute("Format", str(self.tile_format))
  91. size = doc.createElementNS(NS_DEEPZOOM, "Size")
  92. size.setAttribute("Width", str(self.width))
  93. size.setAttribute("Height", str(self.height))
  94. image.appendChild(size)
  95. doc.appendChild(image)
  96. descriptor = doc.toxml(encoding="UTF-8")
  97. file.write(descriptor)
  98. file.close()
  99. @classmethod
  100. def remove(self, filename):
  101. """Remove descriptor file (DZI) and tiles folder."""
  102. _remove(filename)
  103. @property
  104. def num_levels(self):
  105. """Number of levels in the pyramid."""
  106. if self._num_levels is None:
  107. max_dimension = max(self.width, self.height)
  108. self._num_levels = int(math.ceil(math.log(max_dimension, 2))) + 1
  109. return self._num_levels
  110. def get_scale(self, level):
  111. """Scale of a pyramid level."""
  112. assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
  113. max_level = self.num_levels - 1
  114. return math.pow(0.5, max_level - level)
  115. def get_dimensions(self, level):
  116. """Dimensions of level (width, height)"""
  117. assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
  118. scale = self.get_scale(level)
  119. width = int(math.ceil(self.width * scale))
  120. height = int(math.ceil(self.height * scale))
  121. return (width, height)
  122. def get_num_tiles(self, level):
  123. """Number of tiles (columns, rows)"""
  124. assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
  125. w, h = self.get_dimensions(level)
  126. return (
  127. int(math.ceil(float(w) / self.tile_size)),
  128. int(math.ceil(float(h) / self.tile_size)),
  129. )
  130. def get_tile_bounds(self, level, column, row):
  131. """Bounding box of the tile (x1, y1, x2, y2)"""
  132. assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
  133. offset_x = 0 if column == 0 else self.tile_overlap
  134. offset_y = 0 if row == 0 else self.tile_overlap
  135. x = (column * self.tile_size) - offset_x
  136. y = (row * self.tile_size) - offset_y
  137. level_width, level_height = self.get_dimensions(level)
  138. w = self.tile_size + (1 if column == 0 else 2) * self.tile_overlap
  139. h = self.tile_size + (1 if row == 0 else 2) * self.tile_overlap
  140. w = min(w, level_width - x)
  141. h = min(h, level_height - y)
  142. return (x, y, x + w, y + h)
  143. class DeepZoomCollection(object):
  144. def __init__(
  145. self,
  146. filename,
  147. image_quality=0.8,
  148. max_level=7,
  149. tile_size=256,
  150. tile_format="jpg",
  151. tile_background_color="#000000",
  152. items=[],
  153. ):
  154. self.source = filename
  155. self.image_quality = image_quality
  156. self.tile_size = tile_size
  157. self.max_level = max_level
  158. self.tile_format = tile_format
  159. self.tile_background_color = tile_background_color
  160. self.items = deque(items)
  161. self.next_item_id = len(self.items)
  162. # XML
  163. self.doc = xml.dom.minidom.Document()
  164. collection = self.doc.createElementNS(NS_DEEPZOOM, "Collection")
  165. collection.setAttribute("xmlns", NS_DEEPZOOM)
  166. collection.setAttribute("MaxLevel", str(self.max_level))
  167. collection.setAttribute("TileSize", str(self.tile_size))
  168. collection.setAttribute("Format", str(self.tile_format))
  169. collection.setAttribute("Quality", str(self.image_quality))
  170. # TODO: Append items passed in as argument
  171. items = self.doc.createElementNS(NS_DEEPZOOM, "Items")
  172. collection.appendChild(items)
  173. collection.setAttribute("NextItemId", str(self.next_item_id))
  174. self.doc.appendChild(collection)
  175. @classmethod
  176. def from_file(self, filename):
  177. """Open collection descriptor."""
  178. doc = xml.dom.minidom.parse(safe_open(filename))
  179. collection = doc.getElementsByTagName("Collection")[0]
  180. image_quality = float(collection.getAttribute("Quality"))
  181. max_level = int(collection.getAttribute("MaxLevel"))
  182. tile_size = int(collection.getAttribute("TileSize"))
  183. tile_format = collection.getAttribute("Format")
  184. items = [
  185. DeepZoomCollectionItem.from_xml(item)
  186. for item in doc.getElementsByTagName("I")
  187. ]
  188. collection = DeepZoomCollection(
  189. filename,
  190. image_quality=image_quality,
  191. max_level=max_level,
  192. tile_size=tile_size,
  193. tile_format=tile_format,
  194. items=items,
  195. )
  196. return collection
  197. @classmethod
  198. def remove(self, filename):
  199. """Remove collection file (DZC) and tiles folder."""
  200. _remove(filename)
  201. def append(self, source):
  202. descriptor = DeepZoomImageDescriptor()
  203. descriptor.open(source)
  204. item = DeepZoomCollectionItem(
  205. source, descriptor.width, descriptor.height, id=self.next_item_id
  206. )
  207. self.items.append(item)
  208. self.next_item_id += 1
  209. def save(self, pretty_print_xml=False):
  210. """Save collection descriptor."""
  211. collection = self.doc.getElementsByTagName("Collection")[0]
  212. items = self.doc.getElementsByTagName("Items")[0]
  213. while len(self.items) > 0:
  214. item = self.items.popleft()
  215. i = self.doc.createElementNS(NS_DEEPZOOM, "I")
  216. i.setAttribute("Id", str(item.id))
  217. i.setAttribute("N", str(item.id))
  218. i.setAttribute("Source", item.source)
  219. # Size
  220. size = self.doc.createElementNS(NS_DEEPZOOM, "Size")
  221. size.setAttribute("Width", str(item.width))
  222. size.setAttribute("Height", str(item.height))
  223. i.appendChild(size)
  224. items.appendChild(i)
  225. self._append_image(item.source, item.id)
  226. collection.setAttribute("NextItemId", str(self.next_item_id))
  227. with open(self.source, "wb") as f:
  228. if pretty_print_xml:
  229. xml = self.doc.toprettyxml(encoding="UTF-8")
  230. else:
  231. xml = self.doc.toxml(encoding="UTF-8")
  232. f.write(xml)
  233. def _append_image(self, path, i):
  234. descriptor = DeepZoomImageDescriptor()
  235. descriptor.open(path)
  236. files_path = _get_or_create_path(_get_files_path(self.source))
  237. for level in reversed(range(self.max_level + 1)):
  238. level_path = _get_or_create_path("%s/%s" % (files_path, level))
  239. level_size = 2 ** level
  240. images_per_tile = int(math.floor(self.tile_size / level_size))
  241. column, row = self.get_tile_position(i, level, self.tile_size)
  242. tile_path = "%s/%s_%s.%s" % (level_path, column, row, self.tile_format)
  243. if not os.path.exists(tile_path):
  244. tile_image = PIL.Image.new(
  245. "RGB", (self.tile_size, self.tile_size), self.tile_background_color
  246. )
  247. if self.tile_format == "jpg":
  248. jpeg_quality = int(self.image_quality * 100)
  249. tile_image.save(tile_path, "JPEG", quality=jpeg_quality)
  250. else:
  251. tile_image.save(tile_path)
  252. tile_image = PIL.Image.open(tile_path)
  253. source_path = "%s/%s/%s_%s.%s" % (
  254. _get_files_path(path),
  255. level,
  256. 0,
  257. 0,
  258. descriptor.tile_format,
  259. )
  260. # Local
  261. if os.path.exists(source_path):
  262. try:
  263. source_image = PIL.Image.open(safe_open(source_path))
  264. except IOError:
  265. warnings.warn("Skipped invalid level: %s" % source_path)
  266. continue
  267. # Remote
  268. else:
  269. if level == self.max_level:
  270. try:
  271. source_image = PIL.Image.open(safe_open(source_path))
  272. except IOError:
  273. warnings.warn("Skipped invalid image: %s" % source_path)
  274. return
  275. # Expected width & height of the tile
  276. e_w, e_h = descriptor.get_dimensions(level)
  277. # Actual width & height of the tile
  278. w, h = source_image.size
  279. # Correct tile because of IIP bug where low-level tiles have
  280. # wrong dimensions (they are too large)
  281. if w != e_w or h != e_h:
  282. # Resize incorrect tile to correct size
  283. source_image = source_image.resize(
  284. (e_w, e_h), DEFAULT_RESIZE_FILTER
  285. )
  286. # Store new dimensions
  287. w, h = e_w, e_h
  288. else:
  289. w = int(math.ceil(w * 0.5))
  290. h = int(math.ceil(h * 0.5))
  291. source_image.thumbnail((w, h), DEFAULT_RESIZE_FILTER)
  292. column, row = self.get_position(i)
  293. x = (column % images_per_tile) * level_size
  294. y = (row % images_per_tile) * level_size
  295. tile_image.paste(source_image, (x, y))
  296. tile_image.save(tile_path)
  297. def get_position(self, z_order):
  298. """Returns position (column, row) from given Z-order (Morton number.)"""
  299. column = 0
  300. row = 0
  301. for i in range(0, 32, 2):
  302. offset = i // 2
  303. # column
  304. column_offset = i
  305. column_mask = 1 << column_offset
  306. column_value = (z_order & column_mask) >> column_offset
  307. column |= column_value << offset
  308. # row
  309. row_offset = i + 1
  310. row_mask = 1 << row_offset
  311. row_value = (z_order & row_mask) >> row_offset
  312. row |= row_value << offset
  313. return int(column), int(row)
  314. def get_z_order(self, column, row):
  315. """Returns the Z-order (Morton number) from given position."""
  316. z_order = 0
  317. for i in range(32):
  318. z_order |= (column & 1 << i) << i | (row & 1 << i) << (i + 1)
  319. return z_order
  320. def get_tile_position(self, z_order, level, tile_size):
  321. level_size = 2 ** level
  322. x, y = self.get_position(z_order)
  323. return (
  324. int(math.floor((x * level_size) / tile_size)),
  325. int(math.floor((y * level_size) / tile_size)),
  326. )
  327. class DeepZoomCollectionItem(object):
  328. def __init__(self, source, width, height, id=0):
  329. self.id = id
  330. self.source = source
  331. self.width = width
  332. self.height = height
  333. @classmethod
  334. def from_xml(cls, xml):
  335. id = int(xml.getAttribute("Id"))
  336. source = xml.getAttribute("Source")
  337. size = xml.getElementsByTagName("Size")[0]
  338. width = int(size.getAttribute("Width"))
  339. height = int(size.getAttribute("Height"))
  340. return DeepZoomCollectionItem(source, width, height, id)
  341. class ImageCreator(object):
  342. """Creates Deep Zoom images."""
  343. def __init__(
  344. self,
  345. tile_size=254,
  346. tile_overlap=1,
  347. tile_format="jpg",
  348. image_quality=0.8,
  349. resize_filter=None,
  350. copy_metadata=False,
  351. ):
  352. self.tile_size = int(tile_size)
  353. self.tile_format = tile_format
  354. self.tile_overlap = _clamp(int(tile_overlap), 0, 10)
  355. self.image_quality = _clamp(image_quality, 0, 1.0)
  356. if not tile_format in IMAGE_FORMATS:
  357. self.tile_format = DEFAULT_IMAGE_FORMAT
  358. self.resize_filter = resize_filter
  359. self.copy_metadata = copy_metadata
  360. def get_image(self, level):
  361. """Returns the bitmap image at the given level."""
  362. assert (
  363. 0 <= level and level < self.descriptor.num_levels
  364. ), "Invalid pyramid level"
  365. width, height = self.descriptor.get_dimensions(level)
  366. # don't transform to what we already have
  367. if self.descriptor.width == width and self.descriptor.height == height:
  368. return self.image
  369. if (self.resize_filter is None) or (self.resize_filter not in RESIZE_FILTERS):
  370. return self.image.resize((width, height), DEFAULT_RESIZE_FILTER)
  371. return self.image.resize((width, height), RESIZE_FILTERS[self.resize_filter])
  372. def tiles(self, level):
  373. """Iterator for all tiles in the given level. Returns (column, row) of a tile."""
  374. columns, rows = self.descriptor.get_num_tiles(level)
  375. for column in range(columns):
  376. for row in range(rows):
  377. yield (column, row)
  378. def create(self, source, destination):
  379. """Creates Deep Zoom image from source file and saves it to destination."""
  380. if isinstance(source, PIL.Image.Image):
  381. self.image = source
  382. else:
  383. self.image = PIL.Image.open(safe_open(source))
  384. width, height = self.image.size
  385. self.descriptor = DeepZoomImageDescriptor(
  386. width=width,
  387. height=height,
  388. tile_size=self.tile_size,
  389. tile_overlap=self.tile_overlap,
  390. tile_format=self.tile_format,
  391. )
  392. # Create tiles
  393. image_files = _get_or_create_path(_get_files_path(destination))
  394. for level in range(self.descriptor.num_levels):
  395. level_dir = _get_or_create_path(os.path.join(image_files, str(level)))
  396. level_image = self.get_image(level)
  397. for (column, row) in self.tiles(level):
  398. bounds = self.descriptor.get_tile_bounds(level, column, row)
  399. tile = level_image.crop(bounds)
  400. format = self.descriptor.tile_format
  401. tile_path = os.path.join(level_dir, "%s_%s.%s" % (column, row, format))
  402. if self.descriptor.tile_format == "jpg":
  403. jpeg_quality = int(self.image_quality * 100)
  404. tile.save(tile_path, "JPEG", quality=jpeg_quality)
  405. else:
  406. tile.save(tile_path)
  407. # Create descriptor
  408. self.descriptor.save(destination)
  409. class CollectionCreator(object):
  410. """Creates Deep Zoom collections."""
  411. def __init__(
  412. self,
  413. image_quality=0.8,
  414. tile_size=256,
  415. max_level=7,
  416. tile_format="jpg",
  417. copy_metadata=False,
  418. tile_background_color="#000000",
  419. ):
  420. self.image_quality = image_quality
  421. self.tile_size = tile_size
  422. self.max_level = max_level
  423. self.tile_format = tile_format
  424. self.tile_background_color = tile_background_color
  425. # TODO
  426. self.copy_metadata = copy_metadata
  427. def create(self, images, destination):
  428. """Creates a Deep Zoom collection from a list of images."""
  429. collection = DeepZoomCollection(
  430. destination,
  431. image_quality=self.image_quality,
  432. max_level=self.max_level,
  433. tile_size=self.tile_size,
  434. tile_format=self.tile_format,
  435. tile_background_color=self.tile_background_color,
  436. )
  437. for image in images:
  438. collection.append(image)
  439. collection.save()
  440. ################################################################################
  441. def retry(attempts, backoff=2):
  442. """Retries a function or method until it returns or
  443. the number of attempts has been reached."""
  444. if backoff <= 1:
  445. raise ValueError("backoff must be greater than 1")
  446. attempts = int(math.floor(attempts))
  447. if attempts < 0:
  448. raise ValueError("attempts must be 0 or greater")
  449. def deco_retry(f):
  450. def f_retry(*args, **kwargs):
  451. last_exception = None
  452. for _ in range(attempts):
  453. try:
  454. return f(*args, **kwargs)
  455. except Exception as exception:
  456. last_exception = exception
  457. time.sleep(backoff ** (attempts + 1))
  458. raise last_exception
  459. return f_retry
  460. return deco_retry
  461. def _get_or_create_path(path):
  462. if not os.path.exists(path):
  463. os.makedirs(path)
  464. return path
  465. def _clamp(val, min, max):
  466. if val < min:
  467. return min
  468. elif val > max:
  469. return max
  470. return val
  471. def _get_files_path(path):
  472. return os.path.splitext(path)[0] + "_files"
  473. def _remove(path):
  474. os.remove(path)
  475. tiles_path = _get_files_path(path)
  476. shutil.rmtree(tiles_path)
  477. @retry(3)
  478. def safe_open(path):
  479. # `urllib` in Python 2 supported both local paths as well as URLs. To
  480. # continue this in Python 3, we manually add `file://` prefix if `path` is
  481. # not a URL. This change is isolated to this function as we want the output
  482. # XML to still have the original input paths instead of absolute paths:
  483. has_scheme = bool(urlparse(path).scheme)
  484. normalized_path = ("file://%s" % os.path.abspath(path)) if not has_scheme else path
  485. return io.BytesIO(urllib.request.urlopen(normalized_path).read())
  486. ################################################################################
  487. def main():
  488. parser = optparse.OptionParser(usage="Usage: %prog [options] filename")
  489. parser.add_option(
  490. "-d",
  491. "--destination",
  492. dest="destination",
  493. help="Set the destination of the output.",
  494. )
  495. parser.add_option(
  496. "-s",
  497. "--tile_size",
  498. dest="tile_size",
  499. type="int",
  500. default=254,
  501. help="Size of the tiles. Default: 254",
  502. )
  503. parser.add_option(
  504. "-f",
  505. "--tile_format",
  506. dest="tile_format",
  507. default=DEFAULT_IMAGE_FORMAT,
  508. help="Image format of the tiles (jpg or png). Default: jpg",
  509. )
  510. parser.add_option(
  511. "-o",
  512. "--tile_overlap",
  513. dest="tile_overlap",
  514. type="int",
  515. default=1,
  516. help="Overlap of the tiles in pixels (0-10). Default: 1",
  517. )
  518. parser.add_option(
  519. "-q",
  520. "--image_quality",
  521. dest="image_quality",
  522. type="float",
  523. default=0.8,
  524. help="Quality of the image output (0-1). Default: 0.8",
  525. )
  526. parser.add_option(
  527. "-r",
  528. "--resize_filter",
  529. dest="resize_filter",
  530. default=DEFAULT_RESIZE_FILTER,
  531. help="Type of filter for resizing (bicubic, nearest, bilinear, lanczos (best). Default: lanczos",
  532. )
  533. (options, args) = parser.parse_args()
  534. if not args:
  535. parser.print_help()
  536. sys.exit(1)
  537. source = args[0]
  538. if not options.destination:
  539. if os.path.exists(source):
  540. options.destination = os.path.splitext(source)[0] + ".dzi"
  541. else:
  542. options.destination = os.path.splitext(os.path.basename(source))[0] + ".dzi"
  543. if options.resize_filter and options.resize_filter in RESIZE_FILTERS:
  544. options.resize_filter = RESIZE_FILTERS[options.resize_filter]
  545. creator = ImageCreator(
  546. tile_size=options.tile_size,
  547. tile_format=options.tile_format,
  548. image_quality=options.image_quality,
  549. resize_filter=options.resize_filter,
  550. )
  551. creator.create(source, options.destination)
  552. if __name__ == "__main__":
  553. main()

__init__.py at commit 57d62ec, under BSD-3-Clause · at the source

Overview

Authors: Tam Dao1,2, Thanh T. Nguyen1,3, Gia Minh Hoang1,4, Junhyeon Park1, Yunju Jo1,5, Thach Hoang Ngoc6, Diep Hong Pho6, Dien Tran Minh3,6, Emma Anh Ton7, Sunjae Lee8, Hyun Jin Kim2, Vu Chi Dung3,9, Jae Gwan Kim1, Dongryeol Ryu1
  1. Department of Biomedical Science and Engineering Gwangju Institute of Science and Technology (GIST) Gwangju Republic of Korea
  2. Department of Physiology Sungkyunkwan University School of Medicine Suwon Republic of Korea
  3. Center of Endocrinology, Metabolism, Genetic/Genomics and Molecular Therapy Vietnam National Children's Hospital Hanoi Vietnam
  4. Department of Physiology and Biomedical Engineering Mayo Clinic Scottsdale Arizona USA
  5. Department of Microbiology Wonkwang University School of Medicine Iksan Republic of Korea
  6. Department of Pathology Vietnam National Children's Hospital Ha Noi Vietnam
  7. Department of Computer Science Harvard John A. Paulson School of Engineering and Applied Sciences Boston Massachusetts USA
  8. Graduate School of Engineering Biology Korea Advanced Institute of Science & Technology (KAIST) Daejeon Republic of Korea
  9. Department of Pediatrics University of Medicine and Pharmacy—Vietnam National University Ha Noi Vietnam
Journal: Aging cell, volume 25, issue 6, article e70577
Dates: received 17 January 2026; accepted 1 June 2026; published online 15 June 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1111/acel.70577 · PMID 42295036 · PMCID PMC13267430 · OpenAlex W7164832679
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), histology / microscopy (modality), human (organism), other condition (population), cellular / molecular (subfield)
Methods: Statistics, Machine learning
Keywords: cellular senescence, deep learning, nuclear enlargement, skeletal muscle aging, transcriptomics
MeSH: Aging*, Cell Nucleus*, Deep Learning*, Muscle, Skeletal*, Myositis*, Female, Humans (* major topic)
Topic: Muscle Physiology and Disorders (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: Ministry of Health and Welfare (RS‐2024‐00507256); National Research Foundation of Korea (NRF) (RS‐2021‐NR060106)
Citations: not cited yet (Europe PMC); 41 references in the paper

Abstract

Aging reshapes the architecture of human skeletal muscle, yet objective tissue‐level markers that capture this process remain limited. We combined large‐scale histology with deep learning to identify reproducible features of muscle aging and to test their biological relevance. We analyzed 974 hematoxylin–eosin whole‐slide images from a population resource using a dual‐attention convolutional neural network and an independent Mask R‐CNN model to quantify nuclear size and density, verified by manual review. The classifier distinguished young from aged muscle with high accuracy (AUC 0.91; accuracy 86.2%), and attention maps consistently highlighted nuclear enlargement and spatial disorganization as salient features. Nuclear diameter increased with age (Spearman's ρ = 0.71, p < 0.0001) across automated and manual measurements. Transcriptomes matched to the same donors showed that samples with larger nuclei were enriched for pathways related to chromatin remodeling, proteostasis, cellular senescence, mitochondrial activity, and telomere regulation, whereas smaller nuclei aligned with anti‐inflammatory and DNA repair programs. External pediatric inflammatory myopathies exhibited nuclear enlargement comparable to aged muscle, suggesting inflammation‐related premature histologic aging. These findings identify nuclear enlargement as a robust, quantifiable feature that integrates structural and molecular signatures of muscle aging. The proposed deep learning–based nuclear morphometry provides a scalable framework for tissue‐level aging biomarkers and suggests a potential “muscle aging clock” applicable to both physiological aging and disease states.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above.

openzoom/deepzoom.py

License: BSD-3-Clause
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 57d62ec36769996458af1351e102e22058bf6369, 16 April 2026
Languages: Python (4), Shell (1)
Size: 14 files, 5 scripts
Software Heritage: archived
Found in: the text, “Hospital Histological Sample Collection”
Holds: README, license file, environment (setup.py)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: Pillow (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
7 files

tamdao216/nuclei

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 7c560ceb25530186d337e0f74c6140d8e4ad0433, 29 April 2026
Languages: Python (2)
Size: 3 files, 2 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), OpenCV (1 file), Pillow (1 file), PyTorch (1 file), scikit-image (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
2 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 7 scripts, each with its path and the digest of its content;
  • no match between paragraphs and code yet;
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data Availability Statement

The data that support the findings of this study are available in Genotype‐Tissue Expression (GTEx) at https://www.gtexportal.org/home/. These data were derived from the following resources available in the public domain: GTEx Histology Viewer, https://www.gtexportal.org/home/histologyPage. Representative raw images, processed data, and Supporting Information have been deposited in Figshare (link (https://figshare.com/articles/figure/high-resolution_images/31999848)). All clinical data and histological images are subject to strict institutional governance and privacy protection policies. Access to these materials may be granted only upon formal request and approval by the Vietnam National Children's Hospital (VNCH), in accordance with institutional and ethical regulations.

Code Availability Statement: All analysis code, pretrained models, and relevant implementation details are publicly available on GitHub (link Github (https://github.com/tamdao216/nuclei)). These materials are freely accessible for academic research and reproducibility purposes. Any additional information or clarification required to reproduce the analyses can be obtained from the corresponding author upon reasonable request.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 14 authors, 5 keywords, 7 MeSH terms, 2 funders, 34 references.

Cite

This paper

Dao, T., Nguyen, T. T., Hoang, G. M., Park, J., Jo, Y., Ngoc, T. H., Pho, D. H., Minh, D. T., Ton, E. A., Lee, S., Kim, H. J., Dung, V. C., Kim, J. G., & Ryu, D. (2026). Nuclear Enlargement as a Histological Hallmark of Skeletal Muscle Aging, Revealed by Deep Learning-Driven Analysis and Validated in Inflammatory Myopathies. Aging cell, 25(6), e70577. https://doi.org/10.1111/acel.70577

BibTeX

@article{dao2026nuclear,
author = {Dao, Tam and Nguyen, Thanh T. and Hoang, Gia Minh and Park, Junhyeon and Jo, Yunju and Ngoc, Thach Hoang and Pho, Diep Hong and Minh, Dien Tran and Ton, Emma Anh and Lee, Sunjae and Kim, Hyun Jin and Dung, Vu Chi and Kim, Jae Gwan and Ryu, Dongryeol},
title = {{Nuclear Enlargement as a Histological Hallmark of Skeletal Muscle Aging, Revealed by Deep Learning-Driven Analysis and Validated in Inflammatory Myopathies}},
journal = {Aging cell},
year = {2026},
month = jun,
volume = {25},
number = {6},
pages = {e70577},
publisher = {Wiley},
issn = {1474-9718},
doi = {10.1111/acel.70577},
url = {https://doi.org/10.1111/acel.70577},
pmid = {42295036},
pmcid = {PMC13267430}
}

RIS

TY - JOUR
AU - Dao, Tam
AU - Nguyen, Thanh T.
AU - Hoang, Gia Minh
AU - Park, Junhyeon
AU - Jo, Yunju
AU - Ngoc, Thach Hoang
AU - Pho, Diep Hong
AU - Minh, Dien Tran
AU - Ton, Emma Anh
AU - Lee, Sunjae
AU - Kim, Hyun Jin
AU - Dung, Vu Chi
AU - Kim, Jae Gwan
AU - Ryu, Dongryeol
TI - Nuclear Enlargement as a Histological Hallmark of Skeletal Muscle Aging, Revealed by Deep Learning-Driven Analysis and Validated in Inflammatory Myopathies
T2 - Aging cell
J2 - Aging Cell
PY - 2026
DA - 2026/06/01
VL - 25
IS - 6
SP - e70577
SN - 1474-9718
PB - Wiley
DO - 10.1111/acel.70577
UR - https://doi.org/10.1111/acel.70577
LA - en
ER -

CSL-JSON

{
"id": "10.1111/acel.70577",
"type": "article-journal",
"title": "Nuclear Enlargement as a Histological Hallmark of Skeletal Muscle Aging, Revealed by Deep Learning-Driven Analysis and Validated in Inflammatory Myopathies",
"container-title": "Aging cell",
"author": [
{
"family": "Dao",
"given": "Tam"
},
{
"family": "Nguyen",
"given": "Thanh T."
},
{
"family": "Hoang",
"given": "Gia Minh"
},
{
"family": "Park",
"given": "Junhyeon"
},
{
"family": "Jo",
"given": "Yunju"
},
{
"family": "Ngoc",
"given": "Thach Hoang"
},
{
"family": "Pho",
"given": "Diep Hong"
},
{
"family": "Minh",
"given": "Dien Tran"
},
{
"family": "Ton",
"given": "Emma Anh"
},
{
"family": "Lee",
"given": "Sunjae"
},
{
"family": "Kim",
"given": "Hyun Jin"
},
{
"family": "Dung",
"given": "Vu Chi"
},
{
"family": "Kim",
"given": "Jae Gwan"
},
{
"family": "Ryu",
"given": "Dongryeol"
}
],
"container-title-short": "Aging Cell",
"volume": "25",
"issue": "6",
"page": "e70577",
"DOI": "10.1111/acel.70577",
"PMID": "42295036",
"PMCID": "PMC13267430",
"ISSN": "1474-9718",
"publisher": "Wiley",
"URL": "https://doi.org/10.1111/acel.70577",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pone.0344600 [code]
Robust disease prognosis via diagnostic knowledge preservation: A sequential learning approach.
Journal: PloS one
In common: OpenCV, scikit-image, Pillow, 2 other tools, other condition, 2 references
[2] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy, other condition, cellular / molecular
[3] doi:10.7554/elife.107933 [code]
Modality-agnostic decoding of vision and language from fMRI.
Journal: eLife
In common: OpenCV, scikit-image, Pillow, 2 other tools, 1 reference
[4] doi:10.1016/j.patter.2026.101538 [code]
A multi-modal foundation model for brain disease diagnosis and medical imaging.
Journal: Patterns (New York, N.Y.)
In common: OpenCV, scikit-image, Pillow, 2 other tools, 1 reference
[5] doi:10.1038/s41598-026-61605-4 [code]
Learning precise segmentation of neurofibrillary tangles from rapid manual point annotations.
Journal: Scientific reports
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy, other condition
[6] doi:10.1038/s41598-026-57519-w [code]
Automated segmentation of neurons and spinal cord structures in immunofluorescence images using SpineDL.
Journal: Scientific reports
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy, other condition
[7] doi:10.1007/s00429-026-03166-w [code]
Autoencoders for unsupervised analysis of rat myeloarchitecture.
Journal: Brain structure & function
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy, cellular / molecular
[8] doi:10.1038/s41467-026-72709-w [code]
An epifluorescence microscope design for naturalistic behavior and cellular activity in freely moving Caenorhabditis elegans.
Journal: Nature communications
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy, cellular / molecular
[9] doi:10.1038/s41467-026-73045-9 [code]
Aberration-aware 3D localization microscopy via self-supervised neural-physics learning.
Journal: Nature communications
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy, cellular / molecular
[10] doi:10.1371/journal.pcbi.1013499 [code]
VesiclePy: A machine learning vesicle analysis toolbox for volume electron microscopy.
Journal: PLoS computational biology
In common: OpenCV, scikit-image, Pillow, 2 other tools, histology / microscopy

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.