Skip to content

chugit/RootAnalysis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 

Repository files navigation

本项目将介绍植物根系图像中形态参数(总根长、根系平均直径、总表面积、总体积)的批量提取方式,并提供实现批量处理的Python代码。最后,针对图像处理的关键参数,构建具备可交互界面的程序,并将其打包成可执行文件。

本项目最终生成的可执行文件为root_analysis_tool.exe网盘链接

This project introduces a method for batch extraction of morphological parameters (total root length, average root diameter, total surface area, total volume) from plant root images, and provides Python code for batch processing. Finally, an interactive interface program is built for key image processing parameters and packaged into an executable file.

The final executable generated by this project is root_analysis_tool.exe.


根系扫描 Root System Scanning

利用扫描仪、高拍仪、相机等,在固定仪器参数下获取根系图像。同时,在相同条件下获取已知长度的线段/形状的图像,用作校准。

图像中的根系颜色应与背景色有很大的差异,一般分为白底黑根(背景颜色比根系白/亮)或黑底白根(背景颜色比根系黑/深)。

后续分析将在灰度图的基础上进行。

Use scanners, high-speed cameras, or standard cameras to acquire root images under fixed instrument settings. At the same time, capture images of line segments or shapes of known length under the same conditions for calibration.

The root color in the image should have high contrast with the background. Common setups include black roots on a white background (background lighter than the roots) or white roots on a black background (background darker than the roots).

Subsequent analysis will be performed on grayscale images.


根系分析原理 Principles of Root Analysis

本项目使用的根系图像处理流程与 WinRHIZO 等主流根系分析软件一致,关键步骤包括二值化、骨架化、长度测量、投影面积计算以及通过数学模型计算根系的平均直径、总表面积和总体积。经测试与检验,本项目分析所得结果与 WinRHIZO 一致。

The root image processing workflow used in this project is consistent with mainstream root analysis software such as WinRHIZO. Key steps include binarization, skeletonization, length measurement, projected area calculation, and mathematical modeling to compute average diameter, total surface area, and total volume. After testing and validation, the analysis results obtained from this project are consistent with those from WinRHIZO.

图像二值化 Image Binarization

将根系灰度图像通过阈值分割转换为黑白图像,以便区分根系和背景。可以选择固定阈值或Otsu自动阈值法,具体选择基于背景类型和图像光照条件。

二值化后,图像中根系与背景清晰分离,为后续的形态分析提供基础。

Convert the grayscale root image into a binary (black and white) image using thresholding to distinguish roots from background. Either a fixed threshold or Otsu's automatic thresholding method can be used, depending on the background type and image lighting conditions.

After binarization, the roots and background are clearly separated, laying the foundation for subsequent morphological analysis.

骨架化处理 Skeletonization

通过形态学骨架化将根系区域转换为单像素宽度的线条结构。

骨架化在保持根系主干和分支信息的同时去除了多余的粗度信息,得到的骨架线条能更准确地表征根系的生长路径,为长度测量提供便捷的图像形式。

Use morphological skeletonization to convert the root region into a single-pixel-wide line structure.

Skeletonization preserves the main root structure and branch information while removing redundant thickness details. The resulting skeleton lines more accurately represent the root growth path, providing a convenient image form for length measurement.

根系长度计算 Root Length Calculation

统计骨架化图像的像素总数,根据像素与实际距离的转换比例(如每厘米的像素数),计算根系总长度。

像素与实际距离的转换比例(本项目亦称之为校准系数),可以利用ImageJ、Photoshop等图像处理软件,通过测定已知长度的线段像素点数确定。

Count the total number of pixels in the skeletonized image. Using a conversion factor between pixels and actual distance (e.g., pixels per centimeter), compute the total root length.

The conversion factor (referred to as the calibration coefficient in this project) can be determined using image processing software such as ImageJ or Photoshop by measuring the pixel count of a line segment of known length.

根系投影面积 Root Projected Area

统计二值化图像中所有根系像素点总数,根据校准系数,计算根系投影面积。

Count all root pixels in the binarized image and use the calibration coefficient to calculate the projected root area.

平均根径 Average Root Diameter

将根系投影面积与总长度相除,得到平均根径。

Divide the projected root area by the total length to obtain the average root diameter.

根系表面积和总体积 Root Surface Area and Total Volume

假设根系由多个等直径的小圆柱体组成,表面积和体积可通过根系长度和平均直径计算得来:

Assuming the root system is composed of small cylinders of equal diameter, the surface area and volume can be calculated from the total length and average diameter:

$$ \text{总表面积} = \pi \times \text{平均直径} \times \text{总长度} $$

$$ \text{Total Surface Area} = \pi \times \text{Average Diameter} \times \text{Total Length} $$

$$ \text{总体积} = \pi \times \left(\frac{\text{平均直径}}{2}\right)^2 \times \text{总长度} $$

$$ \text{Total Volume} = \pi \times \left(\frac{\text{Average Diameter}}{2}\right)^2 \times \text{Total Length} $$


利用Python实现根系图像分析的批量处理 Batch Processing of Root Image Analysis Using Python

Python及其库/模块的安装方式见chugit/Crawler_Journal_Abbreviation安装Python安装Selenium部分,其中,将“Selenium”替换为下方代码运行所缺失的库/模块,即为Python库/模块的安装方式。

Instructions for installing Python and its libraries/modules can be found in the Install Python and Install Selenium sections of chugit/Crawler_Journal_Abbreviation. To install missing libraries/modules required to run the code below, simply replace "Selenium" with the name of the missing library/module.

白底黑根 White Background with Black Roots

import cv2
import numpy as np
from skimage import morphology
import os
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
from natsort import natsorted

# 设置工作目录 Set working directory
work_dir = 'D:\\R\\RootAnalysis' # 在该目录下自行创建Input文件夹 Create an Input folder manually in this directory
input_dir = os.path.join(work_dir, 'Input') # 待处理的原始图像均放于Input文件夹内 All original images to be processed are placed in the Input folder
output_dir = os.path.join(work_dir, 'Output') # 定义二值化图像、根骨架图像、结果数据的导出位置 Define export location for binary images, root skeleton images, and results
output_csv = os.path.join(output_dir, 'root_analysis_results.csv') # 定义结果数据的导出文件名 Define the filename for the result data export
os.makedirs(output_dir, exist_ok=True)

# 定义像素到厘米的转换比例 Define pixel to centimeter conversion ratio
pixels_per_cm = 130  # 130像素对应1cm 130 pixels correspond to 1 cm

# 定义图像裁剪和阈值方法的相关参数 Define parameters for image cropping and thresholding methods
crop_top_percentage = 0.001  # 裁除图像顶部的0.1%。0为不剪裁 Crop 0.1% from the top of the image. 0 means no cropping
crop_bottom_percentage = 0  # 裁除图像底部的x%。0为不剪裁 Crop x% from the bottom of the image. 0 means no cropping
crop_left_percentage = 0  # 裁除图像左部的x%。0为不剪裁 Crop x% from the left side of the image. 0 means no cropping
crop_right_percentage = 0  # 裁除图像右部的x%。0为不剪裁 Crop x% from the right side of the image. 0 means no cropping
threshold_method = "fixed"  # 阈值方法选择:"fixed" 或 "otsu"(固定阈值法或Otsu自动法) Threshold method selection: "fixed" or "otsu"
fixed_threshold_value = 70  # 指定固定阈值(仅在使用固定阈值法时有效)。白底黑根,阈值越大,根系越厚 Specify fixed threshold (only valid when using fixed threshold method). For white background with black roots, higher threshold results in thicker roots

########## 下方代码全选运行,无需调整 Select all code below and run; no adjustments needed #############################

# 定义图像处理函数 Define image processing function
def process_image(image_file):
    # 构建图像路径并加载图像 Build image path and load image
    image_path = os.path.join(input_dir, image_file)
    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    
    # 检查图像是否加载成功 Check if image loaded successfully
    if image is None:
        print(f"无法加载图像 {image_file},跳过") # print(f"Cannot load image {image_file}, skipping")        
        return None
    
    # 裁剪图像,以移除干扰物所在区域,避免干扰分析 Crop the image to remove areas with interference
    height, width = image.shape
    crop_top = int(height * crop_top_percentage)
    crop_bottom = int(height * crop_bottom_percentage)
    crop_left = int(width * crop_left_percentage)
    crop_right = int(width * crop_right_percentage)
    image_cropped = image[crop_top:height - crop_bottom, crop_left:width - crop_right]
    
    # 根据选择的阈值方法生成二值化图像 Generate binary image based on selected threshold method
    if threshold_method == "fixed":
        _, binary = cv2.threshold(image_cropped, fixed_threshold_value, 255, cv2.THRESH_BINARY_INV)
        threshold_value = fixed_threshold_value
    elif threshold_method == "otsu":
        threshold_value, binary = cv2.threshold(image_cropped, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    else:
        print(f"未定义的阈值方法: {threshold_method}") # print(f"Undefined threshold method: {threshold_method}")
        return None
    
    # (可选)通过腐蚀进一步减小根的边界厚度 (Optional) Further reduce root boundary thickness through erosion
    # kernel = np.ones((2, 2), np.uint8)
    # binary = cv2.erode(binary, kernel, iterations=1)
    
    # 提取根骨架 Extract root skeleton
    skeleton = morphology.skeletonize(binary // 255).astype(np.uint8) * 255

    # 导出处理后的图像 Export processed images
    base_name = os.path.splitext(image_file)[0]
    cv2.imwrite(os.path.join(output_dir, f'{base_name}_binary.jpg'), binary) # 二值化图像 binary image
    cv2.imwrite(os.path.join(output_dir, f'{base_name}_skeleton.jpg'), skeleton) # 根骨架图像 root skeleton image

    # 计算总根长 Calculate total root length
    pixel_length = np.sum(skeleton) / 255
    total_root_length_cm = pixel_length / pixels_per_cm

    # 计算根系投影面积 Calculate projected root area
    projected_area = np.sum(binary // 255) / (pixels_per_cm**2)

    # 计算根系平均直径 Calculate average root diameter
    average_root_diameter_mm = (projected_area / total_root_length_cm) * 10

    # 计算根系总表面积和总体积 Calculate total root surface area and total volume
    root_surface_area = np.pi * (average_root_diameter_mm / 10) * total_root_length_cm
    root_volume = np.pi * ((average_root_diameter_mm / 2) / 10)**2 * total_root_length_cm

    # 返回结果 Return results
    return {
        "Image": image_file,
        "Threshold Value": threshold_value,
        "Projected Area (cm2)": projected_area,
        "Total Root Length (cm)": total_root_length_cm,
        "Average Root Diameter (mm)": average_root_diameter_mm,
        "Root Surface Area (cm2)": root_surface_area,
        "Root Volume (cm3)": root_volume
    }

# 初始化结果列表 Initialize results list
results = []

# 多线程并行运算 Multi-threaded parallel processing
start_time = time.time()
with ThreadPoolExecutor(max_workers=6) as executor: # 此处调用的CPU线程数限定为6 CPU threads limited to 6
    futures = {executor.submit(process_image, image_file): image_file for image_file in os.listdir(input_dir)}
    for future in as_completed(futures):
        result = future.result()
        if result is not None:
            results.append(result)
end_time = time.time()
print(f"多线程并行运算时间: {end_time - start_time:.2f} s") # print(f"Multi-threaded parallel processing time: {end_time - start_time:.2f} s")

# 结果按文件名的自然顺序排序 Sort results in natural order of filenames
results = natsorted(results, key=lambda x: x["Image"])

# 导出结果 Export results
df = pd.DataFrame(results)
df.to_csv(output_csv, index=False)

# 打开 Output 文件夹 Open Output folder
os.startfile(output_dir)

黑底白根 Black Background with White Roots

import cv2
import numpy as np
from skimage import morphology
import os
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
from natsort import natsorted

# 设置工作目录 Set working directory
work_dir = 'D:\\R\\RootAnalysis' # 在该目录下自行创建Input文件夹 Create an Input folder manually in this directory
input_dir = os.path.join(work_dir, 'Input') # 待处理的原始图像均放于Input文件夹内 All original images to be processed are placed in the Input folder
output_dir = os.path.join(work_dir, 'Output') # 定义二值化图像、根骨架图像、结果数据的导出位置 Define export location for binary images, root skeleton images, and results
output_csv = os.path.join(output_dir, 'root_analysis_results.csv') # 定义结果数据的导出文件名 Define the filename for the result data export
os.makedirs(output_dir, exist_ok=True)

# 定义像素到厘米的转换比例 Define pixel to centimeter conversion ratio
pixels_per_cm = 130  # 130像素对应1cm 130 pixels correspond to 1 cm

# 定义图像裁剪和阈值方法的相关参数 Define parameters for image cropping and thresholding methods
crop_top_percentage = 0.001  # 裁除图像顶部的0.1%。0为不剪裁 Crop 0.1% from the top of the image. 0 means no cropping
crop_bottom_percentage = 0  # 裁除图像底部的x%。0为不剪裁 Crop x% from the bottom of the image. 0 means no cropping
crop_left_percentage = 0  # 裁除图像左部的x%。0为不剪裁 Crop x% from the left side of the image. 0 means no cropping
crop_right_percentage = 0  # 裁除图像右部的x%。0为不剪裁 Crop x% from the right side of the image. 0 means no cropping
threshold_method = "fixed"  # 阈值方法选择:"fixed" 或 "otsu"(固定阈值法或Otsu自动法) Threshold method selection: "fixed" or "otsu"
fixed_threshold_value = 15 # 指定固定阈值(仅在使用固定阈值法时有效)。黑底白根,阈值越小,根系越厚 Specify fixed threshold (only valid when using fixed threshold method). For black background with white roots, lower threshold results in thicker roots

########## 下方代码全选运行,无需调整 Select all code below and run; no adjustments needed #############################

def process_image(image_file):
    image_path = os.path.join(input_dir, image_file)
    image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    
    if image is None:
        print(f"无法加载图像 {image_file},跳过") # print(f"Cannot load image {image_file}, skipping")
        return None

    height, width = image.shape
    crop_top = int(height * crop_top_percentage)
    crop_bottom = int(height * crop_bottom_percentage)
    crop_left = int(width * crop_left_percentage)
    crop_right = int(width * crop_right_percentage)
    image_cropped = image[crop_top:height - crop_bottom, crop_left:width - crop_right]
    
    if threshold_method == "fixed":
        _, binary = cv2.threshold(image_cropped, fixed_threshold_value, 255, cv2.THRESH_BINARY)
        threshold_value = fixed_threshold_value
    elif threshold_method == "otsu":
        threshold_value, binary = cv2.threshold(image_cropped, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    else:
        print(f"未定义的阈值方法: {threshold_method}") # print(f"Undefined threshold method: {threshold_method}")
        return None
    
    skeleton = morphology.skeletonize(binary // 255).astype(np.uint8) * 255

    base_name = os.path.splitext(image_file)[0]
    cv2.imwrite(os.path.join(output_dir, f'{base_name}_binary.jpg'), binary)
    cv2.imwrite(os.path.join(output_dir, f'{base_name}_skeleton.jpg'), skeleton)

    pixel_length = np.sum(skeleton) / 255
    total_root_length_cm = pixel_length / pixels_per_cm

    projected_area = np.sum(binary // 255) / (pixels_per_cm**2)

    average_root_diameter_mm = (projected_area / total_root_length_cm) * 10

    root_surface_area = np.pi * (average_root_diameter_mm / 10) * total_root_length_cm
    root_volume = np.pi * ((average_root_diameter_mm / 2) / 10)**2 * total_root_length_cm

    return {
        "Image": image_file,
        "Threshold Value": threshold_value,
        "Projected Area (cm2)": projected_area,
        "Total Root Length (cm)": total_root_length_cm,
        "Average Root Diameter (mm)": average_root_diameter_mm,
        "Root Surface Area (cm2)": root_surface_area,
        "Root Volume (cm3)": root_volume
    }

results = []

start_time = time.time()
with ThreadPoolExecutor(max_workers=6) as executor: # 此处调用的CPU线程数限定为6 CPU threads limited to 6
    futures = {executor.submit(process_image, image_file): image_file for image_file in os.listdir(input_dir)}
    for future in as_completed(futures):
        result = future.result()
        if result is not None:
            results.append(result)
end_time = time.time()
print(f"多线程并行运算时间: {end_time - start_time:.2f} s") # print(f"Multi-threaded parallel processing time: {end_time - start_time:.2f} s")

results = natsorted(results, key=lambda x: x["Image"])

df = pd.DataFrame(results)
df.to_csv(output_csv, index=False)

os.startfile(output_dir)

基于Python的可交互界面 Python-Based Interactive Interface

整合上述两部分代码,生成可交互界面。

Integrate the two parts of code above to create an interactive interface.

import tkinter as tk
from tkinter import filedialog, ttk
import cv2
import numpy as np
from skimage import morphology
import os
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
from natsort import natsorted
import threading
import json

PARAMS_FILE = "params.json"  # 程序参数文件名称。用于储存程序的终止参数,或在程序启动时自动读入以加载为初始参数,生成于工作目录或程序所在文件夹内。 Program parameter file name. Used to store termination parameters of the program, or automatically read at startup to load initial parameters. Generated in the working directory or the folder where the program resides.

def load_params():
    # 检查参数文件是否存在 Check if parameter file exists
    if os.path.exists(PARAMS_FILE):
        with open(PARAMS_FILE, 'r') as f:
            return json.load(f)
    return None

def save_params():
    # 保存参数文件 Save parameter file
    params = {
        "input_dir": input_dir_entry.get(),
        "output_dir": output_dir_entry.get(),
        "pixels_per_cm": pixels_per_cm_entry.get(),
        "crop_top": crop_top_entry.get(),
        "crop_bottom": crop_bottom_entry.get(),
        "crop_left": crop_left_entry.get(),
        "crop_right": crop_right_entry.get(),
        "threshold_method": threshold_method_var.get(),
        "fixed_threshold_value": fixed_threshold_entry.get(),
        "root_background": root_background_var.get(),
        "cpu_threads": cpu_threads_entry.get(),
    }
    with open(PARAMS_FILE, 'w') as f:
        json.dump(params, f)

# 加载初始参数(如果该文件存在的话) Load initial parameters (if the file exists)
saved_params = load_params()

def create_tooltip(widget, text):
    # 定义程序界面提示条格式 Define tooltip format for the program interface
    tooltip = tk.Toplevel(widget, bg="lightyellow", padx=5, pady=5)
    tooltip.withdraw()
    tooltip.overrideredirect(True)
    label = tk.Label(tooltip, text=text, bg="lightyellow")
    label.pack()
    
    def show_tooltip(event):
        tooltip.geometry(f"+{event.x_root + 20}+{event.y_root}")
        tooltip.deiconify()

    def hide_tooltip(event):
        tooltip.withdraw()

    widget.bind("<Enter>", show_tooltip)
    widget.bind("<Leave>", hide_tooltip)

def save_with_increment(filepath, save_function, *args, **kwargs):
    # 分析结果的导出名称。如果存在同名文件,则自动添加编号以区分 Export naming for analysis results. If a file with the same name exists, automatically add a number to distinguish
    base_name, ext = os.path.splitext(filepath)
    counter = 1
    new_filepath = filepath
    while os.path.exists(new_filepath):
        new_filepath = f"{base_name}_{counter}{ext}"
        counter += 1
    save_function(new_filepath, *args, **kwargs)

def run_analysis():
    # 图像分析流程 Image analysis workflow
    status_text.config(state="normal")
    status_text.delete(1.0, tk.END)
    
    input_dir = input_dir_entry.get()
    output_dir = output_dir_entry.get()
    pixels_per_cm = int(pixels_per_cm_entry.get())
    crop_top_percentage = float(crop_top_entry.get()) / 100
    crop_bottom_percentage = float(crop_bottom_entry.get()) / 100
    crop_left_percentage = float(crop_left_entry.get()) / 100
    crop_right_percentage = float(crop_right_entry.get()) / 100
    threshold_method = threshold_method_var.get()
    fixed_threshold_value = int(fixed_threshold_entry.get())
    root_background = root_background_var.get()
    cpu_threads = int(cpu_threads_entry.get())
    
    os.makedirs(output_dir, exist_ok=True)
    output_csv = os.path.join(output_dir, 'root_analysis_results.csv')

    def process_image(image_file):
        image_path = os.path.join(input_dir, image_file)
        try:
            image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
            if image is None:
                raise ValueError("无法加载图像") # raise ValueError("Cannot load image")
        except Exception as e:
            status_text.insert(tk.END, f"无法加载图像 {image_file},跳过\n") # status_text.insert(tk.END, f"Cannot load image {image_file}, skipping\n")
            root.update_idletasks()
            return None

        try:
            height, width = image.shape
            crop_top = int(height * crop_top_percentage)
            crop_bottom = int(height * crop_bottom_percentage)
            crop_left = int(width * crop_left_percentage)
            crop_right = int(width * crop_right_percentage)
            image_cropped = image[crop_top:height - crop_bottom, crop_left:width - crop_right]

            if root_background == "白底黑根": # if root_background == "White background, black roots":
                if threshold_method == "fixed":
                    _, binary = cv2.threshold(image_cropped, fixed_threshold_value, 255, cv2.THRESH_BINARY_INV)
                    threshold_value = fixed_threshold_value
                elif threshold_method == "otsu":
                    threshold_value, binary = cv2.threshold(image_cropped, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
            elif root_background == "黑底白根": # elif root_background == "Black background, white roots":
                if threshold_method == "fixed":
                    _, binary = cv2.threshold(image_cropped, fixed_threshold_value, 255, cv2.THRESH_BINARY)
                    threshold_value = fixed_threshold_value
                elif threshold_method == "otsu":
                    threshold_value, binary = cv2.threshold(image_cropped, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

            skeleton = morphology.skeletonize(binary // 255).astype(np.uint8) * 255
            base_name = os.path.splitext(image_file)[0]
            binary_path = os.path.join(output_dir, f'{base_name}_binary.jpg')
            skeleton_path = os.path.join(output_dir, f'{base_name}_skeleton.jpg')
            save_with_increment(binary_path, cv2.imwrite, binary)
            save_with_increment(skeleton_path, cv2.imwrite, skeleton)

            pixel_length = np.sum(skeleton) / 255
            total_root_length_cm = pixel_length / pixels_per_cm
            projected_area = np.sum(binary // 255) / (pixels_per_cm**2)
            average_root_diameter_mm = (projected_area / total_root_length_cm) * 10
            root_surface_area = np.pi * (average_root_diameter_mm / 10) * total_root_length_cm
            root_volume = np.pi * ((average_root_diameter_mm / 2) / 10)**2 * total_root_length_cm

            return {
                "Image": image_file,
                "Threshold Value": threshold_value,
                "Projected Area (cm2)": projected_area,
                "Total Root Length (cm)": total_root_length_cm,
                "Average Root Diameter (mm)": average_root_diameter_mm,
                "Root Surface Area (cm2)": root_surface_area,
                "Root Volume (cm3)": root_volume
            }
        except Exception as e:
            status_text.insert(tk.END, f"处理图像 {image_file} 时出错,跳过\n") # status_text.insert(tk.END, f"Error processing image {image_file}, skipping\n")
            root.update_idletasks()
            return None

    def background_task():
        results = []
        start_time = time.time()
        with ThreadPoolExecutor(max_workers=cpu_threads) as executor:
            futures = {executor.submit(process_image, image_file): image_file for image_file in os.listdir(input_dir)}
            for future in as_completed(futures):
                result = future.result()
                if result is not None:
                    results.append(result)
        end_time = time.time()
        status_text.insert(tk.END, f"多线程并行运算时间: {end_time - start_time:.2f} s\n") # status_text.insert(tk.END, f"Multi-threaded parallel processing time: {end_time - start_time:.2f} s\n")
        root.update_idletasks()

        results = natsorted(results, key=lambda x: x["Image"])
        df = pd.DataFrame(results)
        save_with_increment(output_csv, df.to_csv, index=False)
        os.startfile(output_dir)

    threading.Thread(target=background_task).start()

# 创建程序窗口 Create program window
root = tk.Tk()
root.title("Root Analysis Tool")
root.geometry("370x370")

# 程序初始参数。载入已保存的程序参数,没有则应用内置默认值 Program initial parameters. Load saved parameters if they exist; otherwise apply built-in defaults
input_dir_entry = tk.Entry(root, width=30)
input_dir_entry.insert(0, saved_params.get("input_dir", "D:\\R\\RootAnalysis\\Input") if saved_params else "D:\\R\\RootAnalysis\\Input")
output_dir_entry = tk.Entry(root, width=30)
output_dir_entry.insert(0, saved_params.get("output_dir", "D:\\R\\RootAnalysis\\Output") if saved_params else "D:\\R\\RootAnalysis\\Output")
pixels_per_cm_entry = tk.Entry(root, width=10)
pixels_per_cm_entry.insert(0, saved_params.get("pixels_per_cm", "130") if saved_params else "130")
crop_top_entry = tk.Entry(root, width=10)
crop_top_entry.insert(0, saved_params.get("crop_top", "0") if saved_params else "0")
crop_bottom_entry = tk.Entry(root, width=10)
crop_bottom_entry.insert(0, saved_params.get("crop_bottom", "0") if saved_params else "0")
crop_left_entry = tk.Entry(root, width=10)
crop_left_entry.insert(0, saved_params.get("crop_left", "0") if saved_params else "0")
crop_right_entry = tk.Entry(root, width=10)
crop_right_entry.insert(0, saved_params.get("crop_right", "0") if saved_params else "0")
threshold_method_var = tk.StringVar(value=saved_params.get("threshold_method", "otsu") if saved_params else "otsu")
fixed_threshold_entry = tk.Entry(root, width=10)
fixed_threshold_entry.insert(0, saved_params.get("fixed_threshold_value", "100") if saved_params else "100")
root_background_var = tk.StringVar(value=saved_params.get("root_background", "白底黑根") if saved_params else "白底黑根") # root_background_var = tk.StringVar(value=saved_params.get("root_background", "White background, black roots") if saved_params else "White background, black roots")
cpu_threads_entry = tk.Entry(root, width=10)
cpu_threads_entry.insert(0, saved_params.get("cpu_threads", "6") if saved_params else "6")

# 程序界面-原始图像位置 Program interface - Original image location
input_dir_label = tk.Label(root, text="原始图像位置:") # input_dir_label = tk.Label(root, text="Original image location:")
input_dir_label.grid(row=0, column=0, sticky="e", padx=0)
input_dir_entry.grid(row=0, column=1, sticky="w")
input_dir_button = tk.Button(root, text="选择", command=lambda: input_dir_entry.insert(0, filedialog.askdirectory())) # input_dir_button = tk.Button(root, text="Browse", command=lambda: input_dir_entry.insert(0, filedialog.askdirectory()))
input_dir_button.grid(row=0, column=2, sticky="w")
create_tooltip(input_dir_label, "原始图像的存放位置") # create_tooltip(input_dir_label, "Location where original images are stored")

# 程序界面-结果导出位置 Program interface - Result export location
output_dir_label = tk.Label(root, text="结果导出位置:") # output_dir_label = tk.Label(root, text="Result export location:")
output_dir_label.grid(row=1, column=0, sticky="e")
output_dir_entry.grid(row=1, column=1, sticky="w")
output_dir_button = tk.Button(root, text="选择", command=lambda: output_dir_entry.insert(0, filedialog.askdirectory())) # output_dir_button = tk.Button(root, text="Browse", command=lambda: output_dir_entry.insert(0, filedialog.askdirectory()))
output_dir_button.grid(row=1, column=2, sticky="w")
create_tooltip(output_dir_label, "二值化图像、根骨架图像、结果数据的导出位置") # create_tooltip(output_dir_label, "Export location for binary images, root skeleton images, and result data")

# 程序界面-校准系数 Program interface - Calibration coefficient
pixels_per_cm_label = tk.Label(root, text="校准系数:") # pixels_per_cm_label = tk.Label(root, text="Calibration coefficient:")
pixels_per_cm_label.grid(row=2, column=0, sticky="e")
pixels_per_cm_entry.grid(row=2, column=1, sticky="w")
create_tooltip(pixels_per_cm_label, "像素到厘米的转换比例") # create_tooltip(pixels_per_cm_label, "Pixel to centimeter conversion ratio")

# 程序界面-图像裁剪百分比 Program interface - Image crop percentages
crop_top_label = tk.Label(root, text="裁除顶部百分比:") # crop_top_label = tk.Label(root, text="Crop top percentage:")
crop_top_label.grid(row=3, column=0, sticky="e")
crop_top_entry.grid(row=3, column=1, sticky="w")

crop_bottom_label = tk.Label(root, text="裁除底部百分比:") # crop_bottom_label = tk.Label(root, text="Crop bottom percentage:")
crop_bottom_label.grid(row=4, column=0, sticky="e")
crop_bottom_entry.grid(row=4, column=1, sticky="w")

crop_left_label = tk.Label(root, text="裁除左部百分比:") # crop_left_label = tk.Label(root, text="Crop left percentage:")
crop_left_label.grid(row=5, column=0, sticky="e")
crop_left_entry.grid(row=5, column=1, sticky="w")

crop_right_label = tk.Label(root, text="裁除右部百分比:") # crop_right_label = tk.Label(root, text="Crop right percentage:")
crop_right_label.grid(row=6, column=0, sticky="e")
crop_right_entry.grid(row=6, column=1, sticky="w")

# 程序界面-图像类型 Program interface - Image type
root_background_label = tk.Label(root, text="图像类型:") # root_background_label = tk.Label(root, text="Image type:")
root_background_label.grid(row=7, column=0, sticky="e")
white_radio = ttk.Radiobutton(root, text="白底黑根", variable=root_background_var, value="白底黑根") # white_radio = ttk.Radiobutton(root, text="White background, black roots", variable=root_background_var, value="White background, black roots")
white_radio.grid(row=7, column=1, sticky="w")
create_tooltip(white_radio, "阈值越大,根系越厚") # create_tooltip(white_radio, "Higher threshold results in thicker roots")
black_radio = ttk.Radiobutton(root, text="黑底白根", variable=root_background_var, value="黑底白根") # black_radio = ttk.Radiobutton(root, text="Black background, white roots", variable=root_background_var, value="Black background, white roots")
black_radio.grid(row=7, column=1, sticky="w", padx=80)
create_tooltip(black_radio, "阈值越小,根系越厚") # create_tooltip(black_radio, "Lower threshold results in thicker roots")

# 程序界面-阈值方法 Program interface - Threshold method
threshold_method_label = tk.Label(root, text="阈值方法:") # threshold_method_label = tk.Label(root, text="Threshold method:")
threshold_method_label.grid(row=8, column=0, sticky="e")
fixed_radio = ttk.Radiobutton(root, text="fixed", variable=threshold_method_var, value="fixed")
fixed_radio.grid(row=8, column=1, sticky="w")
otsu_radio = ttk.Radiobutton(root, text="otsu", variable=threshold_method_var, value="otsu")
otsu_radio.grid(row=8, column=1, sticky="w", padx=80)
create_tooltip(threshold_method_label, "二值化阈值确定方式") # create_tooltip(threshold_method_label, "Method for determining binarization threshold")
create_tooltip(fixed_radio, "固定阈值法") # create_tooltip(fixed_radio, "Fixed threshold method")
create_tooltip(otsu_radio, "自动阈值法") # create_tooltip(otsu_radio, "Automatic threshold method")

# 程序界面-固定阈值 Program interface - Fixed threshold
fixed_threshold_label = tk.Label(root, text="固定阈值:") # fixed_threshold_label = tk.Label(root, text="Fixed threshold:")
fixed_threshold_label.grid(row=9, column=0, sticky="e")
fixed_threshold_entry.grid(row=9, column=1, sticky="w")
create_tooltip(fixed_threshold_label, "固定阈值仅在使用固定阈值法时有效") # create_tooltip(fixed_threshold_label, "Fixed threshold is only effective when using the fixed threshold method")

# 程序界面-CPU线程数 Program interface - CPU threads
cpu_threads_label = tk.Label(root, text="CPU线程数:") # cpu_threads_label = tk.Label(root, text="CPU threads:")
cpu_threads_label.grid(row=10, column=0, sticky="e")
cpu_threads_entry.grid(row=10, column=1, sticky="w")
create_tooltip(cpu_threads_label, "并行运算调用的CPU线程数上限") # create_tooltip(cpu_threads_label, "Maximum number of CPU threads used for parallel processing")

# 程序界面-运行分析按钮 Program interface - Run analysis button
run_button = tk.Button(root, text="运行分析", command=lambda: threading.Thread(target=run_analysis).start()) # run_button = tk.Button(root, text="Run Analysis", command=lambda: threading.Thread(target=run_analysis).start())
run_button.grid(row=11, column=0, columnspan=3)

# 程序界面-状态栏(带滚动条) Program interface - Status bar (with scrollbar)
status_frame = tk.Frame(root)
status_frame.grid(row=12, column=0, columnspan=3, sticky="we")
status_text = tk.Text(status_frame, height=5, wrap="word", state="normal", width=40)
status_text.pack(side="left", fill="both", expand=True)
status_scrollbar = tk.Scrollbar(status_frame, command=status_text.yview)
status_scrollbar.pack(side="right", fill="y")
status_text.config(yscrollcommand=status_scrollbar.set)


# 将保存参数文件函数绑定到窗口关闭事件 Bind save parameter function to window close event
root.protocol("WM_DELETE_WINDOW", lambda: [save_params(), root.destroy()])

root.mainloop()

将Python代码打包为可执行文件 Packaging Python Code into an Executable

为减小可执行文件(exe文件)的体积,本项目采用虚拟环境安装Python代码运行所需的最少库/模块(以避免不必要的模块掺入),并使用UPX压缩。

To reduce the size of the executable (exe file), this project uses a virtual environment to install only the minimal libraries/modules required to run the Python code (to avoid including unnecessary modules) and uses UPX compression.

下载UPX Download UPX

下载UPX压缩包并解压于任一文件夹,要求路径不含中文。

Download the UPX package and extract it to any folder.

安装Anaconda Install Anaconda

本项目利用Anaconda Prompt创建虚拟环境。

自行下载并安装Anaconda软件。安装包也可从镜像网站(如清华镜像站)下载。

This project uses Anaconda Prompt to create a virtual environment.

Download and install Anaconda manually.

创建并激活虚拟环境 Create and Activate the Virtual Environment

从开始菜单运行“Anaconda Prompt”,输入指令。

Run “Anaconda Prompt” from the Start menu and enter the following commands.

创建虚拟环境 Create a virtual environment

conda create -n aotu python=3.12

在创建过程中回复y,成功创建一个名字为aotu,且基于python版本3.12的虚拟环境。

During creation, reply with y. This successfully creates a virtual environment named aotu based on Python version 3.12.

激活虚拟环境 Activate the virtual environment

conda activate aotu

查看虚拟环境 View the virtual environment

conda info --envs
conda list

安装代码运行需要的库 Install libraries required for the code

将代码中的库/模块与虚拟环境中已有的进行比对,安装缺失的库。

Compare the libraries/modules in the code with those already present in the virtual environment, and install any missing ones.

pip install opencv-python numpy scikit-image pandas natsort

同时安装脚本打包模块。

Also install the script packaging module.

pip install pyinstaller

创建可执行文件 Create the Executable

切换路径 Change directory

切换到待打包的py代码文件所处的文件夹。

Switch to the folder containing the Python code file to be packaged.

D:
cd R\RootAnalysis

打包 Package

在py代码文件所处的文件夹,新建版本信息文件version_info.txt,并在其中填入以下内容:

Create a version information file version_info.txt in the folder containing the Python code, and fill it with the following content:

# 这里指定文件版本和产品版本为 1.0.0.0 Specify file version and product version as 1.0.0.0
VSVersionInfo(
   ffi=FixedFileInfo(
      filevers=(1, 0, 0, 0),  # 文件版本 File version
      prodvers=(1, 0, 0, 0),  # 产品版本 Product version
      mask=0x3f,
      flags=0x0,
      OS=0x4,
      fileType=0x1,
      subtype=0x0,
      date=(0, 0)
   ),
   kids=[
      StringFileInfo(
         [
            StringTable(
               u'040904B0',
               [
                  StringStruct(u'ProductName', u'Root Analysis Tool')
               ])
         ]
      ),
      VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
   ]
)

生成可执行文件。继续在Anaconda Prompt激活的虚拟环境中,输入以下指令:

Generate the executable. Continue in the Anaconda Prompt with the virtual environment activated, and enter the following command:

pyinstaller --onefile --noconsole --name root_analysis_tool --version-file D:\R\RootAnalysis\version_info.txt --upx-dir "D:\Program Files\Python312\upx-4.2.4-win64" D:\R\RootAnalysis\root_analysis_tool_package.py

其中,root_analysis_tool为生成的程序名称,D:\R\RootAnalysis\version_info.txt为版本文件所处位置及名称,D:\Program Files\Python312\upx-4.2.4-win64为UPX文件所处位置,D:\R\RootAnalysis\root_analysis_tool_package.py为py代码所处位置及名称。

生成的可执行文件位于dist文件夹内。

Where:

root_analysis_tool is the name of the generated program, D:\R\RootAnalysis\version_info.txt is the location and name of the version file, D:\Program Files\Python312\upx-4.2.4-win64 is the location of the UPX file, D:\R\RootAnalysis\root_analysis_tool_package.py is the location and name of the Python code.

The generated executable is located in the dist folder.

退出并清空虚拟环境 Exit and Clean Up the Virtual Environment

退出虚拟环境 Exit the virtual environment

conda deactivate

删除虚拟环境 Delete the virtual environment

conda remove --name aotu --all

引用 Citation

If you use this software in your research, please cite it as:

Chu, J. C. (2024). RootAnalysis: Batch extraction of root morphological parameters (Version 1.0.0) [Computer software]. https://github.com/chugit/RootAnalysis

BibTeX

@software{Chu_RootAnalysis_Batch_extraction,
  author  = {Chu, J. C.},
  title   = {RootAnalysis: Batch extraction of root morphological parameters},
  version = {1.0.0},
  url     = {https://github.com/chugit/RootAnalysis},
  year    = 2024
}

About

植物根系图像形态参数(总根长、平均直径、总表面积、总体积)批量提取方法及 Python 代码,并提供交互界面程序,支持打包为可执行文件。Batch extraction of root morphological parameters (total length, average diameter, total surface area, total volume) from plant root images with Python code, plus an interactive GUI tool that can be packaged as an executable.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages