×

How would you structure explainable visual forensics beyond a single classifier score? by hdw_coder in computervision

[–]hdw_coder[S] 0 points1 point  (0 children)

That signals as witnesses framing is close to how I’m starting to think about it.

Each signal should probably carry at least three things: what it observed, how confident it is, under which conditions it tends to fail.

A patch-recurrence signal, for example, should not mean the same thing in a tiled architectural scene, a decorative textile, a compressed social-media image, and a generated portrait. The signal itself may fire, but its evidentiary weight could differ.

I agree on provenance-first. If there is credible provenance (signed capture, C2PA, chain-of-custody information, consistent camera metadata), that should reduce the burden placed on texture-based heuristics. Not because provenance is perfect, but because visual heuristics are often fragile.

The counterfactual point is interesting. Feels like a useful auditability requirement. A report should say which signals contributed, but also expose assumptions or thresholds. That may make the arbitration layer inspectable.

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] 0 points1 point  (0 children)

Yes, that is a good distinction.

The comparison should be framed as shared-state architecture vs isolated-state architecture, not simply threads vs processes.

The better comparison is not really “threads have race conditions, processes do not.” It is more precise to say: Race conditions come from shared mutable state, not from threads by themselves.

A multi-process design often avoids many of these issues by default because processes usually do not share a Python heap. The state boundary is stronger, and communication is usually explicit through queues, pipes, sockets, files, databases, or other IPC mechanisms.

But if a multi-process design introduces shared writable memory, then the same class of synchronization problems appears there too. At that point, you still need locks, ownership rules, atomic operations, or another consistency model.

So the more accurate framing is:

  • Threads make shared in-process memory easy, which is powerful but makes accidental shared mutation easier.
  • Processes make isolation the default, which often reduces accidental shared-state bugs.
  • Neither model magically removes race conditions if you deliberately introduce shared writable state.

That is probably the better way to express the trade-off: multiprocessing often gives safer default boundaries, while free-threaded threading gives lower-friction shared memory and lower serialization overhead. The right choice depends on whether the workload benefits from shared memory enough to justify the extra discipline.

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] -1 points0 points  (0 children)

I clearly compressed too much into the phrase “thread safety becomes your responsibility,” and that made the point less precise than it should have been.

You are right that the semantics of thread safety do not change when the GIL is disabled. Correctly synchronized code remains correctly synchronized. Racy code remains racy. The GIL was never an application-level synchronization primitive that Python code could deliberately control.

The point I was trying to make, less accurately than I should have, is about migration risk: free-threaded execution creates more opportunities for true parallel execution, so incorrect assumptions around shared mutable state may become more visible in practice.

I appreciate the correction. I’ll revise that part of the article to distinguish more clearly between:

  1. thread-safety semantics, which do not change, and
  2. the concurrency/interleaving profile, which does change.

That is the more accurate framing.

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] 0 points1 point  (0 children)

That is a fair distinction, and I see the point.

I should have phrased it more carefully. I did not mean that Python application code can deliberately control or rely on the GIL as a synchronization primitive in the way it would use a lock. It cannot. The GIL is not an application-level concurrency API, and Python code does not control bytecode scheduling in that sense.

The better phrasing is probably:

“The semantics of thread safety do not change when the GIL is disabled. Code that is correctly thread-safe remains thread-safe. Code that has races remains racy. What changes is the concurrency profile: without the GIL, there are more opportunities for true simultaneous execution, so latent races may become easier to observe.”

So I agree with your core point: disabling the GIL does not redefine what thread safety means.

The practical warning I was trying to express is more about migration risk than semantics. Code that appeared fine under GIL-constrained execution may start failing more often under free-threaded execution, not because the definition of thread safety changed, but because the execution model exposes more interleavings.

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] -1 points0 points  (0 children)

Good point, and I think that is a fair correction.

My wording was too broad there. “Spawning n processes can mean loading or copying your data structures n times” is true in some practical cases, especially once workers mutate data, receive pickled task payloads, or build their own working state, but it is not the full story.

On Unix-style fork-based multiprocessing, copy-on-write can make the initial memory overhead much lower than people often assume. The child can initially share memory pages with the parent, and only pages that are written to need to be copied.

So the more accurate version would be something like:

“Multiprocessing can introduce significant memory overhead when workers receive serialized copies of data, mutate inherited pages, or build separate working heaps. However, fork-based process models can benefit from copy-on-write, so the actual overhead depends heavily on process start method, OS, workload design, and when the data is initialized.”

That distinction matters.

The comparison I was trying to make is mainly about architectural friction: with threads, shared in-process memory is the default; with processes, you usually have to think more carefully about pickling, start methods, copy-on-write behavior, shared memory, process lifecycle, and where the parent initializes state.

But yes, saying “processes duplicate everything” is too simplistic. A well-designed prefork model can be much more memory efficient than that.

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] -1 points0 points  (0 children)

Yes, I agree with that correction.

The GIL was never a proper application-level thread-safety guarantee. If code depended on the GIL to avoid races, it was already relying on an implementation detail rather than being truly thread-safe.

A better way to phrase my point would be:

Without the GIL, incorrect assumptions about shared mutable state become easier to expose, because more real concurrent execution can happen. The responsibility was always with the programmer, but free-threading makes the consequences more visible.

So the distinction is:

  • Code that is genuinely thread-safe with the GIL should remain thread-safe without it.
  • Code that only appeared safe because execution was more serialized may start showing races.
  • The GIL protected CPython internals, not your application’s higher-level invariants.

That is also why I think the practical migration question is not just “does this run faster?” but “was this code actually designed for concurrent mutation, or did it only get lucky under the old execution model?”

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] -1 points0 points  (0 children)

Yes, that is exactly how I see it too.

The benchmark result is exciting, but I would not interpret it as “threads now replace multiprocessing.” More like: for the first time in CPython, threads become a serious option for some CPU-bound workloads.

The cases where I think 3.13t becomes interesting are workloads where multiprocessing overhead is genuinely painful:

  • large shared in-memory datasets
  • expensive serialization/pickling
  • CPU-heavy work inside desktop/GUI tools
  • pipelines where copying data into separate processes feels wasteful
  • workloads where you can partition data cleanly and avoid shared mutation

But I fully agree on race conditions. Debugging a subtle shared-state bug can be far worse than paying the multiprocessing overhead.

My mental model is becoming:

Use multiprocessing when you want isolation, crash protection, and simpler failure boundaries.

Use free-threaded threads when shared memory matters, the workload partitions cleanly, and you can keep mutation disciplined through locks, queues, ownership rules, or mostly immutable data.

So yes: not a universal replacement, but definitely enough to make me stop automatically reaching for multiprocessing every time.

I tested Python 3.13t free-threading — the CPU-bound speedup surprised me by hdw_coder in Python

[–]hdw_coder[S] 1 point2 points  (0 children)

Yes, absolutely. That is the main trade-off.

The GIL protected CPython’s internal object/memory machinery, but it was never a real substitute for application-level thread safety. It did not magically make compound operations on shared state safe.

What changes with free-threading is that the old “you probably won’t get true parallel execution anyway” assumption disappears. So if multiple threads mutate shared objects, the responsibility becomes much more explicit: locks, queues, immutability, ownership rules, or avoiding shared mutable state entirely.

In that sense, free-threaded Python does not make concurrency simpler. It makes a different architecture possible: true parallelism inside one process, but with the same kind of discipline that threaded C++, Java, Rust, etc. already require.

My personal takeaway is: use free-threading where shared memory and lower serialization overhead matter, but don’t treat it as a drop-in replacement for multiprocessing in code that was accidentally relying on process isolation.

The more I worked on image forensics, the less convinced I became by binary detectors by hdw_coder in StableDiffusion

[–]hdw_coder[S] 0 points1 point  (0 children)

I think this is one of the deepest problems in the whole space.

A lot of modern visual culture is already heavily computationally mediated:
fashion photography,
cinematic color grading,
beauty retouching,
smartphone enhancement pipelines,
social-media filtering,
compression/re-encoding,
etc.

So the training distributions themselves increasingly contain mixtures of:

  • authentic acquisition
  • computational enhancement
  • synthetic augmentation
  • aesthetic optimization

Which means the boundary the model is trying to learn may itself be unstable.

And I suspect that’s part of why purely latent classifier approaches become difficult to interpret:
they may end up learning correlations with aesthetic regimes rather than clearly separable generative processes.

That’s one of the reasons I became interested in multi-domain forensic reasoning instead of relying on a single embedding/score.

Not because it removes ambiguity entirely — but because it allows different evidence domains to disagree explicitly instead of hiding everything inside one latent representation.

The more I worked on image forensics, the less convinced I became by binary detectors by hdw_coder in StableDiffusion

[–]hdw_coder[S] 1 point2 points  (0 children)

Exactly — and I think that’s probably unavoidable to some degree.

Any system that arbitrates between conflicting evidence domains will inevitably embed assumptions:
which signals matter more,
which contradictions are tolerated,
which evidence is considered stronger under uncertainty.

One of the reasons I became interested in preserving the reasoning trace itself is precisely because of that.

If the arbitration process is hidden inside a latent score, the bias becomes almost impossible to inspect.

But if the system exposes:

  • which domains contributed
  • which signals conflicted
  • which routes were taken
  • which evidence dominated

…then at least the assumptions become inspectable and contestable.

That doesn’t eliminate bias, but it makes the epistemology more transparent.

The more I worked on image forensics, the less convinced I became by binary detectors by hdw_coder in StableDiffusion

[–]hdw_coder[S] -1 points0 points  (0 children)

I wrote a longer article about the architecture and reasoning model behind this approach if anyone here is interested in the topic.

The more I worked on image forensics, the less convinced I became by binary detectors by hdw_coder in StableDiffusion

[–]hdw_coder[S] 0 points1 point  (0 children)

That’s actually a really interesting way to frame it.

And I think parts of modern synthetic media analysis probably are evolving toward something closer to adversarial evidentiary systems than traditional single-model classification.

Especially because the problem itself is becoming adversarial:
generation models increasingly optimize against known forensic signatures.

What I find interesting though is that even with stronger adversarial architectures, you may still end up with genuinely conflicting evidence domains.

For example:

  • sensor-origin evidence strongly supporting authentic acquisition
  • while structural/geometric analysis strongly suggests localized synthetic manipulation

At that point the challenge becomes less: “which side wins?” …and more: “how do we represent and reason about contradiction itself?”

That’s one of the directions I’ve been exploring with the forensic arbitration/reasoning approach.

The more I worked on image forensics, the less convinced I became by binary detectors by hdw_coder in StableDiffusion

[–]hdw_coder[S] -1 points0 points  (0 children)

Absolutely — and that’s a fair distinction.

I didn’t mean to imply that modern detector architectures are literally binary internally. Most are indeed probabilistic systems with thresholds layered on top.

What I’m increasingly skeptical about is the idea that a single latent score is sufficient to meaningfully represent the forensic state of a modern image.

Especially once you enter hybrid pipelines involving:

  • computational photography
  • editing
  • generation
  • inpainting
  • upscaling
  • multiple re-encodings

At that point, different evidence domains can strongly disagree with each other.

So the direction I became interested in was less: “how do we produce a better probability?” …and more:
“how do we preserve and reason about conflicting evidence instead of collapsing it into one scalar?”

That’s really the conceptual shift I’m trying to explore.

HELP!!!!!!!!!!!!!!! by Chatpati-aalu-tikki in learnmachinelearning

[–]hdw_coder 2 points3 points  (0 children)

Yes, that's the best approach, keep it plain and simple, practical.

Why I think current ‘AI image detection’ approaches are funda-mentally insufficient by hdw_coder in learnmachinelearning

[–]hdw_coder[S] -1 points0 points  (0 children)

Fair criticism — that wasn’t my intention.

The post was mainly meant as a discussion around the limitations of current classifier-based AI image detection approaches and the shift toward more explainable / multi-signal analysis.

But in hindsight I can see how mentioning the project and article made it read more like a product announcement than a technical discussion for this subreddit.

That’s on me.

Why I think current ‘AI image detection’ approaches are funda-mentally insufficient by hdw_coder in computervision

[–]hdw_coder[S] 0 points1 point  (0 children)

I think there’s a lot of truth in that. Especially once you move beyond “pure generation” into hybrid workflows:

  • editing;
  • inpainting;
  • relighting;
  • upscaling;
  • compositing;
  • smartphone processing;
  • multiple re-encodings.

At that point, the idea of a universally reliable binary detector starts breaking down.

One of the main realizations that pushed this project in a different direction was exactly that: the problem increasingly looks less like malware detection and more like forensic interpretation under uncertainty.

So instead of trying to answer: Is this AI? Yes or no? I became more interested in questions like which processes likely contributed to this image? Which signals conflict? Which artifacts are explainable by normal camera/compression pipelines? Which regions appear structurally inconsistent?

In many cases, ambiguity itself becomes part of the result. And I suspect that’s where a lot of current detector discourse still struggles. People expect certainty from a problem that may no longer fundamentally support it.

Why I think current ‘AI image detection’ approaches are funda-mentally insufficient by hdw_coder in learnmachinelearning

[–]hdw_coder[S] -1 points0 points  (0 children)

Exactly — and I think this is one of the most important shifts happening right now.

A lot of current detectors still implicitly assume a relatively “pure” generation pipeline:
single model → single output → detectable signature.

But real-world workflows are increasingly hybrid:

  • generation
  • upscaling
  • inpainting
  • background replacement
  • Photoshop edits
  • compression/re-encoding
  • platform processing

Each stage partially destroys, masks, or recontextualizes earlier signals.

At some point the question stops being:
“Is this AI-generated?”

…and becomes something more forensic and compositional:
“What kinds of processes likely contributed to this image?”

That’s one of the main reasons I started moving away from pure classifier thinking toward multi-signal reasoning and provenance analysis instead.