Image Forensics using Dash: Unraveling the Truth Behind Digital Images

Image Forensics using Dash Unraveling the Truth Behind Digital Images featured image.jpg

In the age of digital media, images play a pivotal role in communication, entertainment, and information sharing. However, with the advancement of photo editing tools, the authenticity of digital images has come under scrutiny. Image Forensics, a specialized branch of digital forensics, aims to determine the integrity and provenance of digital images. By employing a set of sophisticated image processing functions and external libraries like OpenCV, imageio, and dash, Image Forensics can reveal hidden truths and identify potential manipulations. In this comprehensive article, we will explore the various techniques and tools used in Image Forensics to ensure the veracity of digital imagery.

Image Forensics is a multidisciplinary field that combines principles from computer vision, digital signal processing, and statistics to analyze and interpret digital images. It helps in detecting forged images, identifying cloned regions, and detecting traces of image tampering. In this article we use Python and Dash libraries for detecting the anomalies and plotting interactive graphics of Dash. Let’s dive into the various techniques employed in Image Forensics:

Video showing the output of the Image Forensics App

Histogram Equalization: Shedding Light on Image Enhancements

Histogram equalization is a fundamental image processing technique used in Image Forensics. It enhances the contrast of an image, making it easier to detect hidden details and manipulations. By redistributing the intensity levels of pixels, histogram equalization amplifies certain image features, aiding in forensic analysis.

Luminance Gradient Calculation: Exposing Manipulations

The luminance gradient is a powerful tool to identify areas of an image that have been manipulated. It measures the rate of change of luminance across different regions, and irregularities in this gradient can indicate potential alterations. Luminance gradient analysis helps experts pinpoint areas that might have undergone digital manipulation.

Error Level Analysis: Spotting Digital Anomalies

Error level analysis is an insightful method that detects discrepancies in the compression levels of different regions within an image. When an image is edited, the error levels in the manipulated regions differ from the surrounding unchanged areas. This analysis can be a valuable clue in identifying potential forgeries.

Perceptual Hash: Fingerprinting Images

Perceptual hash generates a unique fingerprint, or hash value, for an image based on its visual features. This technique allows forensic experts to compare images and determine if they are identical or have undergone alterations. Perceptual hash is useful for detecting cloned regions or identifying duplicates.

Leveraging External Libraries

Image Forensics applications are often empowered by external libraries that provide a plethora of image processing capabilities. OpenCV, a popular computer vision library, offers a rich set of tools for image manipulation and analysis. Imageio enables seamless reading and writing of images in various formats, while Dash facilitates building interactive web-based applications for image forensic analysis.

The Role of LSI Keywords in Image Forensics

In Image Forensics, the use of Latent Semantic Indexing (LSI) Keywords helps improve search engine visibility and relevance. LSI Keywords are contextually related terms that contribute to a more comprehensive understanding of the topic. When creating content related to Image Forensics, incorporating LSI Keywords like “photo forensics,” “image manipulation detection,” and “digital image authenticity” enhances the article’s value and credibility.

Image Forensics: Applications and Impact

The impact of Image Forensics reaches far beyond detecting image forgeries. It plays a vital role in areas like criminal investigations, journalistic integrity, and digital art authentication. In criminal investigations, Image Forensics aids in verifying the authenticity of images used as evidence, ensuring a fair and just legal process. For journalists, it serves as a tool to validate images and maintain the integrity of their reporting. Additionally, in the world of digital art, Image Forensics helps verify the originality of artworks, ensuring artists receive appropriate credit and protection.

Unraveling Deepfakes: A Growing Challenge

With the rise of deepfake technology, the need for advanced Image Forensics techniques has become more urgent. Deepfakes are manipulated videos or images that use artificial intelligence to convincingly alter visual content, often leading to misinformation and confusion. Image Forensics is at the forefront of combating this challenge, striving to identify deepfakes and safeguard the credibility of visual information.

Ensuring Credibility: Expert Insights

In the field of Image Forensics, expertise and first-hand experience are invaluable assets. Professionals in this domain are equipped with an arsenal of technical knowledge and analytical skills to unearth the hidden truth within digital images. To ensure the credibility of Image Forensics analysis, experts adhere to ethical practices, maintain transparency, and provide detailed explanations of their findings.

Below is the python code which will help you get a glimpse of this world. GitHub Repository Link

import os
import io
import base64
import cv2
import dash
from dash import dcc,html, dash_table    
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
import plotly.graph_objs as go
import imageio.v2 as imageio
import imagehash
import numpy as np
from PIL import Image
from PIL.ExifTags import TAGS

def perceptual_hash(image):
    image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    hash_value = imagehash.average_hash(Image.fromarray(image))
    return hash_value

def find_duplicates(image, tile_size, step_size, threshold=10):

    hash_value = perceptual_hash(image)
    duplicate_regions = []

    for y in range(0, image.shape[0] - tile_size + 1, step_size):
        for x in range(0, image.shape[1] - tile_size + 1, step_size):
            tile = image[y:y+tile_size, x:x+tile_size]
            tile_hash = perceptual_hash(tile)
            hamming_distance = hash_value - tile_hash

            if hamming_distance <= threshold:
                duplicate_regions.append((y, x, y + tile_size, x + tile_size))

    return duplicate_regions

def clone_detection_function(image, tile_size, step_size, threshold=10):
    if not isinstance(image, np.ndarray):
        image = np.array(image)
    image_clone_detection = image.copy()
    duplicates = find_duplicates(image, tile_size, step_size, threshold)
    for region in duplicates:
        y1, x1, y2, x2 = region
        cv2.rectangle(image_clone_detection, (x1, y1), (x2, y2), (255, 0, 0), 2)
    fig = go.Figure()
    fig.add_trace(go.Image(z=image_clone_detection))
    fig.update_layout(title="Clone Detection Plot")

    return fig

def perform_ela(image, scale=10):
    temp_filename = "temp.jpg"    
    cv2.imwrite(temp_filename, image, [cv2.IMWRITE_JPEG_QUALITY, scale])
    ela_image = cv2.imread(temp_filename)
    ela_image = cv2.absdiff(image, ela_image)
    ela_image = cv2.cvtColor(ela_image, cv2.COLOR_BGR2GRAY)
    ela_image = cv2.equalizeHist(ela_image)
    ela_image = cv2.cvtColor(ela_image, cv2.COLOR_GRAY2RGB)
    os.remove(temp_filename)
    return ela_image

def error_level_analysis_function(image):
    ela_image = perform_ela(image)
    fig = go.Figure()
    fig.add_trace(go.Image(z=ela_image))
    fig.update_layout(title="Error Level Analysis with Histogram Equalization")

    return fig


def histogram_equalization(image):
    gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    equalized_image = cv2.equalizeHist(gray_image)
    return equalized_image

def noise_analysis_function(image):
    equalized_image = histogram_equalization(image)
    noise_image = cv2.absdiff(image, cv2.cvtColor(equalized_image, cv2.COLOR_GRAY2RGB))
    fig = go.Figure()
    fig.add_trace(go.Image(z=noise_image))
    fig.update_layout(title="Noise Analysis Plot (Histogram Equalization)")
    return fig

def adjust_brightness(image, alpha):
    return cv2.convertScaleAbs(image, alpha=alpha, beta=0)

def adjust_opacity(image, opacity):  
    return cv2.addWeighted(image, opacity, np.zeros(image.shape, image.dtype), 1 - opacity, 0)

def level_sweep_function(image, levels, opacities):
    rows = len(levels)
    cols = len(opacities)
    fig = go.Figure()
    for i, alpha in enumerate(levels):
        for j, opacity in enumerate(opacities):
            adjusted_image = adjust_opacity(adjust_brightness(image.copy(), alpha), opacity)

            fig.add_trace(go.Image(z=adjusted_image, opacity=1/(rows*cols), name=f"Alpha={alpha}, Opacity={opacity}"))

    fig.update_layout(title="Level Sweep Plot",
                      grid=dict(rows=rows, columns=cols),
                      showlegend=False,
                      width=2024,
                      height=2000
                      )  
    return fig


def calculate_luminance_gradient(image):
    gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    gradient_x = cv2.Scharr(gray_image, cv2.CV_64F, 1, 0)
    gradient_y = cv2.Scharr(gray_image, cv2.CV_64F, 0, 1)
    magnitude = np.sqrt(gradient_x**2 + gradient_y**2)
    return magnitude

def luminance_gradient_function(image):
    luminance_gradient = calculate_luminance_gradient(image)
    x, y = np.meshgrid(np.arange(luminance_gradient.shape[1]), np.arange(luminance_gradient.shape[0]))
    fig = go.Figure(data=[go.Surface(z=luminance_gradient, colorscale='Viridis')])
    fig.update_layout(title="Luminance Gradient Plot",
                      scene=dict(
                          xaxis_title='X',
                          yaxis_title='Y',
                          zaxis_title='Luminance Gradient'
                      ))

    return fig

def extract_metadata_from_file(file_path):

    try:
        with Image.open(file_path) as img:
            exifdata = img._getexif()
        if exifdata:
            metadata = {Image.TAGS.get(tag): value for tag, value in exifdata.items() if Image.TAGS.get(tag)}
            return metadata
        else:
            return None
    except Exception as e:
        print(f"Error extracting metadata: {e}")
        return None
  

def get_quantization_tables(image_path):
    img = cv2.imread(image_path, cv2.IMREAD_COLOR)

    if img is None:
        return None

    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img_ycrcb = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2YCrCb)

    y_channel = img_ycrcb[:, :, 0]
    cb_channel = img_ycrcb[:, :, 1]
    cr_channel = img_ycrcb[:, :, 2]

    y_quantization_table = np.round(y_channel / 16).astype(int)
    cbcr_quantization_table = np.round((cb_channel + 128) / 16).astype(int)

    return y_quantization_table, cbcr_quantization_table

def create_quantization_table_data(quantization_table):
    return [{"Column {}".format(i+1): value for i, value in enumerate(row)} for row in quantization_table]


# Initialize the Dash app
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

# Layout for the Upload component
upload_layout = html.Div([
    dcc.Upload(
        id='upload-image',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select Image')
        ]),
        style={
            'width': '100%',
            'height': '100px',
            'lineHeight': '100px',
            'borderWidth': '1px',
            'borderStyle': 'dashed',
            'borderRadius': '5px',
            'textAlign': 'center',
            'margin': '10px'
        },
        multiple=False
    ),
    html.Div(id='output-image-upload'),
    
])

# Layout for the Tab content
tab_content_layout = html.Div(
    [
        dcc.Tabs(
            id="tabs",
            value="tab-1",
            children=[
                dcc.Tab(
                    label="Original Image",
                    value="tab-1",
                    children=[
                        html.H4("Original Image"),
                        html.Div(id="tab1-content"),
                    ],
                ),
                dcc.Tab(
                    label="Clone Detection Plot",
                    value="tab-2",
                    children=[
                        html.Div([
                            html.Label("Tile Size (For Clone Detection only)"),
                            dcc.Slider(
                                id="tile-size-slider",
                                min=8,
                                max=128,
                                step=8,
                                value=32,
                                marks={size: str(size) for size in range(8, 129, 8)},
                            ),
                            html.Label("Step Size (For Clone Detection only)"),
                            dcc.Slider(
                                id="step-size-slider",
                                min=8,
                                max=128,
                                step=8,
                                value=16,
                                marks={size: str(size) for size in range(8, 129, 8)},
                            ),
                        ]),
                        html.Div(id="tab2-content"),
                    ],
                ),
                dcc.Tab(
                    label="Error Level Analysis Plot",
                    value="tab-3",
                    children=[
                        html.Div(id="tab3-content"),
                    ],
                ),
                dcc.Tab(
                    label="Noise Analysis Plot",
                    value="tab-4",
                    children=[
                        html.Div(id="tab4-content"),
                    ],
                ),
                dcc.Tab(
                    label="Level Sweep Plot",
                    value="tab-5",
                    children=[
                        html.Div([
                        html.Label("Enter the levels (comma-separated):"),
                        dcc.Input(
                            id='levels-input',
                            type='text',
                            value='0.2, 0.4, 0.6, 0.8, 1.0',
                            style={'width': '100%'}
                        ),
                    ], style={'margin': '10px'}),
                    html.Div([
                        html.Label("Enter the opacities (comma-separated):"),
                        dcc.Input(
                            id='opacities-input',
                            type='text',
                            value='0.2, 0.4, 0.6, 0.8, 1.0',
                            style={'width': '100%'}
                        ),
                    ], style={'margin': '10px'}),
                        html.Div(id="tab5-content"),
                    ],
                ),
                dcc.Tab(
                    label="Luminance Gradient Plot",
                    value="tab-6",
                    children=[
                        html.Div(id="tab6-content"),
                    ],
                ),
                dcc.Tab(
                    label="Geo and Meta Tags",
                    value="tab-7",
                    children=[
                        html.Div(id="tab7-content"),
                    ],
                ),
                dcc.Tab(
                    label="JPEG Quantization Tables",
                    value="tab-8",
                    children=[
                        html.Div(id="tab8-content"),
                    ],
                ),
            ],
        )
    ]
)

# Define the app layout
app.layout = html.Div([
    html.H1("Image Forensics", style={'text-align': 'center'}),
    upload_layout,
    html.Div([
        html.Label("Enter the image path:"),
        dcc.Input(
            id='image-path-input',
            type='text',
            value='C:/Users/Purple/OneDrive/Pictures/Camera Uploads/2013-01-28 19.01.06.jpg',
            style={'width': '100%'}
        ),
    ], style={'margin': '10px'}),
    html.Div([
    
    html.Button(
            id='submit-button',
            n_clicks=0,
            children='Submit',
            style={'width': '100%'}
        ),
    ], style={'margin': '10px'}),
    
    tab_content_layout
])

# Function to read the uploaded image
def read_image(contents):
    _, content_string = contents.split(",")
    decoded = base64.b64decode(content_string)
    image = imageio.imread(io.BytesIO(decoded))
    return image


# Callback to update the image on the Original Image tab
@app.callback(Output('tab1-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [Input('upload-image', 'contents')],
              [State('image-path-input', 'value')])
def update_original_image(n_clicks, contents, file_path):
    if n_clicks is not None:
        if contents is not None:
            image = read_image(contents)
            # Add your code here to perform magnification (if required)
            return html.Div([
                
                html.Img(src=contents, style={'paddingTop': '40px'}),
            ], style={'display': 'flex', 'justify-content': 'center'})
        elif file_path is not None:
            # read the image data using PIL
            image = Image.open(file_path)
            return html.Div([
                
                html.Img(src=image, style={'paddingTop': '40px'}),
            ], style={'display': 'flex', 'justify-content': 'center'})


# Callback to update the Clone Detection Plot with the user-defined tile and step sizes
@app.callback(Output('tab2-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [Input('upload-image', 'contents')],
              [State('image-path-input', 'value')],
               [Input('tile-size-slider', 'value')],
               [Input('step-size-slider', 'value')])
def update_clone_detection_plot(n_clicks,contents, file_path, tile_size, step_size):
    if n_clicks is not None:
        if contents is not None:
            image = read_image(contents)
        elif file_path is not None:
            # read the image data using PIL
            image = Image.open(file_path)
        else:
            # No image or file path provided
            return html.Div([
                html.P("Please upload an image or provide the file path."),
            ])

        clone_detection_plot = clone_detection_function(image, tile_size, step_size)
        return html.Div([
            dcc.Graph(figure=clone_detection_plot),
        ])


    

# Callback to update the Error Level Analysis Plot
@app.callback(Output('tab3-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('upload-image', 'contents'),
               State('image-path-input', 'value')])
def update_error_level_analysis_plot(n_clicks, contents, file_path):
    if contents is not None:
        image = read_image(contents)  
        error_level_analysis_plot = error_level_analysis_function(image)
        return html.Div([
            dcc.Graph(figure=error_level_analysis_plot),
        ])
    
    if n_clicks is not None:
        
        if file_path is not None:
            
            image = Image.open(file_path)
            image = np.array(image)[:, :, ::-1] 
            error_level_analysis_plot = error_level_analysis_function(image)
            return html.Div([
                dcc.Graph(figure=error_level_analysis_plot),
            ])
    else:
        return html.P(['Please upload an image or provide the file path.'])
    

# Callback to update the Noise Analysis Plot
@app.callback(Output('tab4-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('upload-image', 'contents'),
               State('image-path-input', 'value')])
def update_noise_analysis_plot(n_clicks,contents, file_path):
    if contents is not None:
        image = read_image(contents)
        noise_analysis_plot = noise_analysis_function(image)
        return html.Div([
            dcc.Graph(figure=noise_analysis_plot),
        ])
    if n_clicks is not None:
        if file_path is not None:
            
            image = Image.open(file_path)
            image = np.array(image)[:, :, ::-1]  
            noise_analysis_plot = noise_analysis_function(image)
            return html.Div([
                dcc.Graph(figure=noise_analysis_plot),
            ])
    else:
        return html.P(['Please upload an image or provide the file path.'])


# Callback to update the Level Sweep Plot
@app.callback(Output('tab5-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('upload-image', 'contents'),
               State('image-path-input', 'value'),
               State('levels-input', 'value'),
               State('opacities-input', 'value')])
def update_level_sweep_plot(n_clicks, contents, file_path, levels_input, opacities_input):
    if contents is not None:
        image = read_image(contents)
    elif file_path is not None:
       
        image = Image.open(file_path)
        image = np.array(image)[:, :, ::-1]  
    else:
        return html.P(['Please upload an image or provide the file path.'])

    try:
        levels = [float(level.strip()) for level in levels_input.split(',')]
        opacities = [float(opacity.strip()) for opacity in opacities_input.split(',')]
    except ValueError:
        return html.P(['Invalid input. Please enter valid levels and opacities.'])

    level_sweep_plot = level_sweep_function(image, levels, opacities)
    return html.Div([
        dcc.Graph(figure=level_sweep_plot),
    ], style={'Height': '100%', 'Width': '100%'})


# Callback to update the Luminance Gradient Plot
@app.callback(Output('tab6-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('upload-image', 'contents'),
               State('image-path-input', 'value')])
def update_luminance_gradient_plot(n_clicks,contents, file_path):
    if contents is not None:
        image = read_image(contents) 
        luminance_gradient_plot = luminance_gradient_function(image)
        return html.Div([
            dcc.Graph(figure=luminance_gradient_plot),
        ])

    if n_clicks is not None:
        if file_path is not None:
            
            image = Image.open(file_path)
            image = np.array(image)[:, :, ::-1]  
            luminance_gradient_plot = luminance_gradient_function(image)
            return html.Div([
                dcc.Graph(figure=luminance_gradient_plot),
            ])

# Callback to update the Geo and Meta Tags
@app.callback(Output('tab7-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('upload-image', 'contents'),
               State('image-path-input', 'value')])
def update_geo_meta_tags(n_clicks, contents, file_path):
    if contents is not None:
        image = Image.open(io.BytesIO(contents.encode('utf-8')))

    if n_clicks is not None:

        
        if file_path is not None:
            image = Image.open(file_path)
        else:
            return html.Div([
                html.H3("Geo and Meta Tags Analysis"),
                html.P("Please upload an image or provide the image path."),
            ])

        exifdata = image.getexif()
        tags_list = []
        data_list = []
        for tag_id in exifdata:
            tag = TAGS.get(tag_id, tag_id)
            data = exifdata.get(tag_id)
            if isinstance(data, bytes):
                data = data.decode()
            tags_list.append(tag)
            data_list.append(data)
        tags_data_str = "\n".join([f"{tag:25}: {data}" for tag, data in zip(tags_list, data_list)])

        return html.Div([
            html.H3("Geo and Meta Tags Analysis"),
            html.Pre(tags_data_str),
        ])

        
# Callback to update the JPEG and String Analysis
@app.callback(Output('tab8-content', 'children'),
              [Input('submit-button', 'n_clicks')],
              [State('upload-image', 'contents'),
               State('image-path-input', 'value')])
def update_jpeg_string_analysis(n_clicks, contents, file_path):
    if not file_path and not contents:
        return html.Div("Please enter an image path.")
        
    if contents is not None:
        
        y_quantization_table, cbcr_quantization_table = get_quantization_tables(contents)
        if y_quantization_table is None or cbcr_quantization_table is None:
            return html.Div("Error: Image not found or unsupported format.")

        y_table_data = create_quantization_table_data(y_quantization_table)
        cbcr_table_data = create_quantization_table_data(cbcr_quantization_table)

        return html.Div([
            dcc.Tabs(id='tabs-example', value='tab-1-example', children=[
                dcc.Tab(
                    label='Y Quantization Table', 
                    value='tab-2-example',
                    children=[
                        html.Div(dash_table.DataTable(
                        id='y-table',
                        columns=[{'name': col, 'id': col} for col in y_table_data[0].keys()],
                        data=y_table_data,
                        style_table={'overflowX': 'auto'}))]
                        ),
                dcc.Tab(
                    label='CbCr Quantization Table', 
                    value='tab-3-example',
                    children=[
                        html.Div(dash_table.DataTable(
                        id='y-table',
                        columns=[{'name': col, 'id': col} for col in cbcr_table_data[0].keys()],
                        data=cbcr_table_data,
                        style_table={'overflowX': 'auto'}))]
                    ),
            ])
            
        ])
    
    
    elif n_clicks is not None:
            
        y_quantization_table, cbcr_quantization_table = get_quantization_tables(file_path)
        if y_quantization_table is None or cbcr_quantization_table is None:
            return html.Div("Error: Image not found or unsupported format.")

        y_table_data = create_quantization_table_data(y_quantization_table)
        cbcr_table_data = create_quantization_table_data(cbcr_quantization_table)

        return html.Div([
            html.H3("Y Quantization Table:"),
            dash_table.DataTable(
                id='y-table',
                columns=[{'name': col, 'id': col} for col in y_table_data[0].keys()],
                data=y_table_data,
                style_table={'overflowX': 'auto'},
            ),
            html.H3("CbCr Quantization Table:"),
            dash_table.DataTable(
                id='cbcr-table',
                columns=[{'name': col, 'id': col} for col in cbcr_table_data[0].keys()],
                data=cbcr_table_data,
                style_table={'overflowX': 'auto'},
            ),
        ])



if __name__ == '__main__':
    app.run_server(debug=True)

FAQs

Can Image Forensics Identify Photoshopped Images?

Yes, Image Forensics can identify photoshopped images using various techniques like histogram equalization, luminance gradient calculation, error level analysis, and perceptual hash. These methods help reveal inconsistencies and alterations in the image, thereby exposing potential manipulations.

How Accurate is Image Forensics?

The accuracy of Image Forensics depends on the complexity of the image manipulation and the expertise of the forensic analyst. When performed by skilled professionals using robust tools, Image Forensics can achieve high accuracy in detecting image tampering.

Is Image Forensics Only Used for Legal Purposes?

While Image Forensics is commonly used in legal investigations, its applications extend to other domains as well. It is employed in journalism, art authentication, historical research, and digital media analysis to ensure image authenticity and integrity.

Can Image Forensics Detect Deepfakes?

Yes, Image Forensics can be adapted to detect deepfakes by analyzing artifacts and inconsistencies in the manipulated content. However, as deepfake technology evolves, Image Forensics must continually advance to keep up with new challenges.

What Should I Look for in an Image Forensics Service Provider?

When selecting an Image Forensics service provider, consider their expertise, experience, and track record. Look for providers with a strong background in digital forensics, computer vision, and image analysis.

Can Image Forensics Be Used Retroactively?

Yes, Image Forensics can be used retroactively to analyze images even after they have been widely circulated. Forensic experts can still uncover manipulations and verify the authenticity of old images using advanced tools and methodologies.

Conclusion

Image Forensics is an indispensable field that safeguards the integrity of digital imagery. By leveraging image processing functions such as histogram equalization, luminance gradient calculation, error level analysis, and perceptual hash, alongside external libraries like OpenCV, imageio, and dash, experts can detect manipulations and expose the truth hidden within digital images. Whether used in legal investigations, journalistic reporting, or art authentication, Image Forensics plays a vital role in upholding trust and authenticity in the digital age.

%d bloggers like this: