nettracer3d 0.6.7__tar.gz → 0.6.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

Files changed (25) hide show
  1. {nettracer3d-0.6.7/src/nettracer3d.egg-info → nettracer3d-0.6.8}/PKG-INFO +4 -8
  2. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/README.md +3 -7
  3. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/pyproject.toml +1 -1
  4. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/nettracer_gui.py +202 -9
  5. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/proximity.py +1 -1
  6. {nettracer3d-0.6.7 → nettracer3d-0.6.8/src/nettracer3d.egg-info}/PKG-INFO +4 -8
  7. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/LICENSE +0 -0
  8. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/setup.cfg +0 -0
  9. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/__init__.py +0 -0
  10. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/community_extractor.py +0 -0
  11. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/modularity.py +0 -0
  12. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/morphology.py +0 -0
  13. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/nettracer.py +0 -0
  14. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/network_analysis.py +0 -0
  15. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/network_draw.py +0 -0
  16. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/node_draw.py +0 -0
  17. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/run.py +0 -0
  18. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/segmenter.py +0 -0
  19. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/simple_network.py +0 -0
  20. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d/smart_dilate.py +0 -0
  21. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  22. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  23. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  24. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d.egg-info/requires.txt +0 -0
  25. {nettracer3d-0.6.7 → nettracer3d-0.6.8}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <mclaughlinliam99@gmail.com>
6
6
  Project-URL: User_Tutorial, https://www.youtube.com/watch?v=cRatn5VTWDY
@@ -46,12 +46,8 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
46
46
 
47
47
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
48
48
 
49
- -- Version 0.6.7 updates --
49
+ -- Version 0.6.8 updates --
50
50
 
51
- 1. Updated all methods to use dilation to allow the user to select between perfect distance transform based dilation (which can be slower but allows for perfect searching - and is designed to account for scaling differences), or the current pseudo-3d kernel method.
51
+ 1. Added new fill-can and 3D-brush functionalities to the brush mode (press f in brush mode to toggle the fill can. Press d while in brush mode to use the 3D painting tools. Standard Mouse wheel scrolling in the 3D painter will change how many frames you paint on - ie. 5 lets you paint 2 above and 2 below).
52
52
 
53
- 1.5. The dt dilator accounts for scaling by stretching (upsampling) images to equivalent scales before dilating with the distance transform. It will not attempt to downsample. This admittedly will ask for greater memory and some more processing. To give the user the option to use the dt dilator without dealing with this, I added two new options to the resize method. You can now have it upsample your image until its equivalently scaled, or, if you don't need the fidelity, downsample your image until it's equivalently scaled. When the scaling is equivalent, the dt dilator will always just use the regular distance transform without attempting to resize the array.
54
-
55
- 2. Fixed radius finding method to also account for scaling correctly. Previous method scaled wrong. New method predictably accounts for differing scaling in xy vs z dims as well.
56
-
57
- 3. Bug fixes.
53
+ 1.5. Added single-use ctrl-z functionality to the fill can only because of how easily it can mess up. (Leaving the fill can mode will garbage collect the backup image though).
@@ -8,12 +8,8 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
8
8
 
9
9
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
10
10
 
11
- -- Version 0.6.7 updates --
11
+ -- Version 0.6.8 updates --
12
12
 
13
- 1. Updated all methods to use dilation to allow the user to select between perfect distance transform based dilation (which can be slower but allows for perfect searching - and is designed to account for scaling differences), or the current pseudo-3d kernel method.
13
+ 1. Added new fill-can and 3D-brush functionalities to the brush mode (press f in brush mode to toggle the fill can. Press d while in brush mode to use the 3D painting tools. Standard Mouse wheel scrolling in the 3D painter will change how many frames you paint on - ie. 5 lets you paint 2 above and 2 below).
14
14
 
15
- 1.5. The dt dilator accounts for scaling by stretching (upsampling) images to equivalent scales before dilating with the distance transform. It will not attempt to downsample. This admittedly will ask for greater memory and some more processing. To give the user the option to use the dt dilator without dealing with this, I added two new options to the resize method. You can now have it upsample your image until its equivalently scaled, or, if you don't need the fidelity, downsample your image until it's equivalently scaled. When the scaling is equivalent, the dt dilator will always just use the regular distance transform without attempting to resize the array.
16
-
17
- 2. Fixed radius finding method to also account for scaling correctly. Previous method scaled wrong. New method predictably accounts for differing scaling in xy vs z dims as well.
18
-
19
- 3. Bug fixes.
15
+ 1.5. Added single-use ctrl-z functionality to the fill can only because of how easily it can mess up. (Leaving the fill can mode will garbage collect the backup image though).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.6.7"
3
+ version = "0.6.8"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="mclaughlinliam99@gmail.com" },
6
6
  ]
@@ -18,7 +18,7 @@ from nettracer3d import smart_dilate as sdl
18
18
  from matplotlib.colors import LinearSegmentedColormap
19
19
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
20
20
  import pandas as pd
21
- from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QPainter, QPen)
21
+ from PyQt6.QtGui import (QFont, QCursor, QColor, QPixmap, QFontMetrics, QPainter, QPen)
22
22
  import tifffile
23
23
  import copy
24
24
  import multiprocessing as mp
@@ -118,6 +118,9 @@ class ImageViewerWindow(QMainWindow):
118
118
 
119
119
  #For ML segmenting mode
120
120
  self.brush_mode = False
121
+ self.can = False
122
+ self.threed = False
123
+ self.threedthresh = 5
121
124
  self.painting = False
122
125
  self.foreground = True
123
126
  self.machine_window = None
@@ -1712,6 +1715,9 @@ class ImageViewerWindow(QMainWindow):
1712
1715
  self.pen_button.setChecked(False)
1713
1716
  self.pan_mode = False
1714
1717
  self.brush_mode = False
1718
+ self.can = False
1719
+ self.threed = False
1720
+ self.last_change = None
1715
1721
  if self.machine_window is not None:
1716
1722
  self.machine_window.silence_button()
1717
1723
  self.canvas.setCursor(Qt.CursorShape.CrossCursor)
@@ -1729,6 +1735,9 @@ class ImageViewerWindow(QMainWindow):
1729
1735
  self.zoom_button.setChecked(False)
1730
1736
  self.pen_button.setChecked(False)
1731
1737
  self.zoom_mode = False
1738
+ self.can = False
1739
+ self.threed = False
1740
+ self.last_change = None
1732
1741
  self.brush_mode = False
1733
1742
  if self.machine_window is not None:
1734
1743
  self.machine_window.silence_button()
@@ -1749,14 +1758,69 @@ class ImageViewerWindow(QMainWindow):
1749
1758
  self.zoom_mode = False
1750
1759
  self.update_brush_cursor()
1751
1760
  else:
1761
+ self.last_change = None
1762
+ self.can = False
1763
+ self.threed = False
1752
1764
  self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
1753
1765
 
1766
+ def toggle_can(self):
1767
+
1768
+ if not self.can:
1769
+ self.can = True
1770
+ self.update_brush_cursor()
1771
+ else:
1772
+ self.can = False
1773
+ self.last_change = None
1774
+ self.update_brush_cursor()
1775
+
1776
+ def toggle_threed(self):
1777
+
1778
+ if not self.threed:
1779
+ self.threed = True
1780
+ self.threedthresh = 5
1781
+ self.update_brush_cursor()
1782
+ else:
1783
+ self.threed = False
1784
+ self.update_brush_cursor()
1785
+
1754
1786
 
1755
1787
  def on_mpl_scroll(self, event):
1756
1788
  """Handle matplotlib canvas scroll events"""
1757
1789
  #Wheel events
1758
1790
  if self.brush_mode and event.inaxes == self.ax:
1791
+
1792
+ # Get modifiers
1793
+ modifiers = event.guiEvent.modifiers()
1794
+ ctrl_pressed = bool(modifiers & Qt.ControlModifier)
1795
+ shift_pressed = bool(modifiers & Qt.ShiftModifier)
1796
+ alt_pressed = bool(modifiers & Qt.AltModifier)
1797
+
1798
+ # Check if threed is enabled and ONLY if no specific modifiers are pressed
1799
+ if self.threed and not ctrl_pressed and not shift_pressed and not alt_pressed:
1800
+ import math
1801
+ step = 1 if event.button == 'up' else -1
1802
+ self.threedthresh += step
1803
+
1804
+ # Round to appropriate odd integer based on scroll direction
1805
+ if event.button == 'up':
1806
+ # Round up to nearest odd
1807
+ self.threedthresh = math.ceil(self.threedthresh)
1808
+ if self.threedthresh % 2 == 0:
1809
+ self.threedthresh += 1
1810
+ else: # event.button == 'down'
1811
+ # Round down to nearest odd, but not below 1
1812
+ self.threedthresh = math.floor(self.threedthresh)
1813
+ if self.threedthresh % 2 == 0:
1814
+ self.threedthresh -= 1
1815
+ # Ensure not below minimum value of 1
1816
+ self.threedthresh = max(1, self.threedthresh)
1817
+
1818
+ # Update the brush cursor to show the new threshold
1819
+ self.update_brush_cursor()
1820
+ return
1821
+
1759
1822
  # Check if Ctrl is pressed
1823
+
1760
1824
  if event.guiEvent.modifiers() & Qt.ShiftModifier:
1761
1825
  pass
1762
1826
 
@@ -1793,6 +1857,15 @@ class ImageViewerWindow(QMainWindow):
1793
1857
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
1794
1858
 
1795
1859
  def keyPressEvent(self, event):
1860
+
1861
+ if event.key() == Qt.Key_Z and event.modifiers() & Qt.ControlModifier:
1862
+ try:
1863
+ self.load_channel(self.last_change[1], self.last_change[0], True)
1864
+ except:
1865
+ pass
1866
+
1867
+ return # Return to prevent triggering the regular Z key action below
1868
+
1796
1869
  if event.key() == Qt.Key_Z:
1797
1870
  self.zoom_button.click()
1798
1871
  if self.machine_window is not None:
@@ -1800,28 +1873,65 @@ class ImageViewerWindow(QMainWindow):
1800
1873
  self.machine_window.switch_foreground()
1801
1874
  if event.key() == Qt.Key_X:
1802
1875
  self.high_button.click()
1876
+ if self.brush_mode and self.machine_window is None:
1877
+ if event.key() == Qt.Key_F:
1878
+ self.toggle_can()
1879
+ elif event.key() == Qt.Key_D:
1880
+ self.toggle_threed()
1803
1881
 
1804
1882
 
1805
1883
  def update_brush_cursor(self):
1806
1884
  """Update the cursor to show brush size"""
1807
1885
  if not self.brush_mode:
1808
1886
  return
1809
-
1810
- # Create a pixmap for the cursor
1811
- size = self.brush_size * 2 + 2 # Add padding for border
1812
- pixmap = QPixmap(size, size)
1887
+
1888
+ # Get font metrics first to determine text size
1889
+ font = QFont()
1890
+ font.setPointSize(14)
1891
+ font_metrics = QFontMetrics(font)
1892
+ thresh_text = str(self.threedthresh)
1893
+ text_rect = font_metrics.boundingRect(thresh_text)
1894
+
1895
+ # Create a pixmap for the cursor - ensure it's large enough for text
1896
+ brush_size = self.brush_size * 2 + 2 # Add padding for border
1897
+ extra_width = max(0, text_rect.width() + 4 - brush_size) # Extra width for text if needed
1898
+ extra_height = max(0, text_rect.height() + 4 - brush_size) # Extra height for text if needed
1899
+
1900
+ # Make sure pixmap is large enough for both brush and text
1901
+ total_width = brush_size + extra_width
1902
+ total_height = brush_size + extra_height
1903
+ pixmap = QPixmap(total_width, total_height)
1813
1904
  pixmap.fill(Qt.transparent)
1814
1905
 
1815
1906
  # Create painter for the pixmap
1816
1907
  painter = QPainter(pixmap)
1817
1908
  painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1818
1909
 
1910
+ # Calculate center offset for brush ellipse to accommodate text
1911
+ x_offset = extra_width // 2
1912
+ y_offset = extra_height // 2
1913
+
1819
1914
  # Draw circle
1820
- pen = QPen(Qt.white)
1915
+ if not self.threed:
1916
+ pen = QPen(Qt.white)
1917
+ else:
1918
+ pen = QPen(Qt.red)
1821
1919
  pen.setWidth(1)
1822
1920
  painter.setPen(pen)
1823
1921
  painter.setBrush(Qt.transparent)
1824
- painter.drawEllipse(1, 1, size-2, size-2)
1922
+ if not self.can:
1923
+ painter.drawEllipse(1 + x_offset, 1 + y_offset, brush_size-2, brush_size-2)
1924
+
1925
+ # Draw threshold number when threed is True and can is False
1926
+ if self.threed:
1927
+ # Set text properties
1928
+ painter.setFont(font)
1929
+ painter.setPen(QPen(Qt.white)) # White text for visibility
1930
+
1931
+ # Draw the text
1932
+ painter.drawText(2, font_metrics.ascent() + 2, thresh_text)
1933
+ else:
1934
+ painter.drawRect(1 + x_offset, 1 + y_offset, 8, 8)
1825
1935
 
1826
1936
  # Create cursor from pixmap
1827
1937
  cursor = QCursor(pixmap)
@@ -1929,13 +2039,20 @@ class ImageViewerWindow(QMainWindow):
1929
2039
 
1930
2040
  if event.button == 1 or event.button == 3:
1931
2041
 
2042
+ x, y = int(event.xdata), int(event.ydata)
2043
+
2044
+
2045
+ if event.button == 1 and self.can:
2046
+ self.handle_can(x, y)
2047
+ return
2048
+
2049
+
1932
2050
  if event.button == 3:
1933
2051
  self.erase = True
1934
2052
  else:
1935
2053
  self.erase = False
1936
2054
 
1937
2055
  self.painting = True
1938
- x, y = int(event.xdata), int(event.ydata)
1939
2056
  self.last_paint_pos = (x, y)
1940
2057
 
1941
2058
  if self.pen_button.isChecked():
@@ -1995,7 +2112,77 @@ class ImageViewerWindow(QMainWindow):
1995
2112
  for x in range(max(0, center_x - radius), min(width, center_x + radius + 1)):
1996
2113
  # Check if point is within circular brush area
1997
2114
  if (x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2:
1998
- self.channel_data[channel][self.current_slice][y, x] = val
2115
+
2116
+ if self.threed and self.threedthresh > 1:
2117
+ amount = (self.threedthresh - 1) / 2
2118
+ low = max(0, self.current_slice - amount)
2119
+ high = min(self.channel_data[channel].shape[0] - 1, self.current_slice + amount)
2120
+
2121
+ for i in range(int(low), int(high + 1)):
2122
+ self.channel_data[channel][i][y, x] = val
2123
+ else:
2124
+ self.channel_data[channel][self.current_slice][y, x] = val
2125
+
2126
+ def handle_can(self, x, y):
2127
+
2128
+
2129
+ if self.threed:
2130
+ ref = copy.deepcopy(self.channel_data[self.active_channel])
2131
+ the_slice = self.channel_data[self.active_channel]
2132
+
2133
+ # First invert the boolean array
2134
+ inv = n3d.invert_boolean(the_slice)
2135
+
2136
+ # Label the connected components in the inverted array
2137
+ labeled_array, num_features = n3d.label_objects(inv)
2138
+
2139
+ # Get the target label at the clicked point
2140
+ target_label = labeled_array[self.current_slice][y][x]
2141
+
2142
+ # Only fill if we clicked on a valid region (target_label > 0)
2143
+ if target_label > 0:
2144
+ # Create a mask of the connected component we clicked on
2145
+ fill_mask = (labeled_array == target_label) * 255
2146
+
2147
+ self.last_change = [ref, self.active_channel]
2148
+
2149
+ # Add this mask to the original slice
2150
+ the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2151
+
2152
+ # Update the channel data
2153
+ self.load_channel(self.active_channel, the_slice, True)
2154
+ else:
2155
+
2156
+ ref = copy.deepcopy(self.channel_data[self.active_channel])
2157
+
2158
+ the_slice = self.channel_data[self.active_channel][self.current_slice]
2159
+
2160
+ # First invert the boolean array
2161
+ inv = n3d.invert_boolean(the_slice)
2162
+
2163
+ # Label the connected components in the inverted array
2164
+ labeled_array, num_features = n3d.label_objects(inv)
2165
+
2166
+ # Get the target label at the clicked point
2167
+ target_label = labeled_array[y][x]
2168
+
2169
+ # Only fill if we clicked on a valid region (target_label > 0)
2170
+ if target_label > 0:
2171
+ # Create a mask of the connected component we clicked on
2172
+ fill_mask = (labeled_array == target_label) * 255
2173
+
2174
+ self.last_change = [ref, self.active_channel]
2175
+
2176
+ # Add this mask to the original slice
2177
+ the_slice = the_slice | fill_mask # Use logical OR to add the filled region
2178
+
2179
+ # Update the channel data
2180
+ self.channel_data[self.active_channel][self.current_slice] = the_slice
2181
+ self.load_channel(self.active_channel, self.channel_data[self.active_channel], True)
2182
+
2183
+
2184
+
2185
+
1999
2186
 
2000
2187
  def on_mouse_move(self, event):
2001
2188
  """Handle mouse movement events."""
@@ -2082,6 +2269,7 @@ class ImageViewerWindow(QMainWindow):
2082
2269
  points = self.get_line_points(last_x, last_y, x, y)
2083
2270
 
2084
2271
  # Paint at each point along the line
2272
+
2085
2273
  for px, py in points:
2086
2274
  if 0 <= px < width and 0 <= py < height:
2087
2275
  self.paint_at_position(px, py, self.erase, channel)
@@ -6776,6 +6964,9 @@ class MachineWindow(QMainWindow):
6776
6964
 
6777
6965
  if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
6778
6966
  self.parent().pen_button.click()
6967
+ self.parent().threed = False
6968
+ self.parent().can = False
6969
+ self.parent().last_change = None
6779
6970
 
6780
6971
  self.parent().pen_button.setEnabled(False)
6781
6972
 
@@ -7002,6 +7193,8 @@ class MachineWindow(QMainWindow):
7002
7193
  self.parent().zoom_mode = False
7003
7194
  self.parent().update_brush_cursor()
7004
7195
  else:
7196
+ self.threed = False
7197
+ self.can = False
7005
7198
  self.parent().zoom_button.click()
7006
7199
 
7007
7200
  def silence_button(self):
@@ -82,7 +82,7 @@ def _get_node_node_dict(label_array, label, dilate_xy, dilate_z, fastdil = False
82
82
  def process_label(args):
83
83
  """Modified to use pre-computed bounding boxes instead of argwhere"""
84
84
  nodes, label, dilate_xy, dilate_z, array_shape, bounding_boxes = args
85
- #print(f"Processing node {label}")
85
+ print(f"Processing node {label}")
86
86
 
87
87
  # Get the pre-computed bounding box for this label
88
88
  slice_obj = bounding_boxes[label-1] # -1 because label numbers start at 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <mclaughlinliam99@gmail.com>
6
6
  Project-URL: User_Tutorial, https://www.youtube.com/watch?v=cRatn5VTWDY
@@ -46,12 +46,8 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
46
46
 
47
47
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
48
48
 
49
- -- Version 0.6.7 updates --
49
+ -- Version 0.6.8 updates --
50
50
 
51
- 1. Updated all methods to use dilation to allow the user to select between perfect distance transform based dilation (which can be slower but allows for perfect searching - and is designed to account for scaling differences), or the current pseudo-3d kernel method.
51
+ 1. Added new fill-can and 3D-brush functionalities to the brush mode (press f in brush mode to toggle the fill can. Press d while in brush mode to use the 3D painting tools. Standard Mouse wheel scrolling in the 3D painter will change how many frames you paint on - ie. 5 lets you paint 2 above and 2 below).
52
52
 
53
- 1.5. The dt dilator accounts for scaling by stretching (upsampling) images to equivalent scales before dilating with the distance transform. It will not attempt to downsample. This admittedly will ask for greater memory and some more processing. To give the user the option to use the dt dilator without dealing with this, I added two new options to the resize method. You can now have it upsample your image until its equivalently scaled, or, if you don't need the fidelity, downsample your image until it's equivalently scaled. When the scaling is equivalent, the dt dilator will always just use the regular distance transform without attempting to resize the array.
54
-
55
- 2. Fixed radius finding method to also account for scaling correctly. Previous method scaled wrong. New method predictably accounts for differing scaling in xy vs z dims as well.
56
-
57
- 3. Bug fixes.
53
+ 1.5. Added single-use ctrl-z functionality to the fill can only because of how easily it can mess up. (Leaving the fill can mode will garbage collect the backup image though).
File without changes
File without changes